2011-07-04 19:51:35 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
class HtmlTest extends MediaWikiTestCase {
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
private $restoreWarnings;
|
2011-07-04 19:51:35 +00:00
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
protected function setUp() {
|
|
|
|
|
parent::setUp();
|
2012-08-30 09:56:21 +00:00
|
|
|
|
2016-03-09 16:47:58 +00:00
|
|
|
$this->setMwGlobals( [
|
2016-03-24 14:51:13 +00:00
|
|
|
'wgUseMediaWikiUIEverywhere' => false,
|
2016-03-09 16:47:58 +00:00
|
|
|
] );
|
|
|
|
|
|
2018-10-12 18:23:02 +00:00
|
|
|
$contLangObj = Language::factory( 'en' );
|
2012-01-25 03:25:54 +00:00
|
|
|
|
|
|
|
|
// Hardcode namespaces during test runs,
|
|
|
|
|
// so that html output based on existing namespaces
|
|
|
|
|
// can be properly evaluated.
|
2018-10-12 18:23:02 +00:00
|
|
|
$contLangObj->setNamespaces( [
|
2012-01-25 03:25:54 +00:00
|
|
|
-2 => 'Media',
|
|
|
|
|
-1 => 'Special',
|
2013-02-14 11:22:13 +00:00
|
|
|
0 => '',
|
|
|
|
|
1 => 'Talk',
|
|
|
|
|
2 => 'User',
|
|
|
|
|
3 => 'User_talk',
|
|
|
|
|
4 => 'MyWiki',
|
|
|
|
|
5 => 'MyWiki_Talk',
|
|
|
|
|
6 => 'File',
|
|
|
|
|
7 => 'File_talk',
|
|
|
|
|
8 => 'MediaWiki',
|
|
|
|
|
9 => 'MediaWiki_talk',
|
|
|
|
|
10 => 'Template',
|
|
|
|
|
11 => 'Template_talk',
|
|
|
|
|
14 => 'Category',
|
|
|
|
|
15 => 'Category_talk',
|
|
|
|
|
100 => 'Custom',
|
|
|
|
|
101 => 'Custom_talk',
|
2016-02-17 09:09:32 +00:00
|
|
|
] );
|
2018-10-12 18:23:02 +00:00
|
|
|
$this->setContentLang( $contLangObj );
|
|
|
|
|
|
|
|
|
|
$userLangObj = Language::factory( 'es' );
|
|
|
|
|
$userLangObj->setNamespaces( [
|
|
|
|
|
-2 => "Medio",
|
|
|
|
|
-1 => "Especial",
|
|
|
|
|
0 => "",
|
|
|
|
|
1 => "Discusión",
|
|
|
|
|
2 => "Usuario",
|
|
|
|
|
3 => "Usuario discusión",
|
|
|
|
|
4 => "Wiki",
|
|
|
|
|
5 => "Wiki discusión",
|
|
|
|
|
6 => "Archivo",
|
|
|
|
|
7 => "Archivo discusión",
|
|
|
|
|
8 => "MediaWiki",
|
|
|
|
|
9 => "MediaWiki discusión",
|
|
|
|
|
10 => "Plantilla",
|
|
|
|
|
11 => "Plantilla discusión",
|
|
|
|
|
12 => "Ayuda",
|
|
|
|
|
13 => "Ayuda discusión",
|
|
|
|
|
14 => "Categoría",
|
|
|
|
|
15 => "Categoría discusión",
|
|
|
|
|
100 => "Personalizado",
|
|
|
|
|
101 => "Personalizado discusión",
|
|
|
|
|
] );
|
|
|
|
|
$this->setUserLang( $userLangObj );
|
|
|
|
|
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
$this->restoreWarnings = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function tearDown() {
|
2018-10-10 04:41:51 +00:00
|
|
|
Language::factory( 'en' )->resetNamespaces();
|
|
|
|
|
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
if ( $this->restoreWarnings ) {
|
|
|
|
|
$this->restoreWarnings = false;
|
|
|
|
|
Wikimedia\restoreWarnings();
|
|
|
|
|
}
|
2018-10-10 04:41:51 +00:00
|
|
|
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
parent::tearDown();
|
2011-07-04 19:51:35 +00:00
|
|
|
}
|
2012-08-30 09:56:21 +00:00
|
|
|
|
2013-10-24 10:54:02 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::element
|
2017-04-01 01:13:09 +00:00
|
|
|
* @covers Html::rawElement
|
|
|
|
|
* @covers Html::openElement
|
|
|
|
|
* @covers Html::closeElement
|
2013-10-24 10:54:02 +00:00
|
|
|
*/
|
2012-10-08 10:59:55 +00:00
|
|
|
public function testElementBasics() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<img/>',
|
2012-10-08 10:59:55 +00:00
|
|
|
Html::element( 'img', null, '' ),
|
2016-05-18 19:54:49 +00:00
|
|
|
'Self-closing tag for short-tag elements'
|
2012-10-08 10:59:55 +00:00
|
|
|
);
|
2012-08-30 09:56:21 +00:00
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
'<element></element>',
|
|
|
|
|
Html::element( 'element', null, null ),
|
|
|
|
|
'Close tag for empty element (null, null)'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
'<element></element>',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::element( 'element', [], '' ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Close tag for empty element (array, string)'
|
|
|
|
|
);
|
2011-07-04 19:51:35 +00:00
|
|
|
}
|
|
|
|
|
|
2013-05-10 04:04:33 +00:00
|
|
|
public function dataXmlMimeType() {
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2013-05-10 04:04:33 +00:00
|
|
|
// ( $mimetype, $isXmlMimeType )
|
|
|
|
|
# HTML is not an XML MimeType
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'text/html', false ],
|
2013-05-10 04:04:33 +00:00
|
|
|
# XML is an XML MimeType
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'text/xml', true ],
|
|
|
|
|
[ 'application/xml', true ],
|
2013-05-10 04:04:33 +00:00
|
|
|
# XHTML is an XML MimeType
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'application/xhtml+xml', true ],
|
2013-05-10 04:04:33 +00:00
|
|
|
# Make sure other +xml MimeTypes are supported
|
|
|
|
|
# SVG is another random MimeType even though we don't use it
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'image/svg+xml', true ],
|
2013-05-10 04:04:33 +00:00
|
|
|
# Complete random other MimeTypes are not XML
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'text/plain', false ],
|
|
|
|
|
];
|
2013-05-10 04:04:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider dataXmlMimeType
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::isXmlMimeType
|
2013-05-10 04:04:33 +00:00
|
|
|
*/
|
|
|
|
|
public function testXmlMimeType( $mimetype, $isXmlMimeType ) {
|
|
|
|
|
$this->assertEquals( $isXmlMimeType, Html::isXmlMimeType( $mimetype ) );
|
|
|
|
|
}
|
|
|
|
|
|
2013-10-24 10:54:02 +00:00
|
|
|
/**
|
2016-02-09 21:00:38 +00:00
|
|
|
* @covers Html::expandAttributes
|
2013-10-24 10:54:02 +00:00
|
|
|
*/
|
2011-07-04 19:51:35 +00:00
|
|
|
public function testExpandAttributesSkipsNullAndFalse() {
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## EMPTY ########
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEmpty(
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'foo' => null ] ),
|
2011-07-04 19:51:35 +00:00
|
|
|
'skip keys with null value'
|
|
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEmpty(
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'foo' => false ] ),
|
2011-07-04 19:51:35 +00:00
|
|
|
'skip keys with false value'
|
|
|
|
|
);
|
2014-05-10 08:58:19 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
' foo=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'foo' => '' ] ),
|
2011-07-04 19:51:35 +00:00
|
|
|
'keep keys with an empty string'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2013-10-24 10:54:02 +00:00
|
|
|
/**
|
2016-02-09 21:00:38 +00:00
|
|
|
* @covers Html::expandAttributes
|
2013-10-24 10:54:02 +00:00
|
|
|
*/
|
2011-07-04 19:51:35 +00:00
|
|
|
public function testExpandAttributesForBooleans() {
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-07-04 19:51:35 +00:00
|
|
|
'',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'selected' => false ] ),
|
2011-07-04 19:51:35 +00:00
|
|
|
'Boolean attributes do not generates output when value is false'
|
|
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-07-04 19:51:35 +00:00
|
|
|
'',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'selected' => null ] ),
|
2011-07-04 19:51:35 +00:00
|
|
|
'Boolean attributes do not generates output when value is null'
|
|
|
|
|
);
|
|
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' selected=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'selected' => true ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Boolean attributes have no value when value is true'
|
2011-07-04 19:51:35 +00:00
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' selected=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'selected' ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Boolean attributes have no value when value is true (passed as numerical array)'
|
|
|
|
|
);
|
2011-07-04 19:51:35 +00:00
|
|
|
}
|
|
|
|
|
|
2014-05-10 08:58:19 +00:00
|
|
|
/**
|
2016-02-09 21:00:38 +00:00
|
|
|
* @covers Html::expandAttributes
|
2014-05-10 08:58:19 +00:00
|
|
|
*/
|
|
|
|
|
public function testExpandAttributesForNumbers() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' value="1"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'value' => 1 ] ),
|
2014-05-10 08:58:19 +00:00
|
|
|
'Integer value is cast to a string'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' value="1.1"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'value' => 1.1 ] ),
|
2014-05-10 08:58:19 +00:00
|
|
|
'Float value is cast to a string'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-02-09 21:00:38 +00:00
|
|
|
* @covers Html::expandAttributes
|
2014-05-10 08:58:19 +00:00
|
|
|
*/
|
|
|
|
|
public function testExpandAttributesForObjects() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' value="stringValue"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'value' => new HtmlTestValue() ] ),
|
2014-05-10 08:58:19 +00:00
|
|
|
'Object value is converted to a string'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 19:51:35 +00:00
|
|
|
/**
|
|
|
|
|
* Test for Html::expandAttributes()
|
|
|
|
|
* Please note it output a string prefixed with a space!
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::expandAttributes
|
2011-07-04 19:51:35 +00:00
|
|
|
*/
|
|
|
|
|
public function testExpandAttributesVariousExpansions() {
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## NOT EMPTY ####
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
' empty_string=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'empty_string' => '' ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Empty string is always quoted'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' key="value"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'key' => 'value' ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Simple string value needs no quotes'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' one="1"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'one' => 1 ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Number 1 value needs no quotes'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
' zero="0"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'zero' => 0 ] ),
|
2012-10-08 10:59:55 +00:00
|
|
|
'Number 0 value needs no quotes'
|
|
|
|
|
);
|
2011-07-04 19:51:35 +00:00
|
|
|
}
|
2011-09-03 03:55:23 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Html::expandAttributes has special features for HTML
|
|
|
|
|
* attributes that use space separated lists and also
|
|
|
|
|
* allows arrays to be used as values.
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::expandAttributes
|
2011-09-03 03:55:23 +00:00
|
|
|
*/
|
|
|
|
|
public function testExpandAttributesListValueAttributes() {
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## STRING VALUES
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class="redundant spaces here"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => ' redundant spaces here ' ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Normalization should strip redundant spaces'
|
|
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class="foo bar"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => 'foo bar foo bar bar' ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Normalization should remove duplicates in string-lists'
|
|
|
|
|
);
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## "EMPTY" ARRAY VALUES
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [] ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Value with an empty array'
|
|
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [ null, '', ' ', ' ' ] ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Array with null, empty string and spaces'
|
|
|
|
|
);
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## NON-EMPTY ARRAY VALUES
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class="foo bar"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [
|
2011-09-03 03:55:23 +00:00
|
|
|
'foo',
|
|
|
|
|
'bar',
|
|
|
|
|
'foo',
|
|
|
|
|
'bar',
|
|
|
|
|
'bar',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Normalization should remove duplicates in the array'
|
|
|
|
|
);
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals(
|
2011-09-03 03:55:23 +00:00
|
|
|
' class="foo bar"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [
|
2011-09-03 03:55:23 +00:00
|
|
|
'foo bar',
|
|
|
|
|
'bar foo',
|
|
|
|
|
'foo',
|
|
|
|
|
'bar bar',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ] ),
|
2011-09-03 03:55:23 +00:00
|
|
|
'Normalization should remove duplicates in string-lists in the array'
|
|
|
|
|
);
|
|
|
|
|
}
|
2011-10-24 17:45:45 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test feature added by r96188, let pass attributes values as
|
|
|
|
|
* a PHP array. Restricted to class,rel, accesskey.
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::expandAttributes
|
2011-10-24 17:45:45 +00:00
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testExpandAttributesSpaceSeparatedAttributesWithBoolean() {
|
2011-10-24 17:45:45 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
' class="booltrue one"',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [
|
2011-10-24 17:45:45 +00:00
|
|
|
'booltrue' => true,
|
|
|
|
|
'one' => 1,
|
|
|
|
|
|
|
|
|
|
# Method use isset() internally, make sure we do discard
|
2013-02-14 11:22:13 +00:00
|
|
|
# attributes values which have been assigned well known values
|
2011-10-24 17:45:45 +00:00
|
|
|
'emptystring' => '',
|
|
|
|
|
'boolfalse' => false,
|
|
|
|
|
'zero' => 0,
|
|
|
|
|
'null' => null,
|
2016-02-17 09:09:32 +00:00
|
|
|
] ] )
|
2011-10-24 17:45:45 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* How do we handle duplicate keys in HTML attributes expansion?
|
|
|
|
|
* We could pass a "class" the values: 'GREEN' and array( 'GREEN' => false )
|
2016-09-07 20:12:38 +00:00
|
|
|
* The latter will take precedence.
|
2011-10-24 17:45:45 +00:00
|
|
|
*
|
|
|
|
|
* Feature added by r96188
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::expandAttributes
|
2011-10-24 17:45:45 +00:00
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testValueIsAuthoritativeInSpaceSeparatedAttributesArrays() {
|
2011-10-24 17:45:45 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
' class=""',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [ 'class' => [
|
2011-10-24 17:45:45 +00:00
|
|
|
'GREEN',
|
|
|
|
|
'GREEN' => false,
|
|
|
|
|
'GREEN',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ] )
|
2011-10-24 17:45:45 +00:00
|
|
|
);
|
|
|
|
|
}
|
2012-01-25 03:25:54 +00:00
|
|
|
|
2014-05-10 08:58:19 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::expandAttributes
|
|
|
|
|
* @expectedException MWException
|
|
|
|
|
*/
|
|
|
|
|
public function testExpandAttributes_ArrayOnNonListValueAttribute_ThrowsException() {
|
|
|
|
|
// Real-life test case found in the Popups extension (see Gerrit cf0fd64),
|
|
|
|
|
// when used with an outdated BetaFeatures extension (see Gerrit deda1e7)
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::expandAttributes( [
|
|
|
|
|
'src' => [
|
2014-05-10 08:58:19 +00:00
|
|
|
'ltr' => 'ltr.svg',
|
|
|
|
|
'rtl' => 'rtl.svg'
|
2016-02-17 09:09:32 +00:00
|
|
|
]
|
|
|
|
|
] );
|
2014-05-10 08:58:19 +00:00
|
|
|
}
|
|
|
|
|
|
2013-10-24 10:54:02 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::namespaceSelector
|
2017-04-01 01:13:09 +00:00
|
|
|
* @covers Html::namespaceSelectorOptions
|
2013-10-24 10:54:02 +00:00
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testNamespaceSelector() {
|
2012-01-25 03:25:54 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<select id="namespace" name="namespace">' . "\n" .
|
2018-10-12 18:23:02 +00:00
|
|
|
'<option value="0">(Principal)</option>' . "\n" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<option value="1">Talk</option>' . "\n" .
|
|
|
|
|
'<option value="2">User</option>' . "\n" .
|
|
|
|
|
'<option value="3">User talk</option>' . "\n" .
|
|
|
|
|
'<option value="4">MyWiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">MyWiki Talk</option>' . "\n" .
|
|
|
|
|
'<option value="6">File</option>' . "\n" .
|
|
|
|
|
'<option value="7">File talk</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki talk</option>' . "\n" .
|
|
|
|
|
'<option value="10">Template</option>' . "\n" .
|
|
|
|
|
'<option value="11">Template talk</option>' . "\n" .
|
|
|
|
|
'<option value="14">Category</option>' . "\n" .
|
|
|
|
|
'<option value="15">Category talk</option>' . "\n" .
|
|
|
|
|
'<option value="100">Custom</option>' . "\n" .
|
|
|
|
|
'<option value="101">Custom talk</option>' . "\n" .
|
2013-02-14 11:22:13 +00:00
|
|
|
'</select>',
|
2012-01-25 03:25:54 +00:00
|
|
|
Html::namespaceSelector(),
|
|
|
|
|
'Basic namespace selector without custom options'
|
|
|
|
|
);
|
2012-03-07 19:14:20 +00:00
|
|
|
|
2012-01-25 03:25:54 +00:00
|
|
|
$this->assertEquals(
|
2016-12-27 21:14:16 +00:00
|
|
|
'<label for="mw-test-namespace">Select a namespace:</label>' . "\u{00A0}" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
|
2018-10-12 18:23:02 +00:00
|
|
|
'<option value="all">todos</option>' . "\n" .
|
|
|
|
|
'<option value="0">(Principal)</option>' . "\n" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<option value="1">Talk</option>' . "\n" .
|
|
|
|
|
'<option value="2" selected="">User</option>' . "\n" .
|
|
|
|
|
'<option value="3">User talk</option>' . "\n" .
|
|
|
|
|
'<option value="4">MyWiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">MyWiki Talk</option>' . "\n" .
|
|
|
|
|
'<option value="6">File</option>' . "\n" .
|
|
|
|
|
'<option value="7">File talk</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki talk</option>' . "\n" .
|
|
|
|
|
'<option value="10">Template</option>' . "\n" .
|
|
|
|
|
'<option value="11">Template talk</option>' . "\n" .
|
|
|
|
|
'<option value="14">Category</option>' . "\n" .
|
|
|
|
|
'<option value="15">Category talk</option>' . "\n" .
|
|
|
|
|
'<option value="100">Custom</option>' . "\n" .
|
|
|
|
|
'<option value="101">Custom talk</option>' . "\n" .
|
2013-02-14 11:22:13 +00:00
|
|
|
'</select>',
|
2012-01-25 03:25:54 +00:00
|
|
|
Html::namespaceSelector(
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ],
|
|
|
|
|
[ 'name' => 'wpNamespace', 'id' => 'mw-test-namespace' ]
|
2012-01-25 03:25:54 +00:00
|
|
|
),
|
|
|
|
|
'Basic namespace selector with custom values'
|
|
|
|
|
);
|
2012-02-14 09:59:59 +00:00
|
|
|
|
2012-03-07 19:14:20 +00:00
|
|
|
$this->assertEquals(
|
2016-12-27 21:14:16 +00:00
|
|
|
'<label for="namespace">Select a namespace:</label>' . "\u{00A0}" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<select id="namespace" name="namespace">' . "\n" .
|
2018-10-12 18:23:02 +00:00
|
|
|
'<option value="0">(Principal)</option>' . "\n" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<option value="1">Talk</option>' . "\n" .
|
|
|
|
|
'<option value="2">User</option>' . "\n" .
|
|
|
|
|
'<option value="3">User talk</option>' . "\n" .
|
|
|
|
|
'<option value="4">MyWiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">MyWiki Talk</option>' . "\n" .
|
|
|
|
|
'<option value="6">File</option>' . "\n" .
|
|
|
|
|
'<option value="7">File talk</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki talk</option>' . "\n" .
|
|
|
|
|
'<option value="10">Template</option>' . "\n" .
|
|
|
|
|
'<option value="11">Template talk</option>' . "\n" .
|
|
|
|
|
'<option value="14">Category</option>' . "\n" .
|
|
|
|
|
'<option value="15">Category talk</option>' . "\n" .
|
|
|
|
|
'<option value="100">Custom</option>' . "\n" .
|
|
|
|
|
'<option value="101">Custom talk</option>' . "\n" .
|
2013-02-14 11:22:13 +00:00
|
|
|
'</select>',
|
2012-03-07 19:14:20 +00:00
|
|
|
Html::namespaceSelector(
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'label' => 'Select a namespace:' ]
|
2012-03-07 19:14:20 +00:00
|
|
|
),
|
|
|
|
|
'Basic namespace selector with a custom label but no id attribtue for the <select>'
|
|
|
|
|
);
|
2018-09-10 20:14:20 +00:00
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
'<select id="namespace" name="namespace">' . "\n" .
|
|
|
|
|
'<option value="0">(Principal)</option>' . "\n" .
|
|
|
|
|
'<option value="1">Discusión</option>' . "\n" .
|
|
|
|
|
'<option value="2">Usuario</option>' . "\n" .
|
|
|
|
|
'<option value="3">Usuario discusión</option>' . "\n" .
|
|
|
|
|
'<option value="4">Wiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">Wiki discusión</option>' . "\n" .
|
|
|
|
|
'<option value="6">Archivo</option>' . "\n" .
|
|
|
|
|
'<option value="7">Archivo discusión</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki discusión</option>' . "\n" .
|
|
|
|
|
'<option value="10">Plantilla</option>' . "\n" .
|
|
|
|
|
'<option value="11">Plantilla discusión</option>' . "\n" .
|
|
|
|
|
'<option value="12">Ayuda</option>' . "\n" .
|
|
|
|
|
'<option value="13">Ayuda discusión</option>' . "\n" .
|
|
|
|
|
'<option value="14">Categoría</option>' . "\n" .
|
|
|
|
|
'<option value="15">Categoría discusión</option>' . "\n" .
|
|
|
|
|
'<option value="100">Personalizado</option>' . "\n" .
|
|
|
|
|
'<option value="101">Personalizado discusión</option>' . "\n" .
|
|
|
|
|
'</select>',
|
|
|
|
|
Html::namespaceSelector(
|
|
|
|
|
[ 'in-user-lang' => true ]
|
|
|
|
|
),
|
|
|
|
|
'Basic namespace selector in user language'
|
|
|
|
|
);
|
2012-03-07 19:14:20 +00:00
|
|
|
}
|
|
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::namespaceSelector
|
|
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testCanFilterOutNamespaces() {
|
2012-03-07 19:14:20 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<select id="namespace" name="namespace">' . "\n" .
|
|
|
|
|
'<option value="2">User</option>' . "\n" .
|
|
|
|
|
'<option value="4">MyWiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">MyWiki Talk</option>' . "\n" .
|
|
|
|
|
'<option value="6">File</option>' . "\n" .
|
|
|
|
|
'<option value="7">File talk</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki talk</option>' . "\n" .
|
|
|
|
|
'<option value="10">Template</option>' . "\n" .
|
|
|
|
|
'<option value="11">Template talk</option>' . "\n" .
|
|
|
|
|
'<option value="14">Category</option>' . "\n" .
|
|
|
|
|
'<option value="15">Category talk</option>' . "\n" .
|
2013-02-14 11:22:13 +00:00
|
|
|
'</select>',
|
2012-02-13 15:08:26 +00:00
|
|
|
Html::namespaceSelector(
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'exclude' => [ 0, 1, 3, 100, 101 ] ]
|
2012-02-13 15:08:26 +00:00
|
|
|
),
|
2012-02-14 09:59:59 +00:00
|
|
|
'Namespace selector namespace filtering.'
|
|
|
|
|
);
|
2012-03-07 19:14:20 +00:00
|
|
|
}
|
2012-02-14 09:59:59 +00:00
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::namespaceSelector
|
|
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testCanDisableANamespaces() {
|
2012-03-07 19:14:20 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<select id="namespace" name="namespace">' . "\n" .
|
2018-10-12 18:23:02 +00:00
|
|
|
'<option disabled="" value="0">(Principal)</option>' . "\n" .
|
2016-04-20 17:22:51 +00:00
|
|
|
'<option disabled="" value="1">Talk</option>' . "\n" .
|
|
|
|
|
'<option disabled="" value="2">User</option>' . "\n" .
|
|
|
|
|
'<option disabled="" value="3">User talk</option>' . "\n" .
|
|
|
|
|
'<option disabled="" value="4">MyWiki</option>' . "\n" .
|
|
|
|
|
'<option value="5">MyWiki Talk</option>' . "\n" .
|
|
|
|
|
'<option value="6">File</option>' . "\n" .
|
|
|
|
|
'<option value="7">File talk</option>' . "\n" .
|
|
|
|
|
'<option value="8">MediaWiki</option>' . "\n" .
|
|
|
|
|
'<option value="9">MediaWiki talk</option>' . "\n" .
|
|
|
|
|
'<option value="10">Template</option>' . "\n" .
|
|
|
|
|
'<option value="11">Template talk</option>' . "\n" .
|
|
|
|
|
'<option value="14">Category</option>' . "\n" .
|
|
|
|
|
'<option value="15">Category talk</option>' . "\n" .
|
|
|
|
|
'<option value="100">Custom</option>' . "\n" .
|
|
|
|
|
'<option value="101">Custom talk</option>' . "\n" .
|
2013-02-14 11:22:13 +00:00
|
|
|
'</select>',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::namespaceSelector( [
|
|
|
|
|
'disable' => [ 0, 1, 2, 3, 4 ]
|
|
|
|
|
] ),
|
2012-02-14 09:59:59 +00:00
|
|
|
'Namespace selector namespace disabling'
|
2012-02-13 15:08:26 +00:00
|
|
|
);
|
2012-01-25 03:25:54 +00:00
|
|
|
}
|
2012-01-30 10:39:51 +00:00
|
|
|
|
2012-08-30 09:57:22 +00:00
|
|
|
/**
|
2012-10-08 10:59:55 +00:00
|
|
|
* @dataProvider provideHtml5InputTypes
|
2013-10-24 10:54:02 +00:00
|
|
|
* @covers Html::element
|
2012-08-30 09:57:22 +00:00
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
|
2012-08-30 09:57:22 +00:00
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="' . $HTML5InputType . '"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::element( 'input', [ 'type' => $HTML5InputType ] ),
|
2016-02-09 21:00:38 +00:00
|
|
|
'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"'
|
2012-08-30 09:57:22 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-15 19:11:02 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::warningBox
|
|
|
|
|
* @covers Html::messageBox
|
|
|
|
|
*/
|
|
|
|
|
public function testWarningBox() {
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::warningBox( 'warn' ),
|
|
|
|
|
'<div class="warningbox">warn</div>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Html::errorBox
|
|
|
|
|
* @covers Html::messageBox
|
|
|
|
|
*/
|
|
|
|
|
public function testErrorBox() {
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::errorBox( 'err' ),
|
|
|
|
|
'<div class="errorbox">err</div>'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::errorBox( 'err', 'heading' ),
|
|
|
|
|
'<div class="errorbox"><h2>heading</h2>err</div>'
|
|
|
|
|
);
|
2018-08-20 04:46:21 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::errorBox( 'err', '0' ),
|
|
|
|
|
'<div class="errorbox"><h2>0</h2>err</div>'
|
|
|
|
|
);
|
2017-11-15 19:11:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Html::successBox
|
|
|
|
|
* @covers Html::messageBox
|
|
|
|
|
*/
|
|
|
|
|
public function testSuccessBox() {
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::successBox( 'great' ),
|
|
|
|
|
'<div class="successbox">great</div>'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
Html::successBox( '<script>beware no escaping!</script>' ),
|
|
|
|
|
'<div class="successbox"><script>beware no escaping!</script></div>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-30 09:57:22 +00:00
|
|
|
/**
|
|
|
|
|
* List of input element types values introduced by HTML5
|
2016-10-13 05:34:26 +00:00
|
|
|
* Full list at https://www.w3.org/TR/html-markup/input.html
|
2012-08-30 09:57:22 +00:00
|
|
|
*/
|
2013-03-22 02:12:37 +00:00
|
|
|
public static function provideHtml5InputTypes() {
|
2016-02-17 09:09:32 +00:00
|
|
|
$types = [
|
2012-08-30 09:57:22 +00:00
|
|
|
'datetime',
|
|
|
|
|
'datetime-local',
|
|
|
|
|
'date',
|
|
|
|
|
'month',
|
|
|
|
|
'time',
|
|
|
|
|
'week',
|
|
|
|
|
'number',
|
|
|
|
|
'range',
|
|
|
|
|
'email',
|
|
|
|
|
'url',
|
|
|
|
|
'search',
|
|
|
|
|
'tel',
|
|
|
|
|
'color',
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
|
|
|
|
$cases = [];
|
2013-02-14 11:22:13 +00:00
|
|
|
foreach ( $types as $type ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases[] = [ $type ];
|
2012-08-30 09:57:22 +00:00
|
|
|
}
|
2013-04-26 12:00:22 +00:00
|
|
|
|
2012-08-30 09:57:22 +00:00
|
|
|
return $cases;
|
|
|
|
|
}
|
2012-08-16 07:37:56 +00:00
|
|
|
|
|
|
|
|
/**
|
2012-10-30 19:06:16 +00:00
|
|
|
* Test out Html::element drops or enforces default value
|
2013-03-11 03:16:28 +00:00
|
|
|
* @covers Html::dropDefaults
|
2012-08-16 07:37:56 +00:00
|
|
|
* @dataProvider provideElementsWithAttributesHavingDefaultValues
|
|
|
|
|
*/
|
2013-10-23 22:51:31 +00:00
|
|
|
public function testDropDefaults( $expected, $element, $attribs, $message = '' ) {
|
2012-10-08 10:59:55 +00:00
|
|
|
$this->assertEquals( $expected, Html::element( $element, $attribs ), $message );
|
2012-08-16 07:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
public static function provideElementsWithAttributesHavingDefaultValues() {
|
2012-08-16 07:37:56 +00:00
|
|
|
# Use cases in a concise format:
|
|
|
|
|
# <expected>, <element name>, <array of attributes> [, <message>]
|
|
|
|
|
# Will be mapped to Html::element()
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases = [];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## Generic cases, match $attribDefault static array
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<area/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'area', [ 'shape' => 'rect' ]
|
|
|
|
|
];
|
|
|
|
|
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<button type="submit"></button>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'button', [ 'formaction' => 'GET' ]
|
|
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<button type="submit"></button>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<canvas></canvas>',
|
|
|
|
|
'canvas', [ 'height' => '150' ]
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<canvas></canvas>',
|
|
|
|
|
'canvas', [ 'width' => '300' ]
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
# Also check with numeric values
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases[] = [ '<canvas></canvas>',
|
|
|
|
|
'canvas', [ 'height' => 150 ]
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<canvas></canvas>',
|
|
|
|
|
'canvas', [ 'width' => 300 ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<form></form>',
|
|
|
|
|
'form', [ 'action' => 'GET' ]
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<form></form>',
|
|
|
|
|
'form', [ 'autocomplete' => 'on' ]
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<form></form>',
|
|
|
|
|
'form', [ 'enctype' => 'application/x-www-form-urlencoded' ]
|
|
|
|
|
];
|
|
|
|
|
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'formaction' => 'GET' ]
|
|
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'text' ]
|
|
|
|
|
];
|
|
|
|
|
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<keygen/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'keygen', [ 'keytype' => 'rsa' ]
|
|
|
|
|
];
|
|
|
|
|
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<link/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'link', [ 'media' => 'all' ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<menu></menu>',
|
|
|
|
|
'menu', [ 'type' => 'list' ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<script></script>',
|
|
|
|
|
'script', [ 'type' => 'text/javascript' ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<style></style>',
|
|
|
|
|
'style', [ 'media' => 'all' ]
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<style></style>',
|
|
|
|
|
'style', [ 'type' => 'text/css' ]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$cases[] = [ '<textarea></textarea>',
|
|
|
|
|
'textarea', [ 'wrap' => 'soft' ]
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
2015-09-11 13:44:59 +00:00
|
|
|
# ## SPECIFIC CASES
|
2012-08-30 10:49:57 +00:00
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
# <link type="text/css">
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<link/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'link', [ 'type' => 'text/css' ]
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
2012-10-08 10:59:55 +00:00
|
|
|
# <input> specific handling
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input type="checkbox"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'checkbox', 'value' => 'on' ],
|
2012-08-16 07:37:56 +00:00
|
|
|
'Default value "on" is stripped of checkboxes',
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input type="radio"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'radio', 'value' => 'on' ],
|
2012-08-16 07:37:56 +00:00
|
|
|
'Default value "on" is stripped of radio buttons',
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input type="submit" value="Submit"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'submit', 'value' => 'Submit' ],
|
2012-08-16 07:37:56 +00:00
|
|
|
'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input type="color"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'color', 'value' => '' ],
|
|
|
|
|
];
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<input type="range"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'input', [ 'type' => 'range', 'value' => '' ],
|
|
|
|
|
];
|
2012-08-16 07:37:56 +00:00
|
|
|
|
2012-10-30 19:06:16 +00:00
|
|
|
# <button> specific handling
|
2017-06-09 09:55:28 +00:00
|
|
|
# see remarks on https://msdn.microsoft.com/library/ms535211(v=vs.85).aspx
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<button type="submit"></button>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'button', [ 'type' => 'submit' ],
|
2014-04-24 10:05:52 +00:00
|
|
|
'According to standard the default type is "submit". '
|
|
|
|
|
. 'Depending on compatibility mode IE might use "button", instead.',
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2012-10-30 19:06:16 +00:00
|
|
|
|
2014-12-12 08:41:27 +00:00
|
|
|
# <select> specific handling
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<select multiple=""></select>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'select', [ 'size' => '4', 'multiple' => true ],
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
# .. with numeric value
|
2016-04-20 17:22:51 +00:00
|
|
|
$cases[] = [ '<select multiple=""></select>',
|
2016-02-17 09:09:32 +00:00
|
|
|
'select', [ 'size' => 4, 'multiple' => true ],
|
|
|
|
|
];
|
|
|
|
|
$cases[] = [ '<select></select>',
|
|
|
|
|
'select', [ 'size' => '1', 'multiple' => false ],
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
# .. with numeric value
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases[] = [ '<select></select>',
|
|
|
|
|
'select', [ 'size' => 1, 'multiple' => false ],
|
|
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
|
|
|
|
# Passing an array as value
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases[] = [ '<a class="css-class-one css-class-two"></a>',
|
|
|
|
|
'a', [ 'class' => [ 'css-class-one', 'css-class-two' ] ],
|
2012-08-30 10:49:57 +00:00
|
|
|
"dropDefaults accepts values given as an array"
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
|
|
|
|
# FIXME: doDropDefault should remove defaults given in an array
|
|
|
|
|
# Expected should be '<a></a>'
|
2016-02-17 09:09:32 +00:00
|
|
|
$cases[] = [ '<a class=""></a>',
|
|
|
|
|
'a', [ 'class' => [ '', '' ] ],
|
2012-08-30 10:49:57 +00:00
|
|
|
"dropDefaults accepts values given as an array"
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2012-08-30 10:49:57 +00:00
|
|
|
|
2012-08-16 07:37:56 +00:00
|
|
|
# Craft the Html elements
|
2016-02-17 09:09:32 +00:00
|
|
|
$ret = [];
|
2013-02-14 11:22:13 +00:00
|
|
|
foreach ( $cases as $case ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$ret[] = [
|
2012-08-16 07:37:56 +00:00
|
|
|
$case[0],
|
2012-10-08 10:59:55 +00:00
|
|
|
$case[1], $case[2],
|
2017-10-06 22:17:58 +00:00
|
|
|
$case[3] ?? ''
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2012-08-16 07:37:56 +00:00
|
|
|
}
|
2013-04-26 12:00:22 +00:00
|
|
|
|
2012-08-16 07:37:56 +00:00
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::input
|
|
|
|
|
*/
|
2013-03-07 19:03:44 +00:00
|
|
|
public function testWrapperInput() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="radio" value="testval" name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::input( 'testname', 'testval', 'radio' ),
|
|
|
|
|
'Input wrapper with type and value.'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::input( 'testname' ),
|
|
|
|
|
'Input wrapper with all default values.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::check
|
|
|
|
|
*/
|
2013-03-07 19:03:44 +00:00
|
|
|
public function testWrapperCheck() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="checkbox" value="1" name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::check( 'testname' ),
|
|
|
|
|
'Checkbox wrapper unchecked.'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input checked="" type="checkbox" value="1" name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::check( 'testname', true ),
|
|
|
|
|
'Checkbox wrapper checked.'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="checkbox" value="testval" name="testname"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::check( 'testname', false, [ 'value' => 'testval' ] ),
|
2013-03-07 19:03:44 +00:00
|
|
|
'Checkbox wrapper with a value override.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::radio
|
|
|
|
|
*/
|
2013-03-07 19:03:44 +00:00
|
|
|
public function testWrapperRadio() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="radio" value="1" name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::radio( 'testname' ),
|
|
|
|
|
'Radio wrapper unchecked.'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input checked="" type="radio" value="1" name="testname"/>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::radio( 'testname', true ),
|
|
|
|
|
'Radio wrapper checked.'
|
|
|
|
|
);
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<input type="radio" value="testval" name="testname"/>',
|
2016-02-17 09:09:32 +00:00
|
|
|
Html::radio( 'testname', false, [ 'value' => 'testval' ] ),
|
2013-03-07 19:03:44 +00:00
|
|
|
'Radio wrapper with a value override.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Html::label
|
|
|
|
|
*/
|
2013-03-07 19:03:44 +00:00
|
|
|
public function testWrapperLabel() {
|
|
|
|
|
$this->assertEquals(
|
2016-04-20 17:22:51 +00:00
|
|
|
'<label for="testid">testlabel</label>',
|
2013-03-07 19:03:44 +00:00
|
|
|
Html::label( 'testlabel', 'testid' ),
|
|
|
|
|
'Label wrapper'
|
|
|
|
|
);
|
|
|
|
|
}
|
2015-04-03 23:17:13 +00:00
|
|
|
|
|
|
|
|
public static function provideSrcSetImages() {
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
|
|
|
|
[ [], '', 'when there are no images, return empty string' ],
|
|
|
|
|
[
|
|
|
|
|
[ '1x' => '1x.png', '1.5x' => '1_5x.png', '2x' => '2x.png' ],
|
2015-04-03 23:17:13 +00:00
|
|
|
'1x.png 1x, 1_5x.png 1.5x, 2x.png 2x',
|
|
|
|
|
'pixel depth keys may include a trailing "x"'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ '1' => '1x.png', '1.5' => '1_5x.png', '2' => '2x.png' ],
|
2015-04-03 23:17:13 +00:00
|
|
|
'1x.png 1x, 1_5x.png 1.5x, 2x.png 2x',
|
|
|
|
|
'pixel depth keys may omit a trailing "x"'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2016-07-18 12:52:08 +00:00
|
|
|
[
|
|
|
|
|
[ '1' => 'small.png', '1.5' => 'large.png', '2' => 'large.png' ],
|
|
|
|
|
'small.png 1x, large.png 1.5x',
|
|
|
|
|
'omit larger duplicates'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ '1' => 'small.png', '2' => 'large.png', '1.5' => 'large.png' ],
|
|
|
|
|
'small.png 1x, large.png 1.5x',
|
|
|
|
|
'omit larger duplicates in irregular order'
|
|
|
|
|
],
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2015-04-03 23:17:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideSrcSetImages
|
|
|
|
|
* @covers Html::srcSet
|
|
|
|
|
*/
|
|
|
|
|
public function testSrcSet( $images, $expected, $message ) {
|
|
|
|
|
$this->assertEquals( Html::srcSet( $images ), $expected, $message );
|
|
|
|
|
}
|
2018-08-07 15:16:21 +00:00
|
|
|
|
|
|
|
|
public static function provideInlineScript() {
|
|
|
|
|
return [
|
|
|
|
|
'Empty' => [
|
|
|
|
|
'',
|
|
|
|
|
'<script></script>'
|
|
|
|
|
],
|
|
|
|
|
'Simple' => [
|
|
|
|
|
'EXAMPLE.label("foo");',
|
|
|
|
|
'<script>EXAMPLE.label("foo");</script>'
|
|
|
|
|
],
|
|
|
|
|
'Ampersand' => [
|
|
|
|
|
'EXAMPLE.is(a && b);',
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
'<script>EXAMPLE.is(a && b);</script>'
|
2018-08-07 15:16:21 +00:00
|
|
|
],
|
|
|
|
|
'HTML' => [
|
|
|
|
|
'EXAMPLE.label("<a>");',
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
'<script>EXAMPLE.label("<a>");</script>'
|
2018-08-07 15:16:21 +00:00
|
|
|
],
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
'Script closing string (lower)' => [
|
2018-08-07 15:16:21 +00:00
|
|
|
'EXAMPLE.label("</script>");',
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
'<script>/* ERROR: Invalid script */</script>',
|
|
|
|
|
true,
|
2018-08-07 15:16:21 +00:00
|
|
|
],
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
'Script closing with non-standard attributes (mixed)' => [
|
|
|
|
|
'EXAMPLE.label("</SCriPT and STyLE>");',
|
|
|
|
|
'<script>/* ERROR: Invalid script */</script>',
|
|
|
|
|
true,
|
|
|
|
|
],
|
|
|
|
|
'HTML-comment-open and script-open' => [
|
|
|
|
|
// In HTML, <script> contents aren't just plain CDATA until </script>,
|
|
|
|
|
// there are levels of escaping modes, and the below sequence puts an
|
|
|
|
|
// HTML parser in a state where </script> would *not* close the script.
|
|
|
|
|
// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state
|
|
|
|
|
'var a = "<!--<script>";',
|
|
|
|
|
'<script>/* ERROR: Invalid script */</script>',
|
|
|
|
|
true,
|
2018-08-07 15:16:21 +00:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideInlineScript
|
|
|
|
|
* @covers Html::inlineScript
|
|
|
|
|
*/
|
Html: Reject </script> from inlineScript() and leave rest unescaped
There are three problems with the CDATA approach:
1. It doesn't work.
HTML5 already interprets the contents of <script> tags as CDATA,
which means escaping of characters like & is not needed. In fact,
in HTML5 mode, a plain script tag with <script>0&1;</script>
would be a syntax error. Indicating it is not interpreted as
text, but as CDATA. Effectively, the only thing an HTML parser
looks for is </script>.
And that's exactly the problem. Producing an inline script
containing the characters "</string>" for legitimate reasons,
is currently broken.
No alternate wrapping or setting can make it work, either.
See also:
https://people.wikimedia.org/~krinkle/200506-html-inlinescript.html
which contains:
<script>/*<![CDATA[*/
if (true && true) {
console.log('This is a <script></script> tag (original)');
}
/*]]>*/</script>
In a browser, the script is terminated by the first "</script>",
leaving the code unfinished, throwing a SyntaxError, and outputting
the rest of the script as plain text on the page.
2. CDATA is only for XML mode, whereas MediaWiki does not support
the XML/XHTML output mode (since MediaWiki 1.22). Instead, we only
output HTML (5). Code that does need to produce XML, should use the
class from Xml.php instead.
3. It gives a false sense of security.
We could just remove the CDATA code as-is and that in itself would be an
improvement per point 2 and 3, and would break nothing per point 1.
However, this commit attempts to address the underlying bug by rejecting
the characters "</script>" from input. If this is needed in a literal,
it is the responsibility of the caller to escape it in a way that is
appropiate for how it is used (string, comment, regex, etc.).
There are two ways this can be used currently in core:
* User input as exported through JSON (e.g. mw.config, or mw.messages).
This is already fine as both FormatJson::encode and json_encode handle
escape either < or / in the string by default already.
* Previews of edits to user scripts. This is currently already broken and
causes the script to end early and produce arbitrary HTML on the page.
This commit limits the impact by refusing to output such script in a
broken way. I will further address that use case in a follow-up.
Bug: T200506
Change-Id: I67ceb34eabf2f62fd3f3841b8f1459289fad28fb
2018-08-20 00:42:15 +00:00
|
|
|
public function testInlineScript( $code, $expected, $error = false ) {
|
|
|
|
|
if ( $error ) {
|
|
|
|
|
Wikimedia\suppressWarnings();
|
|
|
|
|
$this->restoreWarnings = true;
|
|
|
|
|
}
|
2018-08-07 15:16:21 +00:00
|
|
|
$this->assertSame( Html::inlineScript( $code ), $expected );
|
|
|
|
|
}
|
2011-07-04 19:51:35 +00:00
|
|
|
}
|
2014-05-10 08:58:19 +00:00
|
|
|
|
|
|
|
|
class HtmlTestValue {
|
|
|
|
|
function __toString() {
|
|
|
|
|
return 'stringValue';
|
|
|
|
|
}
|
|
|
|
|
}
|