XmlParser.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. namespace PicoFeed\Parser;
  3. use DOMDocument;
  4. use SimpleXMLElement;
  5. use ZendXml\Exception\RuntimeException;
  6. use ZendXml\Security;
  7. /**
  8. * XML parser class.
  9. *
  10. * Checks for XML eXternal Entity (XXE) and XML Entity Expansion (XEE) attacks on XML documents
  11. *
  12. * @package PicoFeed\Parser
  13. * @author Frederic Guillot
  14. */
  15. class XmlParser
  16. {
  17. /**
  18. * Get a SimpleXmlElement instance or return false.
  19. *
  20. * @static
  21. * @param string $input XML content
  22. * @return mixed
  23. */
  24. public static function getSimpleXml($input)
  25. {
  26. return self::scan($input);
  27. }
  28. /**
  29. * Get a DomDocument instance or return false.
  30. *
  31. * @static
  32. * @param string $input XML content
  33. * @return DOMDocument|bool
  34. */
  35. public static function getDomDocument($input)
  36. {
  37. if (empty($input)) {
  38. return false;
  39. }
  40. $dom = self::scan($input, new DOMDocument());
  41. // The document is empty, there is probably some parsing errors
  42. if ($dom && $dom->childNodes->length === 0) {
  43. return false;
  44. }
  45. return $dom;
  46. }
  47. /**
  48. * Small wrapper around ZendXml to turn their exceptions into PicoFeed exceptions
  49. *
  50. * @static
  51. * @access private
  52. * @param string $input
  53. * @param DOMDocument $dom
  54. * @throws XmlEntityException
  55. * @return SimpleXMLElement|DomDocument|boolean
  56. */
  57. private static function scan($input, $dom = null)
  58. {
  59. try {
  60. return Security::scan($input, $dom);
  61. } catch(RuntimeException $e) {
  62. throw new XmlEntityException($e->getMessage());
  63. }
  64. }
  65. /**
  66. * Load HTML document by using a DomDocument instance or return false on failure.
  67. *
  68. * @static
  69. * @access public
  70. * @param string $input XML content
  71. * @return DOMDocument
  72. */
  73. public static function getHtmlDocument($input)
  74. {
  75. $dom = new DomDocument();
  76. if (empty($input)) {
  77. return $dom;
  78. }
  79. libxml_use_internal_errors(true);
  80. if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
  81. $dom->loadHTML($input, LIBXML_NONET);
  82. } else {
  83. $dom->loadHTML($input);
  84. }
  85. return $dom;
  86. }
  87. /**
  88. * Convert a HTML document to XML.
  89. *
  90. * @static
  91. * @access public
  92. * @param string $html HTML document
  93. * @return string
  94. */
  95. public static function htmlToXml($html)
  96. {
  97. $dom = self::getHtmlDocument('<?xml version="1.0" encoding="UTF-8">'.$html);
  98. return $dom->saveXML($dom->getElementsByTagName('body')->item(0));
  99. }
  100. /**
  101. * Get XML parser errors.
  102. *
  103. * @static
  104. * @access public
  105. * @return string
  106. */
  107. public static function getErrors()
  108. {
  109. $errors = array();
  110. foreach (libxml_get_errors() as $error) {
  111. $errors[] = sprintf('XML error: %s (Line: %d - Column: %d - Code: %d)',
  112. $error->message,
  113. $error->line,
  114. $error->column,
  115. $error->code
  116. );
  117. }
  118. return implode(', ', $errors);
  119. }
  120. /**
  121. * Get the encoding from a xml tag.
  122. *
  123. * @static
  124. * @access public
  125. * @param string $data Input data
  126. * @return string
  127. */
  128. public static function getEncodingFromXmlTag($data)
  129. {
  130. $encoding = '';
  131. if (strpos($data, '<?xml') !== false) {
  132. $data = substr($data, 0, strrpos($data, '?>'));
  133. $data = str_replace("'", '"', $data);
  134. $p1 = strpos($data, 'encoding=');
  135. $p2 = strpos($data, '"', $p1 + 10);
  136. if ($p1 !== false && $p2 !== false) {
  137. $encoding = substr($data, $p1 + 10, $p2 - $p1 - 10);
  138. $encoding = strtolower($encoding);
  139. }
  140. }
  141. return $encoding;
  142. }
  143. /**
  144. * Get the charset from a meta tag.
  145. *
  146. * @static
  147. * @access public
  148. * @param string $data Input data
  149. * @return string
  150. */
  151. public static function getEncodingFromMetaTag($data)
  152. {
  153. $encoding = '';
  154. if (preg_match('/<meta.*?charset\s*=\s*["\']?\s*([^"\'\s\/>;]+)/i', $data, $match) === 1) {
  155. $encoding = strtolower($match[1]);
  156. }
  157. return $encoding;
  158. }
  159. /**
  160. * Rewrite XPath query to use namespace-uri and local-name derived from prefix.
  161. *
  162. * @static
  163. * @access public
  164. * @param string $query XPath query
  165. * @param array $ns Prefix to namespace URI mapping
  166. * @return string
  167. */
  168. public static function replaceXPathPrefixWithNamespaceURI($query, array $ns)
  169. {
  170. return preg_replace_callback('/([A-Z0-9]+):([A-Z0-9]+)/iu', function ($matches) use ($ns) {
  171. // don't try to map the special prefix XML
  172. if (strtolower($matches[1]) === 'xml') {
  173. return $matches[0];
  174. }
  175. return '*[namespace-uri()="'.$ns[$matches[1]].'" and local-name()="'.$matches[2].'"]';
  176. },
  177. $query);
  178. }
  179. /**
  180. * Get the result elements of a XPath query.
  181. *
  182. * @static
  183. * @access public
  184. * @param SimpleXMLElement $xml XML element
  185. * @param string $query XPath query
  186. * @param array $ns Prefix to namespace URI mapping
  187. * @return SimpleXMLElement[]
  188. */
  189. public static function getXPathResult(SimpleXMLElement $xml, $query, array $ns = array())
  190. {
  191. if (!empty($ns)) {
  192. $query = static::replaceXPathPrefixWithNamespaceURI($query, $ns);
  193. }
  194. return $xml->xpath($query);
  195. }
  196. /**
  197. * Get the first Xpath result or SimpleXMLElement value
  198. *
  199. * @static
  200. * @access public
  201. * @param mixed $value
  202. * @return string
  203. */
  204. public static function getValue($value)
  205. {
  206. $result = '';
  207. if (is_array($value) && count($value) > 0) {
  208. $result = (string) $value[0];
  209. } elseif (is_a($value, 'SimpleXMLElement')) {
  210. return $result = (string) $value;
  211. }
  212. return trim($result);
  213. }
  214. }