wiki.techinc.nl/includes/HTMLForm.php

939 lines
22 KiB
PHP
Raw Normal View History

<?php
class HTMLForm {
static $jsAdded = false;
/* The descriptor is an array of arrays.
i.e. array(
2009-06-21 18:26:29 +00:00
'fieldname' => array( 'section' => 'section/subsection',
properties... ),
...
)
*/
2009-06-21 18:26:29 +00:00
static $typeMappings = array(
'text' => 'HTMLTextField',
'select' => 'HTMLSelectField',
'radio' => 'HTMLRadioField',
'multiselect' => 'HTMLMultiSelectField',
'check' => 'HTMLCheckField',
'toggle' => 'HTMLCheckField',
'int' => 'HTMLIntField',
'float' => 'HTMLFloatField',
2009-06-21 18:26:29 +00:00
'info' => 'HTMLInfoField',
'selectorother' => 'HTMLSelectOrOtherField',
Start using some HTML 5 form features autofocus attribute added in some places; this looks like it's respected by both recent Opera and recent WebKit. Its function is self-explanatory. :) I used this in a few obvious places like Special:UserLogin and Special:ResetPass to focus the first field in the form. Could be used in other places too: Special:Search, etc. required attribute added in some places. This is only supported in recent Opera at the moment. Also self-explanatory: it won't allow form submission if the field is empty. For stuff using HTMLForm (i.e., Special:Preferences), validation will be done for integers and floats. Browsers that support this (recent Opera) will not allow non-integers to be submitted for integer fields, will not allow non-floating-point values to be submitted for float fields, and will enforce any min/max values specified. Opera also gives little up and down arrows to allow the user to increment/decrement the value in addition to letting them edit the field as text. For HTMLForm and account creation, the email input type is used for e-mails. This enforces a sane set of values for e-mails (alphanumerics plus some ASCII punctuation, with an @ in it). Again, this is supported only by recent Opera (yay Opera!). Note that this is actually more restrictive than what we currently check for on the server side; it might be sane to tighten up our server-side checks to forbid e-mail addresses that HTML 5 forbids. In all cases, the extra features aren't added if $wgHtml5 is false, and will be ignored by non-supporting browsers. The major room for further improvement here is use of the pattern attribute. We can have the client refuse to submit the form unless it matches a regex! The HTML 5 spec says that if a title attribute is provided, it should be a message that explains what the valid values are and browsers should provide it to the user if the regex doesn't match, so it's not a usability problem. I didn't bother adding that anywhere at this point because it would require adding new messages, but it should be easy to do. Note of course that HTMLForm should be updated to verify that pattern matches on the server side as well -- this way we have a clean, unified way of ensuring that our client and server checks are the same.
2009-08-07 03:32:20 +00:00
# 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',
2009-09-06 15:41:24 +00:00
'password' => 'HTMLTextField',
2009-06-21 18:26:29 +00:00
);
function __construct( $descriptor, $messagePrefix ) {
$this->mMessagePrefix = $messagePrefix;
2009-06-21 18:26:29 +00:00
// Expand out into a tree.
$loadedDescriptor = array();
$this->mFlatFields = array();
2009-06-21 18:26:29 +00:00
foreach( $descriptor as $fieldname => $info ) {
$section = '';
if ( isset( $info['section'] ) )
$section = $info['section'];
2009-06-21 18:26:29 +00:00
$info['name'] = $fieldname;
2009-06-21 18:26:29 +00:00
$field = $this->loadInputFromParameters( $info );
$field->mParent = $this;
2009-06-21 18:26:29 +00:00
$setSection =& $loadedDescriptor;
2009-06-21 18:26:29 +00:00
if( $section ) {
$sectionParts = explode( '/', $section );
2009-06-21 18:26:29 +00:00
while( count( $sectionParts ) ) {
$newName = array_shift( $sectionParts );
2009-06-21 18:26:29 +00:00
if ( !isset( $setSection[$newName] ) ) {
$setSection[$newName] = array();
}
2009-06-21 18:26:29 +00:00
$setSection =& $setSection[$newName];
}
}
2009-06-21 18:26:29 +00:00
$setSection[$fieldname] = $field;
$this->mFlatFields[$fieldname] = $field;
}
2009-06-21 18:26:29 +00:00
$this->mFieldTree = $loadedDescriptor;
2009-06-21 18:26:29 +00:00
$this->mShowReset = true;
}
2009-06-21 18:26:29 +00:00
static function addJS() {
2009-06-21 18:26:29 +00:00
if( self::$jsAdded ) return;
2009-08-09 16:20:14 +00:00
global $wgOut;
2009-06-21 18:26:29 +00:00
2009-08-09 16:20:14 +00:00
$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;
}
2009-06-21 18:26:29 +00:00
if( !$class ) {
throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) );
}
2009-06-21 18:26:29 +00:00
$obj = new $class( $descriptor );
2009-06-21 18:26:29 +00:00
return $obj;
}
function show() {
$html = '';
2009-06-21 18:26:29 +00:00
self::addJS();
2009-06-21 18:26:29 +00:00
// Load data from the request.
$this->loadData();
2009-06-21 18:26:29 +00:00
// Try a submission
global $wgUser, $wgRequest;
$editToken = $wgRequest->getVal( 'wpEditToken' );
2009-06-21 18:26:29 +00:00
$result = false;
if ( $wgUser->matchEditToken( $editToken ) )
$result = $this->trySubmit();
2009-06-21 18:26:29 +00:00
if( $result === true )
return $result;
2009-06-21 18:26:29 +00:00
// Display form.
$this->displayForm( $result );
}
2009-06-21 18:26:29 +00:00
/** 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 ) {
2009-06-21 18:26:29 +00:00
if ( !empty( $field->mParams['nodata'] ) ) continue;
if ( $field->validate( $this->mFieldData[$fieldname],
$this->mFieldData ) !== true ) {
2009-06-21 18:26:29 +00:00
return isset( $this->mValidationErrorMessage ) ?
$this->mValidationErrorMessage : array( 'htmlform-invalid-input' );
}
}
2009-06-21 18:26:29 +00:00
$callback = $this->mSubmitCallback;
2009-06-21 18:26:29 +00:00
$data = $this->filterDataForSubmit( $this->mFieldData );
2009-06-21 18:26:29 +00:00
$res = call_user_func( $callback, $data );
2009-06-21 18:26:29 +00:00
return $res;
}
2009-06-21 18:26:29 +00:00
function setSubmitCallback( $cb ) {
$this->mSubmitCallback = $cb;
}
2009-06-21 18:26:29 +00:00
function setValidationErrorMessage( $msg ) {
$this->mValidationErrorMessage = $msg;
}
2009-06-21 18:26:29 +00:00
function setIntro( $msg ) {
$this->mIntro = $msg;
}
2009-06-21 18:26:29 +00:00
function displayForm( $submitResult ) {
global $wgOut;
2009-06-21 18:26:29 +00:00
if ( $submitResult !== false ) {
$this->displayErrors( $submitResult );
}
2009-06-21 18:26:29 +00:00
if ( isset( $this->mIntro ) ) {
$wgOut->addHTML( $this->mIntro );
}
2009-06-21 18:26:29 +00:00
$html = $this->getBody();
2009-06-21 18:26:29 +00:00
// Hidden fields
$html .= $this->getHiddenFields();
2009-06-21 18:26:29 +00:00
// Buttons
$html .= $this->getButtons();
2009-06-21 18:26:29 +00:00
$html = $this->wrapForm( $html );
2009-06-21 18:26:29 +00:00
$wgOut->addHTML( $html );
}
2009-06-21 18:26:29 +00:00
function wrapForm( $html ) {
2009-09-07 01:47:45 +00:00
return Html::rawElement(
2009-06-21 18:26:29 +00:00
'form',
array(
'action' => $this->getTitle()->getFullURL(),
'method' => 'post',
),
$html
);
}
2009-06-21 18:26:29 +00:00
function getHiddenFields() {
global $wgUser;
$html = '';
2009-06-21 18:26:29 +00:00
2009-09-07 01:47:45 +00:00
$html .= Html::hidden( 'wpEditToken', $wgUser->editToken() ) . "\n";
$html .= Html::hidden( 'title', $this->getTitle() ) . "\n";
2009-06-21 18:26:29 +00:00
return $html;
}
2009-06-21 18:26:29 +00:00
function getButtons() {
$html = '';
2009-06-21 18:26:29 +00:00
$attribs = array();
2009-06-21 18:26:29 +00:00
if ( isset( $this->mSubmitID ) )
$attribs['id'] = $this->mSubmitID;
2009-06-21 18:26:29 +00:00
$attribs['class'] = 'mw-htmlform-submit';
2009-06-21 18:26:29 +00:00
$html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
2009-06-21 18:26:29 +00:00
if( $this->mShowReset ) {
2009-09-07 01:47:45 +00:00
$html .= Html::element(
2009-06-21 18:26:29 +00:00
'input',
array(
'type' => 'reset',
'value' => wfMsg( 'htmlform-reset' )
)
) . "\n";
}
return $html;
}
2009-06-21 18:26:29 +00:00
function getBody() {
return $this->displaySection( $this->mFieldTree );
}
2009-06-21 18:26:29 +00:00
function displayErrors( $errors ) {
if ( is_array( $errors ) ) {
$errorstr = $this->formatErrors( $errors );
} else {
$errorstr = $errors;
}
2009-09-07 01:47:45 +00:00
$errorstr = Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr );
2009-06-21 18:26:29 +00:00
global $wgOut;
$wgOut->addHTML( $errorstr );
}
2009-06-21 18:26:29 +00:00
static function formatErrors( $errors ) {
$errorstr = '';
foreach ( $errors as $error ) {
2009-06-21 18:26:29 +00:00
if( is_array( $error ) ) {
$msg = array_shift( $error );
} else {
$msg = $error;
$error = array();
}
2009-09-07 01:47:45 +00:00
$errorstr .= Html::rawElement(
2009-06-21 18:26:29 +00:00
'li',
null,
wfMsgExt( $msg, array( 'parseinline' ), $error )
);
}
2009-06-21 18:26:29 +00:00
2009-09-07 01:47:45 +00:00
$errorstr = Html::rawElement( 'ul', array(), $errorstr );
2009-06-21 18:26:29 +00:00
return $errorstr;
}
2009-06-21 18:26:29 +00:00
function setSubmitText( $t ) {
$this->mSubmitText = $t;
}
2009-06-21 18:26:29 +00:00
function getSubmitText() {
2009-06-21 18:26:29 +00:00
return isset( $this->mSubmitText ) ? $this->mSubmitText : wfMsg( 'htmlform-submit' );
}
2009-06-21 18:26:29 +00:00
function setSubmitID( $t ) {
$this->mSubmitID = $t;
}
2009-06-21 18:26:29 +00:00
function setMessagePrefix( $p ) {
$this->mMessagePrefix = $p;
}
2009-06-21 18:26:29 +00:00
function setTitle( $t ) {
$this->mTitle = $t;
}
2009-06-21 18:26:29 +00:00
function getTitle() {
return $this->mTitle;
}
2009-06-21 18:26:29 +00:00
function displaySection( $fields ) {
$tableHtml = '';
$subsectionHtml = '';
$hasLeftColumn = false;
2009-06-21 18:26:29 +00:00
foreach( $fields as $key => $value ) {
if ( is_object( $value ) ) {
2009-06-21 18:26:29 +00:00
$v = empty( $value->mParams['nodata'] )
? $this->mFieldData[$key]
: $value->getDefault();
$tableHtml .= $value->getTableRow( $v );
2009-06-21 18:26:29 +00:00
if( $value->getLabel() != '&nbsp;' )
$hasLeftColumn = true;
} elseif ( is_array( $value ) ) {
$section = $this->displaySection( $value );
$legend = wfMsg( "{$this->mMessagePrefix}-$key" );
$subsectionHtml .= Xml::fieldset( $legend, $section ) . "\n";
}
}
2009-06-21 18:26:29 +00:00
$classes = array();
2009-06-21 18:26:29 +00:00
if( !$hasLeftColumn ) // Avoid strange spacing when no labels exist
$classes[] = 'mw-htmlform-nolabel';
$classes = implode( ' ', $classes );
2009-06-21 18:26:29 +00:00
2009-09-07 15:25:22 +00:00
$tableHtml = Html::rawElement( 'table', array( 'class' => $classes ),
Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n";
2009-06-21 18:26:29 +00:00
return $subsectionHtml . "\n" . $tableHtml;
}
2009-06-21 18:26:29 +00:00
function loadData() {
global $wgRequest;
2009-06-21 18:26:29 +00:00
$fieldData = array();
2009-06-21 18:26:29 +00:00
foreach( $this->mFlatFields as $fieldname => $field ) {
2009-06-21 18:26:29 +00:00
if ( !empty( $field->mParams['nodata'] ) ) continue;
if ( !empty( $field->mParams['disabled'] ) ) {
$fieldData[$fieldname] = $field->getDefault();
} else {
$fieldData[$fieldname] = $field->loadDataFromRequest( $wgRequest );
}
}
2009-06-21 18:26:29 +00:00
// Filter data.
foreach( $fieldData as $name => &$value ) {
$field = $this->mFlatFields[$name];
$value = $field->filter( $value, $this->mFlatFields );
}
2009-06-21 18:26:29 +00:00
$this->mFieldData = $fieldData;
}
2009-06-21 18:26:29 +00:00
function importData( $fieldData ) {
// Filter data.
foreach( $fieldData as $name => &$value ) {
$field = $this->mFlatFields[$name];
$value = $field->filter( $value, $this->mFlatFields );
}
2009-06-21 18:26:29 +00:00
foreach( $this->mFlatFields as $fieldname => $field ) {
2009-06-21 18:26:29 +00:00
if ( !isset( $fieldData[$fieldname] ) )
$fieldData[$fieldname] = $field->getDefault();
}
2009-06-21 18:26:29 +00:00
$this->mFieldData = $fieldData;
}
2009-06-21 18:26:29 +00:00
function suppressReset( $suppressReset = true ) {
$this->mShowReset = !$suppressReset;
}
2009-06-21 18:26:29 +00:00
function filterDataForSubmit( $data ) {
return $data;
}
}
abstract class HTMLFormField {
abstract function getInputHTML( $value );
2009-06-21 18:26:29 +00:00
function validate( $value, $alldata ) {
2009-06-21 18:26:29 +00:00
if ( isset( $this->mValidationCallback ) ) {
return call_user_func( $this->mValidationCallback, $value, $alldata );
}
2009-06-21 18:26:29 +00:00
return true;
}
2009-06-21 18:26:29 +00:00
function filter( $value, $alldata ) {
if( isset( $this->mFilterCallback ) ) {
$value = call_user_func( $this->mFilterCallback, $value, $alldata );
}
2009-06-21 18:26:29 +00:00
return $value;
}
2009-06-21 18:26:29 +00:00
/**
* 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 ) {
2009-06-21 18:26:29 +00:00
if( $request->getCheck( $this->mName ) ) {
return $request->getText( $this->mName );
} else {
return $this->getDefault();
}
}
2009-06-21 18:26:29 +00:00
function __construct( $params ) {
$this->mParams = $params;
2009-06-21 18:26:29 +00:00
if( isset( $params['label-message'] ) ) {
$msgInfo = $params['label-message'];
2009-06-21 18:26:29 +00:00
if ( is_array( $msgInfo ) ) {
$msg = array_shift( $msgInfo );
} else {
$msg = $msgInfo;
$msgInfo = array();
}
2009-06-21 18:26:29 +00:00
$this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo );
2009-06-21 18:26:29 +00:00
} elseif ( isset( $params['label'] ) ) {
$this->mLabel = $params['label'];
}
2009-06-21 18:26:29 +00:00
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;
}
2009-06-21 18:26:29 +00:00
if ( isset( $params['default'] ) ) {
$this->mDefault = $params['default'];
}
2009-06-21 18:26:29 +00:00
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;
}
2009-06-21 18:26:29 +00:00
if ( isset( $params['validation-callback'] ) ) {
$this->mValidationCallback = $params['validation-callback'];
}
2009-06-21 18:26:29 +00:00
if ( isset( $params['filter-callback'] ) ) {
$this->mFilterCallback = $params['filter-callback'];
}
}
2009-06-21 18:26:29 +00:00
function getTableRow( $value ) {
// Check for invalid data.
global $wgRequest;
2009-06-21 18:26:29 +00:00
$errors = $this->validate( $value, $this->mParent->mFieldData );
if ( $errors === true || !$wgRequest->wasPosted() ) {
$errors = '';
} else {
2009-09-07 01:47:45 +00:00
$errors = Html::rawElement( 'span', array( 'class' => 'error' ), $errors );
}
2009-06-21 18:26:29 +00:00
$html = '';
2009-06-21 18:26:29 +00:00
# Don't output a for= attribute for labels with no associated input.
# Kind of hacky here, possibly we don't want these to be <label>s at all.
$for = array();
if ( $this->needsLabel() ) {
$for['for'] = $this->mID;
}
2009-09-07 01:47:45 +00:00
$html .= Html::rawElement( 'td', array( 'class' => 'mw-label' ),
Html::rawElement( 'label', $for, $this->getLabel() )
);
2009-09-07 01:47:45 +00:00
$html .= Html::rawElement( 'td', array( 'class' => 'mw-input' ),
$this->getInputHTML( $value ) ."\n$errors" );
2009-06-21 18:26:29 +00:00
$fieldType = get_class( $this );
2009-09-07 01:47:45 +00:00
$html = Html::rawElement( 'tr', array( 'class' => "mw-htmlform-field-$fieldType" ),
$html ) . "\n";
2009-06-21 18:26:29 +00:00
$helptext = null;
2009-06-21 18:26:29 +00:00
if ( isset( $this->mParams['help-message'] ) ) {
$msg = $this->mParams['help-message'];
$helptext = wfMsgExt( $msg, 'parseinline' );
if ( wfEmptyMsg( $msg, $helptext ) ) {
# Never mind
$helptext = null;
}
} elseif ( isset( $this->mParams['help'] ) ) {
$helptext = $this->mParams['help'];
}
if ( !is_null( $helptext ) ) {
2009-09-07 01:47:45 +00:00
$row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
$helptext );
2009-09-07 01:47:45 +00:00
$row = Html::rawElement( 'tr', array(), $row );
$html .= "$row\n";
}
2009-06-21 18:26:29 +00:00
return $html;
}
2009-06-21 18:26:29 +00:00
function getLabel() {
return $this->mLabel;
}
2009-06-21 18:26:29 +00:00
function getDefault() {
if ( isset( $this->mDefault ) ) {
return $this->mDefault;
} else {
return null;
}
}
2009-06-21 18:26:29 +00:00
static function flattenOptions( $options ) {
$flatOpts = array();
2009-06-21 18:26:29 +00:00
foreach( $options as $key => $value ) {
if ( is_array( $value ) ) {
$flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
} else {
$flatOpts[] = $value;
}
}
2009-06-21 18:26:29 +00:00
return $flatOpts;
}
}
class HTMLTextField extends HTMLFormField {
# Override in derived classes to use other Xml::... functions
protected $mFunction = 'input';
function getSize() {
2009-06-21 18:26:29 +00:00
return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 45;
}
function getInputHTML( $value ) {
Start using some HTML 5 form features autofocus attribute added in some places; this looks like it's respected by both recent Opera and recent WebKit. Its function is self-explanatory. :) I used this in a few obvious places like Special:UserLogin and Special:ResetPass to focus the first field in the form. Could be used in other places too: Special:Search, etc. required attribute added in some places. This is only supported in recent Opera at the moment. Also self-explanatory: it won't allow form submission if the field is empty. For stuff using HTMLForm (i.e., Special:Preferences), validation will be done for integers and floats. Browsers that support this (recent Opera) will not allow non-integers to be submitted for integer fields, will not allow non-floating-point values to be submitted for float fields, and will enforce any min/max values specified. Opera also gives little up and down arrows to allow the user to increment/decrement the value in addition to letting them edit the field as text. For HTMLForm and account creation, the email input type is used for e-mails. This enforces a sane set of values for e-mails (alphanumerics plus some ASCII punctuation, with an @ in it). Again, this is supported only by recent Opera (yay Opera!). Note that this is actually more restrictive than what we currently check for on the server side; it might be sane to tighten up our server-side checks to forbid e-mail addresses that HTML 5 forbids. In all cases, the extra features aren't added if $wgHtml5 is false, and will be ignored by non-supporting browsers. The major room for further improvement here is use of the pattern attribute. We can have the client refuse to submit the form unless it matches a regex! The HTML 5 spec says that if a title attribute is provided, it should be a message that explains what the valid values are and browsers should provide it to the user if the regex doesn't match, so it's not a usability problem. I didn't bother adding that anywhere at this point because it would require adding new messages, but it should be easy to do. Note of course that HTMLForm should be updated to verify that pattern matches on the server side as well -- this way we have a clean, unified way of ensuring that our client and server checks are the same.
2009-08-07 03:32:20 +00:00
global $wgHtml5;
2009-09-06 15:07:29 +00:00
$attribs = array(
'id' => $this->mID,
'name' => $this->mName,
'size' => $this->getSize(),
'value' => $value,
);
2009-06-21 18:26:29 +00:00
if ( isset( $this->mParams['maxlength'] ) ) {
$attribs['maxlength'] = $this->mParams['maxlength'];
}
Start using some HTML 5 form features autofocus attribute added in some places; this looks like it's respected by both recent Opera and recent WebKit. Its function is self-explanatory. :) I used this in a few obvious places like Special:UserLogin and Special:ResetPass to focus the first field in the form. Could be used in other places too: Special:Search, etc. required attribute added in some places. This is only supported in recent Opera at the moment. Also self-explanatory: it won't allow form submission if the field is empty. For stuff using HTMLForm (i.e., Special:Preferences), validation will be done for integers and floats. Browsers that support this (recent Opera) will not allow non-integers to be submitted for integer fields, will not allow non-floating-point values to be submitted for float fields, and will enforce any min/max values specified. Opera also gives little up and down arrows to allow the user to increment/decrement the value in addition to letting them edit the field as text. For HTMLForm and account creation, the email input type is used for e-mails. This enforces a sane set of values for e-mails (alphanumerics plus some ASCII punctuation, with an @ in it). Again, this is supported only by recent Opera (yay Opera!). Note that this is actually more restrictive than what we currently check for on the server side; it might be sane to tighten up our server-side checks to forbid e-mail addresses that HTML 5 forbids. In all cases, the extra features aren't added if $wgHtml5 is false, and will be ignored by non-supporting browsers. The major room for further improvement here is use of the pattern attribute. We can have the client refuse to submit the form unless it matches a regex! The HTML 5 spec says that if a title attribute is provided, it should be a message that explains what the valid values are and browsers should provide it to the user if the regex doesn't match, so it's not a usability problem. I didn't bother adding that anywhere at this point because it would require adding new messages, but it should be easy to do. Note of course that HTMLForm should be updated to verify that pattern matches on the server side as well -- this way we have a clean, unified way of ensuring that our client and server checks are the same.
2009-08-07 03:32:20 +00:00
if ( !empty( $this->mParams['disabled'] ) ) {
$attribs['disabled'] = 'disabled';
}
2009-06-21 18:26:29 +00:00
Start using some HTML 5 form features autofocus attribute added in some places; this looks like it's respected by both recent Opera and recent WebKit. Its function is self-explanatory. :) I used this in a few obvious places like Special:UserLogin and Special:ResetPass to focus the first field in the form. Could be used in other places too: Special:Search, etc. required attribute added in some places. This is only supported in recent Opera at the moment. Also self-explanatory: it won't allow form submission if the field is empty. For stuff using HTMLForm (i.e., Special:Preferences), validation will be done for integers and floats. Browsers that support this (recent Opera) will not allow non-integers to be submitted for integer fields, will not allow non-floating-point values to be submitted for float fields, and will enforce any min/max values specified. Opera also gives little up and down arrows to allow the user to increment/decrement the value in addition to letting them edit the field as text. For HTMLForm and account creation, the email input type is used for e-mails. This enforces a sane set of values for e-mails (alphanumerics plus some ASCII punctuation, with an @ in it). Again, this is supported only by recent Opera (yay Opera!). Note that this is actually more restrictive than what we currently check for on the server side; it might be sane to tighten up our server-side checks to forbid e-mail addresses that HTML 5 forbids. In all cases, the extra features aren't added if $wgHtml5 is false, and will be ignored by non-supporting browsers. The major room for further improvement here is use of the pattern attribute. We can have the client refuse to submit the form unless it matches a regex! The HTML 5 spec says that if a title attribute is provided, it should be a message that explains what the valid values are and browsers should provide it to the user if the regex doesn't match, so it's not a usability problem. I didn't bother adding that anywhere at this point because it would require adding new messages, but it should be easy to do. Note of course that HTMLForm should be updated to verify that pattern matches on the server side as well -- this way we have a clean, unified way of ensuring that our client and server checks are the same.
2009-08-07 03:32:20 +00:00
if ( $wgHtml5 ) {
# TODO: Enforce pattern, step, required, readonly on the server
# side as well
foreach ( array( 'min', 'max', 'pattern', 'title', 'step',
'placeholder' ) as $param ) {
if ( isset( $this->mParams[$param] ) ) {
$attribs[$param] = $this->mParams[$param];
}
}
foreach ( array( 'required', 'autofocus', 'multiple', 'readonly' )
as $param ) {
if ( isset( $this->mParams[$param] ) ) {
$attribs[$param] = '';
}
}
if ( isset( $this->mParams['type'] ) ) {
switch ( $this->mParams['type'] ) {
case 'email':
$attribs['type'] = 'email';
break;
case 'int':
$attribs['type'] = 'number';
break;
case 'float':
$attribs['type'] = 'number';
$attribs['step'] = 'any';
break;
2009-09-06 15:07:29 +00:00
case 'password':
$attribs['type'] = 'password';
break;
Start using some HTML 5 form features autofocus attribute added in some places; this looks like it's respected by both recent Opera and recent WebKit. Its function is self-explanatory. :) I used this in a few obvious places like Special:UserLogin and Special:ResetPass to focus the first field in the form. Could be used in other places too: Special:Search, etc. required attribute added in some places. This is only supported in recent Opera at the moment. Also self-explanatory: it won't allow form submission if the field is empty. For stuff using HTMLForm (i.e., Special:Preferences), validation will be done for integers and floats. Browsers that support this (recent Opera) will not allow non-integers to be submitted for integer fields, will not allow non-floating-point values to be submitted for float fields, and will enforce any min/max values specified. Opera also gives little up and down arrows to allow the user to increment/decrement the value in addition to letting them edit the field as text. For HTMLForm and account creation, the email input type is used for e-mails. This enforces a sane set of values for e-mails (alphanumerics plus some ASCII punctuation, with an @ in it). Again, this is supported only by recent Opera (yay Opera!). Note that this is actually more restrictive than what we currently check for on the server side; it might be sane to tighten up our server-side checks to forbid e-mail addresses that HTML 5 forbids. In all cases, the extra features aren't added if $wgHtml5 is false, and will be ignored by non-supporting browsers. The major room for further improvement here is use of the pattern attribute. We can have the client refuse to submit the form unless it matches a regex! The HTML 5 spec says that if a title attribute is provided, it should be a message that explains what the valid values are and browsers should provide it to the user if the regex doesn't match, so it's not a usability problem. I didn't bother adding that anywhere at this point because it would require adding new messages, but it should be easy to do. Note of course that HTMLForm should be updated to verify that pattern matches on the server side as well -- this way we have a clean, unified way of ensuring that our client and server checks are the same.
2009-08-07 03:32:20 +00:00
}
}
}
2009-09-06 15:07:29 +00:00
return Html::element( 'input', $attribs );
}
}
class HTMLFloatField extends HTMLTextField {
function getSize() {
2009-06-21 18:26:29 +00:00
return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 20;
}
2009-06-21 18:26:29 +00:00
function validate( $value, $alldata ) {
2009-06-21 18:26:29 +00:00
$p = parent::validate( $value, $alldata );
if ( $p !== true ) return $p;
2009-06-21 18:26:29 +00:00
if ( floatval( $value ) != $value ) {
return wfMsgExt( 'htmlform-float-invalid', 'parse' );
}
2009-06-21 18:26:29 +00:00
$in_range = true;
2009-06-21 18:26:29 +00:00
# The "int" part of these message names is rather confusing. They make
# equal sense for all numbers.
2009-06-21 18:26:29 +00:00
if ( isset( $this->mParams['min'] ) ) {
$min = $this->mParams['min'];
if ( $min > $value )
2009-06-21 18:26:29 +00:00
return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) );
}
2009-06-21 18:26:29 +00:00
if ( isset( $this->mParams['max'] ) ) {
$max = $this->mParams['max'];
2009-06-21 18:26:29 +00:00
if( $max < $value )
return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) );
}
2009-06-21 18:26:29 +00:00
return true;
}
}
class HTMLIntField extends HTMLFloatField {
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
if ( $p !== true ) return $p;
if ( intval( $value ) != $value ) {
return wfMsgExt( 'htmlform-int-invalid', 'parse' );
}
return true;
}
}
class HTMLCheckField extends HTMLFormField {
function getInputHTML( $value ) {
if ( !empty( $this->mParams['invert'] ) )
$value = !$value;
2009-06-21 18:26:29 +00:00
$attr = array( 'id' => $this->mID );
2009-06-21 18:26:29 +00:00
if( !empty( $this->mParams['disabled'] ) ) {
$attr['disabled'] = 'disabled';
}
2009-06-21 18:26:29 +00:00
return Xml::check( $this->mName, $value, $attr ) . '&nbsp;' .
2009-09-07 01:47:45 +00:00
Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
}
2009-06-21 18:26:29 +00:00
function getLabel() {
return '&nbsp;'; // In the right-hand column.
}
2009-06-21 18:26:29 +00:00
function loadDataFromRequest( $request ) {
$invert = false;
if ( isset( $this->mParams['invert'] ) && $this->mParams['invert'] ) {
$invert = true;
}
2009-06-21 18:26:29 +00:00
// GetCheck won't work like we want for checks.
2009-06-21 18:26:29 +00:00
if( $request->getCheck( 'wpEditToken' ) ) {
// XOR has the following truth table, which is what we want
// INVERT VALUE | OUTPUT
// true true | false
// false true | true
// false false | false
// true false | true
return $request->getBool( $this->mName ) xor $invert;
} else {
return $this->getDefault();
}
}
}
class HTMLSelectField extends HTMLFormField {
2009-06-21 18:26:29 +00:00
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
2009-06-21 18:26:29 +00:00
if( $p !== true ) return $p;
$validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
if ( in_array( $value, $validOptions ) )
return true;
else
return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
}
2009-06-21 18:26:29 +00:00
function getInputHTML( $value ) {
$select = new XmlSelect( $this->mName, $this->mID, strval( $value ) );
// If one of the options' 'name' is int(0), it is automatically selected.
// because PHP sucks and things int(0) == 'some string'.
// Working around this by forcing all of them to strings.
$options = array_map( 'strval', $this->mParams['options'] );
2009-06-21 18:26:29 +00:00
if( !empty( $this->mParams['disabled'] ) ) {
$select->setAttribute( 'disabled', 'disabled' );
}
2009-06-21 18:26:29 +00:00
$select->addOptions( $options );
2009-06-21 18:26:29 +00:00
return $select->getHTML();
}
}
class HTMLSelectOrOtherField extends HTMLTextField {
static $jsAdded = false;
2009-06-21 18:26:29 +00:00
function __construct( $params ) {
if( !in_array( 'other', $params['options'] ) ) {
$params['options'][wfMsg( 'htmlform-selectorother-other' )] = 'other';
}
2009-06-21 18:26:29 +00:00
parent::__construct( $params );
}
static function forceToStringRecursive( $array ) {
if ( is_array($array) ) {
return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array);
} else {
return strval($array);
}
}
2009-06-21 18:26:29 +00:00
function getInputHTML( $value ) {
$valInSelect = false;
2009-06-21 18:26:29 +00:00
if( $value !== false )
$valInSelect = in_array( $value,
2009-06-21 18:26:29 +00:00
HTMLFormField::flattenOptions( $this->mParams['options'] ) );
$selected = $valInSelect ? $value : 'other';
$opts = self::forceToStringRecursive( $this->mParams['options'] );
2009-06-21 18:26:29 +00:00
$select = new XmlSelect( $this->mName, $this->mID, $selected );
$select->addOptions( $opts );
2009-06-21 18:26:29 +00:00
$select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
2009-06-21 18:26:29 +00:00
$tbAttribs = array( 'id' => $this->mID . '-other' );
if( !empty( $this->mParams['disabled'] ) ) {
$select->setAttribute( 'disabled', 'disabled' );
$tbAttribs['disabled'] = 'disabled';
}
2009-06-21 18:26:29 +00:00
$select = $select->getHTML();
2009-06-21 18:26:29 +00:00
if ( isset( $this->mParams['maxlength'] ) ) {
$tbAttribs['maxlength'] = $this->mParams['maxlength'];
}
2009-06-21 18:26:29 +00:00
2009-09-07 01:47:45 +00:00
$textbox = Html::input( $this->mName . '-other',
$this->getSize(),
2009-09-07 01:47:45 +00:00
$valInSelect ? 'text' : $value,
$tbAttribs );
2009-06-21 18:26:29 +00:00
return "$select<br/>\n$textbox";
}
2009-06-21 18:26:29 +00:00
function loadDataFromRequest( $request ) {
2009-06-21 18:26:29 +00:00
if( $request->getCheck( $this->mName ) ) {
$val = $request->getText( $this->mName );
2009-06-21 18:26:29 +00:00
if( $val == 'other' ) {
$val = $request->getText( $this->mName . '-other' );
}
2009-06-21 18:26:29 +00:00
return $val;
} else {
return $this->getDefault();
}
}
}
class HTMLMultiSelectField extends HTMLFormField {
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
2009-06-21 18:26:29 +00:00
if( $p !== true ) return $p;
if( !is_array( $value ) ) return false;
// If all options are valid, array_intersect of the valid options and the provided
// options will return the provided options.
$validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
2009-06-21 18:26:29 +00:00
$validValues = array_intersect( $value, $validOptions );
2009-06-21 18:26:29 +00:00
if ( count( $validValues ) == count( $value ) )
return true;
else
return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
}
2009-06-21 18:26:29 +00:00
function getInputHTML( $value ) {
$html = $this->formatOptions( $this->mParams['options'], $value );
2009-06-21 18:26:29 +00:00
return $html;
}
2009-06-21 18:26:29 +00:00
function formatOptions( $options, $value ) {
$html = '';
2009-06-21 18:26:29 +00:00
$attribs = array();
if ( !empty( $this->mParams['disabled'] ) ) {
$attribs['disabled'] = 'disabled';
}
2009-06-21 18:26:29 +00:00
foreach( $options as $label => $info ) {
2009-06-21 18:26:29 +00:00
if( is_array( $info ) ) {
2009-09-07 01:47:45 +00:00
$html .= Html::rawElement( 'h1', array(), $label ) . "\n";
$html .= $this->formatOptions( $info, $value );
} else {
2009-06-21 18:26:29 +00:00
$thisAttribs = array( 'id' => $this->mID . "-$info", 'value' => $info );
2009-06-21 18:26:29 +00:00
$checkbox = Xml::check( $this->mName . '[]', in_array( $info, $value ),
$attribs + $thisAttribs );
2009-09-07 01:47:45 +00:00
$checkbox .= '&nbsp;' . Html::rawElement( 'label', array( 'for' => $this->mID . "-$info" ), $label );
2009-06-21 18:26:29 +00:00
$html .= $checkbox . '<br />';
}
}
2009-06-21 18:26:29 +00:00
return $html;
}
2009-06-21 18:26:29 +00:00
function loadDataFromRequest( $request ) {
// won't work with getCheck
2009-06-21 18:26:29 +00:00
if( $request->getCheck( 'wpEditToken' ) ) {
$arr = $request->getArray( $this->mName );
2009-06-21 18:26:29 +00:00
if( !$arr )
$arr = array();
2009-06-21 18:26:29 +00:00
return $arr;
} else {
return $this->getDefault();
}
}
2009-06-21 18:26:29 +00:00
function getDefault() {
if ( isset( $this->mDefault ) ) {
return $this->mDefault;
} else {
return array();
}
}
protected function needsLabel() {
return false;
}
}
class HTMLRadioField extends HTMLFormField {
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
2009-06-21 18:26:29 +00:00
if( $p !== true ) return $p;
if( !is_string( $value ) && !is_int( $value ) )
return false;
2009-06-21 18:26:29 +00:00
$validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
2009-06-21 18:26:29 +00:00
if ( in_array( $value, $validOptions ) )
return true;
else
return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
}
2009-06-21 18:26:29 +00:00
function getInputHTML( $value ) {
$html = $this->formatOptions( $this->mParams['options'], $value );
2009-06-21 18:26:29 +00:00
return $html;
}
function formatOptions( $options, $value ) {
$html = '';
2009-06-21 18:26:29 +00:00
$attribs = array();
if ( !empty( $this->mParams['disabled'] ) ) {
$attribs['disabled'] = 'disabled';
}
2009-06-21 18:26:29 +00:00
foreach( $options as $label => $info ) {
2009-06-21 18:26:29 +00:00
if( is_array( $info ) ) {
2009-09-07 01:47:45 +00:00
$html .= Html::rawElement( 'h1', array(), $label ) . "\n";
$html .= $this->formatOptions( $info, $value );
} else {
$id = Sanitizer::escapeId( $this->mID . "-$info" );
$html .= Xml::radio( $this->mName, $info, $info == $value,
$attribs + array( 'id' => $id ) );
$html .= '&nbsp;' .
2009-09-07 01:47:45 +00:00
Html::rawElement( 'label', array( 'for' => $id ), $label );
2009-06-21 18:26:29 +00:00
$html .= "<br/>\n";
}
}
2009-06-21 18:26:29 +00:00
return $html;
}
protected function needsLabel() {
return false;
}
}
class HTMLInfoField extends HTMLFormField {
function __construct( $info ) {
$info['nodata'] = true;
2009-06-21 18:26:29 +00:00
parent::__construct( $info );
}
2009-06-21 18:26:29 +00:00
function getInputHTML( $value ) {
2009-06-21 18:26:29 +00:00
return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value );
}
2009-06-21 18:26:29 +00:00
function getTableRow( $value ) {
2009-06-21 18:26:29 +00:00
if ( !empty( $this->mParams['rawrow'] ) ) {
return $value;
}
2009-06-21 18:26:29 +00:00
return parent::getTableRow( $value );
}
protected function needsLabel() {
return false;
}
}