1 <?php
2
3 /******************************************************************************\
4 * IMPORT REQUIRED CLASSES
5 \******************************************************************************/
6
7 /**
8 * Import the root (base) XAO class.
9 *
10 * All classes in the XAO library should have this class at the top of their
11 * inheritance tree. See documentation on the class itself for more details.
12 * Including the following DomDoc class will automatically include the XaoRoot
13 * definition, however, it is included here for clarity and completeness.
14 *
15 * @import XaoRoot
16 */
17 include_once "XAO_XaoRoot.php";
18
19 /**
20 * Import the basic DomDoc class for inheritance.
21 *
22 * This class is based on DomDoc and hence supports all of it's functionality,
23 * including it's ability to be consumed by another DomDoc based object.
24 *
25 * @import DomDoc
26 */
27 include_once "XAO_DomDoc.php";
28
29 /**
30 * Import an XML parsing utility class used for debugging XML errors.
31 *
32 * @import DomDoc
33 */
34 include_once "XAO_DomFactory.php";
35
36 /**
37 * Utility performing XSLT transformations
38 *
39 * This class encapsulates comprehensive XSLT functionality. It makes available
40 * multiple (pluggable) transformation engines through a unified, simplified API.
41 *
42 * @author Terence Kearns
43 * @version 0.0
44 * @copyright Terence Kearns 2003
45 * @license LGPL
46 * @package XAO
47 * @link http://xao-php.sourceforge.net
48 */
49 class Transformer extends XaoRoot {
50
51 /**
52 * DOM XML object instance of the source document
53 *
54 * This has to be specified in the constructor. The Transformer class will
55 * not work without this object and a DOM XML instnace of the stylsheet.
56 * The constructor may need to ensure this object eventuates by parsing
57 * in external XML data - same applies for the stylesheet.
58 *
59 * @access public
60 * @var object
61 */
62 var $objSrc;
63
64 /**
65 * Root element of source document object
66 *
67 * @access public
68 * @var node
69 */
70 var $ndSrcRoot;
71
72 /**
73 * DOM XML object instance of stylsheet used in transformation
74 *
75 * This has to be specified in the constructor. The Transformer class will
76 * not work without this object and a DOM XML instnace of the source doc.
77 * The constructor may need to ensure this object eventuates by parsing
78 * in external XML data - same applies for the source doc.
79 *
80 * @access public
81 * @var object
82 */
83 var $objStyle;
84
85 /**
86 * Root element of source document object
87 *
88 * @access public
89 * @var node
90 */
91 var $ndStyleRoot;
92
93 var $_uriStyleSheet;
94
95 /**
96 * List of XSL parameters to be passed to the XSLT processor
97 *
98 * Usually these will be set by the application class which inherits DomDoc.
99 * The transform methods of this class should use this associative array to
100 * add these parameters to the processor.
101 * WARNING: RELYING ON THIS METHOD OF SETTING XSL PARAMS WILL MAKE YOUR
102 * STYLSHEET INCOMPATIBLE WITH CLIENT-SIDE TRANSFORMATION.
103 * IMPORTANT NOTE: YOUR PARAMS WILL NOT BE AVAILABLE IN YOUR STYLSHEET IF
104 * YOU DO NOT DECLARE THEM (WITH EMPTY VALUES). THE PROCESSOR WILL ONLY FEED
105 * PARAM VALUES TO THE STYLESHEET BY OVERRIDING THE VALUES OF EXISTING ONES.
106 *
107 * @access public
108 * @var array
109 */
110 var $arrXslParams = array();
111
112 /**
113 * Which XSLT processor to use
114 *
115 * This option allows the user to choose which implemented XSLT processor
116 * to employ. At this stage, possible choices are:
117 * - SABLOTRON which uses the xslt_ functions built into PHP.
118 * - DOMXML which uses the experimental transformation capabilities of the
119 * native PHP domxml extension itself.
120 * Future implementations could use external procesors which may be Java,
121 * Com, or command-line executables.
122 *
123 * @access public
124 * @var string
125 */
126 var $strXsltProcessor = "DOMXML";
127
128 /**
129 * XSLT processing result container
130 *
131 * Regardless of which XSLT processor is used, the result is always stored
132 * in this variable AS A STRING.
133 *
134 * @access public
135 * @var string
136 */
137 var $strXsltResult;
138
139 /**
140 * Transformer constructor
141 *
142 * The main job here is to parse the source XML and the XSLT as DOM XML
143 * objects. Note that this technique incurs unneccesary overhead for
144 * tranform processors like sablotron. But this is neccesary to maintain
145 * the simplicity of the API - it's a trade-off!
146 * The stylesheet and the XML source can be supplied in one of three formats:
147 * 1) DOM XML object (preferrred)
148 * 2) file URI
149 * 3) well formed ascii data
150 *
151 * @param mixed starting XML source data
152 * @param mixed starting stylsheet
153 * @return void
154 * @access public
155 */
156 function Transformer(&$mxdSrc,&$mxdStyle) {
157
158 // set up source XML document
159 if(is_string($mxdSrc)) {
160
161 $objDomFactory =& new DomFactory($mxdSrc);
162
163 if(strlen($objDomFactory->strErrorMsg)) {
164 $this->strDebugData = $objDomFactory->strDebugData;
165 $this->Throw(
166 $objDomFactory->strErrorMsgFull,
167 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
168 );
169 }
170 else {
171 $this->objSrc =& $objDomFactory->objGetObjDoc();
172 }
173 }
174 elseif(is_object($mxdSrc)) {
175 $ndTest = $mxdSrc->document_element()
176 OR $this->Throw(
177 "Transformer: Object is not a PHP DOM XML object.",
178 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
179 );
180 if(is_object($ndTest)) {
181 $this->objSrc =& $mxdSrc;
182 //var_dump($ndTest);
183 //die();
184 }
185 else {
186 $this->Throw(
187 "Transformer: DOM XML object does not have a root element",
188 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
189 );
190 }
191 }
192 else {
193 if(is_null($mxdSrc)) {
194 $this->Throw(
195 "Transformer: NULL source XML argument",
196 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
197 );
198 }
199 else {
200 $this->Throw(
201 "Transformer: Invalid source XML argument",
202 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
203 );
204 }
205 }
206
207 // set up transformation document
208 if(is_string($mxdStyle)) {
209
210 $objDomFactory =& new DomFactory($mxdStyle);
211
212 if(strlen($objDomFactory->strErrorMsg)) {
213 $this->strDebugData = $objDomFactory->strDebugData;
214 $this->Throw(
215 $objDomFactory->strErrorMsgFull,
216 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
217 );
218 }
219 else {
220 $this->objStyle =& $objDomFactory->objGetObjDoc();
221 }
222 $this->_uriStyleSheet = $objDomFactory->uriContextFile;
223 }
224 elseif(is_object($mxdStyle)) {
225 $this->objStyle =& $mxdStyle;
226 }
227 else {
228 if(is_null($mxdStyle)) {
229 $this->Throw(
230 "Transformer: NULL stylesheet argument",
231 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
232 );
233 }
234 else {
235 $this->Throw(
236 "Transformer: Invalid stylesheet argument",
237 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
238 );
239 }
240 }
241
242 if(is_object($this->objStyle)) {
243 $this->ndStyleRoot =& $this->objStyle->document_element();
244 }
245
246 if(is_object($this->objSrc)) {
247 $this->ndSrcRoot =& $this->objSrc->document_element();
248 }
249 }
250
251 function Throw($strErrMsg,$arrAttribs = null) {
252 parent::Throw($strErrMsg,$arrAttribs);
253 // This is probably the ONLY place you
254 // will see a die() statement. This is
255 // because normally, errors are
256 // displayed by way of a stylsheet
257 // template. However if transformation
258 // cannot take place, then the exception
259 // will never be displayed.
260 // Exceptions should never be dealt with
261 // like this. This is a last resort.
262 if(strlen(trim($this->strDebugData)))
263 die("<br />\n".$this->strError.$this->strDebugData);
264 }
265
266 function SetXslParam($strName,$strValue) {
267 // This test should be improved to use
268 // a regex which properly check for
269 // stylesheet param name legality
270 if($this->blnTestSafeName($strName)) {
271 $this->arrXslParams[$strName] = $strValue;
272 }
273 else {
274 $this->Throw(
275 "Please specify a valid XSL parameter name. "
276 ."Multiline, beginning with numbers, non-alpha-numeric "
277 ."characters are NOT allowed. An underscore is allowed.",
278 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
279 );
280 }
281 }
282 /**
283 * Transform contents using stylsheet in $this->_uriStylSheet
284 *
285 * This function calls the Transformer class which takes a DomDoc in the
286 * first argument to the constructor as a reference. The Transformer object
287 * records any errors using the passed DomDoc's Throw() method (so the
288 * errors are bound to that source document). This function makes some
289 * decisions on how errors are released based on options set in $this
290 * DomDoc instance. Since a transformation result isn't always well-formed
291 * XML, the result is returned as a string. Here are the steps taken:
292 * 1) check for the existance of a stylsheet
293 * 2) route the request to the chosen XSLT processor member function
294 * 3) return the result.
295 *
296 * @return string The contents of the transformation result or false
297 * on failure
298 * @access public
299 */
300 function Transform() {
301 // provide a handy alternative for
302 // client side access to server-side
303 // XSL parameters.
304 if(count($this->arrXslParams)) {
305 // establish XAO namespace prefix
306 $strPfx = "";
307 if(strlen($this->strXaoNamespacePrefix))
308 $strPfx = $this->strXaoNamespacePrefix.":";
309 // set up container element for all
310 // params
311 $ndParams = $this->objSrc->create_element($strPfx."xslParams");
312 $ndParams = $this->ndSrcRoot->append_child($ndParams);
313 // create each param in this container
314 foreach($this->arrXslParams AS $strName => $strVal) {
315 $ndParam = $this->objSrc->create_element($strPfx."param");
316 $ndParam = $ndParams->append_child($ndParam);
317 $ndParam->set_attribute("name", $strName);
318 $ndParam->set_content($strVal);
319 }
320 }
321
322 $blnSuccess = false;
323 switch($this->strXsltProcessor) {
324 case "SABLOTRON":
325 $blnSuccess = $this->_TransformWithSablotron();
326 break;
327 case "DOMXML":
328 $blnSuccess = $this->_TransformWithDomxml();
329 break;
330 default:
331 $this->Throw(
332 "Transform: ".$this->
333 strXsltProcessor .": Not a valid XSLT processor.",
334 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
335 );
336 }
337
338 return $blnSuccess;
339 }
340
341 /**
342 * DOMXML XSLT implementation
343 *
344 * This processor implementation uses the native PHP DOM XML processor. At
345 * the time or writing, it is experimental. I noticed that it fails on very
346 * basic functionality and that the error reporting has not yet been refined
347 * The major advantage with this processor is that it is bundled with PHP and
348 * no additional PHP extensions need to be enabled. It is not ready for use
349 * though - neither in a production environement or for development (due to
350 * poor error reporting). SABLOTRON is reccommended but requires the XSLT
351 * extension to be enabled.
352 *
353 * @return boolean Success or failure (true/false).
354 * @access private
355 */
356 function _TransformWithDomxml() {
357 // adding XSL params from the server-
358 // side has important subtlties. The
359 // server should only *replace* existing
360 // xsl params, and only ones that
361 // exist immediately below the root.
362 // This behaviour is at least consistent
363 // with the Sablotron processor.
364 // Also, any existing "name" attrib
365 // should be removed since *either* the
366 // "name" attrib *or* the content is
367 // populated. We populate the element
368 // content because it's easier than
369 // having to create single-quoted
370 // strings. It's probably safer anyway.
371 $strPrfx = $this->ndStyleRoot->prefix();
372 if(strlen($strPrfx)) $strPrfx .= ":";
373 $strPName = $strPrfx."param";
374 $arrExistParamNames = array();
375 $arrNdExists =& $this->ndStyleRoot->child_nodes();
376 foreach($arrNdExists AS $ndExists) {
377 if($ndExists->node_type() == "domelement") {
378 if($ndExists->node_name() == $strPName) {
379 $strParamName = $ndExists->get_attribute("name");
380 if(isset($this->arrXslParams[$strParamName])) {
381 $ndExists->set_content(
382 $this->arrXslParams[$strParamName]
383 );
384 $ndExists->remove_attribute("select");
385 }
386 }
387 }
388 }
389 // convert the DOM doc to a stylsheet
390 // doc
391 if(!$objStylesheet = @domxml_xslt_stylesheet_doc($this->objStyle)) {
392 $this->Throw($this->_uriStyleSheet."
393 is an XML document but it is not a valid not a stylesheet.",
394 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
395 );
396 return false;
397 }
398 // Do the transformation and trap any
399 // error output.
400 ob_start();
401 $objDomResult =& $objStylesheet->process($this->objSrc);
402 $this->strDebugData = ob_get_contents();
403 ob_end_clean();
404 // extract the results
405 if(is_object($objDomResult)) {
406 $this->strXsltResult =& $objDomResult->dump_mem(true);
407 return true;
408 }
409 else {
410 $strErrMsg = "There were errors while trying to transform the
411 document <strong>".$this->_uriStyleSheet."</strong>
412 using the <strong>DOMXML</strong> processor. Be warned that this
413 processor is EXPERIMENTAL and should not be used. Error output
414 (below) not likely to be of much use.\n\n".$phpErr;
415 $this->Throw($strErrMsg,$this->arrSetErrFnc(__FUNCTION__,__LINE__));
416 return false;
417 }
418 }
419
420 /**
421 * Use the built-in SABLOTRON processor to perform XSLT
422 *
423 * The Sablotron XSLT processor (from gingerall.com) is an extremely fast and
424 * stable transformation engine. The class sets the default processor to
425 * SABLOTRON but this assumes that the server has the XSLT extension to PHP
426 * enabled. If it is absolutely not possible to have this extension enabled,
427 * then your application class (which should inherit DomDoc) can set this to
428 * DOMXML in the constructor. There are a number of reasons why you do not
429 * want to do this - see the API documentation for _TransformWithDomxml()
430 *
431 * @return boolean Sucess or failure (true/false).
432 * @access private
433 */
434 function _TransformWithSablotron() {
435 if(!extension_loaded("xslt")) $this->Throw(
436 "_TransformWithSablotron: This PHP server does have the XSLT "
437 ."extension enabled.",$this->arrSetErrFnc(__FUNCTION__,__LINE__)
438 );
439 $xt = @xslt_create() OR $this->Throw(
440 "_TransformWithSablotron: Could not create an XSLT processor.",
441 $this->arrSetErrFnc(__FUNCTION__,__LINE__)
442 );
443 xslt_set_error_handler($xt,array(&$this,"_SablotronErrorTrap"));
444
445 // Sablotron has the ability to
446 // register a base path location
447 // which it can use to resolve
448 // external entities such as import
449 // and include directives.
450 // Here, we provide the facility
451 // but can only do so if the style-
452 // sheet was specified via a URI -
453 // in which case $this->_uriStyleSheet
454 // is populated in the constructor.
455 if(file_exists($this->_uriStyleSheet)) {
456 // sablotron requires a URI protocol
457 // identifier for it's base path.
458 $uriBaseDir = "file://";
459 // determine if the style URI is
460 // relative or absolute.
461 if(
462 substr($this->_uriStyleSheet,0,1) != "/"
463 && substr($this->_uriStyleSheet,1,3) != ":/"
464 ) {
465 // if relative, use the contextual
466 // path prepended to the style uri path
467 $uriBaseDir .=
468 str_replace(
469 "\\",
470 "/",
471 dirname(realpath($_SERVER["PATH_TRANSLATED"]))
472 )."/";
473 }
474 // ends with style path
475 $uriBaseDir .= dirname($this->_uriStyleSheet)."/";
476 // apply the base path
477 xslt_set_base($xt,$uriBaseDir);
478 }
479
480 $args = array(
481 '/_xml' => $this->objSrc->dump_mem(),
482 '/_xsl' => $this->objStyle->dump_mem()
483 );
484
485 $this->strXsltResult = @xslt_process(
486 $xt,
487 "arg:/_xml",
488 "arg:/_xsl",
489 null,
490 $args,
491 $this->
492 arrXslParams );
493
494 if(strlen($this->strXsltResult)) return true;
495
496 return false;
497 }
498
499 function _SablotronErrorTrap(
500 $resSab,
501 $intSabErr,
502 $strSabLvl,
503 $arrSabErrData
504 ) {
505 $strMsg = "Sablotron XSLT error ";
506 // if applicable, reveal the location
507 // of the stylsheet in use.
508 if(strlen($this->_uriStyleSheet)) {
509 $strMsg .= " while transforming file <b>"
510 .$this->_uriStyleSheet."</b>";
511 }
512 // default debug text is stylsheet
513 $xslData =& $this->objStyle->dump_mem(true);
514 // assume that $xalData is the data in
515 // context for the Sablotron error.
516 $blnHaveTheRightFile = true;
517 // cater for errors occuring in
518 // external files processed by Sablotron
519 if(isset($arrSabErrData["URI"])) {
520 if($arrSabErrData["URI"] != "arg:/_xsl") {
521 // The data in context is different
522 $blnHaveTheRightFile = false;
523 $strMsg .= "<div>Actual error occured while processing external"
524 ." entity: <b>".$arrSabErrData["URI"]."</b></div>";
525 if(preg_match("/file\:\/\//i",$arrSabErrData["URI"]))
526 $arrSabErrData["URI"] =
527 substr($arrSabErrData["URI"],strlen("file://"));
528 if(file_exists($arrSabErrData["URI"])) {
529 // change the debug text to the
530 // external file contents.
531 $xslData = implode("",file($arrSabErrData["URI"]));
532 // we've managed to obtain the correct
533 // data in context
534 $blnHaveTheRightFile = true;
535 }
536 }
537 }
538 // dirty great red error message
539 $strMsg .=
540 "<div style=\"font-weight: bold; background: red; color: yellow; padding: 10px;\">"
541 .$arrSabErrData["msg"]."</div>";
542 // supplentary Sablotron debug info
543 foreach($arrSabErrData AS $strSabErrFld=>$strSabErrVal) {
544 if($strSabErrFld != "msg")
545 $strMsg .= $strSabErrFld.":".$strSabErrVal." ";
546 }
547 // do a text debug dump if we have a
548 // line number from sablotron. Don't
549 // populate this if the data is not in
550 // the context of the Sablotron error
551 // (avoid confusion).
552 if(isset($arrSabErrData["line"]) && $blnHaveTheRightFile) {
553 $objDebugData =&
554 new TextDebugger($xslData,$arrSabErrData["line"]);
555 // populate the all-important debug data
556 $this->strDebugData =& $objDebugData->strGetHtml();
557 }
558 elseif(preg_match("/XML parser error/i",$arrSabErrData["msg"])) {
559 $objDebugData =&
560 new TextDebugger($this->objSrc->dump_mem(true));
561 // populate the all-important debug data
562 $this->strDebugData =& $objDebugData->strGetHtml();
563 }
564 else {
565 $this->strDebugData = "<div><b>No debug output available.</b></div>";
566 }
567
568 $this->Throw($strMsg,$this->arrSetErrFnc(__FUNCTION__,__LINE__));
569 }
570
571 }
572 ?>