Merge "(bug 56184) Allow 3-way merge from arbitrary revisions"
[lhc/web/wiklou.git] / includes / htmlform / HTMLFormField.php
1 <?php
2
3 /**
4 * The parent class to generate form fields. Any field type should
5 * be a subclass of this.
6 */
7 abstract class HTMLFormField {
8 public $mParams;
9
10 protected $mValidationCallback;
11 protected $mFilterCallback;
12 protected $mName;
13 protected $mLabel; # String label. Set on construction
14 protected $mID;
15 protected $mClass = '';
16 protected $mDefault;
17
18 /**
19 * @var bool If true will generate an empty div element with no label
20 * @since 1.22
21 */
22 protected $mShowEmptyLabels = true;
23
24 /**
25 * @var HTMLForm
26 */
27 public $mParent;
28
29 /**
30 * This function must be implemented to return the HTML to generate
31 * the input object itself. It should not implement the surrounding
32 * table cells/rows, or labels/help messages.
33 *
34 * @param string $value the value to set the input to; eg a default
35 * text for a text input.
36 *
37 * @return string Valid HTML.
38 */
39 abstract function getInputHTML( $value );
40
41 /**
42 * Get a translated interface message
43 *
44 * This is a wrapper around $this->mParent->msg() if $this->mParent is set
45 * and wfMessage() otherwise.
46 *
47 * Parameters are the same as wfMessage().
48 *
49 * @return Message object
50 */
51 function msg() {
52 $args = func_get_args();
53
54 if ( $this->mParent ) {
55 $callback = array( $this->mParent, 'msg' );
56 } else {
57 $callback = 'wfMessage';
58 }
59
60 return call_user_func_array( $callback, $args );
61 }
62
63 /**
64 * Override this function to add specific validation checks on the
65 * field input. Don't forget to call parent::validate() to ensure
66 * that the user-defined callback mValidationCallback is still run
67 *
68 * @param string $value The value the field was submitted with
69 * @param array $alldata The data collected from the form
70 *
71 * @return Mixed Bool true on success, or String error to display.
72 */
73 function validate( $value, $alldata ) {
74 if ( isset( $this->mParams['required'] )
75 && $this->mParams['required'] !== false
76 && $value === ''
77 ) {
78 return $this->msg( 'htmlform-required' )->parse();
79 }
80
81 if ( isset( $this->mValidationCallback ) ) {
82 return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent );
83 }
84
85 return true;
86 }
87
88 function filter( $value, $alldata ) {
89 if ( isset( $this->mFilterCallback ) ) {
90 $value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent );
91 }
92
93 return $value;
94 }
95
96 /**
97 * Should this field have a label, or is there no input element with the
98 * appropriate id for the label to point to?
99 *
100 * @return bool True to output a label, false to suppress
101 */
102 protected function needsLabel() {
103 return true;
104 }
105
106 /**
107 * Tell the field whether to generate a separate label element if its label
108 * is blank.
109 *
110 * @since 1.22
111 *
112 * @param bool $show Set to false to not generate a label.
113 * @return void
114 */
115 public function setShowEmptyLabel( $show ) {
116 $this->mShowEmptyLabels = $show;
117 }
118
119 /**
120 * Get the value that this input has been set to from a posted form,
121 * or the input's default value if it has not been set.
122 *
123 * @param WebRequest $request
124 * @return String the value
125 */
126 function loadDataFromRequest( $request ) {
127 if ( $request->getCheck( $this->mName ) ) {
128 return $request->getText( $this->mName );
129 } else {
130 return $this->getDefault();
131 }
132 }
133
134 /**
135 * Initialise the object
136 *
137 * @param array $params Associative Array. See HTMLForm doc for syntax.
138 *
139 * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead
140 * @throws MWException
141 */
142 function __construct( $params ) {
143 $this->mParams = $params;
144
145 # Generate the label from a message, if possible
146 if ( isset( $params['label-message'] ) ) {
147 $msgInfo = $params['label-message'];
148
149 if ( is_array( $msgInfo ) ) {
150 $msg = array_shift( $msgInfo );
151 } else {
152 $msg = $msgInfo;
153 $msgInfo = array();
154 }
155
156 $this->mLabel = wfMessage( $msg, $msgInfo )->parse();
157 } elseif ( isset( $params['label'] ) ) {
158 if ( $params['label'] === '&#160;' ) {
159 // Apparently some things set &nbsp directly and in an odd format
160 $this->mLabel = '&#160;';
161 } else {
162 $this->mLabel = htmlspecialchars( $params['label'] );
163 }
164 } elseif ( isset( $params['label-raw'] ) ) {
165 $this->mLabel = $params['label-raw'];
166 }
167
168 $this->mName = "wp{$params['fieldname']}";
169 if ( isset( $params['name'] ) ) {
170 $this->mName = $params['name'];
171 }
172
173 $validName = Sanitizer::escapeId( $this->mName );
174 if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
175 throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
176 }
177
178 $this->mID = "mw-input-{$this->mName}";
179
180 if ( isset( $params['default'] ) ) {
181 $this->mDefault = $params['default'];
182 }
183
184 if ( isset( $params['id'] ) ) {
185 $id = $params['id'];
186 $validId = Sanitizer::escapeId( $id );
187
188 if ( $id != $validId ) {
189 throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
190 }
191
192 $this->mID = $id;
193 }
194
195 if ( isset( $params['cssclass'] ) ) {
196 $this->mClass = $params['cssclass'];
197 }
198
199 if ( isset( $params['validation-callback'] ) ) {
200 $this->mValidationCallback = $params['validation-callback'];
201 }
202
203 if ( isset( $params['filter-callback'] ) ) {
204 $this->mFilterCallback = $params['filter-callback'];
205 }
206
207 if ( isset( $params['flatlist'] ) ) {
208 $this->mClass .= ' mw-htmlform-flatlist';
209 }
210
211 if ( isset( $params['hidelabel'] ) ) {
212 $this->mShowEmptyLabels = false;
213 }
214 }
215
216 /**
217 * Get the complete table row for the input, including help text,
218 * labels, and whatever.
219 *
220 * @param string $value The value to set the input to.
221 *
222 * @return string Complete HTML table row.
223 */
224 function getTableRow( $value ) {
225 list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
226 $inputHtml = $this->getInputHTML( $value );
227 $fieldType = get_class( $this );
228 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
229 $cellAttributes = array();
230
231 if ( !empty( $this->mParams['vertical-label'] ) ) {
232 $cellAttributes['colspan'] = 2;
233 $verticalLabel = true;
234 } else {
235 $verticalLabel = false;
236 }
237
238 $label = $this->getLabelHtml( $cellAttributes );
239
240 $field = Html::rawElement(
241 'td',
242 array( 'class' => 'mw-input' ) + $cellAttributes,
243 $inputHtml . "\n$errors"
244 );
245
246 if ( $verticalLabel ) {
247 $html = Html::rawElement( 'tr', array( 'class' => 'mw-htmlform-vertical-label' ), $label );
248 $html .= Html::rawElement( 'tr',
249 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
250 $field );
251 } else {
252 $html =
253 Html::rawElement( 'tr',
254 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
255 $label . $field );
256 }
257
258 return $html . $helptext;
259 }
260
261 /**
262 * Get the complete div for the input, including help text,
263 * labels, and whatever.
264 * @since 1.20
265 *
266 * @param string $value The value to set the input to.
267 *
268 * @return string Complete HTML table row.
269 */
270 public function getDiv( $value ) {
271 list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
272 $inputHtml = $this->getInputHTML( $value );
273 $fieldType = get_class( $this );
274 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
275 $cellAttributes = array();
276 $label = $this->getLabelHtml( $cellAttributes );
277
278 $outerDivClass = array(
279 'mw-input',
280 'mw-htmlform-nolabel' => ( $label === '' )
281 );
282
283 $field = Html::rawElement(
284 'div',
285 array( 'class' => $outerDivClass ) + $cellAttributes,
286 $inputHtml . "\n$errors"
287 );
288 $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass );
289 if ( $this->mParent->isVForm() ) {
290 $divCssClasses[] = 'mw-ui-vform-div';
291 }
292 $html = Html::rawElement( 'div', array( 'class' => $divCssClasses ), $label . $field );
293 $html .= $helptext;
294
295 return $html;
296 }
297
298 /**
299 * Get the complete raw fields for the input, including help text,
300 * labels, and whatever.
301 * @since 1.20
302 *
303 * @param string $value The value to set the input to.
304 *
305 * @return string Complete HTML table row.
306 */
307 public function getRaw( $value ) {
308 list( $errors, ) = $this->getErrorsAndErrorClass( $value );
309 $inputHtml = $this->getInputHTML( $value );
310 $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
311 $cellAttributes = array();
312 $label = $this->getLabelHtml( $cellAttributes );
313
314 $html = "\n$errors";
315 $html .= $label;
316 $html .= $inputHtml;
317 $html .= $helptext;
318
319 return $html;
320 }
321
322 /**
323 * Generate help text HTML in table format
324 * @since 1.20
325 *
326 * @param string|null $helptext
327 * @return string
328 */
329 public function getHelpTextHtmlTable( $helptext ) {
330 if ( is_null( $helptext ) ) {
331 return '';
332 }
333
334 $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ), $helptext );
335 $row = Html::rawElement( 'tr', array(), $row );
336
337 return $row;
338 }
339
340 /**
341 * Generate help text HTML in div format
342 * @since 1.20
343 *
344 * @param string|null $helptext
345 *
346 * @return String
347 */
348 public function getHelpTextHtmlDiv( $helptext ) {
349 if ( is_null( $helptext ) ) {
350 return '';
351 }
352
353 $div = Html::rawElement( 'div', array( 'class' => 'htmlform-tip' ), $helptext );
354
355 return $div;
356 }
357
358 /**
359 * Generate help text HTML formatted for raw output
360 * @since 1.20
361 *
362 * @param string|null $helptext
363 * @return String
364 */
365 public function getHelpTextHtmlRaw( $helptext ) {
366 return $this->getHelpTextHtmlDiv( $helptext );
367 }
368
369 /**
370 * Determine the help text to display
371 * @since 1.20
372 * @return string
373 */
374 public function getHelpText() {
375 $helptext = null;
376
377 if ( isset( $this->mParams['help-message'] ) ) {
378 $this->mParams['help-messages'] = array( $this->mParams['help-message'] );
379 }
380
381 if ( isset( $this->mParams['help-messages'] ) ) {
382 foreach ( $this->mParams['help-messages'] as $name ) {
383 $helpMessage = (array)$name;
384 $msg = $this->msg( array_shift( $helpMessage ), $helpMessage );
385
386 if ( $msg->exists() ) {
387 if ( is_null( $helptext ) ) {
388 $helptext = '';
389 } else {
390 $helptext .= $this->msg( 'word-separator' )->escaped(); // some space
391 }
392 $helptext .= $msg->parse(); // Append message
393 }
394 }
395 } elseif ( isset( $this->mParams['help'] ) ) {
396 $helptext = $this->mParams['help'];
397 }
398
399 return $helptext;
400 }
401
402 /**
403 * Determine form errors to display and their classes
404 * @since 1.20
405 *
406 * @param string $value The value of the input
407 * @return array
408 */
409 public function getErrorsAndErrorClass( $value ) {
410 $errors = $this->validate( $value, $this->mParent->mFieldData );
411
412 if ( $errors === true ||
413 ( !$this->mParent->getRequest()->wasPosted() && $this->mParent->getMethod() === 'post' )
414 ) {
415 $errors = '';
416 $errorClass = '';
417 } else {
418 $errors = self::formatErrors( $errors );
419 $errorClass = 'mw-htmlform-invalid-input';
420 }
421
422 return array( $errors, $errorClass );
423 }
424
425 function getLabel() {
426 return is_null( $this->mLabel ) ? '' : $this->mLabel;
427 }
428
429 function getLabelHtml( $cellAttributes = array() ) {
430 # Don't output a for= attribute for labels with no associated input.
431 # Kind of hacky here, possibly we don't want these to be <label>s at all.
432 $for = array();
433
434 if ( $this->needsLabel() ) {
435 $for['for'] = $this->mID;
436 }
437
438 $labelValue = trim( $this->getLabel() );
439 $hasLabel = false;
440 if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
441 $hasLabel = true;
442 }
443
444 $displayFormat = $this->mParent->getDisplayFormat();
445 $html = '';
446
447 if ( $displayFormat === 'table' ) {
448 $html =
449 Html::rawElement( 'td',
450 array( 'class' => 'mw-label' ) + $cellAttributes,
451 Html::rawElement( 'label', $for, $labelValue ) );
452 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
453 if ( $displayFormat === 'div' ) {
454 $html =
455 Html::rawElement( 'div',
456 array( 'class' => 'mw-label' ) + $cellAttributes,
457 Html::rawElement( 'label', $for, $labelValue ) );
458 } else {
459 $html = Html::rawElement( 'label', $for, $labelValue );
460 }
461 }
462
463 return $html;
464 }
465
466 function getDefault() {
467 if ( isset( $this->mDefault ) ) {
468 return $this->mDefault;
469 } else {
470 return null;
471 }
472 }
473
474 /**
475 * Returns the attributes required for the tooltip and accesskey.
476 *
477 * @return array Attributes
478 */
479 public function getTooltipAndAccessKey() {
480 if ( empty( $this->mParams['tooltip'] ) ) {
481 return array();
482 }
483
484 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
485 }
486
487 /**
488 * Returns the given attributes from the parameters
489 *
490 * @param array $list List of attributes to get
491 * @return array Attributes
492 */
493 public function getAttributes( array $list ) {
494 static $boolAttribs = array( 'disabled', 'required', 'autofocus', 'multiple', 'readonly' );
495
496 $ret = array();
497
498 foreach( $list as $key ) {
499 if ( in_array( $key, $boolAttribs ) ) {
500 if ( !empty( $this->mParams[$key] ) ) {
501 $ret[$key] = '';
502 }
503 } elseif ( isset( $this->mParams[$key] ) ) {
504 $ret[$key] = $this->mParams[$key];
505 }
506 }
507
508 return $ret;
509 }
510
511 /**
512 * flatten an array of options to a single array, for instance,
513 * a set of "<options>" inside "<optgroups>".
514 *
515 * @param array $options Associative Array with values either Strings
516 * or Arrays
517 * @return array Flattened input
518 */
519 public static function flattenOptions( $options ) {
520 $flatOpts = array();
521
522 foreach ( $options as $value ) {
523 if ( is_array( $value ) ) {
524 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
525 } else {
526 $flatOpts[] = $value;
527 }
528 }
529
530 return $flatOpts;
531 }
532
533 /**
534 * Formats one or more errors as accepted by field validation-callback.
535 *
536 * @param string|Message|array $errors String|Message|Array of strings or Message instances
537 * @return string HTML
538 * @since 1.18
539 */
540 protected static function formatErrors( $errors ) {
541 if ( is_array( $errors ) && count( $errors ) === 1 ) {
542 $errors = array_shift( $errors );
543 }
544
545 if ( is_array( $errors ) ) {
546 $lines = array();
547 foreach ( $errors as $error ) {
548 if ( $error instanceof Message ) {
549 $lines[] = Html::rawElement( 'li', array(), $error->parse() );
550 } else {
551 $lines[] = Html::rawElement( 'li', array(), $error );
552 }
553 }
554
555 return Html::rawElement( 'ul', array( 'class' => 'error' ), implode( "\n", $lines ) );
556 } else {
557 if ( $errors instanceof Message ) {
558 $errors = $errors->parse();
559 }
560
561 return Html::rawElement( 'span', array( 'class' => 'error' ), $errors );
562 }
563 }
564 }