array( 'section' => 'section/subsection', properties... ), ... ) */ static $typeMappings = array( 'text' => 'HTMLTextField', 'select' => 'HTMLSelectField', 'radio' => 'HTMLRadioField', 'multiselect' => 'HTMLMultiSelectField', 'check' => 'HTMLCheckField', 'toggle' => 'HTMLCheckField', 'int' => 'HTMLIntField', 'float' => 'HTMLFloatField', 'info' => 'HTMLInfoField', 'selectorother' => 'HTMLSelectOrOtherField', # HTMLTextField will output the correct type="" attribute automagically. # There are about four zillion other HTML 5 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', ); function __construct( $descriptor, $messagePrefix ) { $this->mMessagePrefix = $messagePrefix; // Expand out into a tree. $loadedDescriptor = array(); $this->mFlatFields = array(); foreach( $descriptor as $fieldname => $info ) { $section = ''; if ( isset( $info['section'] ) ) $section = $info['section']; $info['name'] = $fieldname; $field = $this->loadInputFromParameters( $info ); $field->mParent = $this; $setSection =& $loadedDescriptor; if( $section ) { $sectionParts = explode( '/', $section ); while( count( $sectionParts ) ) { $newName = array_shift( $sectionParts ); if ( !isset( $setSection[$newName] ) ) { $setSection[$newName] = array(); } $setSection =& $setSection[$newName]; } } $setSection[$fieldname] = $field; $this->mFlatFields[$fieldname] = $field; } $this->mFieldTree = $loadedDescriptor; $this->mShowReset = true; } static function addJS() { if( self::$jsAdded ) return; global $wgOut; $wgOut->addScriptClass( 'htmlform' ); } static function loadInputFromParameters( $descriptor ) { if ( isset( $descriptor['class'] ) ) { $class = $descriptor['class']; } elseif ( isset( $descriptor['type'] ) ) { $class = self::$typeMappings[$descriptor['type']]; $descriptor['class'] = $class; } if( !$class ) { throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) ); } $obj = new $class( $descriptor ); return $obj; } function show() { $html = ''; self::addJS(); // Load data from the request. $this->loadData(); // Try a submission global $wgUser, $wgRequest; $editToken = $wgRequest->getVal( 'wpEditToken' ); $result = false; if ( $wgUser->matchEditToken( $editToken ) ) $result = $this->trySubmit(); if( $result === true ) return $result; // Display form. $this->displayForm( $result ); } /** Return values: * TRUE == Successful submission * FALSE == No submission attempted * Anything else == Error to display. */ function trySubmit() { // Check for validation foreach( $this->mFlatFields as $fieldname => $field ) { if ( !empty( $field->mParams['nodata'] ) ) continue; if ( $field->validate( $this->mFieldData[$fieldname], $this->mFieldData ) !== true ) { return isset( $this->mValidationErrorMessage ) ? $this->mValidationErrorMessage : array( 'htmlform-invalid-input' ); } } $callback = $this->mSubmitCallback; $data = $this->filterDataForSubmit( $this->mFieldData ); $res = call_user_func( $callback, $data ); return $res; } function setSubmitCallback( $cb ) { $this->mSubmitCallback = $cb; } function setValidationErrorMessage( $msg ) { $this->mValidationErrorMessage = $msg; } function setIntro( $msg ) { $this->mIntro = $msg; } function displayForm( $submitResult ) { global $wgOut; if ( $submitResult !== false ) { $this->displayErrors( $submitResult ); } if ( isset( $this->mIntro ) ) { $wgOut->addHTML( $this->mIntro ); } $html = $this->getBody(); // Hidden fields $html .= $this->getHiddenFields(); // Buttons $html .= $this->getButtons(); $html = $this->wrapForm( $html ); $wgOut->addHTML( $html ); } function wrapForm( $html ) { return Html::rawElement( 'form', array( 'action' => $this->getTitle()->getFullURL(), 'method' => 'post', ), $html ); } function getHiddenFields() { global $wgUser; $html = ''; $html .= Html::hidden( 'wpEditToken', $wgUser->editToken() ) . "\n"; $html .= Html::hidden( 'title', $this->getTitle() ) . "\n"; return $html; } function getButtons() { $html = ''; $attribs = array(); if ( isset( $this->mSubmitID ) ) $attribs['id'] = $this->mSubmitID; $attribs['class'] = 'mw-htmlform-submit'; $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; if( $this->mShowReset ) { $html .= Html::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'htmlform-reset' ) ) ) . "\n"; } return $html; } function getBody() { return $this->displaySection( $this->mFieldTree ); } function displayErrors( $errors ) { if ( is_array( $errors ) ) { $errorstr = $this->formatErrors( $errors ); } else { $errorstr = $errors; } $errorstr = Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr ); global $wgOut; $wgOut->addHTML( $errorstr ); } static function formatErrors( $errors ) { $errorstr = ''; foreach ( $errors as $error ) { if( is_array( $error ) ) { $msg = array_shift( $error ); } else { $msg = $error; $error = array(); } $errorstr .= Html::rawElement( 'li', null, wfMsgExt( $msg, array( 'parseinline' ), $error ) ); } $errorstr = Html::rawElement( 'ul', array(), $errorstr ); return $errorstr; } function setSubmitText( $t ) { $this->mSubmitText = $t; } function getSubmitText() { return isset( $this->mSubmitText ) ? $this->mSubmitText : wfMsg( 'htmlform-submit' ); } function setSubmitID( $t ) { $this->mSubmitID = $t; } function setMessagePrefix( $p ) { $this->mMessagePrefix = $p; } function setTitle( $t ) { $this->mTitle = $t; } function getTitle() { return $this->mTitle; } function displaySection( $fields ) { $tableHtml = ''; $subsectionHtml = ''; $hasLeftColumn = false; foreach( $fields as $key => $value ) { if ( is_object( $value ) ) { $v = empty( $value->mParams['nodata'] ) ? $this->mFieldData[$key] : $value->getDefault(); $tableHtml .= $value->getTableRow( $v ); if( $value->getLabel() != ' ' ) $hasLeftColumn = true; } elseif ( is_array( $value ) ) { $section = $this->displaySection( $value ); $legend = wfMsg( "{$this->mMessagePrefix}-$key" ); $subsectionHtml .= Xml::fieldset( $legend, $section ) . "\n"; } } $classes = array(); if( !$hasLeftColumn ) // Avoid strange spacing when no labels exist $classes[] = 'mw-htmlform-nolabel'; $classes = implode( ' ', $classes ); $tableHtml = Html::rawElement( 'table', array( 'class' => $classes ), Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n"; return $subsectionHtml . "\n" . $tableHtml; } function loadData() { global $wgRequest; $fieldData = array(); foreach( $this->mFlatFields as $fieldname => $field ) { if ( !empty( $field->mParams['nodata'] ) ) continue; if ( !empty( $field->mParams['disabled'] ) ) { $fieldData[$fieldname] = $field->getDefault(); } else { $fieldData[$fieldname] = $field->loadDataFromRequest( $wgRequest ); } } // Filter data. foreach( $fieldData as $name => &$value ) { $field = $this->mFlatFields[$name]; $value = $field->filter( $value, $this->mFlatFields ); } $this->mFieldData = $fieldData; } function importData( $fieldData ) { // Filter data. foreach( $fieldData as $name => &$value ) { $field = $this->mFlatFields[$name]; $value = $field->filter( $value, $this->mFlatFields ); } foreach( $this->mFlatFields as $fieldname => $field ) { if ( !isset( $fieldData[$fieldname] ) ) $fieldData[$fieldname] = $field->getDefault(); } $this->mFieldData = $fieldData; } function suppressReset( $suppressReset = true ) { $this->mShowReset = !$suppressReset; } function filterDataForSubmit( $data ) { return $data; } } abstract class HTMLFormField { abstract function getInputHTML( $value ); function validate( $value, $alldata ) { if ( isset( $this->mValidationCallback ) ) { return call_user_func( $this->mValidationCallback, $value, $alldata ); } return true; } function filter( $value, $alldata ) { if( isset( $this->mFilterCallback ) ) { $value = call_user_func( $this->mFilterCallback, $value, $alldata ); } return $value; } /** * Should this field have a label, or is there no input element with the * appropriate id for the label to point to? * * @return bool True to output a label, false to suppress */ protected function needsLabel() { return true; } function loadDataFromRequest( $request ) { if( $request->getCheck( $this->mName ) ) { return $request->getText( $this->mName ); } else { return $this->getDefault(); } } function __construct( $params ) { $this->mParams = $params; if( isset( $params['label-message'] ) ) { $msgInfo = $params['label-message']; if ( is_array( $msgInfo ) ) { $msg = array_shift( $msgInfo ); } else { $msg = $msgInfo; $msgInfo = array(); } $this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo ); } elseif ( isset( $params['label'] ) ) { $this->mLabel = $params['label']; } if ( isset( $params['name'] ) ) { $name = $params['name']; $validName = Sanitizer::escapeId( $name ); if( $name != $validName ) { throw new MWException("Invalid name '$name' passed to " . __METHOD__ ); } $this->mName = 'wp'.$name; $this->mID = 'mw-input-'.$name; } if ( isset( $params['default'] ) ) { $this->mDefault = $params['default']; } if ( isset( $params['id'] ) ) { $id = $params['id']; $validId = Sanitizer::escapeId( $id ); if( $id != $validId ) { throw new MWException("Invalid id '$id' passed to " . __METHOD__ ); } $this->mID = $id; } if ( isset( $params['validation-callback'] ) ) { $this->mValidationCallback = $params['validation-callback']; } if ( isset( $params['filter-callback'] ) ) { $this->mFilterCallback = $params['filter-callback']; } } function getTableRow( $value ) { // Check for invalid data. global $wgRequest; $errors = $this->validate( $value, $this->mParent->mFieldData ); if ( $errors === true || !$wgRequest->wasPosted() ) { $errors = ''; } else { $errors = Html::rawElement( 'span', array( 'class' => 'error' ), $errors ); } $html = ''; # Don't output a for= attribute for labels with no associated input. # Kind of hacky here, possibly we don't want these to be