From: Mediawiki
Subject: Fix security issues as fixed in upstream security fix release 1.19.24:
(bug T85848, bug T71210) SECURITY: Don't parse XMP blocks that contain XML entities, to prevent various DoS attacks.
(bug T88310) SECURITY: Always expand xml entities when checking SVG's.
(bug T73394) SECURITY: Escape > in Html::expandAttributes to prevent XSS.
(bug T85855) SECURITY: Don't execute another user's CSS or JS on preview.
(bug T85349, bug T85850, bug T86711) SECURITY: Multiple issues fixed in SVG filtering to prevent XSS and protect viewer's privacy.
Origin: upstream, https://lists.wikimedia.org/pipermail/mediawiki-announce/2015-March/000175.html

diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/EditPage.php mediawiki-1.19.24/includes/EditPage.php
--- mediawiki-1.19.23/includes/EditPage.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/EditPage.php	2015-03-31 13:23:38.000000000 +0000
@@ -1988,14 +1988,19 @@
 				if ( $this->isWrongCaseCssJsPage ) {
 					$wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) );
 				}
+				if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
 				if ( $this->formtype !== 'preview' ) {
-					if ( $this->isCssSubpage )
+						if ( $this->isCssSubpage ) {
 						$wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) );
-					if ( $this->isJsSubpage )
+						}
+
+						if ( $this->isJsSubpage ) {
 						$wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) );
 				}
 			}
 		}
+			}
+		}
 
 		if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
 			# Is the title semi-protected?
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/Html.php mediawiki-1.19.24/includes/Html.php
--- mediawiki-1.19.23/includes/Html.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/Html.php	2015-03-31 13:23:38.000000000 +0000
@@ -525,17 +525,20 @@
 			} else {
 				# Apparently we need to entity-encode \n, \r, \t, although the
 				# spec doesn't mention that.  Since we're doing strtr() anyway,
-				# and we don't need <> escaped here, we may as well not call
-				# htmlspecialchars().
+				# we may as well not call htmlspecialchars().
 				# @todo FIXME: Verify that we actually need to
 				# escape \n\r\t here, and explain why, exactly.
 				#
 				# We could call Sanitizer::encodeAttribute() for this, but we
 				# 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.
 				$map = array(
 					'&' => '&amp;',
 					'"' => '&quot;',
+					'>' => '&gt;',
 					"\n" => '&#10;',
 					"\r" => '&#13;',
 					"\t" => '&#9;'
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/media/BitmapMetadataHandler.php mediawiki-1.19.24/includes/media/BitmapMetadataHandler.php
--- mediawiki-1.19.23/includes/media/BitmapMetadataHandler.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/media/BitmapMetadataHandler.php	2015-03-31 13:23:38.000000000 +0000
@@ -126,7 +126,7 @@
 	 * @throws MWException on invalid file.
 	 */
 	static function Jpeg ( $filename ) {
-		$showXMP = function_exists( 'xml_parser_create_ns' );
+		$showXMP = XMPReader::isSupported();
 		$meta = new self();
 
 		$seg = JpegMetadataExtractor::segmentSplitter( $filename );
@@ -168,7 +168,7 @@
 	 * @return Array Array for storage in img_metadata.
 	 */
 	static public function PNG ( $filename ) {
-		$showXMP = function_exists( 'xml_parser_create_ns' );
+		$showXMP = XMPReader::isSupported();
 
 		$meta = new self();
 		$array = PNGMetadataExtractor::getMetadata( $filename );
@@ -205,7 +205,7 @@
 			$meta->addMetadata( array( 'GIFFileComment' => $baseArray['comment'] ), 'native' );
 		}
 
-		if ( $baseArray['xmp'] !== '' && function_exists( 'xml_parser_create_ns' ) ) {
+		if ( $baseArray['xmp'] !== '' && XMPReader::isSupported() ) {
 			$xmp = new XMPReader();
 			$xmp->parse( $baseArray['xmp'] );
 			$xmpRes = $xmp->getResults();
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/media/JpegMetadataExtractor.php mediawiki-1.19.24/includes/media/JpegMetadataExtractor.php
--- mediawiki-1.19.23/includes/media/JpegMetadataExtractor.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/media/JpegMetadataExtractor.php	2015-03-31 13:23:38.000000000 +0000
@@ -24,7 +24,7 @@
 	* @throws MWException if given invalid file.
 	*/
 	static function segmentSplitter ( $filename ) {
-		$showXMP = function_exists( 'xml_parser_create_ns' );
+		$showXMP = XMPReader::isSupported();
 
 		$segmentCount = 0;
 
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/media/XMP.php mediawiki-1.19.24/includes/media/XMP.php
--- mediawiki-1.19.23/includes/media/XMP.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/media/XMP.php	2015-03-31 13:23:38.000000000 +0000
@@ -40,6 +40,12 @@
 
 	protected $items;
 
+	/** @var int Flag determining if the XMP is safe to parse **/
+	private $parsable = 0;
+
+	/** @var string Buffer of XML to parse **/
+	private $xmlParsableBuffer = '';
+
 	/**
 	* These are various mode constants.
 	* they are used to figure out what to do
@@ -69,6 +75,12 @@
 	const NS_XML = 'http://www.w3.org/XML/1998/namespace';
 
 
+	// States used while determining if XML is safe to parse
+	const PARSABLE_UNKNOWN = 0;
+	const PARSABLE_OK = 1;
+	const PARSABLE_BUFFERING = 2;
+	const PARSABLE_NO = 3;
+
 	/**
 	* Constructor.
 	*
@@ -106,6 +118,9 @@
 			array( $this, 'endElement' ) );
 
 		xml_set_character_data_handler( $this->xmlParser, array( $this, 'char' ) );
+
+		$this->parsable = self::PARSABLE_UNKNOWN;
+		$this->xmlParsableBuffer = '';
 	}
 
 	/** Destroy the xml parser
@@ -117,6 +132,13 @@
 		xml_parser_free( $this->xmlParser );
 	}
 
+	/**
+	 * Check if this instance supports using this class
+	 */
+	public static function isSupported() {
+		return function_exists( 'xml_parser_create_ns' ) && class_exists( 'XMLReader' );
+	}
+
 	/** Get the result array. Do some post-processing before returning
 	* the array, and transform any metadata that is special-cased.
 	*
@@ -263,6 +285,27 @@
 				wfRestoreWarnings();
 			}
 
+			// Ensure the XMP block does not have an xml doctype declaration, which
+			// could declare entities unsafe to parse with xml_parse (T85848/T71210).
+			if ( $this->parsable !== self::PARSABLE_OK ) {
+				if ( $this->parsable === self::PARSABLE_NO ) {
+					throw new MWException( 'Unsafe doctype declaration in XML.' );
+				}
+
+				$content = $this->xmlParsableBuffer . $content;
+				if ( !$this->checkParseSafety( $content ) ) {
+					if ( !$allOfIt && $this->parsable !== self::PARSABLE_NO ) {
+						// parse wasn't Unsuccessful yet, so return true
+						// in this case.
+						return true;
+					}
+					$msg = ( $this->parsable === self::PARSABLE_NO ) ?
+						'Unsafe doctype declaration in XML.' :
+						'No root element found in XML.';
+					throw new MWException( $msg );
+				}
+			}
+
 			$ok = xml_parse( $this->xmlParser, $content, $allOfIt );
 			if ( !$ok ) {
 				$error = xml_error_string( xml_get_error_code( $this->xmlParser ) );
@@ -385,6 +428,59 @@
 
 	}
 
+	/**
+	 * Check if a block of XML is safe to pass to xml_parse, i.e. doesn't
+	 * contain a doctype declaration which could contain a dos attack if we
+	 * parse it and expand internal entities (T85848).
+	 *
+	 * @param string $content xml string to check for parse safety
+	 * @return bool true if the xml is safe to parse, false otherwise
+	 */
+	private function checkParseSafety( $content ) {
+		$reader = new XMLReader();
+		$result = null;
+
+		// For XMLReader to parse incomplete/invalid XML, it has to be open()'ed
+		// instead of using XML().
+		$reader->open(
+			'data://text/plain,' . urlencode( $content ),
+			null,
+			LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET
+		);
+
+		$oldDisable = libxml_disable_entity_loader( true );
+		$reader->setParserProperty( XMLReader::SUBST_ENTITIES, false );
+
+		// Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
+		// when parsing truncated XML, which causes unit tests to fail.
+		wfSuppressWarnings();
+		while ( $reader->read() ) {
+			if ( $reader->nodeType === XMLReader::ELEMENT ) {
+				// Reached the first element without hitting a doctype declaration
+				$this->parsable = self::PARSABLE_OK;
+				$result = true;
+				break;
+			}
+			if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
+				$this->parsable = self::PARSABLE_NO;
+				$result = false;
+				break;
+			}
+		}
+		wfRestoreWarnings();
+		libxml_disable_entity_loader( $oldDisable );
+
+		if ( !is_null( $result ) ) {
+			return $result;
+		}
+
+		// Reached the end of the parsable xml without finding an element
+		// or doctype. Buffer and try again.
+		$this->parsable = self::PARSABLE_BUFFERING;
+		$this->xmlParsableBuffer = $content;
+		return false;
+	}
+
 	/** When we hit a closing element in MODE_IGNORE
 	* Check to see if this is the element we started to ignore,
 	* in which case we get out of MODE_IGNORE
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/OutputPage.php mediawiki-1.19.24/includes/OutputPage.php
--- mediawiki-1.19.23/includes/OutputPage.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/OutputPage.php	2015-03-31 13:23:38.000000000 +0000
@@ -2975,6 +2975,10 @@
 		if ( !$this->getTitle()->isJsSubpage() && !$this->getTitle()->isCssSubpage() ) {
 			return false;
 		}
+		if ( !$this->getTitle()->isSubpageOf( $this->getUser()->getUserPage() ) ) {
+			// Don't execute another user's CSS or JS on preview (T85855)
+			return false;
+		}
 
 		return !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) );
 	}
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/upload/UploadBase.php mediawiki-1.19.24/includes/upload/UploadBase.php
--- mediawiki-1.19.23/includes/upload/UploadBase.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/upload/UploadBase.php	2015-03-31 13:23:38.000000000 +0000
@@ -1210,23 +1210,22 @@
 				}
 			}
 
-			# href with embeded svg as target
-			if( $stripped == 'href' && preg_match( '!data:[^,]*image/svg[^,]*,!sim', $value ) ) {
-				wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
+			# only allow data: targets that should be safe. This prevents vectors like,
+			# image/svg, text/xml, application/xml, and text/html, which can contain scripts
+			if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
+				// rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
+				$parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
+				if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
+					wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
+						. "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
 				return true;
 			}
-
-			# href with embeded (text/xml) svg as target
-			if( $stripped == 'href' && preg_match( '!data:[^,]*text/xml[^,]*,!sim', $value ) ) {
-				wfDebug( __METHOD__ . ": Found href to embedded svg \"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
-				return true;
 			}
 
-			# Change href with animate from (http://html5sec.org/#137). This doesn't seem
-			# possible without embedding the svg, but filter here in case.
-			if ( $stripped == 'from'
+			# Change href with animate from (http://html5sec.org/#137).
+			if ( $stripped === 'attributename'
 				&& $strippedElement === 'animate'
-				&& !preg_match( '!^https?://!im', $value )
+				&& $this->stripXmlNamespace( $value ) == 'href'
 			) {
 				wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
 					. "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
@@ -1300,7 +1299,7 @@
 	private static function checkCssFragment( $value ) {
 
 		# Forbid external stylesheets, for both reliability and to protect viewer's privacy
-		if ( strpos( $value, '@import' ) !== false ) {
+		if ( stripos( $value, '@import' ) !== false ) {
 			return true;
 		}
 
diff -Nruw -x '*~' -x '.js*' -x '.git*' -x '*.xcf' -x '#*#' -x '.#*' -x '.rubocop*' -x .travis.yml -x package.json -x messages -x Gemfile -x '*.png' -x '*.jpg' -x '*.xcf' -x '*.gif' -x '*.svg' -x '*.tiff' -x '*.zip' -x '*.xmp' mediawiki-1.19.23/includes/XmlTypeCheck.php mediawiki-1.19.24/includes/XmlTypeCheck.php
--- mediawiki-1.19.23/includes/XmlTypeCheck.php	2015-03-31 13:24:03.000000000 +0000
+++ mediawiki-1.19.24/includes/XmlTypeCheck.php	2015-03-31 13:23:38.000000000 +0000
@@ -1,11 +1,36 @@
 <?php
+/**
+ * XML syntax and type checker.
+ *
+ * Since 1.24.2, it uses XMLReader instead of xml_parse, which gives us
+ * more control over the expansion of XML entities. When passed to the
+ * callback, entities will be fully expanded, but may report the XML is
+ * invalid if expanding the entities are likely to cause a DoS.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
 
 class XmlTypeCheck {
 	/**
 	 * Will be set to true or false to indicate whether the file is
 	 * well-formed XML. Note that this doesn't check schema validity.
 	 */
-	public $wellFormed = false;
+	public $wellFormed = null;
 
 	/**
 	 * Will be set to true if the optional element filter returned
@@ -44,8 +69,8 @@
 	);
 
 	/**
-	 * @param $file string filename
-	 * @param $filterCallback callable (optional)
+	 * @param string $input a filename
+	 * @param callable $filterCallback (optional)
 	 *        Function to call to do additional custom validity checks from the
 	 *        SAX element handler event. This gives you access to the element
 	 *        namespace, name, attributes, and text contents.
@@ -53,10 +78,10 @@
 	 * @param array $options list of additional parsing options:
 	 *	processing_instruction_handler: Callback for xml_set_processing_instruction_handler
 	 */
-	function __construct( $file, $filterCallback=null, $options=array() ) {
+	function __construct( $input, $filterCallback = null, $options = array() ) {
 		$this->filterCallback = $filterCallback;
 		$this->parserOptions = array_merge( $this->parserOptions, $options );
-		$this->run( $file );
+		$this->validateFromInput( $input, true );
 	}
 
 	/**
@@ -68,119 +93,211 @@
 		return $this->rootElement;
 	}
 
+
 	/**
-	 * @param $fname
+	 * @param string $fname the filename
 	 */
-	private function run( $fname ) {
-		$parser = xml_parser_create_ns( 'UTF-8' );
-
-		// case folding violates XML standard, turn it off
-		xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+	private function validateFromInput( $xml, $isFile ) {
+		$reader = new XMLReader();
+		if ( $isFile ) {
+			$s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+		} else {
+			$s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+		}
+		if ( $s !== true ) {
+			// Couldn't open the XML
+			$this->wellFormed = false;
+		} else {
+			$oldDisable = libxml_disable_entity_loader( true );
+			$reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
+			try {
+				$this->validate( $reader );
+			} catch ( Exception $e ) {
+				// Calling this malformed, because we didn't parse the whole
+				// thing. Maybe just an external entity refernce.
+				$this->wellFormed = false;
+				$reader->close();
+				libxml_disable_entity_loader( $oldDisable );
+				throw $e;
+			}
+			$reader->close();
+			libxml_disable_entity_loader( $oldDisable );
+		}
+	}
 
-		xml_set_element_handler( $parser, array( $this, 'rootElementOpen' ), false );
+	private function readNext( XMLReader $reader ) {
+		set_error_handler( array( $this, 'XmlErrorHandler' ) );
+		$ret = $reader->read();
+		restore_error_handler();
+		return $ret;
+	}
 
-		if ( $this->parserOptions['processing_instruction_handler'] ) {
-			xml_set_processing_instruction_handler(
-				$parser,
-				array( $this, 'processingInstructionHandler' )
-			);
+	public function XmlErrorHandler( $errno, $errstr ) {
+		$this->wellFormed = false;
 		}
 
-		if ( file_exists( $fname ) ) {
-			$file = fopen( $fname, "rb" );
-			if ( $file ) {
+	private function validate( $reader ) {
+
+		// First, move through anything that isn't an element, and
+		// handle any processing instructions with the callback
 				do {
-					$chunk = fread( $file, 32768 );
-					$ret = xml_parse( $parser, $chunk, feof( $file ) );
-					if( $ret == 0 ) {
-						// XML isn't well-formed!
-						fclose( $file );
-						xml_parser_free( $parser );
+			if( !$this->readNext( $reader ) ) {
+				// Hit the end of the document before any elements
+				$this->wellFormed = false;
 						return;
 					}
-				} while( !feof( $file ) );
+			if ( $reader->nodeType === XMLReader::PI ) {
+				$this->processingInstructionHandler( $reader->name, $reader->value );
+			}
+		} while ( $reader->nodeType != XMLReader::ELEMENT );
 
-				fclose( $file );
+		// Process the rest of the document
+		do {
+			switch ( $reader->nodeType ) {
+				case XMLReader::ELEMENT:
+					$name = $this->expandNS(
+						$reader->name,
+						$reader->namespaceURI
+					);
+					if ( $this->rootElement === '' ) {
+						$this->rootElement = $name;
 			}
+					$empty = $reader->isEmptyElement;
+					$attrs = $this->getAttributesArray( $reader );
+					$this->elementOpen( $name, $attrs );
+					if ( $empty ) {
+						$this->elementClose();
+					}
+					break;
+
+				case XMLReader::END_ELEMENT:
+					$this->elementClose();
+					break;
+
+				case XMLReader::WHITESPACE:
+				case XMLReader::SIGNIFICANT_WHITESPACE:
+				case XMLReader::CDATA:
+				case XMLReader::TEXT:
+					$this->elementData( $reader->value );
+					break;
+
+				case XMLReader::ENTITY_REF:
+					// Unexpanded entity (maybe external?),
+					// don't send to the filter (xml_parse didn't)
+					break;
+
+				case XMLReader::COMMENT:
+					// Don't send to the filter (xml_parse didn't)
+					break;
+
+				case XMLReader::PI:
+					// Processing instructions can happen after the header too
+					$this->processingInstructionHandler(
+						$reader->name,
+						$reader->value
+					);
+					break;
+				default:
+					// One of DOC, DOC_TYPE, ENTITY, END_ENTITY,
+					// NOTATION, or XML_DECLARATION
+					// xml_parse didn't send these to the filter, so we won't.
 		}
 
+		} while ( $this->readNext( $reader ) );
+
+		if ( $this->stackDepth !== 0 ) {
+			$this->wellFormed = false;
+		} elseif ( $this->wellFormed === null ) {
 		$this->wellFormed = true;
+		}
 
-		xml_parser_free( $parser );
 	}
 
 	/**
-	 * @param $parser
-	 * @param $name
-	 * @param $attribs
+	 * Get all of the attributes for an XMLReader's current node
+	 * @param $r XMLReader
+	 * @return array of attributes
 	 */
-	private function rootElementOpen( $parser, $name, $attribs ) {
-		$this->rootElement = $name;
+	private function getAttributesArray( XMLReader $r ) {
+		$attrs = array();
+		while ( $r->moveToNextAttribute() ) {
+			if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) {
+				// XMLReader treats xmlns attributes as normal
+				// attributes, while xml_parse doesn't
+				continue;
+			}
+			$name = $this->expandNS( $r->name, $r->namespaceURI );
+			$attrs[$name] = $r->value;
+		}
+		return $attrs;
+	}
 
-		if( is_callable( $this->filterCallback ) ) {
-			xml_set_element_handler(
-				$parser,
-				array( $this, 'elementOpen' ),
-				array( $this, 'elementClose' )
-			);
-			xml_set_character_data_handler( $parser, array( $this, 'elementData' ) );
-			$this->elementOpen( $parser, $name, $attribs );
-		} else {
-			// We only need the first open element
-			xml_set_element_handler( $parser, false, false );
+	/**
+	 * @param $name element or attribute name, maybe with a full or short prefix
+	 * @param $namespaceURI the namespaceURI
+	 * @return string the name prefixed with namespaceURI
+	 */
+	private function expandNS( $name, $namespaceURI ) {
+		if ( $namespaceURI ) {
+			$parts = explode( ':', $name );
+			$localname = array_pop( $parts );
+			return "$namespaceURI:$localname";
 		}
+		return $name;
 	}
 
 	/**
-	 * @param $parser
 	 * @param $name
 	 * @param $attribs
 	 */
-	private function elementOpen( $parser, $name, $attribs ) {
+	private function elementOpen( $name, $attribs ) {
 		$this->elementDataContext[] = array( $name, $attribs );
 		$this->elementData[] = '';
 		$this->stackDepth++;
 	}
 
 	/**
-	 * @param $parser
-	 * @param $name
 	 */
-	private function elementClose( $parser, $name ) {
+	private function elementClose() {
 		list( $name, $attribs ) = array_pop( $this->elementDataContext );
 		$data = array_pop( $this->elementData );
 		$this->stackDepth--;
 
-		if ( call_user_func(
+		if ( is_callable( $this->filterCallback )
+			&& call_user_func(
 			$this->filterCallback,
 			$name,
 			$attribs,
 			$data
-		) ) {
-			// Filter hit!
+			)
+		) {
+			// Filter hit
 			$this->filterMatch = true;
 		}
 	}
 
 	/**
-	 * @param $parser
 	 * @param $data
 	 */
-	private function elementData( $parser, $data ) {
-		// xml_set_character_data_handler breaks the data on & characters, so
-		// we collect any data here, and we'll run the callback in elementClose
+	private function elementData( $data ) {
+		// Collect any data here, and we'll run the callback in elementClose
 		$this->elementData[ $this->stackDepth - 1 ] .= trim( $data );
 	}
 
 	/**
-	 * @param $parser
 	 * @param $target
 	 * @param $data
 	 */
-	private function processingInstructionHandler( $parser, $target, $data ) {
-		if ( call_user_func( $this->parserOptions['processing_instruction_handler'], $target, $data ) ) {
+	private function processingInstructionHandler( $target, $data ) {
+		if ( $this->parserOptions['processing_instruction_handler'] ) {
+			if ( call_user_func(
+				$this->parserOptions['processing_instruction_handler'],
+				$target,
+				$data
+			) ) {
 			// Filter hit!
 			$this->filterMatch = true;
 		}
 	}
 }
+}
