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