MediaWiki  REL1_24
HTMLFormField.php
Go to the documentation of this file.
00001 <?php
00002 
00007 abstract class HTMLFormField {
00008     public $mParams;
00009 
00010     protected $mValidationCallback;
00011     protected $mFilterCallback;
00012     protected $mName;
00013     protected $mLabel; # String label.  Set on construction
00014     protected $mID;
00015     protected $mClass = '';
00016     protected $mHelpClass = false;
00017     protected $mDefault;
00018     protected $mOptions = false;
00019     protected $mOptionsLabelsNotFromMessage = false;
00020     protected $mHideIf = null;
00021 
00026     protected $mShowEmptyLabels = true;
00027 
00031     public $mParent;
00032 
00043     abstract function getInputHTML( $value );
00044 
00055     function msg() {
00056         $args = func_get_args();
00057 
00058         if ( $this->mParent ) {
00059             $callback = array( $this->mParent, 'msg' );
00060         } else {
00061             $callback = 'wfMessage';
00062         }
00063 
00064         return call_user_func_array( $callback, $args );
00065     }
00066 
00067 
00081     protected function getNearestFieldByName( $alldata, $name ) {
00082         $tmp = $this->mName;
00083         $thisKeys = array();
00084         while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) {
00085             array_unshift( $thisKeys, $m[2] );
00086             $tmp = $m[1];
00087         }
00088         if ( substr( $tmp, 0, 2 ) == 'wp' &&
00089             !isset( $alldata[$tmp] ) &&
00090             isset( $alldata[substr( $tmp, 2 )] )
00091         ) {
00092             // Adjust for name mangling.
00093             $tmp = substr( $tmp, 2 );
00094         }
00095         array_unshift( $thisKeys, $tmp );
00096 
00097         $tmp = $name;
00098         $nameKeys = array();
00099         while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) {
00100             array_unshift( $nameKeys, $m[2] );
00101             $tmp = $m[1];
00102         }
00103         array_unshift( $nameKeys, $tmp );
00104 
00105         $testValue = '';
00106         for ( $i = count( $thisKeys ) - 1; $i >= 0; $i-- ) {
00107             $keys = array_merge( array_slice( $thisKeys, 0, $i ), $nameKeys );
00108             $data = $alldata;
00109             while ( $keys ) {
00110                 $key = array_shift( $keys );
00111                 if ( !is_array( $data ) || !isset( $data[$key] ) ) {
00112                     continue 2;
00113                 }
00114                 $data = $data[$key];
00115             }
00116             $testValue = (string)$data;
00117             break;
00118         }
00119 
00120         return $testValue;
00121     }
00122 
00130     protected function isHiddenRecurse( array $alldata, array $params ) {
00131         $origParams = $params;
00132         $op = array_shift( $params );
00133 
00134         try {
00135             switch ( $op ) {
00136                 case 'AND':
00137                     foreach ( $params as $i => $p ) {
00138                         if ( !is_array( $p ) ) {
00139                             throw new MWException(
00140                                 "Expected array, found " . gettype( $p ) . " at index $i"
00141                             );
00142                         }
00143                         if ( !$this->isHiddenRecurse( $alldata, $p ) ) {
00144                             return false;
00145                         }
00146                     }
00147                     return true;
00148 
00149                 case 'OR':
00150                     foreach ( $params as $p ) {
00151                         if ( !is_array( $p ) ) {
00152                             throw new MWException(
00153                                 "Expected array, found " . gettype( $p ) . " at index $i"
00154                             );
00155                         }
00156                         if ( $this->isHiddenRecurse( $alldata, $p ) ) {
00157                             return true;
00158                         }
00159                     }
00160                     return false;
00161 
00162                 case 'NAND':
00163                     foreach ( $params as $i => $p ) {
00164                         if ( !is_array( $p ) ) {
00165                             throw new MWException(
00166                                 "Expected array, found " . gettype( $p ) . " at index $i"
00167                             );
00168                         }
00169                         if ( !$this->isHiddenRecurse( $alldata, $p ) ) {
00170                             return true;
00171                         }
00172                     }
00173                     return false;
00174 
00175                 case 'NOR':
00176                     foreach ( $params as $p ) {
00177                         if ( !is_array( $p ) ) {
00178                             throw new MWException(
00179                                 "Expected array, found " . gettype( $p ) . " at index $i"
00180                             );
00181                         }
00182                         if ( $this->isHiddenRecurse( $alldata, $p ) ) {
00183                             return false;
00184                         }
00185                     }
00186                     return true;
00187 
00188                 case 'NOT':
00189                     if ( count( $params ) !== 1 ) {
00190                         throw new MWException( "NOT takes exactly one parameter" );
00191                     }
00192                     $p = $params[0];
00193                     if ( !is_array( $p ) ) {
00194                         throw new MWException(
00195                             "Expected array, found " . gettype( $p ) . " at index 0"
00196                         );
00197                     }
00198                     return !$this->isHiddenRecurse( $alldata, $p );
00199 
00200                 case '===':
00201                 case '!==':
00202                     if ( count( $params ) !== 2 ) {
00203                         throw new MWException( "$op takes exactly two parameters" );
00204                     }
00205                     list( $field, $value ) = $params;
00206                     if ( !is_string( $field ) || !is_string( $value ) ) {
00207                         throw new MWException( "Parameters for $op must be strings" );
00208                     }
00209                     $testValue = $this->getNearestFieldByName( $alldata, $field );
00210                     switch ( $op ) {
00211                         case '===':
00212                             return ( $value === $testValue );
00213                         case '!==':
00214                             return ( $value !== $testValue );
00215                     }
00216 
00217                 default:
00218                     throw new MWException( "Unknown operation" );
00219             }
00220         } catch ( MWException $ex ) {
00221             throw new MWException(
00222                 "Invalid hide-if specification for $this->mName: " .
00223                 $ex->getMessage() . " in " . var_export( $origParams, true ),
00224                 0, $ex
00225             );
00226         }
00227     }
00228 
00237     function isHidden( $alldata ) {
00238         if ( !$this->mHideIf ) {
00239             return false;
00240         }
00241 
00242         return $this->isHiddenRecurse( $alldata, $this->mHideIf );
00243     }
00244 
00255     function cancelSubmit( $value, $alldata ) {
00256         return false;
00257     }
00258 
00270     function validate( $value, $alldata ) {
00271         if ( $this->isHidden( $alldata ) ) {
00272             return true;
00273         }
00274 
00275         if ( isset( $this->mParams['required'] )
00276             && $this->mParams['required'] !== false
00277             && $value === ''
00278         ) {
00279             return $this->msg( 'htmlform-required' )->parse();
00280         }
00281 
00282         if ( isset( $this->mValidationCallback ) ) {
00283             return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent );
00284         }
00285 
00286         return true;
00287     }
00288 
00289     function filter( $value, $alldata ) {
00290         if ( isset( $this->mFilterCallback ) ) {
00291             $value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent );
00292         }
00293 
00294         return $value;
00295     }
00296 
00303     protected function needsLabel() {
00304         return true;
00305     }
00306 
00316     public function setShowEmptyLabel( $show ) {
00317         $this->mShowEmptyLabels = $show;
00318     }
00319 
00327     function loadDataFromRequest( $request ) {
00328         if ( $request->getCheck( $this->mName ) ) {
00329             return $request->getText( $this->mName );
00330         } else {
00331             return $this->getDefault();
00332         }
00333     }
00334 
00343     function __construct( $params ) {
00344         $this->mParams = $params;
00345 
00346         # Generate the label from a message, if possible
00347         if ( isset( $params['label-message'] ) ) {
00348             $msgInfo = $params['label-message'];
00349 
00350             if ( is_array( $msgInfo ) ) {
00351                 $msg = array_shift( $msgInfo );
00352             } else {
00353                 $msg = $msgInfo;
00354                 $msgInfo = array();
00355             }
00356 
00357             $this->mLabel = wfMessage( $msg, $msgInfo )->parse();
00358         } elseif ( isset( $params['label'] ) ) {
00359             if ( $params['label'] === '&#160;' ) {
00360                 // Apparently some things set &nbsp directly and in an odd format
00361                 $this->mLabel = '&#160;';
00362             } else {
00363                 $this->mLabel = htmlspecialchars( $params['label'] );
00364             }
00365         } elseif ( isset( $params['label-raw'] ) ) {
00366             $this->mLabel = $params['label-raw'];
00367         }
00368 
00369         $this->mName = "wp{$params['fieldname']}";
00370         if ( isset( $params['name'] ) ) {
00371             $this->mName = $params['name'];
00372         }
00373 
00374         $validName = Sanitizer::escapeId( $this->mName );
00375         $validName = str_replace( array( '.5B', '.5D' ), array( '[', ']' ), $validName );
00376         if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
00377             throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
00378         }
00379 
00380         $this->mID = "mw-input-{$this->mName}";
00381 
00382         if ( isset( $params['default'] ) ) {
00383             $this->mDefault = $params['default'];
00384         }
00385 
00386         if ( isset( $params['id'] ) ) {
00387             $id = $params['id'];
00388             $validId = Sanitizer::escapeId( $id );
00389 
00390             if ( $id != $validId ) {
00391                 throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
00392             }
00393 
00394             $this->mID = $id;
00395         }
00396 
00397         if ( isset( $params['cssclass'] ) ) {
00398             $this->mClass = $params['cssclass'];
00399         }
00400 
00401         if ( isset( $params['csshelpclass'] ) ) {
00402             $this->mHelpClass = $params['csshelpclass'];
00403         }
00404 
00405         if ( isset( $params['validation-callback'] ) ) {
00406             $this->mValidationCallback = $params['validation-callback'];
00407         }
00408 
00409         if ( isset( $params['filter-callback'] ) ) {
00410             $this->mFilterCallback = $params['filter-callback'];
00411         }
00412 
00413         if ( isset( $params['flatlist'] ) ) {
00414             $this->mClass .= ' mw-htmlform-flatlist';
00415         }
00416 
00417         if ( isset( $params['hidelabel'] ) ) {
00418             $this->mShowEmptyLabels = false;
00419         }
00420 
00421         if ( isset( $params['hide-if'] ) ) {
00422             $this->mHideIf = $params['hide-if'];
00423         }
00424     }
00425 
00434     function getTableRow( $value ) {
00435         list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
00436         $inputHtml = $this->getInputHTML( $value );
00437         $fieldType = get_class( $this );
00438         $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
00439         $cellAttributes = array();
00440         $rowAttributes = array();
00441         $rowClasses = '';
00442 
00443         if ( !empty( $this->mParams['vertical-label'] ) ) {
00444             $cellAttributes['colspan'] = 2;
00445             $verticalLabel = true;
00446         } else {
00447             $verticalLabel = false;
00448         }
00449 
00450         $label = $this->getLabelHtml( $cellAttributes );
00451 
00452         $field = Html::rawElement(
00453             'td',
00454             array( 'class' => 'mw-input' ) + $cellAttributes,
00455             $inputHtml . "\n$errors"
00456         );
00457 
00458         if ( $this->mHideIf ) {
00459             $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
00460             $rowClasses .= ' mw-htmlform-hide-if';
00461         }
00462 
00463         if ( $verticalLabel ) {
00464             $html = Html::rawElement( 'tr',
00465                 $rowAttributes + array( 'class' => "mw-htmlform-vertical-label $rowClasses" ), $label );
00466             $html .= Html::rawElement( 'tr',
00467                 $rowAttributes + array(
00468                     'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
00469                 ),
00470                 $field );
00471         } else {
00472             $html =
00473                 Html::rawElement( 'tr',
00474                     $rowAttributes + array(
00475                         'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
00476                     ),
00477                     $label . $field );
00478         }
00479 
00480         return $html . $helptext;
00481     }
00482 
00492     public function getDiv( $value ) {
00493         list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
00494         $inputHtml = $this->getInputHTML( $value );
00495         $fieldType = get_class( $this );
00496         $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
00497         $cellAttributes = array();
00498         $label = $this->getLabelHtml( $cellAttributes );
00499 
00500         $outerDivClass = array(
00501             'mw-input',
00502             'mw-htmlform-nolabel' => ( $label === '' )
00503         );
00504 
00505         $field = Html::rawElement(
00506             'div',
00507             array( 'class' => $outerDivClass ) + $cellAttributes,
00508             $inputHtml . "\n$errors"
00509         );
00510         $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass );
00511         if ( $this->mParent->isVForm() ) {
00512             $divCssClasses[] = 'mw-ui-vform-field';
00513         }
00514 
00515         $wrapperAttributes = array(
00516             'class' => $divCssClasses,
00517         );
00518         if ( $this->mHideIf ) {
00519             $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
00520             $wrapperAttributes['class'][] = ' mw-htmlform-hide-if';
00521         }
00522         $html = Html::rawElement( 'div', $wrapperAttributes, $label . $field );
00523         $html .= $helptext;
00524 
00525         return $html;
00526     }
00527 
00537     public function getRaw( $value ) {
00538         list( $errors, ) = $this->getErrorsAndErrorClass( $value );
00539         $inputHtml = $this->getInputHTML( $value );
00540         $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
00541         $cellAttributes = array();
00542         $label = $this->getLabelHtml( $cellAttributes );
00543 
00544         $html = "\n$errors";
00545         $html .= $label;
00546         $html .= $inputHtml;
00547         $html .= $helptext;
00548 
00549         return $html;
00550     }
00551 
00559     public function getHelpTextHtmlTable( $helptext ) {
00560         if ( is_null( $helptext ) ) {
00561             return '';
00562         }
00563 
00564         $rowAttributes = array();
00565         if ( $this->mHideIf ) {
00566             $rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
00567             $rowAttributes['class'] = 'mw-htmlform-hide-if';
00568         }
00569 
00570         $tdClasses = array( 'htmlform-tip' );
00571         if ( $this->mHelpClass !== false ) {
00572             $tdClasses[] = $this->mHelpClass;
00573         }
00574         $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => $tdClasses ), $helptext );
00575         $row = Html::rawElement( 'tr', $rowAttributes, $row );
00576 
00577         return $row;
00578     }
00579 
00588     public function getHelpTextHtmlDiv( $helptext ) {
00589         if ( is_null( $helptext ) ) {
00590             return '';
00591         }
00592 
00593         $wrapperAttributes = array(
00594             'class' => 'htmlform-tip',
00595         );
00596         if ( $this->mHelpClass !== false ) {
00597             $wrapperAttributes['class'] .= " {$this->mHelpClass}";
00598         }
00599         if ( $this->mHideIf ) {
00600             $wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
00601             $wrapperAttributes['class'] .= ' mw-htmlform-hide-if';
00602         }
00603         $div = Html::rawElement( 'div', $wrapperAttributes, $helptext );
00604 
00605         return $div;
00606     }
00607 
00615     public function getHelpTextHtmlRaw( $helptext ) {
00616         return $this->getHelpTextHtmlDiv( $helptext );
00617     }
00618 
00624     public function getHelpText() {
00625         $helptext = null;
00626 
00627         if ( isset( $this->mParams['help-message'] ) ) {
00628             $this->mParams['help-messages'] = array( $this->mParams['help-message'] );
00629         }
00630 
00631         if ( isset( $this->mParams['help-messages'] ) ) {
00632             foreach ( $this->mParams['help-messages'] as $name ) {
00633                 $helpMessage = (array)$name;
00634                 $msg = $this->msg( array_shift( $helpMessage ), $helpMessage );
00635 
00636                 if ( $msg->exists() ) {
00637                     if ( is_null( $helptext ) ) {
00638                         $helptext = '';
00639                     } else {
00640                         $helptext .= $this->msg( 'word-separator' )->escaped(); // some space
00641                     }
00642                     $helptext .= $msg->parse(); // Append message
00643                 }
00644             }
00645         } elseif ( isset( $this->mParams['help'] ) ) {
00646             $helptext = $this->mParams['help'];
00647         }
00648 
00649         return $helptext;
00650     }
00651 
00659     public function getErrorsAndErrorClass( $value ) {
00660         $errors = $this->validate( $value, $this->mParent->mFieldData );
00661 
00662         if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
00663             $errors = '';
00664             $errorClass = '';
00665         } else {
00666             $errors = self::formatErrors( $errors );
00667             $errorClass = 'mw-htmlform-invalid-input';
00668         }
00669 
00670         return array( $errors, $errorClass );
00671     }
00672 
00673     function getLabel() {
00674         return is_null( $this->mLabel ) ? '' : $this->mLabel;
00675     }
00676 
00677     function getLabelHtml( $cellAttributes = array() ) {
00678         # Don't output a for= attribute for labels with no associated input.
00679         # Kind of hacky here, possibly we don't want these to be <label>s at all.
00680         $for = array();
00681 
00682         if ( $this->needsLabel() ) {
00683             $for['for'] = $this->mID;
00684         }
00685 
00686         $labelValue = trim( $this->getLabel() );
00687         $hasLabel = false;
00688         if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
00689             $hasLabel = true;
00690         }
00691 
00692         $displayFormat = $this->mParent->getDisplayFormat();
00693         $html = '';
00694 
00695         if ( $displayFormat === 'table' ) {
00696             $html =
00697                 Html::rawElement( 'td',
00698                     array( 'class' => 'mw-label' ) + $cellAttributes,
00699                     Html::rawElement( 'label', $for, $labelValue ) );
00700         } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
00701             if ( $displayFormat === 'div' ) {
00702                 $html =
00703                     Html::rawElement( 'div',
00704                         array( 'class' => 'mw-label' ) + $cellAttributes,
00705                         Html::rawElement( 'label', $for, $labelValue ) );
00706             } else {
00707                 $html = Html::rawElement( 'label', $for, $labelValue );
00708             }
00709         }
00710 
00711         return $html;
00712     }
00713 
00714     function getDefault() {
00715         if ( isset( $this->mDefault ) ) {
00716             return $this->mDefault;
00717         } else {
00718             return null;
00719         }
00720     }
00721 
00727     public function getTooltipAndAccessKey() {
00728         if ( empty( $this->mParams['tooltip'] ) ) {
00729             return array();
00730         }
00731 
00732         return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
00733     }
00734 
00741     public function getAttributes( array $list ) {
00742         static $boolAttribs = array( 'disabled', 'required', 'autofocus', 'multiple', 'readonly' );
00743 
00744         $ret = array();
00745 
00746         foreach ( $list as $key ) {
00747             if ( in_array( $key, $boolAttribs ) ) {
00748                 if ( !empty( $this->mParams[$key] ) ) {
00749                     $ret[$key] = '';
00750                 }
00751             } elseif ( isset( $this->mParams[$key] ) ) {
00752                 $ret[$key] = $this->mParams[$key];
00753             }
00754         }
00755 
00756         return $ret;
00757     }
00758 
00766     private function lookupOptionsKeys( $options ) {
00767         $ret = array();
00768         foreach ( $options as $key => $value ) {
00769             $key = $this->msg( $key )->plain();
00770             $ret[$key] = is_array( $value )
00771                 ? $this->lookupOptionsKeys( $value )
00772                 : strval( $value );
00773         }
00774         return $ret;
00775     }
00776 
00784     static function forceToStringRecursive( $array ) {
00785         if ( is_array( $array ) ) {
00786             return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array );
00787         } else {
00788             return strval( $array );
00789         }
00790     }
00791 
00798     public function getOptions() {
00799         if ( $this->mOptions === false ) {
00800             if ( array_key_exists( 'options-messages', $this->mParams ) ) {
00801                 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'] );
00802             } elseif ( array_key_exists( 'options', $this->mParams ) ) {
00803                 $this->mOptionsLabelsNotFromMessage = true;
00804                 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
00805             } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
00807                 $message = $this->msg( $this->mParams['options-message'] )->inContentLanguage()->plain();
00808 
00809                 $optgroup = false;
00810                 $this->mOptions = array();
00811                 foreach ( explode( "\n", $message ) as $option ) {
00812                     $value = trim( $option );
00813                     if ( $value == '' ) {
00814                         continue;
00815                     } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
00816                         # A new group is starting...
00817                         $value = trim( substr( $value, 1 ) );
00818                         $optgroup = $value;
00819                     } elseif ( substr( $value, 0, 2 ) == '**' ) {
00820                         # groupmember
00821                         $opt = trim( substr( $value, 2 ) );
00822                         if ( $optgroup === false ) {
00823                             $this->mOptions[$opt] = $opt;
00824                         } else {
00825                             $this->mOptions[$optgroup][$opt] = $opt;
00826                         }
00827                     } else {
00828                         # groupless reason list
00829                         $optgroup = false;
00830                         $this->mOptions[$option] = $option;
00831                     }
00832                 }
00833             } else {
00834                 $this->mOptions = null;
00835             }
00836         }
00837 
00838         return $this->mOptions;
00839     }
00840 
00848     public static function flattenOptions( $options ) {
00849         $flatOpts = array();
00850 
00851         foreach ( $options as $value ) {
00852             if ( is_array( $value ) ) {
00853                 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
00854             } else {
00855                 $flatOpts[] = $value;
00856             }
00857         }
00858 
00859         return $flatOpts;
00860     }
00861 
00869     protected static function formatErrors( $errors ) {
00870         if ( is_array( $errors ) && count( $errors ) === 1 ) {
00871             $errors = array_shift( $errors );
00872         }
00873 
00874         if ( is_array( $errors ) ) {
00875             $lines = array();
00876             foreach ( $errors as $error ) {
00877                 if ( $error instanceof Message ) {
00878                     $lines[] = Html::rawElement( 'li', array(), $error->parse() );
00879                 } else {
00880                     $lines[] = Html::rawElement( 'li', array(), $error );
00881                 }
00882             }
00883 
00884             return Html::rawElement( 'ul', array( 'class' => 'error' ), implode( "\n", $lines ) );
00885         } else {
00886             if ( $errors instanceof Message ) {
00887                 $errors = $errors->parse();
00888             }
00889 
00890             return Html::rawElement( 'span', array( 'class' => 'error' ), $errors );
00891         }
00892     }
00893 }