20use UnexpectedValueException;
46 private const VOID_ELEMENTS = [
68 private const BOOL_ATTRIBS = [
77 'formnovalidate' =>
true,
94 'typemustmatch' =>
true,
101 private const ATTRIBS_DEFAULTS = [
102 'area' => [
'shape' =>
'rect' ],
104 'formaction' =>
'GET',
105 'formenctype' =>
'application/x-www-form-urlencoded',
113 'autocomplete' =>
'on',
114 'enctype' =>
'application/x-www-form-urlencoded',
117 'formaction' =>
'GET',
120 'keygen' => [
'keytype' =>
'rsa' ],
123 'type' =>
'text/css',
125 'menu' => [
'type' =>
'list' ],
126 'script' => [
'type' =>
'text/javascript' ],
129 'type' =>
'text/css',
131 'textarea' => [
'wrap' =>
'soft' ],
138 private const SPACE_SEPARATED_LIST_ATTRIBUTES = [
147 private const INPUT_ELEMENT_VALID_TYPES = [
161 'datetime-local' =>
true,
210 public static function addClass( &$classes,
string $class ): void {
211 $classes = (array)$classes;
213 foreach ( $classes as $key => $val ) {
215 ( is_int( $key ) && is_string( $val ) ) ||
216 ( is_string( $key ) && is_bool( $val ) )
221 wfWarn( __METHOD__ .
": Argument doesn't look like a class array: " . var_export( $classes,
true ) );
237 public static function linkButton( $text, array $attrs, array $modifiers = [] ) {
255 public static function submitButton( $contents, array $attrs = [], array $modifiers = [] ) {
256 $attrs[
'type'] =
'submit';
257 $attrs[
'value'] = $contents;
283 public static function rawElement( $element, $attribs = [], $contents =
'' ) {
284 $start = self::openElement( $element, $attribs );
285 if ( isset( self::VOID_ELEMENTS[$element] ) ) {
288 $contents = Sanitizer::escapeCombiningChar( $contents ??
'' );
289 return $start . $contents . self::closeElement( $element );
309 public static function element( $element, $attribs = [], $contents =
'' ) {
310 return self::rawElement(
313 strtr( $contents ??
'', [
334 $attribs = (array)$attribs;
337 $element = strtolower( $element );
341 if ( str_contains( $element,
' ' ) ) {
342 wfWarn( __METHOD__ .
" given element name with space '$element'" );
346 if ( $element ==
'input' ) {
347 if ( isset( $attribs[
'type'] ) && !isset( self::INPUT_ELEMENT_VALID_TYPES[$attribs[
'type']] ) ) {
348 unset( $attribs[
'type'] );
355 if ( $element ==
'button' && !isset( $attribs[
'type'] ) ) {
356 $attribs[
'type'] =
'submit';
359 return "<$element" . self::expandAttributes(
360 self::dropDefaults( $element, $attribs ) ) .
'>';
371 $element = strtolower( $element );
373 return "</$element>";
393 private static function dropDefaults( $element, array $attribs ) {
394 foreach ( $attribs as $attrib => $value ) {
395 if ( $attrib ===
'class' ) {
396 if ( $value ===
'' || $value === [] || $value === [
'' ] ) {
397 unset( $attribs[$attrib] );
399 } elseif ( isset( self::ATTRIBS_DEFAULTS[$element][$attrib] ) ) {
400 if ( is_array( $value ) ) {
401 $value = implode(
' ', $value );
403 $value = strval( $value );
405 if ( self::ATTRIBS_DEFAULTS[$element][$attrib] == $value ) {
406 unset( $attribs[$attrib] );
412 if ( $element ===
'input' ) {
413 $type = $attribs[
'type'] ??
null;
414 $value = $attribs[
'value'] ??
null;
415 if ( $type ===
'checkbox' || $type ===
'radio' ) {
419 if ( $value ===
'on' ) {
420 unset( $attribs[
'value'] );
422 } elseif ( $type ===
'submit' ) {
430 if ( $value ===
'' ) {
431 unset( $attribs[
'value'] );
435 if ( $element ===
'select' && isset( $attribs[
'size'] ) ) {
436 $multiple = ( $attribs[
'multiple'] ?? false ) !==
false ||
437 in_array(
'multiple', $attribs );
438 $default = $multiple ? 4 : 1;
439 if ( (
int)$attribs[
'size'] === $default ) {
440 unset( $attribs[
'size'] );
460 if ( is_array( $classes ) ) {
463 foreach ( $classes as $k => $v ) {
464 if ( is_string( $v ) ) {
467 if ( !isset( $classes[$v] ) ) {
471 foreach ( explode(
' ', $v ) as $part ) {
474 if ( $part !==
'' && $part !==
' ' ) {
475 $arrayValue[] = $part;
486 $arrayValue = explode(
' ', $classes );
489 $arrayValue = array_diff( $arrayValue, [
'',
' ' ] );
493 return implode(
' ', array_unique( $arrayValue ) );
536 foreach ( $attribs as $key => $value ) {
538 if ( $value ===
false || $value ===
null ) {
544 if ( is_int( $key ) && isset( self::BOOL_ATTRIBS[strtolower( $value )] ) ) {
550 $key = strtolower( $key );
553 if ( isset( self::SPACE_SEPARATED_LIST_ATTRIBUTES[$key] ) ) {
555 $value = self::expandClassList( $value );
562 } elseif ( is_array( $value ) ) {
563 throw new UnexpectedValueException(
"HTML attribute $key can not contain a list of values" );
566 if ( isset( self::BOOL_ATTRIBS[$key] ) ) {
567 $ret .=
" $key=\"\"";
572 $encValue = htmlspecialchars( $value, ENT_QUOTES );
576 $encValue = strtr( $encValue, [
581 $ret .=
" $key=\"$encValue\"";
601 if ( preg_match(
'/<\/?script/i', $contents ) ) {
602 wfLogWarning( __METHOD__ .
': Illegal character sequence found in inline script.' );
603 $contents =
'/* ERROR: Invalid script */';
606 return self::rawElement(
'script', [], $contents );
618 $attrs = [
'src' =>
$url ];
619 if ( $nonce !==
null ) {
620 $attrs[
'nonce'] = $nonce;
621 } elseif ( ContentSecurityPolicy::isNonceRequired( MediaWikiServices::getInstance()->getMainConfig() ) ) {
622 wfWarn(
"no nonce set on script. CSP will break it" );
625 return self::element(
'script', $attrs );
640 public static function inlineStyle( $contents, $media =
'all', $attribs = [] ) {
645 $contents = strtr( $contents, [
649 ']]>' =>
'\5D\5D\3E '
652 if ( preg_match(
'/[<&]/', $contents ) ) {
653 $contents =
"/*<![CDATA[*/$contents/*]]>*/";
656 return self::rawElement(
'style', [
658 ] + $attribs, $contents );
670 return self::element(
'link', [
671 'rel' =>
'stylesheet',
688 public static function input( $name, $value =
'', $type =
'text', array $attribs = [] ) {
689 $attribs[
'type'] = $type;
690 $attribs[
'value'] = $value;
691 $attribs[
'name'] = $name;
692 return self::element(
'input', $attribs );
703 public static function check( $name, $checked =
false, array $attribs = [] ) {
704 $value = $attribs[
'value'] ?? 1;
705 unset( $attribs[
'value'] );
706 return self::element(
'input', [
708 'checked' => (
bool)$checked,
709 'type' =>
'checkbox',
726 private static function messageBox( $html, $className, $heading =
'', $iconClassName =
'', array $attribs = [] ) {
727 if ( $heading !==
'' ) {
728 $html = self::element(
'h2', [], $heading ) . $html;
730 self::addClass( $className,
'cdx-message' );
731 self::addClass( $className,
'cdx-message--block' );
732 return self::rawElement(
'div', array_merge( [
'class' => $className ], $attribs ),
733 self::element(
'span', [
'class' => [
737 self::rawElement(
'div', [
738 'class' =>
'cdx-message__content'
758 public static function noticeBox( $html, $className =
'', $heading =
'', $iconClassName =
'' ) {
759 return self::messageBox( $html, [
760 'cdx-message--notice',
762 ], $heading, $iconClassName );
779 public static function warningBox( $html, $className =
'' ) {
780 return self::messageBox( $html, [
781 'cdx-message--warning', $className ],
'',
'', [
'aria-live' =>
'polite' ] );
799 public static function errorBox( $html, $heading =
'', $className =
'' ) {
800 return self::messageBox( $html, [
801 'cdx-message--error', $className ], $heading,
'', [
'role' =>
'alert' ] );
818 public static function successBox( $html, $className =
'' ) {
819 return self::messageBox( $html, [
820 'cdx-message--success', $className ] );
831 public static function radio( $name, $checked =
false, array $attribs = [] ) {
832 $value = $attribs[
'value'] ?? 1;
833 unset( $attribs[
'value'] );
834 return self::element(
'input', [
836 'checked' => (
bool)$checked,
851 public static function label( $label, $id, array $attribs = [] ) {
855 return self::element(
'label', $attribs, $label );
867 public static function hidden( $name, $value, array $attribs = [] ) {
868 return self::element(
'input', [
888 public static function textarea( $name, $value =
'', array $attribs = [] ) {
889 $attribs[
'name'] = $name;
891 if ( str_starts_with( $value ??
'',
"\n" ) ) {
896 $spacedValue =
"\n" . $value;
898 $spacedValue = $value;
900 return self::element(
'textarea', $attribs, $spacedValue );
909 if ( !isset( $params[
'exclude'] ) || !is_array( $params[
'exclude'] ) ) {
910 $params[
'exclude'] = [];
913 if ( $params[
'in-user-lang'] ??
false ) {
914 $lang = RequestContext::getMain()->getLanguage();
916 $lang = MediaWikiServices::getInstance()->getContentLanguage();
920 if ( isset( $params[
'all'] ) ) {
923 $optionsOut[$params[
'all']] =
wfMessage(
'namespacesall' )->text();
926 $options = $lang->getFormattedNamespaces();
928 foreach ( $options as $nsId => $nsName ) {
929 if ( $nsId <
NS_MAIN || in_array( $nsId, $params[
'exclude'] ) ) {
933 isset( $params[
'include'] ) &&
934 is_array( $params[
'include'] ) &&
935 !in_array( $nsId, $params[
'include'] )
943 $nsName =
wfMessage(
'blanknamespace' )->text();
944 } elseif ( is_int( $nsId ) ) {
945 $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
946 ->getLanguageConverter( $lang );
947 $nsName = $converter->convertNamespace( $nsId );
949 $optionsOut[$nsId] = $nsName;
973 array $selectAttribs = []
975 ksort( $selectAttribs );
978 if ( isset( $params[
'selected'] ) ) {
981 if ( !is_int( $params[
'selected'] ) && ctype_digit( (
string)$params[
'selected'] ) ) {
982 $params[
'selected'] = (int)$params[
'selected'];
986 $params[
'selected'] =
'';
989 if ( !isset( $params[
'disable'] ) || !is_array( $params[
'disable'] ) ) {
990 $params[
'disable'] = [];
994 $options = self::namespaceSelectorOptions( $params );
998 foreach ( $options as $nsId => $nsName ) {
999 $optionsHtml[] = self::element(
1002 'disabled' => in_array( $nsId, $params[
'disable'] ),
1004 'selected' => $nsId === $params[
'selected'],
1010 $selectAttribs[
'id'] ??=
'namespace';
1011 $selectAttribs[
'name'] ??=
'namespace';
1014 if ( isset( $params[
'label'] ) ) {
1015 $label = self::element(
'label', [
'for' => $selectAttribs[
'id'] ],
1021 return $label . self::rawElement(
'select', $selectAttribs,
1022 "\n" . implode(
"\n", $optionsHtml ) .
"\n"
1036 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1037 $html5Version = $mainConfig->get( MainConfigNames::Html5Version );
1038 $mimeType = $mainConfig->get( MainConfigNames::MimeType );
1039 $xhtmlNamespaces = $mainConfig->get( MainConfigNames::XhtmlNamespaces );
1041 $isXHTML = self::isXmlMimeType( $mimeType );
1046 $ret .=
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
1049 $attribs[
'xmlns'] =
'http://www.w3.org/1999/xhtml';
1052 foreach ( $xhtmlNamespaces as $tag => $ns ) {
1053 $attribs[
"xmlns:$tag"] = $ns;
1056 $ret .=
"<!DOCTYPE html>\n";
1059 if ( $html5Version ) {
1060 $attribs[
'version'] = $html5Version;
1063 $ret .= self::openElement(
'html', $attribs );
1078 # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
1079 return (
bool)preg_match(
'!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
1104 public static function srcSet( array $urls ) {
1106 foreach ( $urls as $density =>
$url ) {
1109 $density = (string)(
float)$density;
1110 $candidates[$density] =
$url;
1114 ksort( $candidates, SORT_NUMERIC );
1115 $candidates = array_unique( $candidates );
1118 foreach ( $candidates as $density =>
$url ) {
1119 $candidates[$density] =
$url .
' ' . $density .
'x';
1122 return implode(
", ", $candidates );
1141 return $value->value;
1143 return FormatJson::encode( $value, $pretty, FormatJson::UTF8_OK );
1161 $encodedArgs = self::encodeJsList( $args, $pretty );
1162 if ( $encodedArgs ===
false ) {
1165 return "$name($encodedArgs);";
1178 foreach ( $args as &$arg ) {
1179 $arg = self::encodeJsVar( $arg, $pretty );
1180 if ( $arg ===
false ) {
1185 return ' ' . implode(
', ', $args ) .
' ';
1187 return implode(
',', $args );
1207 if ( isset( $params[
'other'] ) ) {
1208 $options[ $params[
'other'] ] =
'other';
1212 foreach ( explode(
"\n", $list ) as $option ) {
1213 $value = trim( $option );
1214 if ( $value ==
'' ) {
1217 if ( str_starts_with( $value,
'*' ) && !str_starts_with( $value,
'**' ) ) {
1218 # A new group is starting...
1219 $value = trim( substr( $value, 1 ) );
1220 if ( $value !==
'' &&
1222 ( !isset( $params[
'other'] ) || $value !== $params[
'other'] )
1228 } elseif ( str_starts_with( $value,
'**' ) ) {
1230 $opt = trim( substr( $value, 2 ) );
1231 if ( $optgroup ===
false ) {
1232 $options[$opt] = $opt;
1234 $options[$optgroup][$opt] = $opt;
1237 # groupless reason list
1239 $options[$option] = $option;
1257 foreach ( $options as $text => $value ) {
1258 if ( is_array( $value ) ) {
1259 $optionsOoui[] = [
'optgroup' => (string)$text ];
1260 foreach ( $value as $text2 => $value2 ) {
1261 $optionsOoui[] = [
'data' => (string)$value2,
'label' => (
string)$text2 ];
1264 $optionsOoui[] = [
'data' => (string)$value,
'label' => (
string)$text ];
1268 return $optionsOoui;
1282 foreach ( $options as $text => $value ) {
1283 if ( is_array( $value ) ) {
1285 'label' => (string)$text,
1286 'items' => array_map(
static function ( $text2, $value2 ) {
1287 return [
'label' => (string)$text2,
'value' => (
string)$value2 ];
1288 }, array_keys( $value ), $value )
1291 $optionsCodex[] = [
'label' => (string)$text,
'value' => (
string)$value ];
1294 return $optionsCodex;
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Group all the pieces relevant to the context of a request into one instance.
A class containing constants representing the names of configuration variables.
Handle sending Content-Security-Policy headers.