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