From 1944d2ee8bb123b5eb518e30273efa14db95f919 Mon Sep 17 00:00:00 2001
From: Brian Wolff <bawolff+wn@gmail.com>
Date: Wed, 20 Apr 2016 13:22:51 -0400
Subject: [PATCH] [SECURITY] Remove support for $wgWellFormedXml=false

* Extra options add extra complexity and maintenance burden
* Options which are used by very few people tend to get tested less
* Escaping is an area of code where we should be very conservative
* Having escaping rules depend on making assumptions about which
    characters various browsers consider "whitespace" is scary
* $wgWellFormedXml=false has had a negative security impact in the
    past (Usually not directly its fault, but has made other bugs
    more exploitable)

Bug: T57548
Change-Id: I5c922e0980d3f9eb39adb5bb5833e158afda42ed
---
 includes/DefaultSettings.php                       |  18 --
 includes/Html.php                                  |  62 +----
 tests/parser/parserTest.inc                        |   1 -
 tests/phpunit/includes/HtmlTest.php                | 271 ++++++++++-----------
 tests/phpunit/includes/LinkerTest.php              |   3 -
 tests/phpunit/includes/OutputPageTest.php          |   8 +-
 tests/phpunit/includes/XmlSelectTest.php           |   3 -
 tests/phpunit/includes/XmlTest.php                 |   1 -
 tests/phpunit/includes/content/JsonContentTest.php |   6 -
 tests/phpunit/includes/parser/NewParserTest.php    |   2 -
 10 files changed, 136 insertions(+), 239 deletions(-)

diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 10e6adb..2d2f57f 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -3106,24 +3106,6 @@ $wgHTMLFormAllowTableFormat = true;
 $wgUseMediaWikiUIEverywhere = false;
 
 /**
- * Should we try to make our HTML output well-formed XML?  If set to false,
- * output will be a few bytes shorter, and the HTML will arguably be more
- * readable.  If set to true, life will be much easier for the authors of
- * screen-scraping bots, and the HTML will arguably be more readable.
- *
- * Setting this to false may omit quotation marks on some attributes, omit
- * slashes from some self-closing tags, omit some ending tags, etc., where
- * permitted by HTML5.  Setting it to true will not guarantee that all pages
- * will be well-formed, although non-well-formed pages should be rare and it's
- * a bug if you find one.  Conversely, setting it to false doesn't mean that
- * all XML-y constructs will be omitted, just that they might be.
- *
- * Because of compatibility with screen-scraping bots, and because it's
- * controversial, this is currently left to true by default.
- */
-$wgWellFormedXml = true;
-
-/**
  * Permit other namespaces in addition to the w3.org default.
  *
  * Use the prefix for the key and the namespace for the value.
diff --git a/includes/Html.php b/includes/Html.php
index 890beb0..e5128d1 100644
--- a/includes/Html.php
+++ b/includes/Html.php
@@ -38,8 +38,6 @@
  *
  * $wgMimeType: If this is set to an xml MIME type then output should be
  *     valid XHTML5.
- * $wgWellFormedXml: If this is set to true, then all output should be
- *     well-formed XML (quotes on attributes, self-closing tags, etc.).
  *
  * This class is meant to be confined to utility functions that are called from
  * trusted code paths.  It does not do enforcement of policy like not allowing
@@ -199,8 +197,7 @@ class Html {
 	 * This is quite similar to Xml::tags(), but it implements some useful
 	 * HTML-specific logic.  For instance, there is no $allowShortTag
 	 * parameter: the closing tag is magically omitted if $element has an empty
-	 * content model.  If $wgWellFormedXml is false, then a few bytes will be
-	 * shaved off the HTML output as well.
+	 * content model.
 	 *
 	 * @param string $element The element's name, e.g., 'a'
 	 * @param array $attribs Associative array of attributes, e.g., array(
@@ -211,14 +208,10 @@ class Html {
 	 * @return string Raw HTML
 	 */
 	public static function rawElement( $element, $attribs = [], $contents = '' ) {
-		global $wgWellFormedXml;
 		$start = self::openElement( $element, $attribs );
 		if ( in_array( $element, self::$voidElements ) ) {
-			if ( $wgWellFormedXml ) {
-				// Silly XML.
-				return substr( $start, 0, -1 ) . '/>';
-			}
-			return $start;
+			// Silly XML.
+			return substr( $start, 0, -1 ) . '/>';
 		} else {
 			return "$start$contents" . self::closeElement( $element );
 		}
@@ -443,8 +436,6 @@ class Html {
 	 * 'http://www.mediawiki.org/' ) becomes something like
 	 * ' href="http://www.mediawiki.org"'.  Again, this is like
 	 * Xml::expandAttributes(), but it implements some HTML-specific logic.
-	 * For instance, it will omit quotation marks if $wgWellFormedXml is false,
-	 * and will treat boolean attributes specially.
 	 *
 	 * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
 	 * values are allowed as well, which will automagically be normalized
@@ -479,8 +470,6 @@ class Html {
 	 *   (starting with a space if at least one attribute is output)
 	 */
 	public static function expandAttributes( array $attribs ) {
-		global $wgWellFormedXml;
-
 		$ret = '';
 		foreach ( $attribs as $key => $value ) {
 			// Support intuitive array( 'checked' => true/false ) form
@@ -564,31 +553,10 @@ class Html {
 				throw new MWException( "HTML attribute $key can not contain a list of values" );
 			}
 
-			// See the "Attributes" section in the HTML syntax part of HTML5,
-			// 9.1.2.3 as of 2009-08-10.  Most attributes can have quotation
-			// marks omitted, but not all.  (Although a literal " is not
-			// permitted, we don't check for that, since it will be escaped
-			// anyway.)
-
-			// See also research done on further characters that need to be
-			// escaped: http://code.google.com/p/html5lib/issues/detail?id=93
-			$badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
-				. "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
-				. "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
-			if ( $wgWellFormedXml || $value === '' || preg_match( "![$badChars]!u", $value ) ) {
-				$quote = '"';
-			} else {
-				$quote = '';
-			}
+			$quote = '"';
 
 			if ( in_array( $key, self::$boolAttribs ) ) {
-				// In HTML5, we can leave the value empty. If we don't need
-				// well-formed XML, we can omit the = entirely.
-				if ( !$wgWellFormedXml ) {
-					$ret .= " $key";
-				} else {
-					$ret .= " $key=\"\"";
-				}
+				$ret .= " $key=\"\"";
 			} else {
 				// Apparently we need to entity-encode \n, \r, \t, although the
 				// spec doesn't mention that.  Since we're doing strtr() anyway,
@@ -599,22 +567,18 @@ class Html {
 				// don't because we're stubborn and like our marginal savings on
 				// byte size from not having to encode unnecessary quotes.
 				// The only difference between this transform and the one by
-				// Sanitizer::encodeAttribute() is '<' is only encoded here if
-				// $wgWellFormedXml is set, and ' is not encoded.
+				// Sanitizer::encodeAttribute() is ' is not encoded.
 				$map = [
 					'&' => '&amp;',
 					'"' => '&quot;',
 					'>' => '&gt;',
+					// '<' allegedly allowed per spec
+					// but breaks some tools if not escaped.
+					"<" => '&lt;',
 					"\n" => '&#10;',
 					"\r" => '&#13;',
 					"\t" => '&#9;'
 				];
-				if ( $wgWellFormedXml ) {
-					// This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
-					// But reportedly it breaks some XML tools?
-					// @todo FIXME: Is this really true?
-					$map['<'] = '&lt;';
-				}
 				$ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
 			}
 		}
@@ -631,11 +595,9 @@ class Html {
 	 * @return string Raw HTML
 	 */
 	public static function inlineScript( $contents ) {
-		global $wgWellFormedXml;
-
 		$attrs = [];
 
-		if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+		if ( preg_match( '/[<&]/', $contents ) ) {
 			$contents = "/*<![CDATA[*/$contents/*]]>*/";
 		}
 
@@ -665,9 +627,7 @@ class Html {
 	 * @return string Raw HTML
 	 */
 	public static function inlineStyle( $contents, $media = 'all' ) {
-		global $wgWellFormedXml;
-
-		if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+		if ( preg_match( '/[<&]/', $contents ) ) {
 			$contents = "/*<![CDATA[*/$contents/*]]>*/";
 		}
 
diff --git a/tests/parser/parserTest.inc b/tests/parser/parserTest.inc
index 56108c9..7bcdcfd 100644
--- a/tests/parser/parserTest.inc
+++ b/tests/parser/parserTest.inc
@@ -896,7 +896,6 @@ class ParserTest {
 			'wgExperimentalHtmlIds' => false,
 			'wgExternalLinkTarget' => false,
 			'wgHtml5' => true,
-			'wgWellFormedXml' => true,
 			'wgAdaptiveMessageCache' => true,
 			'wgDisableLangConversion' => false,
 			'wgDisableTitleConversion' => false,
diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php
index 6c92b8c..a01f2f5 100644
--- a/tests/phpunit/includes/HtmlTest.php
+++ b/tests/phpunit/includes/HtmlTest.php
@@ -7,7 +7,6 @@ class HtmlTest extends MediaWikiTestCase {
 		parent::setUp();
 
 		$this->setMwGlobals( [
-			'wgWellFormedXml' => false,
 			'wgUseMediaWikiUIEverywhere' => false,
 		] );
 
@@ -45,7 +44,7 @@ class HtmlTest extends MediaWikiTestCase {
 	 */
 	public function testElementBasics() {
 		$this->assertEquals(
-			'<img>',
+			'<img/>',
 			Html::element( 'img', null, '' ),
 			'No close tag for short-tag elements'
 		);
@@ -62,12 +61,10 @@ class HtmlTest extends MediaWikiTestCase {
 			'Close tag for empty element (array, string)'
 		);
 
-		$this->setMwGlobals( 'wgWellFormedXml', true );
-
 		$this->assertEquals(
 			'<img/>',
 			Html::element( 'img', null, '' ),
-			'Self-closing tag for short-tag elements (wgWellFormedXml = true)'
+			'Self-closing tag for short-tag elements'
 		);
 	}
 
@@ -134,22 +131,20 @@ class HtmlTest extends MediaWikiTestCase {
 		);
 
 		$this->assertEquals(
-			' selected',
+			' selected=""',
 			Html::expandAttributes( [ 'selected' => true ] ),
 			'Boolean attributes have no value when value is true'
 		);
 		$this->assertEquals(
-			' selected',
+			' selected=""',
 			Html::expandAttributes( [ 'selected' ] ),
 			'Boolean attributes have no value when value is true (passed as numerical array)'
 		);
 
-		$this->setMwGlobals( 'wgWellFormedXml', true );
-
 		$this->assertEquals(
 			' selected=""',
 			Html::expandAttributes( [ 'selected' => true ] ),
-			'Boolean attributes have empty string value when value is true (wgWellFormedXml)'
+			'Boolean attributes have empty string value when value is true'
 		);
 	}
 
@@ -158,12 +153,12 @@ class HtmlTest extends MediaWikiTestCase {
 	 */
 	public function testExpandAttributesForNumbers() {
 		$this->assertEquals(
-			' value=1',
+			' value="1"',
 			Html::expandAttributes( [ 'value' => 1 ] ),
 			'Integer value is cast to a string'
 		);
 		$this->assertEquals(
-			' value=1.1',
+			' value="1.1"',
 			Html::expandAttributes( [ 'value' => 1.1 ] ),
 			'Float value is cast to a string'
 		);
@@ -174,7 +169,7 @@ class HtmlTest extends MediaWikiTestCase {
 	 */
 	public function testExpandAttributesForObjects() {
 		$this->assertEquals(
-			' value=stringValue',
+			' value="stringValue"',
 			Html::expandAttributes( [ 'value' => new HtmlTestValue() ] ),
 			'Object value is converted to a string'
 		);
@@ -193,43 +188,21 @@ class HtmlTest extends MediaWikiTestCase {
 			'Empty string is always quoted'
 		);
 		$this->assertEquals(
-			' key=value',
+			' key="value"',
 			Html::expandAttributes( [ 'key' => 'value' ] ),
 			'Simple string value needs no quotes'
 		);
 		$this->assertEquals(
-			' one=1',
+			' one="1"',
 			Html::expandAttributes( [ 'one' => 1 ] ),
 			'Number 1 value needs no quotes'
 		);
 		$this->assertEquals(
-			' zero=0',
+			' zero="0"',
 			Html::expandAttributes( [ 'zero' => 0 ] ),
 			'Number 0 value needs no quotes'
 		);
 
-		$this->setMwGlobals( 'wgWellFormedXml', true );
-
-		$this->assertEquals(
-			' empty_string=""',
-			Html::expandAttributes( [ 'empty_string' => '' ] ),
-			'Attribute values are always quoted (wgWellFormedXml): Empty string'
-		);
-		$this->assertEquals(
-			' key="value"',
-			Html::expandAttributes( [ 'key' => 'value' ] ),
-			'Attribute values are always quoted (wgWellFormedXml): Simple string'
-		);
-		$this->assertEquals(
-			' one="1"',
-			Html::expandAttributes( [ 'one' => 1 ] ),
-			'Attribute values are always quoted (wgWellFormedXml): Number 1'
-		);
-		$this->assertEquals(
-			' zero="0"',
-			Html::expandAttributes( [ 'zero' => 0 ] ),
-			'Attribute values are always quoted (wgWellFormedXml): Number 0'
-		);
 	}
 
 	/**
@@ -346,48 +319,48 @@ class HtmlTest extends MediaWikiTestCase {
 	 */
 	public function testNamespaceSelector() {
 		$this->assertEquals(
-			'<select id=namespace name=namespace>' . "\n" .
-				'<option value=0>(Main)</option>' . "\n" .
-				'<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" .
+			'<select id="namespace" name="namespace">' . "\n" .
+				'<option value="0">(Main)</option>' . "\n" .
+				'<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" .
 				'</select>',
 			Html::namespaceSelector(),
 			'Basic namespace selector without custom options'
 		);
 
 		$this->assertEquals(
-			'<label for=mw-test-namespace>Select a namespace:</label>&#160;' .
-				'<select id=mw-test-namespace name=wpNamespace>' . "\n" .
-				'<option value=all>all</option>' . "\n" .
-				'<option value=0>(Main)</option>' . "\n" .
-				'<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" .
+			'<label for="mw-test-namespace">Select a namespace:</label>&#160;' .
+				'<select id="mw-test-namespace" name="wpNamespace">' . "\n" .
+				'<option value="all">all</option>' . "\n" .
+				'<option value="0">(Main)</option>' . "\n" .
+				'<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" .
 				'</select>',
 			Html::namespaceSelector(
 				[ 'selected' => '2', 'all' => 'all', 'label' => 'Select a namespace:' ],
@@ -397,24 +370,24 @@ class HtmlTest extends MediaWikiTestCase {
 		);
 
 		$this->assertEquals(
-			'<label for=namespace>Select a namespace:</label>&#160;' .
-				'<select id=namespace name=namespace>' . "\n" .
-				'<option value=0>(Main)</option>' . "\n" .
-				'<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" .
+			'<label for="namespace">Select a namespace:</label>&#160;' .
+				'<select id="namespace" name="namespace">' . "\n" .
+				'<option value="0">(Main)</option>' . "\n" .
+				'<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" .
 				'</select>',
 			Html::namespaceSelector(
 				[ 'label' => 'Select a namespace:' ]
@@ -425,18 +398,18 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testCanFilterOutNamespaces() {
 		$this->assertEquals(
-			'<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" .
+			'<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" .
 				'</select>',
 			Html::namespaceSelector(
 				[ 'exclude' => [ 0, 1, 3, 100, 101 ] ]
@@ -447,23 +420,23 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testCanDisableANamespaces() {
 		$this->assertEquals(
-			'<select id=namespace name=namespace>' . "\n" .
-				'<option disabled value=0>(Main)</option>' . "\n" .
-				'<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" .
+			'<select id="namespace" name="namespace">' . "\n" .
+				'<option disabled="" value="0">(Main)</option>' . "\n" .
+				'<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" .
 				'</select>',
 			Html::namespaceSelector( [
 				'disable' => [ 0, 1, 2, 3, 4 ]
@@ -478,7 +451,7 @@ class HtmlTest extends MediaWikiTestCase {
 	 */
 	public function testHtmlElementAcceptsNewHtml5TypesInHtml5Mode( $HTML5InputType ) {
 		$this->assertEquals(
-			'<input type=' . $HTML5InputType . '>',
+			'<input type="' . $HTML5InputType . '"/>',
 			Html::element( 'input', [ 'type' => $HTML5InputType ] ),
 			'In HTML5, Html::element() should accept type="' . $HTML5InputType . '"'
 		);
@@ -528,14 +501,14 @@ class HtmlTest extends MediaWikiTestCase {
 		$cases = [];
 
 		# ## Generic cases, match $attribDefault static array
-		$cases[] = [ '<area>',
+		$cases[] = [ '<area/>',
 			'area', [ 'shape' => 'rect' ]
 		];
 
-		$cases[] = [ '<button type=submit></button>',
+		$cases[] = [ '<button type="submit"></button>',
 			'button', [ 'formaction' => 'GET' ]
 		];
-		$cases[] = [ '<button type=submit></button>',
+		$cases[] = [ '<button type="submit"></button>',
 			'button', [ 'formenctype' => 'application/x-www-form-urlencoded' ]
 		];
 
@@ -553,7 +526,7 @@ class HtmlTest extends MediaWikiTestCase {
 			'canvas', [ 'width' => 300 ]
 		];
 
-		$cases[] = [ '<command>',
+		$cases[] = [ '<command/>',
 			'command', [ 'type' => 'command' ]
 		];
 
@@ -567,18 +540,18 @@ class HtmlTest extends MediaWikiTestCase {
 			'form', [ 'enctype' => 'application/x-www-form-urlencoded' ]
 		];
 
-		$cases[] = [ '<input>',
+		$cases[] = [ '<input/>',
 			'input', [ 'formaction' => 'GET' ]
 		];
-		$cases[] = [ '<input>',
+		$cases[] = [ '<input/>',
 			'input', [ 'type' => 'text' ]
 		];
 
-		$cases[] = [ '<keygen>',
+		$cases[] = [ '<keygen/>',
 			'keygen', [ 'keytype' => 'rsa' ]
 		];
 
-		$cases[] = [ '<link>',
+		$cases[] = [ '<link/>',
 			'link', [ 'media' => 'all' ]
 		];
 
@@ -604,44 +577,44 @@ class HtmlTest extends MediaWikiTestCase {
 		# ## SPECIFIC CASES
 
 		# <link type="text/css">
-		$cases[] = [ '<link>',
+		$cases[] = [ '<link/>',
 			'link', [ 'type' => 'text/css' ]
 		];
 
 		# <input> specific handling
-		$cases[] = [ '<input type=checkbox>',
+		$cases[] = [ '<input type="checkbox"/>',
 			'input', [ 'type' => 'checkbox', 'value' => 'on' ],
 			'Default value "on" is stripped of checkboxes',
 		];
-		$cases[] = [ '<input type=radio>',
+		$cases[] = [ '<input type="radio"/>',
 			'input', [ 'type' => 'radio', 'value' => 'on' ],
 			'Default value "on" is stripped of radio buttons',
 		];
-		$cases[] = [ '<input type=submit value=Submit>',
+		$cases[] = [ '<input type="submit" value="Submit"/>',
 			'input', [ 'type' => 'submit', 'value' => 'Submit' ],
 			'Default value "Submit" is kept on submit buttons (for possible l10n issues)',
 		];
-		$cases[] = [ '<input type=color>',
+		$cases[] = [ '<input type="color"/>',
 			'input', [ 'type' => 'color', 'value' => '' ],
 		];
-		$cases[] = [ '<input type=range>',
+		$cases[] = [ '<input type="range"/>',
 			'input', [ 'type' => 'range', 'value' => '' ],
 		];
 
 		# <button> specific handling
 		# see remarks on http://msdn.microsoft.com/en-us/library/ie/ms535211%28v=vs.85%29.aspx
-		$cases[] = [ '<button type=submit></button>',
+		$cases[] = [ '<button type="submit"></button>',
 			'button', [ 'type' => 'submit' ],
 			'According to standard the default type is "submit". '
 				. 'Depending on compatibility mode IE might use "button", instead.',
 		];
 
 		# <select> specific handling
-		$cases[] = [ '<select multiple></select>',
+		$cases[] = [ '<select multiple=""></select>',
 			'select', [ 'size' => '4', 'multiple' => true ],
 		];
 		# .. with numeric value
-		$cases[] = [ '<select multiple></select>',
+		$cases[] = [ '<select multiple=""></select>',
 			'select', [ 'size' => 4, 'multiple' => true ],
 		];
 		$cases[] = [ '<select></select>',
@@ -693,7 +666,7 @@ class HtmlTest extends MediaWikiTestCase {
 			'Blacklist form validation attributes.'
 		);
 		$this->assertEquals(
-			' step=any',
+			' step="any"',
 			Html::expandAttributes(
 				[
 					'min' => 1,
@@ -709,12 +682,12 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testWrapperInput() {
 		$this->assertEquals(
-			'<input type=radio value=testval name=testname>',
+			'<input type="radio" value="testval" name="testname"/>',
 			Html::input( 'testname', 'testval', 'radio' ),
 			'Input wrapper with type and value.'
 		);
 		$this->assertEquals(
-			'<input name=testname>',
+			'<input name="testname"/>',
 			Html::input( 'testname' ),
 			'Input wrapper with all default values.'
 		);
@@ -722,17 +695,17 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testWrapperCheck() {
 		$this->assertEquals(
-			'<input type=checkbox value=1 name=testname>',
+			'<input type="checkbox" value="1" name="testname"/>',
 			Html::check( 'testname' ),
 			'Checkbox wrapper unchecked.'
 		);
 		$this->assertEquals(
-			'<input checked type=checkbox value=1 name=testname>',
+			'<input checked="" type="checkbox" value="1" name="testname"/>',
 			Html::check( 'testname', true ),
 			'Checkbox wrapper checked.'
 		);
 		$this->assertEquals(
-			'<input type=checkbox value=testval name=testname>',
+			'<input type="checkbox" value="testval" name="testname"/>',
 			Html::check( 'testname', false, [ 'value' => 'testval' ] ),
 			'Checkbox wrapper with a value override.'
 		);
@@ -740,17 +713,17 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testWrapperRadio() {
 		$this->assertEquals(
-			'<input type=radio value=1 name=testname>',
+			'<input type="radio" value="1" name="testname"/>',
 			Html::radio( 'testname' ),
 			'Radio wrapper unchecked.'
 		);
 		$this->assertEquals(
-			'<input checked type=radio value=1 name=testname>',
+			'<input checked="" type="radio" value="1" name="testname"/>',
 			Html::radio( 'testname', true ),
 			'Radio wrapper checked.'
 		);
 		$this->assertEquals(
-			'<input type=radio value=testval name=testname>',
+			'<input type="radio" value="testval" name="testname"/>',
 			Html::radio( 'testname', false, [ 'value' => 'testval' ] ),
 			'Radio wrapper with a value override.'
 		);
@@ -758,7 +731,7 @@ class HtmlTest extends MediaWikiTestCase {
 
 	public function testWrapperLabel() {
 		$this->assertEquals(
-			'<label for=testid>testlabel</label>',
+			'<label for="testid">testlabel</label>',
 			Html::label( 'testlabel', 'testid' ),
 			'Label wrapper'
 		);
diff --git a/tests/phpunit/includes/LinkerTest.php b/tests/phpunit/includes/LinkerTest.php
index e50b4f1..f47660d 100644
--- a/tests/phpunit/includes/LinkerTest.php
+++ b/tests/phpunit/includes/LinkerTest.php
@@ -13,7 +13,6 @@ class LinkerTest extends MediaWikiLangTestCase {
 	public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
 		$this->setMwGlobals( [
 			'wgArticlePath' => '/wiki/$1',
-			'wgWellFormedXml' => true,
 		] );
 
 		$this->assertEquals( $expected,
@@ -112,7 +111,6 @@ class LinkerTest extends MediaWikiLangTestCase {
 		$this->setMwGlobals( [
 			'wgScript' => '/wiki/index.php',
 			'wgArticlePath' => '/wiki/$1',
-			'wgWellFormedXml' => true,
 			'wgCapitalLinks' => true,
 			'wgConf' => $conf,
 		] );
@@ -277,7 +275,6 @@ class LinkerTest extends MediaWikiLangTestCase {
 		$this->setMwGlobals( [
 			'wgScript' => '/wiki/index.php',
 			'wgArticlePath' => '/wiki/$1',
-			'wgWellFormedXml' => true,
 			'wgCapitalLinks' => true,
 			'wgConf' => $conf,
 		] );
diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php
index 8d4a347..9934749 100644
--- a/tests/phpunit/includes/OutputPageTest.php
+++ b/tests/phpunit/includes/OutputPageTest.php
@@ -149,14 +149,14 @@ class OutputPageTest extends MediaWikiTestCase {
 			[
 				// Don't condition wrap raw modules (like the startup module)
 				[ 'test.raw', ResourceLoaderModule::TYPE_SCRIPTS ],
-				'<script async src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
+				'<script async="" src="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.raw&amp;only=scripts&amp;skin=fallback"></script>'
 			],
 			// Load module styles only
 			// This also tests the order the modules are put into the url
 			[
 				[ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
 
-				'<link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback">'
+				'<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
 			],
 			// Load private module (only=scripts)
 			[
@@ -181,7 +181,7 @@ class OutputPageTest extends MediaWikiTestCase {
 			// noscript group
 			[
 				[ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
-				'<noscript><link rel=stylesheet href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"></noscript>'
+				'<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
 			],
 			// Load two modules in separate groups
 			[
@@ -210,8 +210,6 @@ class OutputPageTest extends MediaWikiTestCase {
 		$this->setMwGlobals( [
 			'wgResourceLoaderDebug' => false,
 			'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
-			// Affects whether CDATA is inserted
-			'wgWellFormedXml' => false,
 		] );
 		$class = new ReflectionClass( 'OutputPage' );
 		$method = $class->getMethod( 'makeResourceLoaderLink' );
diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php
index 0d10c1a..f80f512 100644
--- a/tests/phpunit/includes/XmlSelectTest.php
+++ b/tests/phpunit/includes/XmlSelectTest.php
@@ -12,9 +12,6 @@ class XmlSelectTest extends MediaWikiTestCase {
 
 	protected function setUp() {
 		parent::setUp();
-		$this->setMwGlobals( [
-			'wgWellFormedXml' => true,
-		] );
 		$this->select = new XmlSelect();
 	}
 
diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php
index 00d429e..dbd1299 100644
--- a/tests/phpunit/includes/XmlTest.php
+++ b/tests/phpunit/includes/XmlTest.php
@@ -30,7 +30,6 @@ class XmlTest extends MediaWikiTestCase {
 
 		$this->setMwGlobals( [
 			'wgLang' => $langObj,
-			'wgWellFormedXml' => true,
 			'wgUseMediaWikiUIEverywhere' => false,
 		] );
 	}
diff --git a/tests/phpunit/includes/content/JsonContentTest.php b/tests/phpunit/includes/content/JsonContentTest.php
index 8a48734..de8e371 100644
--- a/tests/phpunit/includes/content/JsonContentTest.php
+++ b/tests/phpunit/includes/content/JsonContentTest.php
@@ -6,12 +6,6 @@
  */
 class JsonContentTest extends MediaWikiLangTestCase {
 
-	protected function setUp() {
-		parent::setUp();
-
-		$this->setMwGlobals( 'wgWellFormedXml', true );
-	}
-
 	public static function provideValidConstruction() {
 		return [
 			[ 'foo', false, null ],
diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php
index b1119a1..8a0e06b 100644
--- a/tests/phpunit/includes/parser/NewParserTest.php
+++ b/tests/phpunit/includes/parser/NewParserTest.php
@@ -101,7 +101,6 @@ class NewParserTest extends MediaWikiTestCase {
 		$tmpGlobals['wgUseImageResize'] = true;
 		$tmpGlobals['wgAllowExternalImages'] = true;
 		$tmpGlobals['wgRawHtml'] = false;
-		$tmpGlobals['wgWellFormedXml'] = true;
 		$tmpGlobals['wgExperimentalHtmlIds'] = false;
 		$tmpGlobals['wgAdaptiveMessageCache'] = true;
 		$tmpGlobals['wgUseDatabaseMessages'] = true;
@@ -433,7 +432,6 @@ class NewParserTest extends MediaWikiTestCase {
 			'wgThumbLimits' => [ self::getOptionValue( 'thumbsize', $opts, 180 ) ],
 			'wgMaxTocLevel' => $maxtoclevel,
 			'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
-			'wgWellFormedXml' => true,
 			'wgMathDirectory' => $uploadDir . '/math',
 			'wgDefaultLanguageVariant' => $variant,
 			'wgLinkHolderBatchSize' => $linkHolderBatchSize,
-- 
2.0.1

