X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FHTMLForm.php;h=7326bf5c1e045c988a43f691df22ddebc8b39a00;hb=55cb51e4c05b477cda4e3baa1b8351c54239cc9e;hp=636e0d0e4c00be1ab9269564ceccf82b86a86d12;hpb=9610b8d444d135ad2ab1a1c5bd7d6ab69a1b2c36;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 636e0d0e4c..7326bf5c1e 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -1,55 +1,61 @@ $info, * where $info is an Associative Array with any of the following: - * - * 'class' -- the subclass of HTMLFormField that will be used - * to create the object. *NOT* the CSS class! - * 'type' -- roughly translates into the type attribute. + * if 'class' is not specified, this is used as a map + * through HTMLForm::$typeMappings to get the class name. + * 'default' -- default value when the form is displayed + * 'id' -- HTML id attribute + * 'cssclass' -- CSS class + * 'options' -- varies according to the specific object. + * 'label-message' -- message key for a message to use as the label. + * can be an array of msg key and then parameters to + * the message. + * 'label' -- alternatively, a raw text message. Overridden by + * label-message + * 'help-message' -- message key for a message to use as a help text. + * can be an array of msg key and then parameters to + * the message. + * Overwrites 'help-messages'. + * 'help-messages' -- array of message key. As above, each item can + * be an array of msg key and then parameters. + * Overwrites 'help-message'. + * 'required' -- passed through to the object, indicating that it + * is a required field. + * 'size' -- the length of text fields + * 'filter-callback -- a function name to give you the chance to + * massage the inputted value before it's processed. + * @see HTMLForm::filter() + * 'validation-callback' -- a function name to give you the chance + * to impose extra validation on the field input. + * @see HTMLForm::validate() + * 'name' -- By default, the 'name' attribute of the input field + * is "wp{$fieldname}". If you want a different name + * (eg one without the "wp" prefix), specify it here and + * it will be used without modification. + * * TODO: Document 'section' / 'subsection' stuff */ -class HTMLForm { - static $jsAdded = false; +class HTMLForm extends ContextSource { - # A mapping of 'type' inputs onto standard HTMLFormField subclasses + // A mapping of 'type' inputs onto standard HTMLFormField subclasses static $typeMappings = array( 'text' => 'HTMLTextField', 'textarea' => 'HTMLTextAreaField', @@ -62,77 +68,109 @@ class HTMLForm { 'float' => 'HTMLFloatField', 'info' => 'HTMLInfoField', 'selectorother' => 'HTMLSelectOrOtherField', + 'selectandother' => 'HTMLSelectAndOtherField', 'submit' => 'HTMLSubmitField', 'hidden' => 'HTMLHiddenField', 'edittools' => 'HTMLEditTools', - - # HTMLTextField will output the correct type="" attribute automagically. - # There are about four zillion other HTML5 input types, like url, but - # we don't use those at the moment, so no point in adding all of them. + + // HTMLTextField will output the correct type="" attribute automagically. + // There are about four zillion other HTML5 input types, like url, but + // we don't use those at the moment, so no point in adding all of them. 'email' => 'HTMLTextField', 'password' => 'HTMLTextField', ); - + protected $mMessagePrefix; + + /** @var HTMLFormField[] */ protected $mFlatFields; + protected $mFieldTree; protected $mShowReset = false; public $mFieldData; - + protected $mSubmitCallback; protected $mValidationErrorMessage; - + protected $mPre = ''; protected $mHeader = ''; protected $mFooter = ''; + protected $mSectionHeaders = array(); + protected $mSectionFooters = array(); protected $mPost = ''; protected $mId; - + protected $mSubmitID; protected $mSubmitName; protected $mSubmitText; protected $mSubmitTooltip; + protected $mTitle; - + protected $mMethod = 'post'; + + /** + * Form action URL. false means we will use the URL to set Title + * @since 1.19 + * @var false|string + */ + protected $mAction = false; + protected $mUseMultipart = false; protected $mHiddenFields = array(); protected $mButtons = array(); - + protected $mWrapperLegend = false; + + /** + * If true, sections that contain both fields and subsections will + * render their subsections before their fields. + * + * Subclasses may set this to false to render subsections after fields + * instead. + */ + protected $mSubSectionBeforeFields = true; /** * Build a new HTMLForm from an array of field attributes * @param $descriptor Array of Field constructs, as described above + * @param $context IContextSource available since 1.18, will become compulsory in 1.18. + * Obviates the need to call $form->setTitle() * @param $messagePrefix String a prefix to go in front of default messages */ - public function __construct( $descriptor, $messagePrefix='' ) { - $this->mMessagePrefix = $messagePrefix; + public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { + if( $context instanceof IContextSource ){ + $this->setContext( $context ); + $this->mTitle = false; // We don't need them to set a title + $this->mMessagePrefix = $messagePrefix; + } else { + // B/C since 1.18 + if( is_string( $context ) && $messagePrefix === '' ){ + // it's actually $messagePrefix + $this->mMessagePrefix = $context; + } + } // Expand out into a tree. $loadedDescriptor = array(); $this->mFlatFields = array(); - foreach( $descriptor as $fieldname => $info ) { - $section = isset( $info['section'] ) + foreach ( $descriptor as $fieldname => $info ) { + $section = isset( $info['section'] ) ? $info['section'] : ''; - $info['name'] = isset( $info['name'] ) - ? $info['name'] - : $fieldname; - - if ( isset( $info['type'] ) && $info['type'] == 'file' ){ + if ( isset( $info['type'] ) && $info['type'] == 'file' ) { $this->mUseMultipart = true; } - $field = self::loadInputFromParameters( $info ); + $field = self::loadInputFromParameters( $fieldname, $info ); $field->mParent = $this; $setSection =& $loadedDescriptor; - if( $section ) { + if ( $section ) { $sectionParts = explode( '/', $section ); - while( count( $sectionParts ) ) { + while ( count( $sectionParts ) ) { $newName = array_shift( $sectionParts ); if ( !isset( $setSection[$newName] ) ) { @@ -151,73 +189,95 @@ class HTMLForm { } /** - * Add the HTMLForm-specific JavaScript, if it hasn't been + * Add the HTMLForm-specific JavaScript, if it hasn't been * done already. + * @deprecated since 1.18 load modules with ResourceLoader instead */ - static function addJS() { - if( self::$jsAdded ) return; - - global $wgOut, $wgStylePath; - - $wgOut->addScriptFile( "$wgStylePath/common/htmlform.js" ); - } + static function addJS() { wfDeprecated( __METHOD__, '1.18' ); } /** * Initialise a new Object for the field - * @param $descriptor input Descriptor, as described above + * @param $fieldname string + * @param $descriptor string input Descriptor, as described above * @return HTMLFormField subclass */ - static function loadInputFromParameters( $descriptor ) { + static function loadInputFromParameters( $fieldname, $descriptor ) { if ( isset( $descriptor['class'] ) ) { $class = $descriptor['class']; } elseif ( isset( $descriptor['type'] ) ) { $class = self::$typeMappings[$descriptor['type']]; $descriptor['class'] = $class; + } else { + $class = null; } - if( !$class ) { + if ( !$class ) { throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) ); } + $descriptor['fieldname'] = $fieldname; + $obj = new $class( $descriptor ); return $obj; } /** - * The here's-one-I-made-earlier option: do the submission if - * posted, or display the form with or without funky valiation - * errors - * @return Bool whether submission was successful. + * Prepare form for submission */ - function show() { - // Check if we have the info we need - if ( ! $this->mTitle ) { + function prepareForm() { + # Check if we have the info we need + if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) { throw new MWException( "You must call setTitle() on an HTMLForm" ); } - if ( ! $this->mSubmitCallback ) { - throw new MWException( "You must set a submission callback" ); - } - - $html = ''; - - self::addJS(); # Load data from the request. $this->loadData(); + } - # Try a submission - global $wgUser, $wgRequest; - $editToken = $wgRequest->getVal( 'wpEditToken' ); - + /** + * Try submitting, with edit token check first + * @return Status|boolean + */ + function tryAuthorizedSubmit() { $result = false; - if ( $wgUser->matchEditToken( $editToken ) ) + + $submit = false; + if ( $this->getMethod() != 'post' ) { + $submit = true; // no session check needed + } elseif ( $this->getRequest()->wasPosted() ) { + $editToken = $this->getRequest()->getVal( 'wpEditToken' ); + if ( $this->getUser()->isLoggedIn() || $editToken != null ) { + // Session tokens for logged-out users have no security value. + // However, if the user gave one, check it in order to give a nice + // "session expired" error instead of "permission denied" or such. + $submit = $this->getUser()->matchEditToken( $editToken ); + } else { + $submit = true; + } + } + + if ( $submit ) { $result = $this->trySubmit(); + } + + return $result; + } - if( $result === true ) + /** + * The here's-one-I-made-earlier option: do the submission if + * posted, or display the form with or without funky valiation + * errors + * @return Bool or Status whether submission was successful. + */ + function show() { + $this->prepareForm(); + + $result = $this->tryAuthorizedSubmit(); + if ( $result === true || ( $result instanceof Status && $result->isGood() ) ){ return $result; + } - # Display form. $this->displayForm( $result ); return false; } @@ -226,20 +286,20 @@ class HTMLForm { * Validate all the fields, and call the submision callback * function if everything is kosher. * @return Mixed Bool true == Successful submission, Bool false - * == No submission attempted, anything else == Error to + * == No submission attempted, anything else == Error to * display. */ function trySubmit() { # Check for validation - foreach( $this->mFlatFields as $fieldname => $field ) { - if ( !empty( $field->mParams['nodata'] ) ){ + foreach ( $this->mFlatFields as $fieldname => $field ) { + if ( !empty( $field->mParams['nodata'] ) ) { continue; } - if ( $field->validate( + if ( $field->validate( $this->mFieldData[$fieldname], - $this->mFieldData ) - !== true ) - { + $this->mFieldData ) + !== true + ) { return isset( $this->mValidationErrorMessage ) ? $this->mValidationErrorMessage : array( 'htmlform-invalid-input' ); @@ -250,7 +310,7 @@ class HTMLForm { $data = $this->filterDataForSubmit( $this->mFieldData ); - $res = call_user_func( $callback, $data ); + $res = call_user_func( $callback, $data, $this ); return $res; } @@ -268,66 +328,140 @@ class HTMLForm { } /** - * Set a message to display on a validation error. + * Set a message to display on a validation error. * @param $msg Mixed String or Array of valid inputs to wfMsgExt() * (so each entry can be either a String or Array) */ function setValidationErrorMessage( $msg ) { $this->mValidationErrorMessage = $msg; } - + + /** + * Set the introductory message, overwriting any existing message. + * @param $msg String complete text of message to display + */ + function setIntro( $msg ) { + $this->setPreText( $msg ); + } + /** * Set the introductory message, overwriting any existing message. + * @since 1.19 * @param $msg String complete text of message to display */ - function setIntro( $msg ) { $this->mPre = $msg; } + function setPreText( $msg ) { $this->mPre = $msg; } /** * Add introductory text. * @param $msg String complete text of message to display */ function addPreText( $msg ) { $this->mPre .= $msg; } - + /** * Add header text, inside the form. * @param $msg String complete text of message to display + * @param $section string The section to add the header to */ - function addHeaderText( $msg ) { $this->mHeader .= $msg; } - + function addHeaderText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mHeader .= $msg; + } else { + if ( !isset( $this->mSectionHeaders[$section] ) ) { + $this->mSectionHeaders[$section] = ''; + } + $this->mSectionHeaders[$section] .= $msg; + } + } + + /** + * Set header text, inside the form. + * @since 1.19 + * @param $msg String complete text of message to display + * @param $section The section to add the header to + */ + function setHeaderText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mHeader = $msg; + } else { + $this->mSectionHeaders[$section] = $msg; + } + } + /** * Add footer text, inside the form. * @param $msg String complete text of message to display + * @param $section string The section to add the footer text to */ - function addFooterText( $msg ) { $this->mFooter .= $msg; } - + function addFooterText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mFooter .= $msg; + } else { + if ( !isset( $this->mSectionFooters[$section] ) ) { + $this->mSectionFooters[$section] = ''; + } + $this->mSectionFooters[$section] .= $msg; + } + } + + /** + * Set footer text, inside the form. + * @since 1.19 + * @param $msg String complete text of message to display + * @param $section string The section to add the footer text to + */ + function setFooterText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mFooter = $msg; + } else { + $this->mSectionFooters[$section] = $msg; + } + } + /** * Add text to the end of the display. * @param $msg String complete text of message to display */ function addPostText( $msg ) { $this->mPost .= $msg; } - + + /** + * Set text at the end of the display. + * @param $msg String complete text of message to display + */ + function setPostText( $msg ) { $this->mPost = $msg; } + /** * Add a hidden field to the output - * @param $name String field name + * @param $name String field name. This will be used exactly as entered * @param $value String field value * @param $attribs Array */ - public function addHiddenField( $name, $value, $attribs=array() ){ + public function addHiddenField( $name, $value, $attribs = array() ) { $attribs += array( 'name' => $name ); $this->mHiddenFields[] = array( $value, $attribs ); } - - public function addButton( $name, $value, $id=null, $attribs=null ){ + + public function addButton( $name, $value, $id = null, $attribs = null ) { $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' ); } /** - * Display the form (sending to wgOut), with an appropriate error + * Display the form (sending to $wgOut), with an appropriate error * message or stack of messages, and any validation errors, etc. * @param $submitResult Mixed output from HTMLForm::trySubmit() */ function displayForm( $submitResult ) { - global $wgOut; + $this->getOutput()->addHTML( $this->getHTML( $submitResult ) ); + } + + /** + * Returns the raw HTML generated by the form + * @param $submitResult Mixed output from HTMLForm::trySubmit() + * @return string + */ + function getHTML( $submitResult ) { + # For good measure (it is the default) + $this->getOutput()->preventClickjacking(); + $this->getOutput()->addModules( 'mediawiki.htmlform' ); $html = '' . $this->getErrors( $submitResult ) @@ -340,11 +474,7 @@ class HTMLForm { $html = $this->wrapForm( $html ); - $wgOut->addHTML( '' - . $this->mPre - . $html - . $this->mPost - ); + return '' . $this->mPre . $html . $this->mPost; } /** @@ -353,25 +483,26 @@ class HTMLForm { * @return String wrapped HTML. */ function wrapForm( $html ) { - + # Include a
wrapper for style, if requested. - if ( $this->mWrapperLegend !== false ){ + if ( $this->mWrapperLegend !== false ) { $html = Xml::fieldset( $this->mWrapperLegend, $html ); } # Use multipart/form-data - $encType = $this->mUseMultipart + $encType = $this->mUseMultipart ? 'multipart/form-data' : 'application/x-www-form-urlencoded'; # Attributes $attribs = array( - 'action' => $this->getTitle()->getFullURL(), - 'method' => 'post', + 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction, + 'method' => $this->mMethod, 'class' => 'visualClear', - 'enctype' => $encType, + 'enctype' => $encType, ); - if ( !empty( $this->mId ) ) + if ( !empty( $this->mId ) ) { $attribs['id'] = $this->mId; - + } + return Html::rawElement( 'form', $attribs, $html ); } @@ -380,13 +511,19 @@ class HTMLForm { * @return String HTML. */ function getHiddenFields() { - global $wgUser; + global $wgUsePathInfo; + $html = ''; + if( $this->getMethod() == 'post' ){ + $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; + $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; + } - $html .= Html::hidden( 'wpEditToken', $wgUser->editToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; - $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; - - foreach( $this->mHiddenFields as $data ){ + if ( !$wgUsePathInfo && $this->getMethod() == 'get' ) { + $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; + } + + foreach ( $this->mHiddenFields as $data ) { list( $value, $attribs ) = $data; $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n"; } @@ -400,23 +537,25 @@ class HTMLForm { */ function getButtons() { $html = ''; - $attribs = array(); - if ( isset( $this->mSubmitID ) ) + if ( isset( $this->mSubmitID ) ) { $attribs['id'] = $this->mSubmitID; - if ( isset( $this->mSubmitName ) ) + } + + if ( isset( $this->mSubmitName ) ) { $attribs['name'] = $this->mSubmitName; + } + if ( isset( $this->mSubmitTooltip ) ) { - global $wgUser; - $attribs += $wgUser->getSkin()->tooltipAndAccessKeyAttribs( $this->mSubmitTooltip ); + $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); } $attribs['class'] = 'mw-htmlform-submit'; $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; - if( $this->mShowReset ) { + if ( $this->mShowReset ) { $html .= Html::element( 'input', array( @@ -425,25 +564,31 @@ class HTMLForm { ) ) . "\n"; } - - foreach( $this->mButtons as $button ){ + + foreach ( $this->mButtons as $button ) { $attrs = array( 'type' => 'submit', 'name' => $button['name'], 'value' => $button['value'] ); - if ( $button['attribs'] ) - $attrs += $button['attribs']; - if( isset( $button['id'] ) ) + + if ( $button['attribs'] ) { + $attrs += $button['attribs']; + } + + if ( isset( $button['id'] ) ) { $attrs['id'] = $button['id']; + } + $html .= Html::element( 'input', $attrs ); } - + return $html; } /** * Get the whole body of the form. + * @return String */ function getBody() { return $this->displaySection( $this->mFieldTree ); @@ -451,16 +596,22 @@ class HTMLForm { /** * Format and display an error message stack. - * @param $errors Mixed String or Array of message keys + * @param $errors String|Array|Status * @return String */ function getErrors( $errors ) { - if ( is_array( $errors ) ) { + if ( $errors instanceof Status ) { + if ( $errors->isOK() ) { + $errorstr = ''; + } else { + $errorstr = $this->getOutput()->parse( $errors->getWikiText() ); + } + } elseif ( is_array( $errors ) ) { $errorstr = $this->formatErrors( $errors ); } else { $errorstr = $errors; } - + return $errorstr ? Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr ) : ''; @@ -471,18 +622,20 @@ class HTMLForm { * @param $errors Array of message keys/values * @return String HTML, a