MediaWiki 1.44.0
HTMLForm.php
Go to the documentation of this file.
1<?php
2
24namespace MediaWiki\HTMLForm;
25
26use DomainException;
27use InvalidArgumentException;
28use LogicException;
31use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
80use StatusValue;
81use Stringable;
84
209class HTMLForm extends ContextSource {
210 use ProtectedHookAccessorTrait;
211
213 public static $typeMappings = [
214 'api' => HTMLApiField::class,
215 'text' => HTMLTextField::class,
216 'textwithbutton' => HTMLTextFieldWithButton::class,
217 'textarea' => HTMLTextAreaField::class,
218 'select' => HTMLSelectField::class,
219 'combobox' => HTMLComboboxField::class,
220 'radio' => HTMLRadioField::class,
221 'multiselect' => HTMLMultiSelectField::class,
222 'limitselect' => HTMLSelectLimitField::class,
223 'check' => HTMLCheckField::class,
224 'toggle' => HTMLCheckField::class,
225 'int' => HTMLIntField::class,
226 'file' => HTMLFileField::class,
227 'float' => HTMLFloatField::class,
228 'info' => HTMLInfoField::class,
229 'selectorother' => HTMLSelectOrOtherField::class,
230 'selectandother' => HTMLSelectAndOtherField::class,
231 'namespaceselect' => HTMLSelectNamespace::class,
232 'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
233 'tagfilter' => HTMLTagFilter::class,
234 'sizefilter' => HTMLSizeFilterField::class,
235 'submit' => HTMLSubmitField::class,
236 'hidden' => HTMLHiddenField::class,
237 'edittools' => HTMLEditTools::class,
238 'checkmatrix' => HTMLCheckMatrix::class,
239 'cloner' => HTMLFormFieldCloner::class,
240 'autocompleteselect' => HTMLAutoCompleteSelectField::class,
241 'language' => HTMLSelectLanguageField::class,
242 'date' => HTMLDateTimeField::class,
243 'time' => HTMLDateTimeField::class,
244 'datetime' => HTMLDateTimeField::class,
245 'expiry' => HTMLExpiryField::class,
246 'timezone' => HTMLTimezoneField::class,
247 // HTMLTextField will output the correct type="" attribute automagically.
248 // There are about four zillion other HTML5 input types, like range, but
249 // we don't use those at the moment, so no point in adding all of them.
250 'email' => HTMLTextField::class,
251 'password' => HTMLTextField::class,
252 'url' => HTMLTextField::class,
253 'title' => HTMLTitleTextField::class,
254 'user' => HTMLUserTextField::class,
255 'tagmultiselect' => HTMLTagMultiselectField::class,
256 'orderedmultiselect' => HTMLOrderedMultiselectField::class,
257 'usersmultiselect' => HTMLUsersMultiselectField::class,
258 'titlesmultiselect' => HTMLTitlesMultiselectField::class,
259 'namespacesmultiselect' => HTMLNamespacesMultiselectField::class,
260 ];
261
264
267
269 protected $mFlatFields = [];
271 protected $mFieldTree = [];
273 protected $mShowSubmit = true;
275 protected $mSubmitFlags = [ 'primary', 'progressive' ];
277 protected $mShowCancel = false;
279 protected $mCancelTarget;
280
288
290 protected $mPre = '';
292 protected $mHeader = '';
294 protected $mFooter = '';
296 protected $mSectionHeaders = [];
298 protected $mSectionFooters = [];
300 protected $mPost = '';
302 protected $mId;
304 protected $mName;
306 protected $mTableId = '';
307
309 protected $mSubmitID;
311 protected $mSubmitName;
313 protected $mSubmitText;
316
320 protected $mSingleForm = false;
321
323 protected $mTitle;
325 protected $mMethod = 'post';
327 protected $mWasSubmitted = false;
328
334 protected $mAction = false;
335
341 protected $mCollapsible = false;
342
348 protected $mCollapsed = false;
349
355 protected $mAutocomplete = null;
356
358 protected $mUseMultipart = false;
363 protected $mHiddenFields = [];
368 protected $mButtons = [];
369
371 protected $mWrapperLegend = false;
373 protected $mWrapperAttributes = [];
374
379 protected $mTokenSalt = '';
380
393 protected $mSections = [];
394
403 protected $mSubSectionBeforeFields = true;
404
410 protected $displayFormat = 'table';
411
417 'table',
418 'div',
419 'raw',
420 'inline',
421 ];
422
428 'vform',
429 'codex',
430 'ooui',
431 ];
432
437 private $hiddenTitleAddedToForm = false;
438
452 public static function factory(
453 $displayFormat, $descriptor, IContextSource $context, $messagePrefix = ''
454 ) {
455 switch ( $displayFormat ) {
456 case 'codex':
457 return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
458 case 'vform':
459 return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
460 case 'ooui':
461 return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
462 default:
463 $form = new self( $descriptor, $context, $messagePrefix );
464 $form->setDisplayFormat( $displayFormat );
465 return $form;
466 }
467 }
468
480 public function __construct(
481 $descriptor, IContextSource $context, $messagePrefix = ''
482 ) {
483 $this->setContext( $context );
484 $this->mMessagePrefix = $messagePrefix;
485 $this->addFields( $descriptor );
486 }
487
497 public function addFields( $descriptor ) {
498 $loadedDescriptor = [];
499
500 foreach ( $descriptor as $fieldname => $info ) {
501 $section = $info['section'] ?? '';
502
503 if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
504 $this->mUseMultipart = true;
505 }
506
507 $field = static::loadInputFromParameters( $fieldname, $info, $this );
508
509 $setSection =& $loadedDescriptor;
510 if ( $section ) {
511 foreach ( explode( '/', $section ) as $newName ) {
512 $setSection[$newName] ??= [];
513 $setSection =& $setSection[$newName];
514 }
515 }
516
517 $setSection[$fieldname] = $field;
518 $this->mFlatFields[$fieldname] = $field;
519 }
520
521 $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
522
523 return $this;
524 }
525
530 public function hasField( $fieldname ) {
531 return isset( $this->mFlatFields[$fieldname] );
532 }
533
539 public function getField( $fieldname ) {
540 if ( !$this->hasField( $fieldname ) ) {
541 throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
542 }
543 return $this->mFlatFields[$fieldname];
544 }
545
555 public function setDisplayFormat( $format ) {
556 if (
557 in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
558 in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
559 ) {
560 throw new LogicException( 'Cannot change display format after creation, ' .
561 'use HTMLForm::factory() instead' );
562 }
563
564 if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
565 throw new InvalidArgumentException( 'Display format must be one of ' .
566 print_r(
567 array_merge(
568 $this->availableDisplayFormats,
569 $this->availableSubclassDisplayFormats
570 ),
571 true
572 ) );
573 }
574
575 $this->displayFormat = $format;
576
577 return $this;
578 }
579
585 public function getDisplayFormat() {
587 }
588
605 public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
606 if ( isset( $descriptor['class'] ) ) {
607 $class = $descriptor['class'];
608 } elseif ( isset( $descriptor['type'] ) ) {
609 $class = static::$typeMappings[$descriptor['type']];
610 $descriptor['class'] = $class;
611 } else {
612 $class = null;
613 }
614
615 if ( !$class ) {
616 throw new InvalidArgumentException( "Descriptor with no class for $fieldname: "
617 . print_r( $descriptor, true ) );
618 }
619
620 return $class;
621 }
622
635 public static function loadInputFromParameters( $fieldname, $descriptor,
636 ?HTMLForm $parent = null
637 ) {
638 $class = static::getClassFromDescriptor( $fieldname, $descriptor );
639
640 $descriptor['fieldname'] = $fieldname;
641 if ( $parent ) {
642 $descriptor['parent'] = $parent;
643 }
644
645 # @todo This will throw a fatal error whenever someone try to use
646 # 'class' to feed a CSS class instead of 'cssclass'. Would be
647 # great to avoid the fatal error and show a nice error.
648 return new $class( $descriptor );
649 }
650
659 public function prepareForm() {
660 # Load data from the request.
661 if (
662 $this->mFormIdentifier === null ||
663 $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier ||
664 ( $this->mSingleForm && $this->getMethod() === 'get' )
665 ) {
666 $this->loadFieldData();
667 } else {
668 $this->mFieldData = [];
669 }
670
671 return $this;
672 }
673
678 public function tryAuthorizedSubmit() {
679 $result = false;
680
681 if ( $this->mFormIdentifier === null ) {
682 $identOkay = true;
683 } else {
684 $identOkay = $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier;
685 }
686
687 $tokenOkay = false;
688 if ( $this->getMethod() !== 'post' ) {
689 $tokenOkay = true; // no session check needed
690 } elseif ( $this->getRequest()->wasPosted() ) {
691 $editToken = $this->getRequest()->getVal( 'wpEditToken' );
692 if ( $this->getUser()->isRegistered() || $editToken !== null ) {
693 // Session tokens for logged-out users have no security value.
694 // However, if the user gave one, check it in order to give a nice
695 // "session expired" error instead of "permission denied" or such.
696 $tokenOkay = $this->getCsrfTokenSet()->matchTokenField(
697 CsrfTokenSet::DEFAULT_FIELD_NAME, $this->mTokenSalt
698 );
699 } else {
700 $tokenOkay = true;
701 }
702 }
703
704 if ( $tokenOkay && $identOkay ) {
705 $this->mWasSubmitted = true;
706 $result = $this->trySubmit();
707 }
708
709 return $result;
710 }
711
719 public function show() {
720 $this->prepareForm();
721
722 $result = $this->tryAuthorizedSubmit();
723 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
724 return $result;
725 }
726
727 $this->displayForm( $result );
728
729 return false;
730 }
731
737 public function showAlways() {
738 $this->prepareForm();
739
740 $result = $this->tryAuthorizedSubmit();
741
742 $this->displayForm( $result );
743
744 return $result;
745 }
746
758 public function trySubmit() {
759 $valid = true;
760 $hoistedErrors = Status::newGood();
761 if ( $this->mValidationErrorMessage ) {
762 foreach ( $this->mValidationErrorMessage as $error ) {
763 $hoistedErrors->fatal( ...$error );
764 }
765 } else {
766 $hoistedErrors->fatal( 'htmlform-invalid-input' );
767 }
768
769 $this->mWasSubmitted = true;
770
771 # Check for cancelled submission
772 foreach ( $this->mFlatFields as $fieldname => $field ) {
773 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
774 continue;
775 }
776 if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
777 $this->mWasSubmitted = false;
778 return false;
779 }
780 }
781
782 # Check for validation
783 $hasNonDefault = false;
784 foreach ( $this->mFlatFields as $fieldname => $field ) {
785 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
786 continue;
787 }
788 $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
789 if ( $field->isDisabled( $this->mFieldData ) ) {
790 continue;
791 }
792 $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
793 if ( $res !== true ) {
794 $valid = false;
795 if ( $res !== false && !$field->canDisplayErrors() ) {
796 if ( is_string( $res ) ) {
797 $hoistedErrors->fatal( 'rawmessage', $res );
798 } else {
799 $hoistedErrors->fatal( $res );
800 }
801 }
802 }
803 }
804
805 if ( !$valid ) {
806 // Treat as not submitted if got nothing from the user on GET forms.
807 if ( !$hasNonDefault && $this->getMethod() === 'get' &&
808 ( $this->mFormIdentifier === null ||
809 $this->getRequest()->getCheck( 'wpFormIdentifier' ) )
810 ) {
811 $this->mWasSubmitted = false;
812 return false;
813 }
814 return $hoistedErrors;
815 }
816
817 $callback = $this->mSubmitCallback;
818 if ( !is_callable( $callback ) ) {
819 throw new LogicException( 'HTMLForm: no submit callback provided. Use ' .
820 'setSubmitCallback() to set one.' );
821 }
822
823 $data = $this->filterDataForSubmit( $this->mFieldData );
824
825 $res = $callback( $data, $this );
826 if ( $res === false ) {
827 $this->mWasSubmitted = false;
828 } elseif ( $res instanceof StatusValue ) {
829 // DWIM - callbacks are not supposed to return a StatusValue but it's easy to mix up.
830 $res = Status::wrap( $res );
831 }
832
833 return $res;
834 }
835
847 public function wasSubmitted() {
849 }
850
861 public function setSubmitCallback( $cb ) {
862 $this->mSubmitCallback = $cb;
863
864 return $this;
865 }
866
876 public function setValidationErrorMessage( $msg ) {
877 $this->mValidationErrorMessage = $msg;
878
879 return $this;
880 }
881
890 public function setPreHtml( $html ) {
891 $this->mPre = $html;
892
893 return $this;
894 }
895
904 public function addPreHtml( $html ) {
905 $this->mPre .= $html;
906
907 return $this;
908 }
909
916 public function getPreHtml() {
917 return $this->mPre;
918 }
919
929 public function addHeaderHtml( $html, $section = null ) {
930 if ( $section === null ) {
931 $this->mHeader .= $html;
932 } else {
933 $this->mSectionHeaders[$section] ??= '';
934 $this->mSectionHeaders[$section] .= $html;
935 }
936
937 return $this;
938 }
939
949 public function setHeaderHtml( $html, $section = null ) {
950 if ( $section === null ) {
951 $this->mHeader = $html;
952 } else {
953 $this->mSectionHeaders[$section] = $html;
954 }
955
956 return $this;
957 }
958
967 public function getHeaderHtml( $section = null ) {
968 return $section ? $this->mSectionHeaders[$section] ?? '' : $this->mHeader;
969 }
970
980 public function addFooterHtml( $html, $section = null ) {
981 if ( $section === null ) {
982 $this->mFooter .= $html;
983 } else {
984 $this->mSectionFooters[$section] ??= '';
985 $this->mSectionFooters[$section] .= $html;
986 }
987
988 return $this;
989 }
990
1000 public function setFooterHtml( $html, $section = null ) {
1001 if ( $section === null ) {
1002 $this->mFooter = $html;
1003 } else {
1004 $this->mSectionFooters[$section] = $html;
1005 }
1006
1007 return $this;
1008 }
1009
1017 public function getFooterHtml( $section = null ) {
1018 return $section ? $this->mSectionFooters[$section] ?? '' : $this->mFooter;
1019 }
1020
1029 public function addPostHtml( $html ) {
1030 $this->mPost .= $html;
1031
1032 return $this;
1033 }
1034
1043 public function setPostHtml( $html ) {
1044 $this->mPost = $html;
1045
1046 return $this;
1047 }
1048
1055 public function getPostHtml() {
1056 return $this->mPost;
1057 }
1058
1068 public function setSections( $sections ) {
1069 if ( $this->getDisplayFormat() !== 'codex' ) {
1070 throw new \InvalidArgumentException(
1071 "Non-Codex HTMLForms do not support additional section information."
1072 );
1073 }
1074
1075 $this->mSections = $sections;
1076
1077 return $this;
1078 }
1079
1090 public function addHiddenField( $name, $value, array $attribs = [] ) {
1091 if ( !is_array( $value ) ) {
1092 // Per WebRequest::getVal: Array values are discarded for security reasons.
1093 $attribs += [ 'name' => $name ];
1094 $this->mHiddenFields[] = [ $value, $attribs ];
1095 }
1096
1097 return $this;
1098 }
1099
1111 public function addHiddenFields( array $fields ) {
1112 foreach ( $fields as $name => $value ) {
1113 if ( is_array( $value ) ) {
1114 // Per WebRequest::getVal: Array values are discarded for security reasons.
1115 continue;
1116 }
1117 $this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
1118 }
1119
1120 return $this;
1121 }
1122
1146 public function addButton( $data ) {
1147 if ( !is_array( $data ) ) {
1148 $args = func_get_args();
1149 if ( count( $args ) < 2 || count( $args ) > 4 ) {
1150 throw new InvalidArgumentException(
1151 'Incorrect number of arguments for deprecated calling style'
1152 );
1153 }
1154 $data = [
1155 'name' => $args[0],
1156 'value' => $args[1],
1157 'id' => $args[2] ?? null,
1158 'attribs' => $args[3] ?? null,
1159 ];
1160 } else {
1161 if ( !isset( $data['name'] ) ) {
1162 throw new InvalidArgumentException( 'A name is required' );
1163 }
1164 if ( !isset( $data['value'] ) ) {
1165 throw new InvalidArgumentException( 'A value is required' );
1166 }
1167 }
1168 $this->mButtons[] = $data + [
1169 'id' => null,
1170 'attribs' => null,
1171 'flags' => null,
1172 'framed' => true,
1173 ];
1174
1175 return $this;
1176 }
1177
1187 public function setTokenSalt( $salt ) {
1188 $this->mTokenSalt = $salt;
1189
1190 return $this;
1191 }
1192
1207 public function displayForm( $submitResult ) {
1208 $this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
1209 }
1210
1214 private function getHiddenTitle(): string {
1215 if ( $this->hiddenTitleAddedToForm ) {
1216 return '';
1217 }
1218
1219 $html = '';
1220 if ( $this->getMethod() === 'post' ||
1221 $this->getAction() === $this->getConfig()->get( MainConfigNames::Script )
1222 ) {
1223 $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1224 }
1225 $this->hiddenTitleAddedToForm = true;
1226 return $html;
1227 }
1228
1239 public function getHTML( $submitResult ) {
1240 # For good measure (it is the default)
1241 $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1242 $this->getOutput()->addModules( 'mediawiki.htmlform' );
1243 $this->getOutput()->addModuleStyles( [
1244 'mediawiki.htmlform.styles',
1245 // Html::errorBox and Html::warningBox used by HtmlFormField and HtmlForm::getErrorsOrWarnings
1246 'mediawiki.codex.messagebox.styles'
1247 ] );
1248
1249 if ( $this->mCollapsible ) {
1250 // Preload jquery.makeCollapsible for mediawiki.htmlform
1251 $this->getOutput()->addModules( 'jquery.makeCollapsible' );
1252 }
1253
1254 $headerHtml = $this->getHeaderHtml();
1255 $footerHtml = $this->getFooterHtml();
1256 $html = $this->getErrorsOrWarnings( $submitResult, 'error' )
1257 . $this->getErrorsOrWarnings( $submitResult, 'warning' )
1258 . $headerHtml
1259 . $this->getHiddenTitle()
1260 . $this->getBody()
1261 . $this->getHiddenFields()
1262 . $this->getButtons()
1263 . $footerHtml;
1264
1265 return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1266 }
1267
1275 public function setCollapsibleOptions( $collapsedByDefault = false ) {
1276 $this->mCollapsible = true;
1277 $this->mCollapsed = $collapsedByDefault;
1278 return $this;
1279 }
1280
1286 protected function getFormAttributes() {
1287 # Use multipart/form-data
1288 $encType = $this->mUseMultipart
1289 ? 'multipart/form-data'
1290 : 'application/x-www-form-urlencoded';
1291 # Attributes
1292 $attribs = [
1293 'class' => 'mw-htmlform',
1294 'action' => $this->getAction(),
1295 'method' => $this->getMethod(),
1296 'enctype' => $encType,
1297 ];
1298 if ( $this->mId ) {
1299 $attribs['id'] = $this->mId;
1300 }
1301 if ( is_string( $this->mAutocomplete ) ) {
1302 $attribs['autocomplete'] = $this->mAutocomplete;
1303 }
1304 if ( $this->mName ) {
1305 $attribs['name'] = $this->mName;
1306 }
1307 if ( $this->needsJSForHtml5FormValidation() ) {
1308 $attribs['novalidate'] = true;
1309 }
1310 return $attribs;
1311 }
1312
1320 public function wrapForm( $html ) {
1321 # Include a <fieldset> wrapper for style, if requested.
1322 if ( $this->mWrapperLegend !== false ) {
1323 $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1324 $html = Html::rawElement(
1325 'fieldset',
1326 $this->mWrapperAttributes,
1327 ( $legend ? Html::element( 'legend', [], $legend ) : '' ) . $html
1328 );
1329 }
1330
1331 return Html::rawElement(
1332 'form',
1333 $this->getFormAttributes(),
1334 $html
1335 );
1336 }
1337
1342 public function getHiddenFields() {
1343 $html = '';
1344
1345 // add the title as a hidden file if it hasn't been added yet and if it is necessary
1346 // added for backward compatibility with the previous version of this public method
1347 $html .= $this->getHiddenTitle();
1348
1349 if ( $this->mFormIdentifier !== null ) {
1350 $html .= Html::hidden(
1351 'wpFormIdentifier',
1352 $this->mFormIdentifier
1353 ) . "\n";
1354 }
1355 if ( $this->getMethod() === 'post' ) {
1356 $html .= Html::hidden(
1357 'wpEditToken',
1358 $this->getUser()->getEditToken( $this->mTokenSalt ),
1359 [ 'id' => 'wpEditToken' ]
1360 ) . "\n";
1361 }
1362
1363 foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1364 $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1365 }
1366
1367 return $html;
1368 }
1369
1375 public function getButtons() {
1376 $buttons = '';
1377
1378 if ( $this->mShowSubmit ) {
1379 $attribs = [];
1380
1381 if ( $this->mSubmitID !== null ) {
1382 $attribs['id'] = $this->mSubmitID;
1383 }
1384
1385 if ( $this->mSubmitName !== null ) {
1386 $attribs['name'] = $this->mSubmitName;
1387 }
1388
1389 if ( $this->mSubmitTooltip !== null ) {
1390 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1391 }
1392
1393 $attribs['class'] = [ 'mw-htmlform-submit' ];
1394
1395 $buttons .= Html::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1396 }
1397
1398 if ( $this->mShowCancel ) {
1399 $target = $this->getCancelTargetURL();
1400 $buttons .= Html::element(
1401 'a',
1402 [
1403 'href' => $target,
1404 ],
1405 $this->msg( 'cancel' )->text()
1406 ) . "\n";
1407 }
1408
1409 foreach ( $this->mButtons as $button ) {
1410 $attrs = [
1411 'type' => 'submit',
1412 'name' => $button['name'],
1413 'value' => $button['value']
1414 ];
1415
1416 if ( isset( $button['label-message'] ) ) {
1417 $label = $this->getMessage( $button['label-message'] )->parse();
1418 } elseif ( isset( $button['label'] ) ) {
1419 $label = htmlspecialchars( $button['label'] );
1420 } elseif ( isset( $button['label-raw'] ) ) {
1421 $label = $button['label-raw'];
1422 } else {
1423 $label = htmlspecialchars( $button['value'] );
1424 }
1425
1426 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in self::addButton
1427 if ( $button['attribs'] ) {
1428 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in self::addButton
1429 $attrs += $button['attribs'];
1430 }
1431
1432 if ( isset( $button['id'] ) ) {
1433 $attrs['id'] = $button['id'];
1434 }
1435
1436 $buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1437 }
1438
1439 if ( !$buttons ) {
1440 return '';
1441 }
1442
1443 return Html::rawElement( 'span',
1444 [ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1445 }
1446
1452 public function getBody() {
1453 return $this->displaySection( $this->mFieldTree, $this->mTableId );
1454 }
1455
1465 public function getErrorsOrWarnings( $elements, $elementsType ) {
1466 if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) {
1467 throw new DomainException( $elementsType . ' is not a valid type.' );
1468 }
1469 $elementstr = false;
1470 if ( $elements instanceof Status ) {
1471 [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1472 $status = $elementsType === 'error' ? $errorStatus : $warningStatus;
1473 if ( $status->isGood() ) {
1474 $elementstr = '';
1475 } else {
1476 $elementstr = $status
1477 ->getMessage()
1478 ->setContext( $this )
1479 ->setInterfaceMessageFlag( true )
1480 ->parse();
1481 }
1482 } elseif ( $elementsType === 'error' ) {
1483 if ( is_array( $elements ) ) {
1484 $elementstr = $this->formatErrors( $elements );
1485 } elseif ( $elements && $elements !== true ) {
1486 $elementstr = (string)$elements;
1487 }
1488 }
1489
1490 if ( !$elementstr ) {
1491 return '';
1492 } elseif ( $elementsType === 'error' ) {
1493 return Html::errorBox( $elementstr );
1494 } else { // $elementsType can only be 'warning'
1495 return Html::warningBox( $elementstr );
1496 }
1497 }
1498
1506 public function formatErrors( $errors ) {
1507 $errorstr = '';
1508
1509 foreach ( $errors as $error ) {
1510 $errorstr .= Html::rawElement(
1511 'li',
1512 [],
1513 $this->getMessage( $error )->parse()
1514 );
1515 }
1516
1517 return Html::rawElement( 'ul', [], $errorstr );
1518 }
1519
1527 public function setSubmitText( $t ) {
1528 $this->mSubmitText = $t;
1529
1530 return $this;
1531 }
1532
1539 public function setSubmitDestructive() {
1540 $this->mSubmitFlags = [ 'destructive', 'primary' ];
1541
1542 return $this;
1543 }
1544
1553 public function setSubmitTextMsg( $msg ) {
1554 if ( !$msg instanceof Message ) {
1555 $msg = $this->msg( $msg );
1556 }
1557 $this->setSubmitText( $msg->text() );
1558
1559 return $this;
1560 }
1561
1566 public function getSubmitText() {
1567 return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1568 }
1569
1575 public function setSubmitName( $name ) {
1576 $this->mSubmitName = $name;
1577
1578 return $this;
1579 }
1580
1586 public function setSubmitTooltip( $name ) {
1587 $this->mSubmitTooltip = $name;
1588
1589 return $this;
1590 }
1591
1600 public function setSubmitID( $t ) {
1601 $this->mSubmitID = $t;
1602
1603 return $this;
1604 }
1605
1624 public function setFormIdentifier( string $ident, bool $single = false ) {
1625 $this->mFormIdentifier = $ident;
1626 $this->mSingleForm = $single;
1627
1628 return $this;
1629 }
1630
1641 public function suppressDefaultSubmit( $suppressSubmit = true ) {
1642 $this->mShowSubmit = !$suppressSubmit;
1643
1644 return $this;
1645 }
1646
1653 public function showCancel( $show = true ) {
1654 $this->mShowCancel = $show;
1655 return $this;
1656 }
1657
1664 public function setCancelTarget( $target ) {
1665 if ( $target instanceof PageReference ) {
1666 $target = TitleValue::castPageToLinkTarget( $target );
1667 }
1668
1669 $this->mCancelTarget = $target;
1670 return $this;
1671 }
1672
1677 protected function getCancelTargetURL() {
1678 if ( is_string( $this->mCancelTarget ) ) {
1679 return $this->mCancelTarget;
1680 } else {
1681 // TODO: use a service to get the local URL for a LinkTarget, see T282283
1682 $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1683 return $target->getLocalURL();
1684 }
1685 }
1686
1696 public function setTableId( $id ) {
1697 $this->mTableId = $id;
1698
1699 return $this;
1700 }
1701
1707 public function setId( $id ) {
1708 $this->mId = $id;
1709
1710 return $this;
1711 }
1712
1717 public function setName( $name ) {
1718 $this->mName = $name;
1719
1720 return $this;
1721 }
1722
1734 public function setWrapperLegend( $legend ) {
1735 $this->mWrapperLegend = $legend;
1736
1737 return $this;
1738 }
1739
1747 public function setWrapperAttributes( $attributes ) {
1748 $this->mWrapperAttributes = $attributes;
1749
1750 return $this;
1751 }
1752
1762 public function setWrapperLegendMsg( $msg ) {
1763 if ( !$msg instanceof Message ) {
1764 $msg = $this->msg( $msg );
1765 }
1766 $this->setWrapperLegend( $msg->text() );
1767
1768 return $this;
1769 }
1770
1780 public function setMessagePrefix( $p ) {
1781 $this->mMessagePrefix = $p;
1782
1783 return $this;
1784 }
1785
1793 public function setTitle( $t ) {
1794 // TODO: make mTitle a PageReference when we have a better way to get URLs, see T282283.
1795 $this->mTitle = Title::castFromPageReference( $t );
1796
1797 return $this;
1798 }
1799
1803 public function getTitle() {
1804 return $this->mTitle ?: $this->getContext()->getTitle();
1805 }
1806
1814 public function setMethod( $method = 'post' ) {
1815 $this->mMethod = strtolower( $method );
1816
1817 return $this;
1818 }
1819
1823 public function getMethod() {
1824 return $this->mMethod;
1825 }
1826
1837 protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
1838 return Html::rawElement(
1839 'fieldset',
1840 $attributes,
1841 Html::element( 'legend', [], $legend ) . $section
1842 ) . "\n";
1843 }
1844
1863 public function displaySection( $fields,
1864 $sectionName = '',
1865 $fieldsetIDPrefix = '',
1866 &$hasUserVisibleFields = false
1867 ) {
1868 if ( $this->mFieldData === null ) {
1869 throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1870 . 'You probably called displayForm() without calling prepareForm() first.' );
1871 }
1872
1873 $html = [];
1874 $subsectionHtml = '';
1875 $hasLabel = false;
1876
1877 foreach ( $fields as $key => $value ) {
1878 if ( $value instanceof HTMLFormField ) {
1879 $v = array_key_exists( $key, $this->mFieldData )
1880 ? $this->mFieldData[$key]
1881 : $value->getDefault();
1882
1883 $retval = $this->formatField( $value, $v ?? '' );
1884
1885 // check, if the form field should be added to
1886 // the output.
1887 if ( $value->hasVisibleOutput() ) {
1888 $html[] = $retval;
1889
1890 $labelValue = trim( $value->getLabel() );
1891 if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
1892 $hasLabel = true;
1893 }
1894
1895 $hasUserVisibleFields = true;
1896 }
1897 } elseif ( is_array( $value ) ) {
1898 $subsectionHasVisibleFields = false;
1899 $section =
1900 $this->displaySection( $value,
1901 "mw-htmlform-$key",
1902 "$fieldsetIDPrefix$key-",
1903 $subsectionHasVisibleFields );
1904
1905 if ( $subsectionHasVisibleFields === true ) {
1906 // Display the section with various niceties.
1907 $hasUserVisibleFields = true;
1908
1909 $legend = $this->getLegend( $key );
1910
1911 $headerHtml = $this->getHeaderHtml( $key );
1912 $footerHtml = $this->getFooterHtml( $key );
1913 $section = $headerHtml .
1914 $section .
1915 $footerHtml;
1916
1917 $attributes = [];
1918 if ( $fieldsetIDPrefix ) {
1919 $attributes['id'] = Sanitizer::escapeIdForAttribute( "$fieldsetIDPrefix$key" );
1920 }
1921 $subsectionHtml .= $this->wrapFieldSetSection(
1922 $legend, $section, $attributes, $fields === $this->mFieldTree
1923 );
1924 } else {
1925 // Just return the inputs, nothing fancy.
1926 $subsectionHtml .= $section;
1927 }
1928 }
1929 }
1930
1931 $html = $this->formatSection( $html, $sectionName, $hasLabel );
1932
1933 if ( $subsectionHtml ) {
1934 if ( $this->mSubSectionBeforeFields ) {
1935 return $subsectionHtml . "\n" . $html;
1936 } else {
1937 return $html . "\n" . $subsectionHtml;
1938 }
1939 } else {
1940 return $html;
1941 }
1942 }
1943
1952 protected function formatField( HTMLFormField $field, $value ) {
1953 $displayFormat = $this->getDisplayFormat();
1954 switch ( $displayFormat ) {
1955 case 'table':
1956 return $field->getTableRow( $value );
1957 case 'div':
1958 return $field->getDiv( $value );
1959 case 'raw':
1960 return $field->getRaw( $value );
1961 case 'inline':
1962 return $field->getInline( $value );
1963 default:
1964 throw new LogicException( 'Not implemented' );
1965 }
1966 }
1967
1976 protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1977 if ( !$fieldsHtml ) {
1978 // Do not generate any wrappers for empty sections. Sections may be empty if they only have
1979 // subsections, but no fields. A legend will still be added in wrapFieldSetSection().
1980 return '';
1981 }
1982
1983 $displayFormat = $this->getDisplayFormat();
1984 $html = implode( '', $fieldsHtml );
1985
1986 if ( $displayFormat === 'raw' ) {
1987 return $html;
1988 }
1989
1990 // Avoid strange spacing when no labels exist
1991 $attribs = $anyFieldHasLabel ? [] : [ 'class' => 'mw-htmlform-nolabel' ];
1992
1993 if ( $sectionName ) {
1994 $attribs['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
1995 }
1996
1997 if ( $displayFormat === 'table' ) {
1998 return Html::rawElement( 'table',
1999 $attribs,
2000 Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
2001 } elseif ( $displayFormat === 'inline' ) {
2002 return Html::rawElement( 'span', $attribs, "\n$html\n" );
2003 } else {
2004 return Html::rawElement( 'div', $attribs, "\n$html\n" );
2005 }
2006 }
2007
2011 public function loadData() {
2012 $this->prepareForm();
2013 }
2014
2018 protected function loadFieldData() {
2019 $fieldData = [];
2020 $request = $this->getRequest();
2021
2022 foreach ( $this->mFlatFields as $fieldname => $field ) {
2023 if ( $field->skipLoadData( $request ) ) {
2024 continue;
2025 }
2026 if ( $field->mParams['disabled'] ?? false ) {
2027 $fieldData[$fieldname] = $field->getDefault();
2028 } else {
2029 $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2030 }
2031 }
2032
2033 // Reset to default for fields that are supposed to be disabled.
2034 // FIXME: Handle dependency chains, fields that a field checks on may need a reset too.
2035 foreach ( $fieldData as $name => &$value ) {
2036 $field = $this->mFlatFields[$name];
2037 if ( $field->isDisabled( $fieldData ) ) {
2038 $value = $field->getDefault();
2039 }
2040 }
2041
2042 # Filter data.
2043 foreach ( $fieldData as $name => &$value ) {
2044 $field = $this->mFlatFields[$name];
2045 $value = $field->filter( $value, $fieldData );
2046 }
2047
2048 $this->mFieldData = $fieldData;
2049 }
2050
2061 public function filterDataForSubmit( $data ) {
2062 return $data;
2063 }
2064
2074 public function getLegend( $key ) {
2075 return $this->msg( $this->mMessagePrefix ? "{$this->mMessagePrefix}-$key" : $key )->text();
2076 }
2077
2088 public function setAction( $action ) {
2089 $this->mAction = $action;
2090
2091 return $this;
2092 }
2093
2101 public function getAction() {
2102 // If an action is already provided, return it
2103 if ( $this->mAction !== false ) {
2104 return $this->mAction;
2105 }
2106
2107 $articlePath = $this->getConfig()->get( MainConfigNames::ArticlePath );
2108 // Check whether we are in GET mode and the ArticlePath contains a "?"
2109 // meaning that getLocalURL() would return something like "index.php?title=...".
2110 // As browser remove the query string before submitting GET forms,
2111 // it means that the title would be lost. In such case use script path instead
2112 // and put title in a hidden field (see getHiddenFields()).
2113 if ( str_contains( $articlePath, '?' ) && $this->getMethod() === 'get' ) {
2114 return $this->getConfig()->get( MainConfigNames::Script );
2115 }
2116
2117 return $this->getTitle()->getLocalURL();
2118 }
2119
2130 public function setAutocomplete( $autocomplete ) {
2131 $this->mAutocomplete = $autocomplete;
2132
2133 return $this;
2134 }
2135
2142 protected function getMessage( $value ) {
2143 return Message::newFromSpecifier( $value )->setContext( $this );
2144 }
2145
2156 foreach ( $this->mFlatFields as $field ) {
2157 if ( $field->needsJSForHtml5FormValidation() ) {
2158 return true;
2159 }
2160 }
2161 return false;
2162 }
2163}
2164
2166class_alias( HTMLForm::class, 'HTMLForm' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:82
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
getCsrfTokenSet()
Get a repository to obtain and match CSRF tokens.
setContext(IContextSource $context)
Text field for selecting a value from a large list of possible values, with auto-completion and optio...
A checkbox matrix Operates similarly to HTMLMultiSelectField, but instead of using an array of option...
A field that will contain a date and/or time.
Expiry Field that allows the user to specify a precise date or a relative date string.
A field that will contain a numeric value.
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
An information field (text blob), not a proper input.
A field that must contain a number.
Implements a tag multiselect input field for namespaces.
Implements a tag multiselect input field with a searchable dropdown containing valid tags.
Double field with a dropdown list constructed from a system message in the format.
A limit dropdown, which accepts any valid number.
Creates a Html::namespaceSelector input field with a button assigned to the input field.
Wrapper for Html::namespaceSelector to use in HTMLForm.
Select dropdown field, with an additional "other" textbox.
A size filter field for use on query-type special pages.
Add a submit button inline in the form (as opposed to HTMLForm::addButton(), which will add it at the...
Wrapper for ChangeTags::buildTagFilterSelector to use in HTMLForm.
Implements a tag multiselect input field for arbitrary values.
Creates a text input field with a button assigned to the input field.
Dropdown widget that allows the user to select a timezone, either by choosing a geographic zone,...
Implements a text input field for page titles.
Implements a tag multiselect input field for titles.
Implements a text input field for user names.
Implements a tag multiselect input field for user names.
The parent class to generate form fields.
getDiv( $value)
Get the complete div for the input, including help text, labels, and whatever.
getTableRow( $value)
Get the complete table row for the input, including help text, labels, and whatever.
getRaw( $value)
Get the complete raw fields for the input, including help text, labels, and whatever.
getInline( $value)
Get the complete field as an inline element.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
wrapForm( $html)
Wrap the form innards in an actual "<form>" element.
displayForm( $submitResult)
Display the form (sending to the context's OutputPage object), with an appropriate error message or s...
setHeaderHtml( $html, $section=null)
Set header HTML, inside the form.
Definition HTMLForm.php:949
needsJSForHtml5FormValidation()
Whether this form, with its current fields, requires the user agent to have JavaScript enabled for th...
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
showCancel( $show=true)
Show a cancel button (or prevent it).
setMessagePrefix( $p)
Set the prefix for various default messages.
array[] $mSections
Additional information about form sections.
Definition HTMLForm.php:393
wrapFieldSetSection( $legend, $section, $attributes, $isRoot)
Wraps the given $section into a user-visible fieldset.
callable null $mSubmitCallback
Definition HTMLForm.php:282
getHeaderHtml( $section=null)
Get header HTML.
Definition HTMLForm.php:967
bool $mCollapsed
Whether the form is collapsed by default.
Definition HTMLForm.php:348
setFormIdentifier(string $ident, bool $single=false)
Set an internal identifier for this form.
trySubmit()
Validate all the fields, and call the submission callback function if everything is kosher.
Definition HTMLForm.php:758
array $availableSubclassDisplayFormats
Available formats in which to display the form.
Definition HTMLForm.php:427
setAction( $action)
Set the value for the action attribute of the form.
setPreHtml( $html)
Set the introductory message HTML, overwriting any existing message.
Definition HTMLForm.php:890
setTokenSalt( $salt)
Set the salt for the edit token.
setFooterHtml( $html, $section=null)
Set footer HTML, inside the form.
suppressDefaultSubmit( $suppressSubmit=true)
Stop a default submit button being shown for this form.
loadFieldData()
Load data of form fields from the request.
tryAuthorizedSubmit()
Try submitting, with edit token check first.
Definition HTMLForm.php:678
wasSubmitted()
Test whether the form was considered to have been submitted or not, i.e.
Definition HTMLForm.php:847
getFormAttributes()
Get HTML attributes for the <form> tag.
static getClassFromDescriptor( $fieldname, &$descriptor)
Get the HTMLFormField subclass for this descriptor.
Definition HTMLForm.php:605
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
prepareForm()
Prepare form for submission.
Definition HTMLForm.php:659
LinkTarget string null $mCancelTarget
Definition HTMLForm.php:279
bool $mCollapsible
Whether the form can be collapsed.
Definition HTMLForm.php:341
addHiddenField( $name, $value, array $attribs=[])
Add a hidden field to the output Array values are discarded for security reasons (per WebRequest::get...
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
setAutocomplete( $autocomplete)
Set the value for the autocomplete attribute of the form.
getBody()
Get the whole body of the form.
bool $mSubSectionBeforeFields
If true, sections that contain both fields and subsections will render their subsections before their...
Definition HTMLForm.php:403
string $displayFormat
Format in which to display form.
Definition HTMLForm.php:410
setWrapperAttributes( $attributes)
For internal use only.
string null $mAutocomplete
Form attribute autocomplete.
Definition HTMLForm.php:355
getDisplayFormat()
Getter for displayFormat.
Definition HTMLForm.php:585
formatSection(array $fieldsHtml, $sectionName, $anyFieldHasLabel)
Put a form section together from the individual fields' HTML, merging it and wrapping.
setDisplayFormat( $format)
Set format in which to display the form.
Definition HTMLForm.php:555
string array $mTokenSalt
Salt for the edit token.
Definition HTMLForm.php:379
getHTML( $submitResult)
Returns the raw HTML generated by the form.
addButton( $data)
Add a button to the form.
getHiddenFields()
Get the hidden fields that should go inside the form.
setTableId( $id)
Set the id of the <table> or outermost <div> element.
setWrapperLegend( $legend)
Prompt the whole form to be wrapped in a "<fieldset>", with this text as its "<legend>" element.
addFooterHtml( $html, $section=null)
Add footer HTML, inside the form.
Definition HTMLForm.php:980
showAlways()
Same as self::show with the difference, that the form will be added to the output,...
Definition HTMLForm.php:737
displaySection( $fields, $sectionName='', $fieldsetIDPrefix='', &$hasUserVisibleFields=false)
addFields( $descriptor)
Add fields to the form.
Definition HTMLForm.php:497
setSubmitText( $t)
Set the text for the submit button.
setPostHtml( $html)
Set HTML at the end of the display.
getFooterHtml( $section=null)
Get footer HTML.
setTitle( $t)
Set the title for form submission.
setSubmitCallback( $cb)
Set a callback to a function to do something with the form once it's been successfully validated.
Definition HTMLForm.php:861
getSubmitText()
Get the text for the submit button, either customised or a default.
addHiddenFields(array $fields)
Add an array of hidden fields to the output Array values are discarded for security reasons (per WebR...
show()
The here's-one-I-made-earlier option: do the submission if posted, or display the form with or withou...
Definition HTMLForm.php:719
formatField(HTMLFormField $field, $value)
Generate the HTML for an individual field in the current display format.
setSections( $sections)
Set an array of information about sections.
formatErrors( $errors)
Format a stack of error messages into a single HTML string.
setValidationErrorMessage( $msg)
Set a message to display on a validation error.
Definition HTMLForm.php:876
HTMLFormField[] $mFlatFields
Definition HTMLForm.php:269
setSubmitDestructive()
Identify that the submit button in the form has a destructive action.
static loadInputFromParameters( $fieldname, $descriptor, ?HTMLForm $parent=null)
Initialise a new Object for the field.
Definition HTMLForm.php:635
getErrorsOrWarnings( $elements, $elementsType)
Returns a formatted list of errors or warnings from the given elements.
addPreHtml( $html)
Add HTML to introductory message.
Definition HTMLForm.php:904
setSubmitID( $t)
Set the id for the submit button.
array $availableDisplayFormats
Available formats in which to display the form.
Definition HTMLForm.php:416
getPostHtml()
Get HTML at the end of the display.
getLegend( $key)
Get a string to go in the "<legend>" of a section fieldset.
setCancelTarget( $target)
Sets the target where the user is redirected to after clicking cancel.
addHeaderHtml( $html, $section=null)
Add HTML to the header, inside the form.
Definition HTMLForm.php:929
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition HTMLForm.php:452
setCollapsibleOptions( $collapsedByDefault=false)
Enable collapsible mode, and set whether the form is collapsed by default.
getAction()
Get the value for the action attribute of the form.
setMethod( $method='post')
Set the method used to submit the form.
getPreHtml()
Get the introductory message HTML.
Definition HTMLForm.php:916
getButtons()
Get the submit and (potentially) reset buttons.
static string[] $typeMappings
A mapping of 'type' inputs onto standard HTMLFormField subclasses.
Definition HTMLForm.php:213
filterDataForSubmit( $data)
Overload this if you want to apply special filtration routines to the form as a whole,...
__construct( $descriptor, IContextSource $context, $messagePrefix='')
Build a new HTMLForm from an array of field attributes.
Definition HTMLForm.php:480
string false $mAction
Form action URL.
Definition HTMLForm.php:334
addPostHtml( $html)
Add HTML to the end of the display.
Compact stacked vertical format for forms, implemented using OOUI widgets.
Compact stacked vertical format for forms.
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Some internal bits split of from Skin.php.
Definition Linker.php:61
A class containing constants representing the names of configuration variables.
const ArticlePath
Name constant for the ArticlePath setting, for use with Config::get()
const Script
Name constant for the Script setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:516
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:78
Generic operation result class Has warning/error list, boolean status and arbitrary value.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Value object representing a message parameter that consists of a list of values.
Interface for objects which can provide a MediaWiki context on request.
Represents the target of a wiki link.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
element(SerializerNode $parent, SerializerNode $node, $contents)