MediaWiki
REL1_20
|
00001 <?php 00095 class HTMLForm extends ContextSource { 00096 00097 // A mapping of 'type' inputs onto standard HTMLFormField subclasses 00098 static $typeMappings = array( 00099 'api' => 'HTMLApiField', 00100 'text' => 'HTMLTextField', 00101 'textarea' => 'HTMLTextAreaField', 00102 'select' => 'HTMLSelectField', 00103 'radio' => 'HTMLRadioField', 00104 'multiselect' => 'HTMLMultiSelectField', 00105 'check' => 'HTMLCheckField', 00106 'toggle' => 'HTMLCheckField', 00107 'int' => 'HTMLIntField', 00108 'float' => 'HTMLFloatField', 00109 'info' => 'HTMLInfoField', 00110 'selectorother' => 'HTMLSelectOrOtherField', 00111 'selectandother' => 'HTMLSelectAndOtherField', 00112 'submit' => 'HTMLSubmitField', 00113 'hidden' => 'HTMLHiddenField', 00114 'edittools' => 'HTMLEditTools', 00115 00116 // HTMLTextField will output the correct type="" attribute automagically. 00117 // There are about four zillion other HTML5 input types, like url, but 00118 // we don't use those at the moment, so no point in adding all of them. 00119 'email' => 'HTMLTextField', 00120 'password' => 'HTMLTextField', 00121 ); 00122 00123 protected $mMessagePrefix; 00124 00126 protected $mFlatFields; 00127 00128 protected $mFieldTree; 00129 protected $mShowReset = false; 00130 public $mFieldData; 00131 00132 protected $mSubmitCallback; 00133 protected $mValidationErrorMessage; 00134 00135 protected $mPre = ''; 00136 protected $mHeader = ''; 00137 protected $mFooter = ''; 00138 protected $mSectionHeaders = array(); 00139 protected $mSectionFooters = array(); 00140 protected $mPost = ''; 00141 protected $mId; 00142 00143 protected $mSubmitID; 00144 protected $mSubmitName; 00145 protected $mSubmitText; 00146 protected $mSubmitTooltip; 00147 00148 protected $mTitle; 00149 protected $mMethod = 'post'; 00150 00156 protected $mAction = false; 00157 00158 protected $mUseMultipart = false; 00159 protected $mHiddenFields = array(); 00160 protected $mButtons = array(); 00161 00162 protected $mWrapperLegend = false; 00163 00171 protected $mSubSectionBeforeFields = true; 00172 00178 protected $displayFormat = 'table'; 00179 00184 protected $availableDisplayFormats = array( 00185 'table', 00186 'div', 00187 'raw', 00188 ); 00189 00197 public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { 00198 if ( $context instanceof IContextSource ) { 00199 $this->setContext( $context ); 00200 $this->mTitle = false; // We don't need them to set a title 00201 $this->mMessagePrefix = $messagePrefix; 00202 } else { 00203 // B/C since 1.18 00204 if ( is_string( $context ) && $messagePrefix === '' ) { 00205 // it's actually $messagePrefix 00206 $this->mMessagePrefix = $context; 00207 } 00208 } 00209 00210 // Expand out into a tree. 00211 $loadedDescriptor = array(); 00212 $this->mFlatFields = array(); 00213 00214 foreach ( $descriptor as $fieldname => $info ) { 00215 $section = isset( $info['section'] ) 00216 ? $info['section'] 00217 : ''; 00218 00219 if ( isset( $info['type'] ) && $info['type'] == 'file' ) { 00220 $this->mUseMultipart = true; 00221 } 00222 00223 $field = self::loadInputFromParameters( $fieldname, $info ); 00224 $field->mParent = $this; 00225 00226 $setSection =& $loadedDescriptor; 00227 if ( $section ) { 00228 $sectionParts = explode( '/', $section ); 00229 00230 while ( count( $sectionParts ) ) { 00231 $newName = array_shift( $sectionParts ); 00232 00233 if ( !isset( $setSection[$newName] ) ) { 00234 $setSection[$newName] = array(); 00235 } 00236 00237 $setSection =& $setSection[$newName]; 00238 } 00239 } 00240 00241 $setSection[$fieldname] = $field; 00242 $this->mFlatFields[$fieldname] = $field; 00243 } 00244 00245 $this->mFieldTree = $loadedDescriptor; 00246 } 00247 00255 public function setDisplayFormat( $format ) { 00256 if ( !in_array( $format, $this->availableDisplayFormats ) ) { 00257 throw new MWException ( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) ); 00258 } 00259 $this->displayFormat = $format; 00260 return $this; 00261 } 00262 00268 public function getDisplayFormat() { 00269 return $this->displayFormat; 00270 } 00271 00277 static function addJS() { wfDeprecated( __METHOD__, '1.18' ); } 00278 00285 static function loadInputFromParameters( $fieldname, $descriptor ) { 00286 if ( isset( $descriptor['class'] ) ) { 00287 $class = $descriptor['class']; 00288 } elseif ( isset( $descriptor['type'] ) ) { 00289 $class = self::$typeMappings[$descriptor['type']]; 00290 $descriptor['class'] = $class; 00291 } else { 00292 $class = null; 00293 } 00294 00295 if ( !$class ) { 00296 throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) ); 00297 } 00298 00299 $descriptor['fieldname'] = $fieldname; 00300 00301 # TODO 00302 # This will throw a fatal error whenever someone try to use 00303 # 'class' to feed a CSS class instead of 'cssclass'. Would be 00304 # great to avoid the fatal error and show a nice error. 00305 $obj = new $class( $descriptor ); 00306 00307 return $obj; 00308 } 00309 00318 function prepareForm() { 00319 # Check if we have the info we need 00320 if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) { 00321 throw new MWException( "You must call setTitle() on an HTMLForm" ); 00322 } 00323 00324 # Load data from the request. 00325 $this->loadData(); 00326 return $this; 00327 } 00328 00333 function tryAuthorizedSubmit() { 00334 $result = false; 00335 00336 $submit = false; 00337 if ( $this->getMethod() != 'post' ) { 00338 $submit = true; // no session check needed 00339 } elseif ( $this->getRequest()->wasPosted() ) { 00340 $editToken = $this->getRequest()->getVal( 'wpEditToken' ); 00341 if ( $this->getUser()->isLoggedIn() || $editToken != null ) { 00342 // Session tokens for logged-out users have no security value. 00343 // However, if the user gave one, check it in order to give a nice 00344 // "session expired" error instead of "permission denied" or such. 00345 $submit = $this->getUser()->matchEditToken( $editToken ); 00346 } else { 00347 $submit = true; 00348 } 00349 } 00350 00351 if ( $submit ) { 00352 $result = $this->trySubmit(); 00353 } 00354 00355 return $result; 00356 } 00357 00364 function show() { 00365 $this->prepareForm(); 00366 00367 $result = $this->tryAuthorizedSubmit(); 00368 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { 00369 return $result; 00370 } 00371 00372 $this->displayForm( $result ); 00373 return false; 00374 } 00375 00383 function trySubmit() { 00384 # Check for validation 00385 foreach ( $this->mFlatFields as $fieldname => $field ) { 00386 if ( !empty( $field->mParams['nodata'] ) ) { 00387 continue; 00388 } 00389 if ( $field->validate( 00390 $this->mFieldData[$fieldname], 00391 $this->mFieldData ) 00392 !== true 00393 ) { 00394 return isset( $this->mValidationErrorMessage ) 00395 ? $this->mValidationErrorMessage 00396 : array( 'htmlform-invalid-input' ); 00397 } 00398 } 00399 00400 $callback = $this->mSubmitCallback; 00401 if ( !is_callable( $callback ) ) { 00402 throw new MWException( 'HTMLForm: no submit callback provided. Use setSubmitCallback() to set one.' ); 00403 } 00404 00405 $data = $this->filterDataForSubmit( $this->mFieldData ); 00406 00407 $res = call_user_func( $callback, $data, $this ); 00408 00409 return $res; 00410 } 00411 00421 function setSubmitCallback( $cb ) { 00422 $this->mSubmitCallback = $cb; 00423 return $this; 00424 } 00425 00432 function setValidationErrorMessage( $msg ) { 00433 $this->mValidationErrorMessage = $msg; 00434 return $this; 00435 } 00436 00442 function setIntro( $msg ) { 00443 $this->setPreText( $msg ); 00444 return $this; 00445 } 00446 00453 function setPreText( $msg ) { 00454 $this->mPre = $msg; 00455 return $this; 00456 } 00457 00463 function addPreText( $msg ) { 00464 $this->mPre .= $msg; 00465 return $this; 00466 } 00467 00474 function addHeaderText( $msg, $section = null ) { 00475 if ( is_null( $section ) ) { 00476 $this->mHeader .= $msg; 00477 } else { 00478 if ( !isset( $this->mSectionHeaders[$section] ) ) { 00479 $this->mSectionHeaders[$section] = ''; 00480 } 00481 $this->mSectionHeaders[$section] .= $msg; 00482 } 00483 return $this; 00484 } 00485 00493 function setHeaderText( $msg, $section = null ) { 00494 if ( is_null( $section ) ) { 00495 $this->mHeader = $msg; 00496 } else { 00497 $this->mSectionHeaders[$section] = $msg; 00498 } 00499 return $this; 00500 } 00501 00508 function addFooterText( $msg, $section = null ) { 00509 if ( is_null( $section ) ) { 00510 $this->mFooter .= $msg; 00511 } else { 00512 if ( !isset( $this->mSectionFooters[$section] ) ) { 00513 $this->mSectionFooters[$section] = ''; 00514 } 00515 $this->mSectionFooters[$section] .= $msg; 00516 } 00517 return $this; 00518 } 00519 00527 function setFooterText( $msg, $section = null ) { 00528 if ( is_null( $section ) ) { 00529 $this->mFooter = $msg; 00530 } else { 00531 $this->mSectionFooters[$section] = $msg; 00532 } 00533 return $this; 00534 } 00535 00541 function addPostText( $msg ) { 00542 $this->mPost .= $msg; 00543 return $this; 00544 } 00545 00551 function setPostText( $msg ) { 00552 $this->mPost = $msg; 00553 return $this; 00554 } 00555 00563 public function addHiddenField( $name, $value, $attribs = array() ) { 00564 $attribs += array( 'name' => $name ); 00565 $this->mHiddenFields[] = array( $value, $attribs ); 00566 return $this; 00567 } 00568 00577 public function addButton( $name, $value, $id = null, $attribs = null ) { 00578 $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' ); 00579 return $this; 00580 } 00581 00593 function displayForm( $submitResult ) { 00594 $this->getOutput()->addHTML( $this->getHTML( $submitResult ) ); 00595 } 00596 00602 function getHTML( $submitResult ) { 00603 # For good measure (it is the default) 00604 $this->getOutput()->preventClickjacking(); 00605 $this->getOutput()->addModules( 'mediawiki.htmlform' ); 00606 00607 $html = '' 00608 . $this->getErrors( $submitResult ) 00609 . $this->mHeader 00610 . $this->getBody() 00611 . $this->getHiddenFields() 00612 . $this->getButtons() 00613 . $this->mFooter 00614 ; 00615 00616 $html = $this->wrapForm( $html ); 00617 00618 return '' . $this->mPre . $html . $this->mPost; 00619 } 00620 00626 function wrapForm( $html ) { 00627 00628 # Include a <fieldset> wrapper for style, if requested. 00629 if ( $this->mWrapperLegend !== false ) { 00630 $html = Xml::fieldset( $this->mWrapperLegend, $html ); 00631 } 00632 # Use multipart/form-data 00633 $encType = $this->mUseMultipart 00634 ? 'multipart/form-data' 00635 : 'application/x-www-form-urlencoded'; 00636 # Attributes 00637 $attribs = array( 00638 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction, 00639 'method' => $this->mMethod, 00640 'class' => 'visualClear', 00641 'enctype' => $encType, 00642 ); 00643 if ( !empty( $this->mId ) ) { 00644 $attribs['id'] = $this->mId; 00645 } 00646 00647 return Html::rawElement( 'form', $attribs, $html ); 00648 } 00649 00654 function getHiddenFields() { 00655 global $wgArticlePath; 00656 00657 $html = ''; 00658 if ( $this->getMethod() == 'post' ) { 00659 $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; 00660 $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; 00661 } 00662 00663 if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() == 'get' ) { 00664 $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; 00665 } 00666 00667 foreach ( $this->mHiddenFields as $data ) { 00668 list( $value, $attribs ) = $data; 00669 $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n"; 00670 } 00671 00672 return $html; 00673 } 00674 00679 function getButtons() { 00680 $html = ''; 00681 $attribs = array(); 00682 00683 if ( isset( $this->mSubmitID ) ) { 00684 $attribs['id'] = $this->mSubmitID; 00685 } 00686 00687 if ( isset( $this->mSubmitName ) ) { 00688 $attribs['name'] = $this->mSubmitName; 00689 } 00690 00691 if ( isset( $this->mSubmitTooltip ) ) { 00692 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); 00693 } 00694 00695 $attribs['class'] = 'mw-htmlform-submit'; 00696 00697 $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; 00698 00699 if ( $this->mShowReset ) { 00700 $html .= Html::element( 00701 'input', 00702 array( 00703 'type' => 'reset', 00704 'value' => $this->msg( 'htmlform-reset' )->text() 00705 ) 00706 ) . "\n"; 00707 } 00708 00709 foreach ( $this->mButtons as $button ) { 00710 $attrs = array( 00711 'type' => 'submit', 00712 'name' => $button['name'], 00713 'value' => $button['value'] 00714 ); 00715 00716 if ( $button['attribs'] ) { 00717 $attrs += $button['attribs']; 00718 } 00719 00720 if ( isset( $button['id'] ) ) { 00721 $attrs['id'] = $button['id']; 00722 } 00723 00724 $html .= Html::element( 'input', $attrs ); 00725 } 00726 00727 return $html; 00728 } 00729 00734 function getBody() { 00735 return $this->displaySection( $this->mFieldTree ); 00736 } 00737 00743 function getErrors( $errors ) { 00744 if ( $errors instanceof Status ) { 00745 if ( $errors->isOK() ) { 00746 $errorstr = ''; 00747 } else { 00748 $errorstr = $this->getOutput()->parse( $errors->getWikiText() ); 00749 } 00750 } elseif ( is_array( $errors ) ) { 00751 $errorstr = $this->formatErrors( $errors ); 00752 } else { 00753 $errorstr = $errors; 00754 } 00755 00756 return $errorstr 00757 ? Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr ) 00758 : ''; 00759 } 00760 00766 public static function formatErrors( $errors ) { 00767 $errorstr = ''; 00768 00769 foreach ( $errors as $error ) { 00770 if ( is_array( $error ) ) { 00771 $msg = array_shift( $error ); 00772 } else { 00773 $msg = $error; 00774 $error = array(); 00775 } 00776 00777 $errorstr .= Html::rawElement( 00778 'li', 00779 array(), 00780 wfMessage( $msg, $error )->parse() 00781 ); 00782 } 00783 00784 $errorstr = Html::rawElement( 'ul', array(), $errorstr ); 00785 00786 return $errorstr; 00787 } 00788 00794 function setSubmitText( $t ) { 00795 $this->mSubmitText = $t; 00796 return $this; 00797 } 00798 00805 public function setSubmitTextMsg( $msg ) { 00806 $this->setSubmitText( $this->msg( $msg )->text() ); 00807 return $this; 00808 } 00809 00814 function getSubmitText() { 00815 return $this->mSubmitText 00816 ? $this->mSubmitText 00817 : $this->msg( 'htmlform-submit' )->text(); 00818 } 00819 00824 public function setSubmitName( $name ) { 00825 $this->mSubmitName = $name; 00826 return $this; 00827 } 00828 00833 public function setSubmitTooltip( $name ) { 00834 $this->mSubmitTooltip = $name; 00835 return $this; 00836 } 00837 00844 function setSubmitID( $t ) { 00845 $this->mSubmitID = $t; 00846 return $this; 00847 } 00848 00853 public function setId( $id ) { 00854 $this->mId = $id; 00855 return $this; 00856 } 00864 public function setWrapperLegend( $legend ) { 00865 $this->mWrapperLegend = $legend; 00866 return $this; 00867 } 00868 00876 public function setWrapperLegendMsg( $msg ) { 00877 $this->setWrapperLegend( $this->msg( $msg )->text() ); 00878 return $this; 00879 } 00880 00888 function setMessagePrefix( $p ) { 00889 $this->mMessagePrefix = $p; 00890 return $this; 00891 } 00892 00898 function setTitle( $t ) { 00899 $this->mTitle = $t; 00900 return $this; 00901 } 00902 00907 function getTitle() { 00908 return $this->mTitle === false 00909 ? $this->getContext()->getTitle() 00910 : $this->mTitle; 00911 } 00912 00918 public function setMethod( $method = 'post' ) { 00919 $this->mMethod = $method; 00920 return $this; 00921 } 00922 00923 public function getMethod() { 00924 return $this->mMethod; 00925 } 00926 00934 public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) { 00935 $displayFormat = $this->getDisplayFormat(); 00936 00937 $html = ''; 00938 $subsectionHtml = ''; 00939 $hasLabel = false; 00940 00941 $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat ); 00942 00943 foreach ( $fields as $key => $value ) { 00944 if ( $value instanceof HTMLFormField ) { 00945 $v = empty( $value->mParams['nodata'] ) 00946 ? $this->mFieldData[$key] 00947 : $value->getDefault(); 00948 $html .= $value->$getFieldHtmlMethod( $v ); 00949 00950 $labelValue = trim( $value->getLabel() ); 00951 if ( $labelValue != ' ' && $labelValue !== '' ) { 00952 $hasLabel = true; 00953 } 00954 } elseif ( is_array( $value ) ) { 00955 $section = $this->displaySection( $value, $key ); 00956 $legend = $this->getLegend( $key ); 00957 if ( isset( $this->mSectionHeaders[$key] ) ) { 00958 $section = $this->mSectionHeaders[$key] . $section; 00959 } 00960 if ( isset( $this->mSectionFooters[$key] ) ) { 00961 $section .= $this->mSectionFooters[$key]; 00962 } 00963 $attributes = array(); 00964 if ( $fieldsetIDPrefix ) { 00965 $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" ); 00966 } 00967 $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n"; 00968 } 00969 } 00970 00971 if ( $displayFormat !== 'raw' ) { 00972 $classes = array(); 00973 00974 if ( !$hasLabel ) { // Avoid strange spacing when no labels exist 00975 $classes[] = 'mw-htmlform-nolabel'; 00976 } 00977 00978 $attribs = array( 00979 'class' => implode( ' ', $classes ), 00980 ); 00981 00982 if ( $sectionName ) { 00983 $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" ); 00984 } 00985 00986 if ( $displayFormat === 'table' ) { 00987 $html = Html::rawElement( 'table', $attribs, 00988 Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n"; 00989 } elseif ( $displayFormat === 'div' ) { 00990 $html = Html::rawElement( 'div', $attribs, "\n$html\n" ); 00991 } 00992 } 00993 00994 if ( $this->mSubSectionBeforeFields ) { 00995 return $subsectionHtml . "\n" . $html; 00996 } else { 00997 return $html . "\n" . $subsectionHtml; 00998 } 00999 } 01000 01004 function loadData() { 01005 $fieldData = array(); 01006 01007 foreach ( $this->mFlatFields as $fieldname => $field ) { 01008 if ( !empty( $field->mParams['nodata'] ) ) { 01009 continue; 01010 } elseif ( !empty( $field->mParams['disabled'] ) ) { 01011 $fieldData[$fieldname] = $field->getDefault(); 01012 } else { 01013 $fieldData[$fieldname] = $field->loadDataFromRequest( $this->getRequest() ); 01014 } 01015 } 01016 01017 # Filter data. 01018 foreach ( $fieldData as $name => &$value ) { 01019 $field = $this->mFlatFields[$name]; 01020 $value = $field->filter( $value, $this->mFlatFields ); 01021 } 01022 01023 $this->mFieldData = $fieldData; 01024 } 01025 01032 function suppressReset( $suppressReset = true ) { 01033 $this->mShowReset = !$suppressReset; 01034 return $this; 01035 } 01036 01044 function filterDataForSubmit( $data ) { 01045 return $data; 01046 } 01047 01054 public function getLegend( $key ) { 01055 return $this->msg( "{$this->mMessagePrefix}-$key" )->text(); 01056 } 01057 01067 public function setAction( $action ) { 01068 $this->mAction = $action; 01069 return $this; 01070 } 01071 01072 } 01073 01078 abstract class HTMLFormField { 01079 01080 protected $mValidationCallback; 01081 protected $mFilterCallback; 01082 protected $mName; 01083 public $mParams; 01084 protected $mLabel; # String label. Set on construction 01085 protected $mID; 01086 protected $mClass = ''; 01087 protected $mDefault; 01088 01092 public $mParent; 01093 01102 abstract function getInputHTML( $value ); 01103 01114 function msg() { 01115 $args = func_get_args(); 01116 01117 if ( $this->mParent ) { 01118 $callback = array( $this->mParent, 'msg' ); 01119 } else { 01120 $callback = 'wfMessage'; 01121 } 01122 01123 return call_user_func_array( $callback, $args ); 01124 } 01125 01134 function validate( $value, $alldata ) { 01135 if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value === '' ) { 01136 return $this->msg( 'htmlform-required' )->parse(); 01137 } 01138 01139 if ( isset( $this->mValidationCallback ) ) { 01140 return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent ); 01141 } 01142 01143 return true; 01144 } 01145 01146 function filter( $value, $alldata ) { 01147 if ( isset( $this->mFilterCallback ) ) { 01148 $value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent ); 01149 } 01150 01151 return $value; 01152 } 01153 01160 protected function needsLabel() { 01161 return true; 01162 } 01163 01170 function loadDataFromRequest( $request ) { 01171 if ( $request->getCheck( $this->mName ) ) { 01172 return $request->getText( $this->mName ); 01173 } else { 01174 return $this->getDefault(); 01175 } 01176 } 01177 01182 function __construct( $params ) { 01183 $this->mParams = $params; 01184 01185 # Generate the label from a message, if possible 01186 if ( isset( $params['label-message'] ) ) { 01187 $msgInfo = $params['label-message']; 01188 01189 if ( is_array( $msgInfo ) ) { 01190 $msg = array_shift( $msgInfo ); 01191 } else { 01192 $msg = $msgInfo; 01193 $msgInfo = array(); 01194 } 01195 01196 $this->mLabel = wfMessage( $msg, $msgInfo )->parse(); 01197 } elseif ( isset( $params['label'] ) ) { 01198 $this->mLabel = $params['label']; 01199 } 01200 01201 $this->mName = "wp{$params['fieldname']}"; 01202 if ( isset( $params['name'] ) ) { 01203 $this->mName = $params['name']; 01204 } 01205 01206 $validName = Sanitizer::escapeId( $this->mName ); 01207 if ( $this->mName != $validName && !isset( $params['nodata'] ) ) { 01208 throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ ); 01209 } 01210 01211 $this->mID = "mw-input-{$this->mName}"; 01212 01213 if ( isset( $params['default'] ) ) { 01214 $this->mDefault = $params['default']; 01215 } 01216 01217 if ( isset( $params['id'] ) ) { 01218 $id = $params['id']; 01219 $validId = Sanitizer::escapeId( $id ); 01220 01221 if ( $id != $validId ) { 01222 throw new MWException( "Invalid id '$id' passed to " . __METHOD__ ); 01223 } 01224 01225 $this->mID = $id; 01226 } 01227 01228 if ( isset( $params['cssclass'] ) ) { 01229 $this->mClass = $params['cssclass']; 01230 } 01231 01232 if ( isset( $params['validation-callback'] ) ) { 01233 $this->mValidationCallback = $params['validation-callback']; 01234 } 01235 01236 if ( isset( $params['filter-callback'] ) ) { 01237 $this->mFilterCallback = $params['filter-callback']; 01238 } 01239 01240 if ( isset( $params['flatlist'] ) ) { 01241 $this->mClass .= ' mw-htmlform-flatlist'; 01242 } 01243 } 01244 01251 function getTableRow( $value ) { 01252 list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); 01253 $inputHtml = $this->getInputHTML( $value ); 01254 $fieldType = get_class( $this ); 01255 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); 01256 $cellAttributes = array(); 01257 01258 if ( !empty( $this->mParams['vertical-label'] ) ) { 01259 $cellAttributes['colspan'] = 2; 01260 $verticalLabel = true; 01261 } else { 01262 $verticalLabel = false; 01263 } 01264 01265 $label = $this->getLabelHtml( $cellAttributes ); 01266 01267 $field = Html::rawElement( 01268 'td', 01269 array( 'class' => 'mw-input' ) + $cellAttributes, 01270 $inputHtml . "\n$errors" 01271 ); 01272 01273 if ( $verticalLabel ) { 01274 $html = Html::rawElement( 'tr', 01275 array( 'class' => 'mw-htmlform-vertical-label' ), $label ); 01276 $html .= Html::rawElement( 'tr', 01277 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), 01278 $field ); 01279 } else { 01280 $html = Html::rawElement( 'tr', 01281 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), 01282 $label . $field ); 01283 } 01284 01285 return $html . $helptext; 01286 } 01287 01295 public function getDiv( $value ) { 01296 list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); 01297 $inputHtml = $this->getInputHTML( $value ); 01298 $fieldType = get_class( $this ); 01299 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() ); 01300 $cellAttributes = array(); 01301 $label = $this->getLabelHtml( $cellAttributes ); 01302 01303 $field = Html::rawElement( 01304 'div', 01305 array( 'class' => 'mw-input' ) + $cellAttributes, 01306 $inputHtml . "\n$errors" 01307 ); 01308 $html = Html::rawElement( 'div', 01309 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), 01310 $label . $field ); 01311 $html .= $helptext; 01312 return $html; 01313 } 01314 01322 public function getRaw( $value ) { 01323 list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); 01324 $inputHtml = $this->getInputHTML( $value ); 01325 $fieldType = get_class( $this ); 01326 $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() ); 01327 $cellAttributes = array(); 01328 $label = $this->getLabelHtml( $cellAttributes ); 01329 01330 $html = "\n$errors"; 01331 $html .= $label; 01332 $html .= $inputHtml; 01333 $html .= $helptext; 01334 return $html; 01335 } 01336 01343 public function getHelpTextHtmlTable( $helptext ) { 01344 if ( is_null( $helptext ) ) { 01345 return ''; 01346 } 01347 01348 $row = Html::rawElement( 01349 'td', 01350 array( 'colspan' => 2, 'class' => 'htmlform-tip' ), 01351 $helptext 01352 ); 01353 $row = Html::rawElement( 'tr', array(), $row ); 01354 return $row; 01355 } 01356 01363 public function getHelpTextHtmlDiv( $helptext ) { 01364 if ( is_null( $helptext ) ) { 01365 return ''; 01366 } 01367 01368 $div = Html::rawElement( 'div', array( 'class' => 'htmlform-tip' ), $helptext ); 01369 return $div; 01370 } 01371 01378 public function getHelpTextHtmlRaw( $helptext ) { 01379 return $this->getHelpTextHtmlDiv( $helptext ); 01380 } 01381 01387 public function getHelpText() { 01388 $helptext = null; 01389 01390 if ( isset( $this->mParams['help-message'] ) ) { 01391 $this->mParams['help-messages'] = array( $this->mParams['help-message'] ); 01392 } 01393 01394 if ( isset( $this->mParams['help-messages'] ) ) { 01395 foreach ( $this->mParams['help-messages'] as $name ) { 01396 $helpMessage = (array)$name; 01397 $msg = $this->msg( array_shift( $helpMessage ), $helpMessage ); 01398 01399 if ( $msg->exists() ) { 01400 if ( is_null( $helptext ) ) { 01401 $helptext = ''; 01402 } else { 01403 $helptext .= $this->msg( 'word-separator' )->escaped(); // some space 01404 } 01405 $helptext .= $msg->parse(); // Append message 01406 } 01407 } 01408 } 01409 elseif ( isset( $this->mParams['help'] ) ) { 01410 $helptext = $this->mParams['help']; 01411 } 01412 return $helptext; 01413 } 01414 01421 public function getErrorsAndErrorClass( $value ) { 01422 $errors = $this->validate( $value, $this->mParent->mFieldData ); 01423 01424 if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) { 01425 $errors = ''; 01426 $errorClass = ''; 01427 } else { 01428 $errors = self::formatErrors( $errors ); 01429 $errorClass = 'mw-htmlform-invalid-input'; 01430 } 01431 return array( $errors, $errorClass ); 01432 } 01433 01434 function getLabel() { 01435 return $this->mLabel; 01436 } 01437 01438 function getLabelHtml( $cellAttributes = array() ) { 01439 # Don't output a for= attribute for labels with no associated input. 01440 # Kind of hacky here, possibly we don't want these to be <label>s at all. 01441 $for = array(); 01442 01443 if ( $this->needsLabel() ) { 01444 $for['for'] = $this->mID; 01445 } 01446 01447 $displayFormat = $this->mParent->getDisplayFormat(); 01448 $labelElement = Html::rawElement( 'label', $for, $this->getLabel() ); 01449 01450 if ( $displayFormat == 'table' ) { 01451 return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, 01452 Html::rawElement( 'label', $for, $this->getLabel() ) 01453 ); 01454 } elseif ( $displayFormat == 'div' ) { 01455 return Html::rawElement( 'div', array( 'class' => 'mw-label' ) + $cellAttributes, 01456 Html::rawElement( 'label', $for, $this->getLabel() ) 01457 ); 01458 } else { 01459 return $labelElement; 01460 } 01461 } 01462 01463 function getDefault() { 01464 if ( isset( $this->mDefault ) ) { 01465 return $this->mDefault; 01466 } else { 01467 return null; 01468 } 01469 } 01470 01476 public function getTooltipAndAccessKey() { 01477 if ( empty( $this->mParams['tooltip'] ) ) { 01478 return array(); 01479 } 01480 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] ); 01481 } 01482 01490 public static function flattenOptions( $options ) { 01491 $flatOpts = array(); 01492 01493 foreach ( $options as $value ) { 01494 if ( is_array( $value ) ) { 01495 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) ); 01496 } else { 01497 $flatOpts[] = $value; 01498 } 01499 } 01500 01501 return $flatOpts; 01502 } 01503 01510 protected static function formatErrors( $errors ) { 01511 if ( is_array( $errors ) && count( $errors ) === 1 ) { 01512 $errors = array_shift( $errors ); 01513 } 01514 01515 if ( is_array( $errors ) ) { 01516 $lines = array(); 01517 foreach ( $errors as $error ) { 01518 if ( $error instanceof Message ) { 01519 $lines[] = Html::rawElement( 'li', array(), $error->parse() ); 01520 } else { 01521 $lines[] = Html::rawElement( 'li', array(), $error ); 01522 } 01523 } 01524 return Html::rawElement( 'ul', array( 'class' => 'error' ), implode( "\n", $lines ) ); 01525 } else { 01526 if ( $errors instanceof Message ) { 01527 $errors = $errors->parse(); 01528 } 01529 return Html::rawElement( 'span', array( 'class' => 'error' ), $errors ); 01530 } 01531 } 01532 } 01533 01534 class HTMLTextField extends HTMLFormField { 01535 function getSize() { 01536 return isset( $this->mParams['size'] ) 01537 ? $this->mParams['size'] 01538 : 45; 01539 } 01540 01541 function getInputHTML( $value ) { 01542 $attribs = array( 01543 'id' => $this->mID, 01544 'name' => $this->mName, 01545 'size' => $this->getSize(), 01546 'value' => $value, 01547 ) + $this->getTooltipAndAccessKey(); 01548 01549 if ( $this->mClass !== '' ) { 01550 $attribs['class'] = $this->mClass; 01551 } 01552 01553 if ( !empty( $this->mParams['disabled'] ) ) { 01554 $attribs['disabled'] = 'disabled'; 01555 } 01556 01557 # TODO: Enforce pattern, step, required, readonly on the server side as 01558 # well 01559 $allowedParams = array( 'min', 'max', 'pattern', 'title', 'step', 01560 'placeholder', 'list', 'maxlength' ); 01561 foreach ( $allowedParams as $param ) { 01562 if ( isset( $this->mParams[$param] ) ) { 01563 $attribs[$param] = $this->mParams[$param]; 01564 } 01565 } 01566 01567 foreach ( array( 'required', 'autofocus', 'multiple', 'readonly' ) as $param ) { 01568 if ( isset( $this->mParams[$param] ) ) { 01569 $attribs[$param] = ''; 01570 } 01571 } 01572 01573 # Implement tiny differences between some field variants 01574 # here, rather than creating a new class for each one which 01575 # is essentially just a clone of this one. 01576 if ( isset( $this->mParams['type'] ) ) { 01577 switch ( $this->mParams['type'] ) { 01578 case 'email': 01579 $attribs['type'] = 'email'; 01580 break; 01581 case 'int': 01582 $attribs['type'] = 'number'; 01583 break; 01584 case 'float': 01585 $attribs['type'] = 'number'; 01586 $attribs['step'] = 'any'; 01587 break; 01588 # Pass through 01589 case 'password': 01590 case 'file': 01591 $attribs['type'] = $this->mParams['type']; 01592 break; 01593 } 01594 } 01595 01596 return Html::element( 'input', $attribs ); 01597 } 01598 } 01599 class HTMLTextAreaField extends HTMLFormField { 01600 function getCols() { 01601 return isset( $this->mParams['cols'] ) 01602 ? $this->mParams['cols'] 01603 : 80; 01604 } 01605 01606 function getRows() { 01607 return isset( $this->mParams['rows'] ) 01608 ? $this->mParams['rows'] 01609 : 25; 01610 } 01611 01612 function getInputHTML( $value ) { 01613 $attribs = array( 01614 'id' => $this->mID, 01615 'name' => $this->mName, 01616 'cols' => $this->getCols(), 01617 'rows' => $this->getRows(), 01618 ) + $this->getTooltipAndAccessKey(); 01619 01620 if ( $this->mClass !== '' ) { 01621 $attribs['class'] = $this->mClass; 01622 } 01623 01624 if ( !empty( $this->mParams['disabled'] ) ) { 01625 $attribs['disabled'] = 'disabled'; 01626 } 01627 01628 if ( !empty( $this->mParams['readonly'] ) ) { 01629 $attribs['readonly'] = 'readonly'; 01630 } 01631 01632 if ( isset( $this->mParams['placeholder'] ) ) { 01633 $attribs['placeholder'] = $this->mParams['placeholder']; 01634 } 01635 01636 foreach ( array( 'required', 'autofocus' ) as $param ) { 01637 if ( isset( $this->mParams[$param] ) ) { 01638 $attribs[$param] = ''; 01639 } 01640 } 01641 01642 return Html::element( 'textarea', $attribs, $value ); 01643 } 01644 } 01645 01649 class HTMLFloatField extends HTMLTextField { 01650 function getSize() { 01651 return isset( $this->mParams['size'] ) 01652 ? $this->mParams['size'] 01653 : 20; 01654 } 01655 01656 function validate( $value, $alldata ) { 01657 $p = parent::validate( $value, $alldata ); 01658 01659 if ( $p !== true ) { 01660 return $p; 01661 } 01662 01663 $value = trim( $value ); 01664 01665 # http://dev.w3.org/html5/spec/common-microsyntaxes.html#real-numbers 01666 # with the addition that a leading '+' sign is ok. 01667 if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) { 01668 return $this->msg( 'htmlform-float-invalid' )->parseAsBlock(); 01669 } 01670 01671 # The "int" part of these message names is rather confusing. 01672 # They make equal sense for all numbers. 01673 if ( isset( $this->mParams['min'] ) ) { 01674 $min = $this->mParams['min']; 01675 01676 if ( $min > $value ) { 01677 return $this->msg( 'htmlform-int-toolow', $min )->parseAsBlock(); 01678 } 01679 } 01680 01681 if ( isset( $this->mParams['max'] ) ) { 01682 $max = $this->mParams['max']; 01683 01684 if ( $max < $value ) { 01685 return $this->msg( 'htmlform-int-toohigh', $max )->parseAsBlock(); 01686 } 01687 } 01688 01689 return true; 01690 } 01691 } 01692 01696 class HTMLIntField extends HTMLFloatField { 01697 function validate( $value, $alldata ) { 01698 $p = parent::validate( $value, $alldata ); 01699 01700 if ( $p !== true ) { 01701 return $p; 01702 } 01703 01704 # http://dev.w3.org/html5/spec/common-microsyntaxes.html#signed-integers 01705 # with the addition that a leading '+' sign is ok. Note that leading zeros 01706 # are fine, and will be left in the input, which is useful for things like 01707 # phone numbers when you know that they are integers (the HTML5 type=tel 01708 # input does not require its value to be numeric). If you want a tidier 01709 # value to, eg, save in the DB, clean it up with intval(). 01710 if ( !preg_match( '/^((\+|\-)?\d+)?$/', trim( $value ) ) 01711 ) { 01712 return $this->msg( 'htmlform-int-invalid' )->parseAsBlock(); 01713 } 01714 01715 return true; 01716 } 01717 } 01718 01722 class HTMLCheckField extends HTMLFormField { 01723 function getInputHTML( $value ) { 01724 if ( !empty( $this->mParams['invert'] ) ) { 01725 $value = !$value; 01726 } 01727 01728 $attr = $this->getTooltipAndAccessKey(); 01729 $attr['id'] = $this->mID; 01730 01731 if ( !empty( $this->mParams['disabled'] ) ) { 01732 $attr['disabled'] = 'disabled'; 01733 } 01734 01735 if ( $this->mClass !== '' ) { 01736 $attr['class'] = $this->mClass; 01737 } 01738 01739 return Xml::check( $this->mName, $value, $attr ) . ' ' . 01740 Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); 01741 } 01742 01748 function getLabel() { 01749 return ' '; 01750 } 01751 01756 function loadDataFromRequest( $request ) { 01757 $invert = false; 01758 if ( isset( $this->mParams['invert'] ) && $this->mParams['invert'] ) { 01759 $invert = true; 01760 } 01761 01762 // GetCheck won't work like we want for checks. 01763 // Fetch the value in either one of the two following case: 01764 // - we have a valid token (form got posted or GET forged by the user) 01765 // - checkbox name has a value (false or true), ie is not null 01766 if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName ) !== null ) { 01767 // XOR has the following truth table, which is what we want 01768 // INVERT VALUE | OUTPUT 01769 // true true | false 01770 // false true | true 01771 // false false | false 01772 // true false | true 01773 return $request->getBool( $this->mName ) xor $invert; 01774 } else { 01775 return $this->getDefault(); 01776 } 01777 } 01778 } 01779 01783 class HTMLSelectField extends HTMLFormField { 01784 function validate( $value, $alldata ) { 01785 $p = parent::validate( $value, $alldata ); 01786 01787 if ( $p !== true ) { 01788 return $p; 01789 } 01790 01791 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] ); 01792 01793 if ( in_array( $value, $validOptions ) ) 01794 return true; 01795 else 01796 return $this->msg( 'htmlform-select-badoption' )->parse(); 01797 } 01798 01799 function getInputHTML( $value ) { 01800 $select = new XmlSelect( $this->mName, $this->mID, strval( $value ) ); 01801 01802 # If one of the options' 'name' is int(0), it is automatically selected. 01803 # because PHP sucks and thinks int(0) == 'some string'. 01804 # Working around this by forcing all of them to strings. 01805 foreach ( $this->mParams['options'] as &$opt ) { 01806 if ( is_int( $opt ) ) { 01807 $opt = strval( $opt ); 01808 } 01809 } 01810 unset( $opt ); # PHP keeps $opt around as a reference, which is a bit scary 01811 01812 if ( !empty( $this->mParams['disabled'] ) ) { 01813 $select->setAttribute( 'disabled', 'disabled' ); 01814 } 01815 01816 if ( $this->mClass !== '' ) { 01817 $select->setAttribute( 'class', $this->mClass ); 01818 } 01819 01820 $select->addOptions( $this->mParams['options'] ); 01821 01822 return $select->getHTML(); 01823 } 01824 } 01825 01829 class HTMLSelectOrOtherField extends HTMLTextField { 01830 static $jsAdded = false; 01831 01832 function __construct( $params ) { 01833 if ( !in_array( 'other', $params['options'], true ) ) { 01834 $msg = isset( $params['other'] ) ? 01835 $params['other'] : 01836 wfMessage( 'htmlform-selectorother-other' )->text(); 01837 $params['options'][$msg] = 'other'; 01838 } 01839 01840 parent::__construct( $params ); 01841 } 01842 01843 static function forceToStringRecursive( $array ) { 01844 if ( is_array( $array ) ) { 01845 return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array ); 01846 } else { 01847 return strval( $array ); 01848 } 01849 } 01850 01851 function getInputHTML( $value ) { 01852 $valInSelect = false; 01853 01854 if ( $value !== false ) { 01855 $valInSelect = in_array( 01856 $value, 01857 HTMLFormField::flattenOptions( $this->mParams['options'] ) 01858 ); 01859 } 01860 01861 $selected = $valInSelect ? $value : 'other'; 01862 01863 $opts = self::forceToStringRecursive( $this->mParams['options'] ); 01864 01865 $select = new XmlSelect( $this->mName, $this->mID, $selected ); 01866 $select->addOptions( $opts ); 01867 01868 $select->setAttribute( 'class', 'mw-htmlform-select-or-other' ); 01869 01870 $tbAttribs = array( 'id' => $this->mID . '-other', 'size' => $this->getSize() ); 01871 01872 if ( !empty( $this->mParams['disabled'] ) ) { 01873 $select->setAttribute( 'disabled', 'disabled' ); 01874 $tbAttribs['disabled'] = 'disabled'; 01875 } 01876 01877 $select = $select->getHTML(); 01878 01879 if ( isset( $this->mParams['maxlength'] ) ) { 01880 $tbAttribs['maxlength'] = $this->mParams['maxlength']; 01881 } 01882 01883 if ( $this->mClass !== '' ) { 01884 $tbAttribs['class'] = $this->mClass; 01885 } 01886 01887 $textbox = Html::input( 01888 $this->mName . '-other', 01889 $valInSelect ? '' : $value, 01890 'text', 01891 $tbAttribs 01892 ); 01893 01894 return "$select<br />\n$textbox"; 01895 } 01896 01901 function loadDataFromRequest( $request ) { 01902 if ( $request->getCheck( $this->mName ) ) { 01903 $val = $request->getText( $this->mName ); 01904 01905 if ( $val == 'other' ) { 01906 $val = $request->getText( $this->mName . '-other' ); 01907 } 01908 01909 return $val; 01910 } else { 01911 return $this->getDefault(); 01912 } 01913 } 01914 } 01915 01919 class HTMLMultiSelectField extends HTMLFormField { 01920 01921 function validate( $value, $alldata ) { 01922 $p = parent::validate( $value, $alldata ); 01923 01924 if ( $p !== true ) { 01925 return $p; 01926 } 01927 01928 if ( !is_array( $value ) ) { 01929 return false; 01930 } 01931 01932 # If all options are valid, array_intersect of the valid options 01933 # and the provided options will return the provided options. 01934 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] ); 01935 01936 $validValues = array_intersect( $value, $validOptions ); 01937 if ( count( $validValues ) == count( $value ) ) { 01938 return true; 01939 } else { 01940 return $this->msg( 'htmlform-select-badoption' )->parse(); 01941 } 01942 } 01943 01944 function getInputHTML( $value ) { 01945 $html = $this->formatOptions( $this->mParams['options'], $value ); 01946 01947 return $html; 01948 } 01949 01950 function formatOptions( $options, $value ) { 01951 $html = ''; 01952 01953 $attribs = array(); 01954 01955 if ( !empty( $this->mParams['disabled'] ) ) { 01956 $attribs['disabled'] = 'disabled'; 01957 } 01958 01959 foreach ( $options as $label => $info ) { 01960 if ( is_array( $info ) ) { 01961 $html .= Html::rawElement( 'h1', array(), $label ) . "\n"; 01962 $html .= $this->formatOptions( $info, $value ); 01963 } else { 01964 $thisAttribs = array( 'id' => "{$this->mID}-$info", 'value' => $info ); 01965 01966 $checkbox = Xml::check( 01967 $this->mName . '[]', 01968 in_array( $info, $value, true ), 01969 $attribs + $thisAttribs ); 01970 $checkbox .= ' ' . Html::rawElement( 'label', array( 'for' => "{$this->mID}-$info" ), $label ); 01971 01972 $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $checkbox ); 01973 } 01974 } 01975 01976 return $html; 01977 } 01978 01983 function loadDataFromRequest( $request ) { 01984 if ( $this->mParent->getMethod() == 'post' ) { 01985 if ( $request->wasPosted() ) { 01986 # Checkboxes are just not added to the request arrays if they're not checked, 01987 # so it's perfectly possible for there not to be an entry at all 01988 return $request->getArray( $this->mName, array() ); 01989 } else { 01990 # That's ok, the user has not yet submitted the form, so show the defaults 01991 return $this->getDefault(); 01992 } 01993 } else { 01994 # This is the impossible case: if we look at $_GET and see no data for our 01995 # field, is it because the user has not yet submitted the form, or that they 01996 # have submitted it with all the options unchecked? We will have to assume the 01997 # latter, which basically means that you can't specify 'positive' defaults 01998 # for GET forms. 01999 # @todo FIXME... 02000 return $request->getArray( $this->mName, array() ); 02001 } 02002 } 02003 02004 function getDefault() { 02005 if ( isset( $this->mDefault ) ) { 02006 return $this->mDefault; 02007 } else { 02008 return array(); 02009 } 02010 } 02011 02012 protected function needsLabel() { 02013 return false; 02014 } 02015 } 02016 02027 class HTMLSelectAndOtherField extends HTMLSelectField { 02028 02029 function __construct( $params ) { 02030 if ( array_key_exists( 'other', $params ) ) { 02031 } elseif ( array_key_exists( 'other-message', $params ) ) { 02032 $params['other'] = wfMessage( $params['other-message'] )->plain(); 02033 } else { 02034 $params['other'] = null; 02035 } 02036 02037 if ( array_key_exists( 'options', $params ) ) { 02038 # Options array already specified 02039 } elseif ( array_key_exists( 'options-message', $params ) ) { 02040 # Generate options array from a system message 02041 $params['options'] = self::parseMessage( 02042 wfMessage( $params['options-message'] )->inContentLanguage()->plain(), 02043 $params['other'] 02044 ); 02045 } else { 02046 # Sulk 02047 throw new MWException( 'HTMLSelectAndOtherField called without any options' ); 02048 } 02049 $this->mFlatOptions = self::flattenOptions( $params['options'] ); 02050 02051 parent::__construct( $params ); 02052 } 02053 02061 public static function parseMessage( $string, $otherName = null ) { 02062 if ( $otherName === null ) { 02063 $otherName = wfMessage( 'htmlform-selectorother-other' )->plain(); 02064 } 02065 02066 $optgroup = false; 02067 $options = array( $otherName => 'other' ); 02068 02069 foreach ( explode( "\n", $string ) as $option ) { 02070 $value = trim( $option ); 02071 if ( $value == '' ) { 02072 continue; 02073 } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) { 02074 # A new group is starting... 02075 $value = trim( substr( $value, 1 ) ); 02076 $optgroup = $value; 02077 } elseif ( substr( $value, 0, 2 ) == '**' ) { 02078 # groupmember 02079 $opt = trim( substr( $value, 2 ) ); 02080 if ( $optgroup === false ) { 02081 $options[$opt] = $opt; 02082 } else { 02083 $options[$optgroup][$opt] = $opt; 02084 } 02085 } else { 02086 # groupless reason list 02087 $optgroup = false; 02088 $options[$option] = $option; 02089 } 02090 } 02091 02092 return $options; 02093 } 02094 02095 function getInputHTML( $value ) { 02096 $select = parent::getInputHTML( $value[1] ); 02097 02098 $textAttribs = array( 02099 'id' => $this->mID . '-other', 02100 'size' => $this->getSize(), 02101 ); 02102 02103 if ( $this->mClass !== '' ) { 02104 $textAttribs['class'] = $this->mClass; 02105 } 02106 02107 foreach ( array( 'required', 'autofocus', 'multiple', 'disabled' ) as $param ) { 02108 if ( isset( $this->mParams[$param] ) ) { 02109 $textAttribs[$param] = ''; 02110 } 02111 } 02112 02113 $textbox = Html::input( 02114 $this->mName . '-other', 02115 $value[2], 02116 'text', 02117 $textAttribs 02118 ); 02119 02120 return "$select<br />\n$textbox"; 02121 } 02122 02127 function loadDataFromRequest( $request ) { 02128 if ( $request->getCheck( $this->mName ) ) { 02129 02130 $list = $request->getText( $this->mName ); 02131 $text = $request->getText( $this->mName . '-other' ); 02132 02133 if ( $list == 'other' ) { 02134 $final = $text; 02135 } elseif ( !in_array( $list, $this->mFlatOptions ) ) { 02136 # User has spoofed the select form to give an option which wasn't 02137 # in the original offer. Sulk... 02138 $final = $text; 02139 } elseif ( $text == '' ) { 02140 $final = $list; 02141 } else { 02142 $final = $list . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $text; 02143 } 02144 02145 } else { 02146 $final = $this->getDefault(); 02147 02148 $list = 'other'; 02149 $text = $final; 02150 foreach ( $this->mFlatOptions as $option ) { 02151 $match = $option . $this->msg( 'colon-separator' )->inContentLanguage()->text(); 02152 if ( strpos( $text, $match ) === 0 ) { 02153 $list = $option; 02154 $text = substr( $text, strlen( $match ) ); 02155 break; 02156 } 02157 } 02158 } 02159 return array( $final, $list, $text ); 02160 } 02161 02162 function getSize() { 02163 return isset( $this->mParams['size'] ) 02164 ? $this->mParams['size'] 02165 : 45; 02166 } 02167 02168 function validate( $value, $alldata ) { 02169 # HTMLSelectField forces $value to be one of the options in the select 02170 # field, which is not useful here. But we do want the validation further up 02171 # the chain 02172 $p = parent::validate( $value[1], $alldata ); 02173 02174 if ( $p !== true ) { 02175 return $p; 02176 } 02177 02178 if ( isset( $this->mParams['required'] ) && $this->mParams['required'] !== false && $value[1] === '' ) { 02179 return $this->msg( 'htmlform-required' )->parse(); 02180 } 02181 02182 return true; 02183 } 02184 } 02185 02189 class HTMLRadioField extends HTMLFormField { 02190 02191 02192 function validate( $value, $alldata ) { 02193 $p = parent::validate( $value, $alldata ); 02194 02195 if ( $p !== true ) { 02196 return $p; 02197 } 02198 02199 if ( !is_string( $value ) && !is_int( $value ) ) { 02200 return false; 02201 } 02202 02203 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] ); 02204 02205 if ( in_array( $value, $validOptions ) ) { 02206 return true; 02207 } else { 02208 return $this->msg( 'htmlform-select-badoption' )->parse(); 02209 } 02210 } 02211 02218 function getInputHTML( $value ) { 02219 $html = $this->formatOptions( $this->mParams['options'], $value ); 02220 02221 return $html; 02222 } 02223 02224 function formatOptions( $options, $value ) { 02225 $html = ''; 02226 02227 $attribs = array(); 02228 if ( !empty( $this->mParams['disabled'] ) ) { 02229 $attribs['disabled'] = 'disabled'; 02230 } 02231 02232 # TODO: should this produce an unordered list perhaps? 02233 foreach ( $options as $label => $info ) { 02234 if ( is_array( $info ) ) { 02235 $html .= Html::rawElement( 'h1', array(), $label ) . "\n"; 02236 $html .= $this->formatOptions( $info, $value ); 02237 } else { 02238 $id = Sanitizer::escapeId( $this->mID . "-$info" ); 02239 $radio = Xml::radio( 02240 $this->mName, 02241 $info, 02242 $info == $value, 02243 $attribs + array( 'id' => $id ) 02244 ); 02245 $radio .= ' ' . 02246 Html::rawElement( 'label', array( 'for' => $id ), $label ); 02247 02248 $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $radio ); 02249 } 02250 } 02251 02252 return $html; 02253 } 02254 02255 protected function needsLabel() { 02256 return false; 02257 } 02258 } 02259 02263 class HTMLInfoField extends HTMLFormField { 02264 public function __construct( $info ) { 02265 $info['nodata'] = true; 02266 02267 parent::__construct( $info ); 02268 } 02269 02270 public function getInputHTML( $value ) { 02271 return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value ); 02272 } 02273 02274 public function getTableRow( $value ) { 02275 if ( !empty( $this->mParams['rawrow'] ) ) { 02276 return $value; 02277 } 02278 02279 return parent::getTableRow( $value ); 02280 } 02281 02285 public function getDiv( $value ) { 02286 if ( !empty( $this->mParams['rawrow'] ) ) { 02287 return $value; 02288 } 02289 02290 return parent::getDiv( $value ); 02291 } 02292 02296 public function getRaw( $value ) { 02297 if ( !empty( $this->mParams['rawrow'] ) ) { 02298 return $value; 02299 } 02300 02301 return parent::getRaw( $value ); 02302 } 02303 02304 protected function needsLabel() { 02305 return false; 02306 } 02307 } 02308 02309 class HTMLHiddenField extends HTMLFormField { 02310 public function __construct( $params ) { 02311 parent::__construct( $params ); 02312 02313 # Per HTML5 spec, hidden fields cannot be 'required' 02314 # http://dev.w3.org/html5/spec/states-of-the-type-attribute.html#hidden-state 02315 unset( $this->mParams['required'] ); 02316 } 02317 02318 public function getTableRow( $value ) { 02319 $params = array(); 02320 if ( $this->mID ) { 02321 $params['id'] = $this->mID; 02322 } 02323 02324 $this->mParent->addHiddenField( 02325 $this->mName, 02326 $this->mDefault, 02327 $params 02328 ); 02329 02330 return ''; 02331 } 02332 02336 public function getDiv( $value ) { 02337 return $this->getTableRow( $value ); 02338 } 02339 02343 public function getRaw( $value ) { 02344 return $this->getTableRow( $value ); 02345 } 02346 02347 public function getInputHTML( $value ) { return ''; } 02348 } 02349 02354 class HTMLSubmitField extends HTMLFormField { 02355 02356 public function __construct( $info ) { 02357 $info['nodata'] = true; 02358 parent::__construct( $info ); 02359 } 02360 02361 public function getInputHTML( $value ) { 02362 return Xml::submitButton( 02363 $value, 02364 array( 02365 'class' => 'mw-htmlform-submit ' . $this->mClass, 02366 'name' => $this->mName, 02367 'id' => $this->mID, 02368 ) 02369 ); 02370 } 02371 02372 protected function needsLabel() { 02373 return false; 02374 } 02375 02382 public function validate( $value, $alldata ) { 02383 return true; 02384 } 02385 } 02386 02387 class HTMLEditTools extends HTMLFormField { 02388 public function getInputHTML( $value ) { 02389 return ''; 02390 } 02391 02392 public function getTableRow( $value ) { 02393 $msg = $this->formatMsg(); 02394 02395 return '<tr><td></td><td class="mw-input">' 02396 . '<div class="mw-editTools">' 02397 . $msg->parseAsBlock() 02398 . "</div></td></tr>\n"; 02399 } 02400 02404 public function getDiv( $value ) { 02405 $msg = $this->formatMsg(); 02406 return '<div class="mw-editTools">' . $msg->parseAsBlock() . '</div>'; 02407 } 02408 02412 public function getRaw( $value ) { 02413 return $this->getDiv( $value ); 02414 } 02415 02416 protected function formatMsg() { 02417 if ( empty( $this->mParams['message'] ) ) { 02418 $msg = $this->msg( 'edittools' ); 02419 } else { 02420 $msg = $this->msg( $this->mParams['message'] ); 02421 if ( $msg->isDisabled() ) { 02422 $msg = $this->msg( 'edittools' ); 02423 } 02424 } 02425 $msg->inContentLanguage(); 02426 return $msg; 02427 } 02428 } 02429 02430 class HTMLApiField extends HTMLFormField { 02431 public function getTableRow( $value ) { 02432 return ''; 02433 } 02434 02435 public function getDiv( $value ) { 02436 return $this->getTableRow( $value ); 02437 } 02438 02439 public function getRaw( $value ) { 02440 return $this->getTableRow( $value ); 02441 } 02442 02443 public function getInputHTML( $value ) { 02444 return ''; 02445 } 02446 }