2014-05-20 20:20:27 +02:00
|
|
|
<?php
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
namespace PicoFeed\Parser;
|
2014-05-20 20:20:27 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
use Closure;
|
2014-05-20 20:20:27 +02:00
|
|
|
use DomDocument;
|
|
|
|
use SimpleXmlElement;
|
|
|
|
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* XML parser class.
|
2014-05-20 20:20:27 +02:00
|
|
|
*
|
|
|
|
* Checks for XML eXternal Entity (XXE) and XML Entity Expansion (XEE) attacks on XML documents
|
|
|
|
*
|
|
|
|
* @author Frederic Guillot
|
|
|
|
*/
|
|
|
|
class XmlParser
|
|
|
|
{
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get a SimpleXmlElement instance or return false.
|
2014-05-20 20:20:27 +02:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $input XML content
|
|
|
|
*
|
2014-05-20 20:20:27 +02:00
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public static function getSimpleXml($input)
|
|
|
|
{
|
|
|
|
$dom = self::getDomDocument($input);
|
|
|
|
|
|
|
|
if ($dom !== false) {
|
|
|
|
$simplexml = simplexml_import_dom($dom);
|
|
|
|
|
2015-10-20 04:49:30 +02:00
|
|
|
if (!$simplexml instanceof SimpleXmlElement) {
|
2014-05-20 20:20:27 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $simplexml;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Scan the input for XXE attacks.
|
2014-05-20 20:20:27 +02:00
|
|
|
*
|
2015-10-20 04:49:30 +02:00
|
|
|
* @param string $input Unsafe input
|
|
|
|
* @param Closure $callback Callback called to build the dom.
|
|
|
|
* Must be an instance of DomDocument and receives the input as argument
|
2014-12-24 03:28:26 +01:00
|
|
|
*
|
2015-10-20 04:49:30 +02:00
|
|
|
* @return bool|DomDocument False if an XXE attack was discovered,
|
|
|
|
* otherwise the return of the callback
|
2014-05-20 20:20:27 +02:00
|
|
|
*/
|
2014-12-24 03:28:26 +01:00
|
|
|
private static function scanInput($input, Closure $callback)
|
2014-05-20 20:20:27 +02:00
|
|
|
{
|
2015-04-11 02:34:48 +02:00
|
|
|
$isRunningFpm = substr(php_sapi_name(), 0, 3) === 'fpm';
|
|
|
|
|
|
|
|
if ($isRunningFpm) {
|
2014-05-20 20:20:27 +02:00
|
|
|
|
|
|
|
// If running with PHP-FPM and an entity is detected we refuse to parse the feed
|
|
|
|
// @see https://bugs.php.net/bug.php?id=64938
|
|
|
|
if (strpos($input, '<!ENTITY') !== false) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-10-20 04:49:30 +02:00
|
|
|
} else {
|
2015-04-11 02:34:48 +02:00
|
|
|
$entityLoaderDisabled = libxml_disable_entity_loader(true);
|
2014-05-20 20:20:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
libxml_use_internal_errors(true);
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$dom = $callback($input);
|
2014-05-20 20:20:27 +02:00
|
|
|
|
|
|
|
// Scan for potential XEE attacks using ENTITY
|
|
|
|
foreach ($dom->childNodes as $child) {
|
|
|
|
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
|
|
|
|
if ($child->entities->length > 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-11 02:34:48 +02:00
|
|
|
if ($isRunningFpm === false) {
|
|
|
|
libxml_disable_entity_loader($entityLoaderDisabled);
|
|
|
|
}
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
return $dom;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get a DomDocument instance or return false.
|
2014-05-20 20:20:27 +02:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $input XML content
|
|
|
|
*
|
2015-03-01 19:56:11 +01:00
|
|
|
* @return \DOMNDocument
|
2014-05-20 20:20:27 +02:00
|
|
|
*/
|
2014-12-24 03:28:26 +01:00
|
|
|
public static function getDomDocument($input)
|
2014-05-20 20:20:27 +02:00
|
|
|
{
|
2015-03-01 19:56:11 +01:00
|
|
|
if (empty($input)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$dom = self::scanInput($input, function ($in) {
|
2015-10-20 04:49:30 +02:00
|
|
|
$dom = new DomDocument();
|
2014-12-24 03:28:26 +01:00
|
|
|
$dom->loadXml($in, LIBXML_NONET);
|
2015-10-20 04:49:30 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
return $dom;
|
|
|
|
});
|
2014-05-20 20:20:27 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// The document is empty, there is probably some parsing errors
|
|
|
|
if ($dom && $dom->childNodes->length === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $dom;
|
|
|
|
}
|
2014-05-20 20:20:27 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Load HTML document by using a DomDocument instance or return false on failure.
|
2014-12-24 03:28:26 +01:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $input XML content
|
|
|
|
*
|
2014-12-24 03:28:26 +01:00
|
|
|
* @return \DOMDocument
|
|
|
|
*/
|
|
|
|
public static function getHtmlDocument($input)
|
|
|
|
{
|
2014-12-26 16:56:50 +01:00
|
|
|
if (empty($input)) {
|
2015-10-20 04:49:30 +02:00
|
|
|
return new DomDocument();
|
2014-12-26 16:56:50 +01:00
|
|
|
}
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
|
2014-12-24 03:28:26 +01:00
|
|
|
$callback = function ($in) {
|
2015-10-20 04:49:30 +02:00
|
|
|
$dom = new DomDocument();
|
2014-12-24 03:28:26 +01:00
|
|
|
$dom->loadHTML($in, LIBXML_NONET);
|
2015-10-20 04:49:30 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
return $dom;
|
|
|
|
};
|
2015-10-20 04:49:30 +02:00
|
|
|
} else {
|
2014-12-24 03:28:26 +01:00
|
|
|
$callback = function ($in) {
|
2015-10-20 04:49:30 +02:00
|
|
|
$dom = new DomDocument();
|
2014-12-24 03:28:26 +01:00
|
|
|
$dom->loadHTML($in);
|
2015-10-20 04:49:30 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
return $dom;
|
|
|
|
};
|
2014-05-20 20:20:27 +02:00
|
|
|
}
|
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
return self::scanInput($input, $callback);
|
2014-05-20 20:20:27 +02:00
|
|
|
}
|
|
|
|
|
2014-10-19 20:42:31 +02:00
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Convert a HTML document to XML.
|
2014-10-19 20:42:31 +02:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $html HTML document
|
|
|
|
*
|
2014-10-19 20:42:31 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
2015-10-20 04:49:30 +02:00
|
|
|
public static function htmlToXml($html)
|
2014-10-19 20:42:31 +02:00
|
|
|
{
|
|
|
|
$dom = self::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$html);
|
2015-10-20 04:49:30 +02:00
|
|
|
|
2014-10-19 20:42:31 +02:00
|
|
|
return $dom->saveXML($dom->getElementsByTagName('body')->item(0));
|
|
|
|
}
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get XML parser errors.
|
2014-05-20 20:20:27 +02:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
2014-05-20 20:20:27 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getErrors()
|
|
|
|
{
|
|
|
|
$errors = array();
|
|
|
|
|
2015-10-20 04:49:30 +02:00
|
|
|
foreach (libxml_get_errors() as $error) {
|
2014-05-20 20:20:27 +02:00
|
|
|
$errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)',
|
|
|
|
$error->message,
|
|
|
|
$error->line,
|
|
|
|
$error->column,
|
|
|
|
$error->code
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode(', ', $errors);
|
|
|
|
}
|
2014-05-25 14:47:03 +02:00
|
|
|
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get the encoding from a xml tag.
|
2014-05-25 14:47:03 +02:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $data Input data
|
|
|
|
*
|
2014-05-25 14:47:03 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getEncodingFromXmlTag($data)
|
|
|
|
{
|
|
|
|
$encoding = '';
|
|
|
|
|
|
|
|
if (strpos($data, '<?xml') !== false) {
|
|
|
|
$data = substr($data, 0, strrpos($data, '?>'));
|
|
|
|
$data = str_replace("'", '"', $data);
|
|
|
|
|
|
|
|
$p1 = strpos($data, 'encoding=');
|
|
|
|
$p2 = strpos($data, '"', $p1 + 10);
|
|
|
|
|
2015-01-28 02:13:16 +01:00
|
|
|
if ($p1 !== false && $p2 !== false) {
|
|
|
|
$encoding = substr($data, $p1 + 10, $p2 - $p1 - 10);
|
|
|
|
$encoding = strtolower($encoding);
|
|
|
|
}
|
2014-05-25 14:47:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $encoding;
|
|
|
|
}
|
2014-10-19 20:42:31 +02:00
|
|
|
|
2015-03-01 19:56:11 +01:00
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get the charset from a meta tag.
|
2015-03-01 19:56:11 +01:00
|
|
|
*
|
|
|
|
* @static
|
2015-10-20 04:49:30 +02:00
|
|
|
*
|
|
|
|
* @param string $data Input data
|
|
|
|
*
|
2015-03-01 19:56:11 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getEncodingFromMetaTag($data)
|
|
|
|
{
|
|
|
|
$encoding = '';
|
|
|
|
|
2015-03-26 00:59:41 +01:00
|
|
|
if (preg_match('/<meta.*?charset\s*=\s*["\']?\s*([^"\'\s\/>;]+)/i', $data, $match) === 1) {
|
|
|
|
$encoding = strtolower($match[1]);
|
2015-03-01 19:56:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $encoding;
|
|
|
|
}
|
|
|
|
|
2014-10-19 20:42:31 +02:00
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Rewrite XPath query to use namespace-uri and local-name derived from prefix.
|
|
|
|
*
|
|
|
|
* @param string $query XPath query
|
|
|
|
* @param array $ns Prefix to namespace URI mapping
|
2014-10-19 20:42:31 +02:00
|
|
|
*
|
2015-07-19 17:19:26 +02:00
|
|
|
* @return string
|
2014-10-19 20:42:31 +02:00
|
|
|
*/
|
2015-10-20 04:49:30 +02:00
|
|
|
public static function replaceXPathPrefixWithNamespaceURI($query, array $ns)
|
|
|
|
{
|
|
|
|
return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) {
|
2015-07-19 17:19:26 +02:00
|
|
|
// don't try to map the special prefix XML
|
|
|
|
if (strtolower($matches[1]) === 'xml') {
|
|
|
|
return $matches[0];
|
|
|
|
}
|
2014-10-19 20:42:31 +02:00
|
|
|
|
2015-07-19 17:19:26 +02:00
|
|
|
return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]';
|
|
|
|
},
|
|
|
|
$query);
|
2014-10-19 20:42:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-10-20 04:49:30 +02:00
|
|
|
* Get the result elements of a XPath query.
|
|
|
|
*
|
|
|
|
* @param \SimpleXMLElement $xml XML element
|
|
|
|
* @param string $query XPath query
|
|
|
|
* @param array $ns Prefix to namespace URI mapping
|
2014-10-19 20:42:31 +02:00
|
|
|
*
|
2015-07-19 17:19:26 +02:00
|
|
|
* @return \SimpleXMLElement
|
2014-10-19 20:42:31 +02:00
|
|
|
*/
|
2015-07-19 17:19:26 +02:00
|
|
|
public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array())
|
2014-10-19 20:42:31 +02:00
|
|
|
{
|
2015-10-20 04:49:30 +02:00
|
|
|
if (!empty($ns)) {
|
2015-07-19 17:19:26 +02:00
|
|
|
$query = static::replaceXPathPrefixWithNamespaceURI($query, $ns);
|
2014-10-19 20:42:31 +02:00
|
|
|
}
|
|
|
|
|
2015-07-19 17:19:26 +02:00
|
|
|
return $xml->xpath($query);
|
2014-10-19 20:42:31 +02:00
|
|
|
}
|
2014-05-20 20:20:27 +02:00
|
|
|
}
|