PageRenderTime 37ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/core/src/main/php/xml/DomXSLProcessor.class.php

https://github.com/Gamepay/xp-framework
PHP | 464 lines | 206 code | 60 blank | 198 comment | 36 complexity | fe2a5b517fc2591c11cc2deeb1150adf MD5 | raw file
  1. <?php
  2. /* This class is part of the XP framework
  3. *
  4. * $Id$
  5. */
  6. uses(
  7. 'xml.Tree',
  8. 'xml.TransformerException',
  9. 'io.FileNotFoundException',
  10. 'xml.IXSLProcessor',
  11. 'xml.XSLCallback',
  12. 'xml.xslt.XSLDateCallback',
  13. 'xml.xslt.XSLStringCallback'
  14. );
  15. /**
  16. * XSL Processor using DomXML
  17. *
  18. * Usage example [Transform two files]
  19. * <code>
  20. * $proc= new DomXSLProcessor();
  21. * $proc->setXSLFile('test.xsl');
  22. * $proc->setXMLFile('test.xml');
  23. * $proc->setProfiling('/tmp/xsltprofiles.txt');
  24. *
  25. * try {
  26. * $proc->run();
  27. * } catch(TransformerException $e) {
  28. * $e->printStackTrace();
  29. * exit();
  30. * }
  31. *
  32. * var_dump($proc->output());
  33. * </code>
  34. *
  35. * @purpose Transform XML/XSLT using PHPs XSLT functions
  36. * @ext xslt
  37. * @test xp://net.xp_framework.unittest.xml.DomXslProcessorTest
  38. */
  39. class DomXSLProcessor extends Object implements IXSLProcessor {
  40. public
  41. $processor = NULL,
  42. $stylesheet = NULL,
  43. $document = NULL,
  44. $params = array(),
  45. $output = '',
  46. $outputEncoding = '',
  47. $baseURI = '',
  48. $profiling = '';
  49. public
  50. $_instances = array(),
  51. $_base = '';
  52. static function __static() {
  53. libxml_use_internal_errors(TRUE);
  54. // Forwards compatibility hack: In XSL, we use XSLCallback::invoke(), but
  55. // the class in namespaced PHP is called xml::XSLCallback. Ensure a class
  56. // called "XSLCallback" (without namespaces) exists instead of requiring
  57. // everybody to change their XSL files!
  58. class_exists('XSLCallback', FALSE) || eval('class XSLCallback extends '.xp::reflect('xml.XSLCallback').' {}');
  59. }
  60. /**
  61. * Constructor
  62. *
  63. */
  64. public function __construct() {
  65. $this->registerInstance('xp.date', new XSLDateCallback());
  66. $this->registerInstance('xp.string', new XSLStringCallback());
  67. }
  68. /**
  69. * Set and activate profiling output
  70. *
  71. * @param string path location of output
  72. */
  73. public function setProfiling($path) {
  74. $this->profiling = $path;
  75. }
  76. /**
  77. * Retrieve profiling setting
  78. *
  79. * @return string value
  80. */
  81. public function getProfiling() {
  82. return $this->profiling;
  83. }
  84. /**
  85. * Set base directory
  86. *
  87. * @param string dir
  88. */
  89. public function setBase($dir) {
  90. $this->_base= strpos($dir, '://') === FALSE
  91. ? rtrim(realpath($dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR
  92. : $dir;
  93. }
  94. /**
  95. * Get base
  96. *
  97. * @return string
  98. */
  99. public function getBase() {
  100. return $this->_base;
  101. }
  102. /**
  103. * Set a scheme handler
  104. *
  105. * @param var callback
  106. * @see php://xslt_set_scheme_handlers
  107. */
  108. public function setSchemeHandler($defines) {
  109. // Not implemented in DOM
  110. }
  111. /**
  112. * Set XSL file
  113. *
  114. * @param string file file name
  115. * @throws io.FileNotFoundException
  116. */
  117. public function setXSLFile($file) {
  118. if (!file_exists($this->_base.urldecode($file)))
  119. throw new FileNotFoundException($this->_base.$file.' not found');
  120. libxml_get_last_error() && libxml_clear_errors();
  121. $this->stylesheet= new DOMDocument();
  122. $this->stylesheet->load($this->_base.urldecode($file));
  123. $this->baseURI= $this->_base.$file;
  124. $this->_checkErrors($file);
  125. }
  126. /**
  127. * Set XSL document
  128. *
  129. * @param php.DOMDocument doc
  130. */
  131. public function setXSLDoc(DOMDocument $doc) {
  132. $this->stylesheet= $doc;
  133. }
  134. /**
  135. * Set XSL buffer
  136. *
  137. * @param string xsl the XSL as a string
  138. */
  139. public function setXSLBuf($xsl) {
  140. libxml_get_last_error() && libxml_clear_errors();
  141. $this->stylesheet= new DOMDocument();
  142. $this->stylesheet->loadXML($xsl);
  143. strlen($this->_base) && $this->stylesheet->documentURI= $this->_base;
  144. $this->baseURI= $this->_base.':string';
  145. $this->_checkErrors($xsl);
  146. }
  147. /**
  148. * Set XSL from a tree
  149. *
  150. * @param xml.Tree xsl
  151. */
  152. public function setXSLTree(Tree $xsl) {
  153. libxml_get_last_error() && libxml_clear_errors();
  154. $this->stylesheet= new DOMDocument();
  155. $this->stylesheet->loadXML($xsl->getDeclaration().$xsl->getSource(INDENT_NONE));
  156. strlen($this->_base) && $this->stylesheet->documentURI= $this->_base;
  157. $this->baseURI= $this->_base.':tree';
  158. $this->_checkErrors($xsl);
  159. }
  160. /**
  161. * Set XML file
  162. *
  163. * @param string file file name
  164. */
  165. public function setXMLFile($file) {
  166. if (!file_exists($this->_base.urldecode($file))) {
  167. throw new FileNotFoundException($this->_base.$file.' not found');
  168. }
  169. libxml_get_last_error() && libxml_clear_errors();
  170. $this->document= new DOMDocument();
  171. $this->document->load(urldecode($file));
  172. $this->_checkErrors($file);
  173. }
  174. /**
  175. * Set XML buffer
  176. *
  177. * @param string xml the XML as a string
  178. */
  179. public function setXMLBuf($xml) {
  180. libxml_get_last_error() && libxml_clear_errors();
  181. $this->document= new DOMDocument();
  182. $this->document->loadXML($xml);
  183. $this->_checkErrors($xml);
  184. }
  185. /**
  186. * Set XML from a tree
  187. *
  188. * @param xml.Tree xml
  189. */
  190. public function setXMLTree(Tree $xml) {
  191. libxml_get_last_error() && libxml_clear_errors();
  192. $this->document= new DOMDocument();
  193. $this->document->loadXML($xml->getDeclaration().$xml->getSource(INDENT_NONE));
  194. $this->_checkErrors($xml);
  195. }
  196. /**
  197. * Set XML document
  198. *
  199. * @param php.DOMDocument doc
  200. */
  201. public function setXMLDoc(DOMDocument $doc) {
  202. $this->document= $doc;
  203. }
  204. /**
  205. * Set XSL transformation parameters
  206. *
  207. * @param array params associative array { param_name => param_value }
  208. */
  209. public function setParams($params) {
  210. $this->params= $params;
  211. }
  212. /**
  213. * Set XSL transformation parameter
  214. *
  215. * @param string name
  216. * @param string value
  217. */
  218. public function setParam($name, $val) {
  219. $this->params[$name]= $val;
  220. }
  221. /**
  222. * Retrieve XSL transformation parameter
  223. *
  224. * @param string name
  225. * @return string value
  226. */
  227. public function getParam($name) {
  228. return $this->params[$name];
  229. }
  230. /**
  231. * Retrieve messages generate during processing.
  232. *
  233. * @return string[]
  234. */
  235. public function getMessages() {
  236. return libxml_get_last_error();
  237. }
  238. /**
  239. * Register object instance under defined name
  240. * for access from XSL callbacks.
  241. *
  242. * @param string name
  243. * @param lang.Object instance
  244. */
  245. function registerInstance($name, $instance) {
  246. $this->_instances[$name]= $instance;
  247. }
  248. /**
  249. * Determine output encoding.
  250. *
  251. * Note: This is a workaround for the problem that when calling:
  252. * <code>
  253. * $result= $processor->transformToXML($xml);
  254. * </code>
  255. * the charset of $result (a string) is unknown.
  256. *
  257. * When using:
  258. * <code>
  259. * $result= $processor->transformToDoc($xml);
  260. * </code>
  261. * the charset can be retrieved by having a look at $result's actualEncoding
  262. * property, but then again the output method is neglected and we do not
  263. * know what to save the result as (saveHTML? saveXML?)
  264. *
  265. * Thus we manually check for xsl:output in the stylesheet and all its
  266. * includes and imports (recursively!) - the overhead is typically about
  267. * 8 to 10 milliseconds.
  268. *
  269. * @param php.DOMElement root
  270. * @param string base
  271. * @return string encoding or NULL if no user-defined encoding could be found
  272. * @throws xml.TransformerException in case an include or import cannot be found
  273. */
  274. protected function determineOutputEncoding(DOMElement $root, $base) {
  275. static $xsltNs= 'http://www.w3.org/1999/XSL/Transform';
  276. // Check whether a xsl:output-method tag exists and if it has an
  277. // encoding attribute - in this case, we've one.
  278. if ($output= $root->getElementsByTagNameNS($xsltNs, 'output')->item(0)) {
  279. if ($e= $output->getAttribute('encoding')) return $e;
  280. }
  281. $baseDir= dirname($base);
  282. // Check xsl:include nodes
  283. foreach ($root->getElementsByTagNameNS($xsltNs, 'include') as $include) {
  284. $dom= new DOMDocument();
  285. $href= $include->getAttribute('href');
  286. if (!('/' === $href{0} || strstr($href, '://') || ':/' === substr($href, 1, 2))) {
  287. $href= $baseDir.'/'.$href; // Relative
  288. }
  289. if (!($dom->load(urldecode($href)))) {
  290. throw new TransformerException('Cannot find include '.$href."\n at ".$base);
  291. }
  292. if ($e= $this->determineOutputEncoding($dom->documentElement, $href)) return $e;
  293. }
  294. // Check xsl:import nodes
  295. foreach ($root->getElementsByTagNameNS($xsltNs, 'import') as $import) {
  296. $dom= new DOMDocument();
  297. $href= $import->getAttribute('href');
  298. if (!('/' === $href{0} || strstr($href, '://') || ':/' === substr($href, 1, 2))) {
  299. $href= $baseDir.'/'.$href; // Relative
  300. }
  301. if (!($dom->load(urldecode($href)))) {
  302. throw new TransformerException('Cannot find import '.$href."\n at ".$base);
  303. }
  304. if ($e= $this->determineOutputEncoding($dom->documentElement, $href)) return $e;
  305. }
  306. // Cannot determine encoding
  307. return NULL;
  308. }
  309. /**
  310. * Run the XSL transformation
  311. *
  312. * @return bool success
  313. * @throws xml.TransformerException
  314. */
  315. public function run() {
  316. libxml_get_last_error() && libxml_clear_errors();
  317. $this->processor= new XSLTProcessor();
  318. $this->processor->importStyleSheet($this->stylesheet);
  319. $this->processor->setParameter('', $this->params);
  320. if('' !== $this->getProfiling()) {
  321. $this->processor->setProfiling($this->getProfiling() .'.'. getmypid());
  322. }
  323. // If we have registered instances, register them in XSLCallback
  324. if (sizeof($this->_instances)) {
  325. $cb= XSLCallback::getInstance();
  326. foreach ($this->_instances as $name => $instance) {
  327. $cb->registerInstance($name, $instance);
  328. }
  329. }
  330. $this->processor->registerPHPFunctions(array('XSLCallback::invoke'));
  331. if (NULL === ($this->outputEncoding= $this->determineOutputEncoding(
  332. $this->stylesheet->documentElement,
  333. $this->baseURI
  334. ))) {
  335. $this->outputEncoding= 'utf-8'; // Use default
  336. }
  337. // Start transformation
  338. $this->output= $this->processor->transformToXML($this->document);
  339. // Perform cleanup when necessary (free singleton for further use)
  340. sizeof($this->_instances) && XSLCallback::getInstance()->clearInstances();
  341. // Check for transformation errors
  342. if (FALSE === $this->output) {
  343. // Check for errors, also non-fatal errors, otherwise indicate unknown
  344. // transformation error
  345. $this->_checkErrors(NULL, TRUE);
  346. throw new TransformerException('Unknown XSL transformation error while transforming '.$this->baseURI);
  347. }
  348. // Check for left-over errors that did not make the transformation fail
  349. $this->_checkErrors('<transformation>');
  350. return TRUE;
  351. }
  352. /**
  353. * Check for XML/XSLT errors and throw exceptions accordingly.
  354. *
  355. * In case fatal is TRUE, any libxml error will be treated as a
  356. * fatal one, resulting in an exception.
  357. *
  358. *
  359. * @param string source default NULL
  360. * @param bool fatal default FALSE
  361. * @throws xml.TransformerException in case an XML error has occurred
  362. */
  363. protected function _checkErrors($source= NULL, $fatal= FALSE) {
  364. if (sizeof($errors= libxml_get_errors())) {
  365. libxml_clear_errors();
  366. $message= '';
  367. foreach ($errors as $error) {
  368. if (
  369. LIBXML_ERR_FATAL == $error->level || // In general: Fatals
  370. 1212 == $error->code || // Invalid number of arguments
  371. strpos($error->message, 'has not been declared') // Undeclared variables
  372. ) $fatal= TRUE;
  373. $message.= sprintf(
  374. " #%d: %s\n at %s, line %d, column %d\n",
  375. $error->code,
  376. trim($error->message, " \n"),
  377. strlen($error->file) ? $error->file : xp::stringOf($source),
  378. $error->line,
  379. $error->column
  380. );
  381. }
  382. if ($fatal) throw new TransformerException(
  383. 'XSL Transformation error: '.trim($message, " \n")
  384. );
  385. }
  386. }
  387. /**
  388. * Retrieve the transformation's result
  389. *
  390. * @return string
  391. */
  392. public function output() {
  393. return (string)$this->output;
  394. }
  395. /**
  396. * Retrieve the transformation's result's encoding
  397. *
  398. * @return string
  399. */
  400. public function outputEncoding() {
  401. return $this->outputEncoding;
  402. }
  403. }
  404. ?>