19use UnexpectedValueException;
45 private const VOID_ELEMENTS = [
67 private const BOOL_ATTRIBS = [
76 'formnovalidate' =>
true,
93 'typemustmatch' =>
true,
100 private const ATTRIBS_DEFAULTS = [
101 'area' => [
'shape' =>
'rect' ],
103 'formaction' =>
'GET',
104 'formenctype' =>
'application/x-www-form-urlencoded',
112 'autocomplete' =>
'on',
113 'enctype' =>
'application/x-www-form-urlencoded',
116 'formaction' =>
'GET',
119 'keygen' => [
'keytype' =>
'rsa' ],
122 'type' =>
'text/css',
124 'menu' => [
'type' =>
'list' ],
125 'script' => [
'type' =>
'text/javascript' ],
128 'type' =>
'text/css',
130 'textarea' => [
'wrap' =>
'soft' ],
137 private const SPACE_SEPARATED_LIST_ATTRIBUTES = [
146 private const INPUT_ELEMENT_VALID_TYPES = [
160 'datetime-local' =>
true,
209 public static function addClass( &$classes,
string $class ): void {
210 $classes = (array)$classes;
212 foreach ( $classes as $key => $val ) {
214 ( is_int( $key ) && is_string( $val ) ) ||
215 ( is_string( $key ) && is_bool( $val ) )
220 wfWarn( __METHOD__ .
": Argument doesn't look like a class array: " . var_export( $classes,
true ) );
236 public static function linkButton( $text, array $attrs, array $modifiers = [] ) {
254 public static function submitButton( $contents, array $attrs = [], array $modifiers = [] ) {
255 $attrs[
'type'] =
'submit';
256 $attrs[
'value'] = $contents;
282 public static function rawElement( $element, $attribs = [], $contents =
'' ) {
283 $start = self::openElement( $element, $attribs );
284 if ( isset( self::VOID_ELEMENTS[$element] ) ) {
287 $contents = Sanitizer::escapeCombiningChar( $contents ??
'' );
288 return $start . $contents . self::closeElement( $element );
308 public static function element( $element, $attribs = [], $contents =
'' ) {
309 return self::rawElement(
312 strtr( $contents ??
'', [
333 $attribs = (array)$attribs;
336 $element = strtolower( $element );
340 if ( str_contains( $element,
' ' ) ) {
341 wfWarn( __METHOD__ .
" given element name with space '$element'" );
345 if ( $element ==
'input' ) {
346 if ( isset( $attribs[
'type'] ) && !isset( self::INPUT_ELEMENT_VALID_TYPES[$attribs[
'type']] ) ) {
347 unset( $attribs[
'type'] );
354 if ( $element ==
'button' && !isset( $attribs[
'type'] ) ) {
355 $attribs[
'type'] =
'submit';
358 return "<$element" . self::expandAttributes(
359 self::dropDefaults( $element, $attribs ) ) .
'>';
370 $element = strtolower( $element );
372 return "</$element>";
392 private static function dropDefaults( $element, array $attribs ) {
393 foreach ( $attribs as $attrib => $value ) {
394 if ( $attrib ===
'class' ) {
395 if ( $value ===
'' || $value === [] || $value === [
'' ] ) {
396 unset( $attribs[$attrib] );
398 } elseif ( isset( self::ATTRIBS_DEFAULTS[$element][$attrib] ) ) {
399 if ( is_array( $value ) ) {
400 $value = implode(
' ', $value );
402 $value = strval( $value );
404 if ( self::ATTRIBS_DEFAULTS[$element][$attrib] == $value ) {
405 unset( $attribs[$attrib] );
411 if ( $element ===
'input' ) {
412 $type = $attribs[
'type'] ??
null;
413 $value = $attribs[
'value'] ??
null;
414 if ( $type ===
'checkbox' || $type ===
'radio' ) {
418 if ( $value ===
'on' ) {
419 unset( $attribs[
'value'] );
421 } elseif ( $type ===
'submit' ) {
429 if ( $value ===
'' ) {
430 unset( $attribs[
'value'] );
434 if ( $element ===
'select' && isset( $attribs[
'size'] ) ) {
435 $multiple = ( $attribs[
'multiple'] ?? false ) !==
false ||
436 in_array(
'multiple', $attribs );
437 $default = $multiple ? 4 : 1;
438 if ( (
int)$attribs[
'size'] === $default ) {
439 unset( $attribs[
'size'] );
459 if ( is_array( $classes ) ) {
462 foreach ( $classes as $k => $v ) {
463 if ( is_string( $v ) ) {
466 if ( !isset( $classes[$v] ) ) {
470 foreach ( explode(
' ', $v ) as $part ) {
473 if ( $part !==
'' && $part !==
' ' ) {
474 $arrayValue[] = $part;
485 $arrayValue = explode(
' ', $classes );
488 $arrayValue = array_diff( $arrayValue, [
'',
' ' ] );
492 return implode(
' ', array_unique( $arrayValue ) );
535 foreach ( $attribs as $key => $value ) {
537 if ( $value ===
false || $value ===
null ) {
543 if ( is_int( $key ) && isset( self::BOOL_ATTRIBS[strtolower( $value )] ) ) {
549 $key = strtolower( $key );
552 if ( isset( self::SPACE_SEPARATED_LIST_ATTRIBUTES[$key] ) ) {
554 $value = self::expandClassList( $value );
561 } elseif ( is_array( $value ) ) {
562 throw new UnexpectedValueException(
"HTML attribute $key can not contain a list of values" );
565 if ( isset( self::BOOL_ATTRIBS[$key] ) ) {
566 $ret .=
" $key=\"\"";
571 $encValue = htmlspecialchars( $value, ENT_QUOTES );
575 $encValue = strtr( $encValue, [
580 $ret .=
" $key=\"$encValue\"";
600 if ( preg_match(
'/<\/?script/i', $contents ) ) {
601 wfLogWarning( __METHOD__ .
': Illegal character sequence found in inline script.' );
602 $contents =
'/* ERROR: Invalid script */';
605 return self::rawElement(
'script', [], $contents );
617 $attrs = [
'src' =>
$url ];
618 if ( $nonce !==
null ) {
619 $attrs[
'nonce'] = $nonce;
620 } elseif ( ContentSecurityPolicy::isNonceRequired( MediaWikiServices::getInstance()->getMainConfig() ) ) {
621 wfWarn(
"no nonce set on script. CSP will break it" );
624 return self::element(
'script', $attrs );
639 public static function inlineStyle( $contents, $media =
'all', $attribs = [] ) {
644 $contents = strtr( $contents, [
648 ']]>' =>
'\5D\5D\3E '
651 if ( preg_match(
'/[<&]/', $contents ) ) {
652 $contents =
"/*<![CDATA[*/$contents/*]]>*/";
655 return self::rawElement(
'style', [
657 ] + $attribs, $contents );
669 return self::element(
'link', [
670 'rel' =>
'stylesheet',
687 public static function input( $name, $value =
'', $type =
'text', array $attribs = [] ) {
688 $attribs[
'type'] = $type;
689 $attribs[
'value'] = $value;
690 $attribs[
'name'] = $name;
691 return self::element(
'input', $attribs );
702 public static function check( $name, $checked =
false, array $attribs = [] ) {
703 $value = $attribs[
'value'] ?? 1;
704 unset( $attribs[
'value'] );
705 return self::element(
'input', [
707 'checked' => (
bool)$checked,
708 'type' =>
'checkbox',
724 private static function messageBox( $html, $className, $heading =
'', $iconClassName =
'' ) {
725 if ( $heading !==
'' ) {
726 $html = self::element(
'h2', [], $heading ) . $html;
728 self::addClass( $className,
'cdx-message' );
729 self::addClass( $className,
'cdx-message--block' );
730 return self::rawElement(
'div', [
'class' => $className ],
731 self::element(
'span', [
'class' => [
735 self::rawElement(
'div', [
736 'class' =>
'cdx-message__content'
756 public static function noticeBox( $html, $className =
'', $heading =
'', $iconClassName =
'' ) {
757 return self::messageBox( $html, [
758 'cdx-message--notice',
760 ], $heading, $iconClassName );
777 public static function warningBox( $html, $className =
'' ) {
778 return self::messageBox( $html, [
779 'cdx-message--warning', $className ] );
797 public static function errorBox( $html, $heading =
'', $className =
'' ) {
798 return self::messageBox( $html, [
799 'cdx-message--error', $className ], $heading );
816 public static function successBox( $html, $className =
'' ) {
817 return self::messageBox( $html, [
818 'cdx-message--success', $className ] );
829 public static function radio( $name, $checked =
false, array $attribs = [] ) {
830 $value = $attribs[
'value'] ?? 1;
831 unset( $attribs[
'value'] );
832 return self::element(
'input', [
834 'checked' => (
bool)$checked,
849 public static function label( $label, $id, array $attribs = [] ) {
853 return self::element(
'label', $attribs, $label );
865 public static function hidden( $name, $value, array $attribs = [] ) {
866 return self::element(
'input', [
886 public static function textarea( $name, $value =
'', array $attribs = [] ) {
887 $attribs[
'name'] = $name;
889 if ( str_starts_with( $value ??
'',
"\n" ) ) {
894 $spacedValue =
"\n" . $value;
896 $spacedValue = $value;
898 return self::element(
'textarea', $attribs, $spacedValue );
907 if ( !isset( $params[
'exclude'] ) || !is_array( $params[
'exclude'] ) ) {
908 $params[
'exclude'] = [];
911 if ( $params[
'in-user-lang'] ??
false ) {
915 $lang = MediaWikiServices::getInstance()->getContentLanguage();
919 if ( isset( $params[
'all'] ) ) {
922 $optionsOut[$params[
'all']] =
wfMessage(
'namespacesall' )->text();
925 $options = $lang->getFormattedNamespaces();
927 foreach ( $options as $nsId => $nsName ) {
928 if ( $nsId <
NS_MAIN || in_array( $nsId, $params[
'exclude'] ) ) {
932 isset( $params[
'include'] ) &&
933 is_array( $params[
'include'] ) &&
934 !in_array( $nsId, $params[
'include'] )
942 $nsName =
wfMessage(
'blanknamespace' )->text();
943 } elseif ( is_int( $nsId ) ) {
944 $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory()
945 ->getLanguageConverter( $lang );
946 $nsName = $converter->convertNamespace( $nsId );
948 $optionsOut[$nsId] = $nsName;
972 array $selectAttribs = []
974 ksort( $selectAttribs );
977 if ( isset( $params[
'selected'] ) ) {
980 if ( !is_int( $params[
'selected'] ) && ctype_digit( (
string)$params[
'selected'] ) ) {
981 $params[
'selected'] = (int)$params[
'selected'];
985 $params[
'selected'] =
'';
988 if ( !isset( $params[
'disable'] ) || !is_array( $params[
'disable'] ) ) {
989 $params[
'disable'] = [];
993 $options = self::namespaceSelectorOptions( $params );
997 foreach ( $options as $nsId => $nsName ) {
998 $optionsHtml[] = self::element(
1001 'disabled' => in_array( $nsId, $params[
'disable'] ),
1003 'selected' => $nsId === $params[
'selected'],
1009 $selectAttribs[
'id'] ??=
'namespace';
1010 $selectAttribs[
'name'] ??=
'namespace';
1013 if ( isset( $params[
'label'] ) ) {
1014 $label = self::element(
'label', [
'for' => $selectAttribs[
'id'] ],
1020 return $label . self::rawElement(
'select', $selectAttribs,
1021 "\n" . implode(
"\n", $optionsHtml ) .
"\n"
1035 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1036 $html5Version = $mainConfig->get( MainConfigNames::Html5Version );
1037 $mimeType = $mainConfig->get( MainConfigNames::MimeType );
1038 $xhtmlNamespaces = $mainConfig->get( MainConfigNames::XhtmlNamespaces );
1040 $isXHTML = self::isXmlMimeType( $mimeType );
1045 $ret .=
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
1048 $attribs[
'xmlns'] =
'http://www.w3.org/1999/xhtml';
1051 foreach ( $xhtmlNamespaces as $tag => $ns ) {
1052 $attribs[
"xmlns:$tag"] = $ns;
1055 $ret .=
"<!DOCTYPE html>\n";
1058 if ( $html5Version ) {
1059 $attribs[
'version'] = $html5Version;
1062 $ret .= self::openElement(
'html', $attribs );
1077 # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
1078 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(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgLang
if(!defined('MW_SETUP_CALLBACK'))
A class containing constants representing the names of configuration variables.
Handle sending Content-Security-Policy headers.