| [ Index ] |
|
Code source de eGroupWare 1.2.106-2 |
1 <?php 2 /** 3 * Php.XPath 4 * 5 * +======================================================================================================+ 6 * | A php class for searching an XML document using XPath, and making modifications using a DOM 7 * | style API. Does not require the DOM XML PHP library. 8 * | 9 * +======================================================================================================+ 10 * | What Is XPath: 11 * | -------------- 12 * | - "What SQL is for a relational database, XPath is for an XML document." -- Sam Blum 13 * | - "The primary purpose of XPath is to address parts of an XML document. In support of this 14 * | primary purpose, it also provides basic facilities for manipulting it." -- W3C 15 * | 16 * | XPath in action and a very nice intro is under: 17 * | http://www.zvon.org/xxl/XPathTutorial/General/examples.html 18 * | Specs Can be found under: 19 * | http://www.w3.org/TR/xpath W3C XPath Recommendation 20 * | http://www.w3.org/TR/xpath20 W3C XPath Recommendation 21 * | 22 * | NOTE: Most of the XPath-spec has been realized, but not all. Usually this should not be 23 * | problem as the missing part is either rarely used or it's simpler to do with PHP itself. 24 * +------------------------------------------------------------------------------------------------------+ 25 * | Requires PHP version 4.0.5 and up 26 * +------------------------------------------------------------------------------------------------------+ 27 * | Main Active Authors: 28 * | -------------------- 29 * | Nigel Swinson <nigelswinson@users.sourceforge.net> 30 * | Started around 2001-07, saved phpxml from near death and renamed to Php.XPath 31 * | Restructured XPath code to stay in line with XPath spec. 32 * | Sam Blum <bs_php@infeer.com> 33 * | Started around 2001-09 1st major restruct (V2.0) and testbench initiator. 34 * | 2nd (V3.0) major rewrite in 2002-02 35 * | Daniel Allen <bigredlinux@yahoo.com> 36 * | Started around 2001-10 working to make Php.XPath adhere to specs 37 * | Main Former Author: Michael P. Mehl <mpm@phpxml.org> 38 * | Inital creator of V 1.0. Stoped activities around 2001-03 39 * +------------------------------------------------------------------------------------------------------+ 40 * | Code Structure: 41 * | --------------_ 42 * | The class is split into 3 main objects. To keep usability easy all 3 43 * | objects are in this file (but may be split in 3 file in future). 44 * | +-------------+ 45 * | | XPathBase | XPathBase holds general and debugging functions. 46 * | +------+------+ 47 * | v 48 * | +-------------+ XPathEngine is the implementation of the W3C XPath spec. It contains the 49 * | | XPathEngine | XML-import (parser), -export and can handle xPathQueries. It's a fully 50 * | +------+------+ functional class but has no functions to modify the XML-document (see following). 51 * | v 52 * | +-------------+ 53 * | | XPath | XPath extends the functionality with actions to modify the XML-document. 54 * | +-------------+ We tryed to implement a DOM - like interface. 55 * +------------------------------------------------------------------------------------------------------+ 56 * | Usage: 57 * | ------ 58 * | Scroll to the end of this php file and you will find a short sample code to get you started 59 * +------------------------------------------------------------------------------------------------------+ 60 * | Glossary: 61 * | --------- 62 * | To understand how to use the functions and to pass the right parameters, read following: 63 * | 64 * | Document: (full node tree, XML-tree) 65 * | After a XML-source has been imported and parsed, it's stored as a tree of nodes sometimes 66 * | refered to as 'document'. 67 * | 68 * | AbsoluteXPath: (xPath, xPathSet) 69 * | A absolute XPath is a string. It 'points' to *one* node in the XML-document. We use the 70 * | term 'absolute' to emphasise that it is not an xPath-query (see xPathQuery). A valid xPath 71 * | has the form like '/AAA[1]/BBB[2]/CCC[1]'. Usually functions that require a node (see Node) 72 * | will also accept an abs. XPath. 73 * | 74 * | Node: (node, nodeSet, node-tree) 75 * | Some funtions require or return a node (or a whole node-tree). Nodes are only used with the 76 * | XPath-interface and have an internal structure. Every node in a XML document has a unique 77 * | corresponding abs. xPath. That's why public functions that accept a node, will usually also 78 * | accept a abs. xPath (a string) 'pointing' to an existing node (see absolutXPath). 79 * | 80 * | XPathQuery: (xquery, query) 81 * | A xPath-query is a string that is matched against the XML-document. The result of the match 82 * | is a xPathSet (vector of xPath's). It's always possible to pass a single absoluteXPath 83 * | instead of a xPath-query. A valid xPathQuery could look like this: 84 * | '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to learn more). 85 * | 86 * | 87 * +------------------------------------------------------------------------------------------------------+ 88 * | Internals: 89 * | ---------- 90 * | - The Node Tree 91 * | ------------- 92 * | A central role of the package is how the XML-data is stored. The whole data is in a node-tree. 93 * | A node can be seen as the equvalent to a tag in the XML soure with some extra info. 94 * | For instance the following XML 95 * | <AAA foo="x">***<BBB/><CCC/>**<BBB/>*</AAA> 96 * | Would produce folowing node-tree: 97 * | 'super-root' <-- $nodeRoot (Very handy) 98 * | | 99 * | 'depth' 0 AAA[1] <-- top node. The 'textParts' of this node would be 100 * | / | \ 'textParts' => array('***','','**','*') 101 * | 'depth' 1 BBB[1] CCC[1] BBB[2] (NOTE: Is always size of child nodes+1) 102 * | - The Node 103 * | -------- 104 * | The node itself is an structure desiged mainly to be used in connection with the interface of PHP.XPath. 105 * | That means it's possible for functions to return a sub-node-tree that can be used as input of an other 106 * | PHP.XPath function. 107 * | 108 * | The main structure of a node is: 109 * | $node = array( 110 * | 'name' => '', # The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO' 111 * | 'attributes' => array(), # The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa') 112 * | 'textParts' => array(), # Array of text parts surrounding the children E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd') 113 * | 'childNodes' => array(), # Array of refences (pointers) to child nodes. 114 * | 115 * | For optimisation reasions some additional data is stored in the node too: 116 * | 'parentNode' => NULL # Reference (pointer) to the parent node (or NULL if it's 'super root') 117 * | 'depth' => 0, # The tag depth (or tree level) starting with the root tag at 0. 118 * | 'pos' => 0, # Is the zero-based position this node has in the parent's 'childNodes'-list. 119 * | 'contextPos' => 1, # Is the one-based position this node has by counting the siblings tags (tags with same name) 120 * | 'xpath' => '' # Is the abs. XPath to this node. 121 * | 'generated_id'=> '' # The id returned for this node by generate-id() (attribute and text nodes not supported) 122 * | 123 * | - The NodeIndex 124 * | ------------- 125 * | Every node in the tree has an absolute XPath. E.g '/AAA[1]/BBB[2]' the $nodeIndex is a hash array 126 * | to all the nodes in the node-tree. The key used is the absolute XPath (a string). 127 * | 128 * +------------------------------------------------------------------------------------------------------+ 129 * | License: 130 * | -------- 131 * | The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); 132 * | you may not use this file except in compliance with the License. You may obtain a copy of the 133 * | License at http://www.mozilla.org/MPL/ 134 * | 135 * | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY 136 * | OF ANY KIND, either express or implied. See the License for the specific language governing 137 * | rights and limitations under the License. 138 * | 139 * | The Original Code is <phpXML/>. 140 * | 141 * | The Initial Developer of the Original Code is Michael P. Mehl. Portions created by Michael 142 * | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved. 143 * | 144 * | Contributor(s): N.Swinson / S.Blum / D.Allen 145 * | 146 * | Alternatively, the contents of this file may be used under the terms of either of the GNU 147 * | General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public 148 * | License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the 149 * | LGPL License are applicable instead of those above. If you wish to allow use of your version 150 * | of this file only under the terms of the GPL or the LGPL License and not to allow others to 151 * | use your version of this file under the MPL, indicate your decision by deleting the 152 * | provisions above and replace them with the notice and other provisions required by the 153 * | GPL or the LGPL License. If you do not delete the provisions above, a recipient may use 154 * | your version of this file under either the MPL, the GPL or the LGPL License. 155 * | 156 * +======================================================================================================+ 157 * 158 * @author S.Blum / N.Swinson / D.Allen / (P.Mehl) 159 * @link http://sourceforge.net/projects/phpxpath/ 160 * @version 3.5 161 * @CVS $Id: XPath.class.php 21228 2006-04-06 13:52:08Z ralfbecker $ 162 */ 163 164 // Include guard, protects file being included twice 165 $ConstantName = 'INCLUDED_'.strtoupper(__FILE__); 166 if (defined($ConstantName)) return; 167 define($ConstantName,1, TRUE); 168 169 /************************************************************************************************ 170 * =============================================================================================== 171 * X P a t h B a s e - Class 172 * =============================================================================================== 173 ************************************************************************************************/ 174 class XPathBase { 175 var $_lastError; 176 177 // As debugging of the xml parse is spread across several functions, we need to make this a member. 178 var $bDebugXmlParse = FALSE; 179 180 // do we want to do profiling? 181 var $bClassProfiling = FALSE; 182 183 // Used to help navigate through the begin/end debug calls 184 var $iDebugNextLinkNumber = 1; 185 var $aDebugOpenLinks = array(); 186 var $aDebugFunctions = array( 187 //'_evaluatePrimaryExpr', 188 //'_evaluateExpr', 189 //'_evaluateStep', 190 //'_checkPredicates', 191 //'_evaluateFunction', 192 //'_evaluateOperator', 193 //'_evaluatePathExpr', 194 ); 195 196 /** 197 * Constructor 198 */ 199 function XPathBase() { 200 # $this->bDebugXmlParse = TRUE; 201 $this->properties['verboseLevel'] = 1; // 0=silent, 1 and above produce verbose output (an echo to screen). 202 203 if (!isSet($_ENV)) { // Note: $_ENV introduced in 4.1.0. In earlier versions, use $HTTP_ENV_VARS. 204 $_ENV = $GLOBALS['HTTP_ENV_VARS']; 205 } 206 207 // Windows 95/98 do not support file locking. Detecting OS (Operation System) and setting the 208 // properties['OS_supports_flock'] to FALSE if win 95/98 is detected. 209 // This will surpress the file locking error reported from win 98 users when exportToFile() is called. 210 // May have to add more OS's to the list in future (Macs?). 211 // ### Note that it's only the FAT and NFS file systems that are really a problem. NTFS and 212 // the latest php libs do support flock() 213 $_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS'; 214 switch ($_ENV['OS']) { 215 case 'Windows_95': 216 case 'Windows_98': 217 case 'Unknown OS': 218 // should catch Mac OS X compatible environment 219 if (!empty($_SERVER['SERVER_SOFTWARE']) 220 && preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) { 221 // fall-through 222 } else { 223 $this->properties['OS_supports_flock'] = FALSE; 224 break; 225 } 226 default: 227 $this->properties['OS_supports_flock'] = TRUE; 228 } 229 } 230 231 232 /** 233 * Resets the object so it's able to take a new xml sting/file 234 * 235 * Constructing objects is slow. If you can, reuse ones that you have used already 236 * by using this reset() function. 237 */ 238 function reset() { 239 $this->_lastError = ''; 240 } 241 242 //----------------------------------------------------------------------------------------- 243 // XPathBase ------ Helpers ------ 244 //----------------------------------------------------------------------------------------- 245 246 /** 247 * This method checks the right amount and match of brackets 248 * 249 * @param $term (string) String in which is checked. 250 * @return (bool) TRUE: OK / FALSE: KO 251 */ 252 function _bracketsCheck($term) { 253 $leng = strlen($term); 254 $brackets = 0; 255 $bracketMisscount = $bracketMissmatsh = FALSE; 256 $stack = array(); 257 for ($i=0; $i<$leng; $i++) { 258 switch ($term[$i]) { 259 case '(' : 260 case '[' : 261 $stack[$brackets] = $term[$i]; 262 $brackets++; 263 break; 264 case ')': 265 $brackets--; 266 if ($brackets<0) { 267 $bracketMisscount = TRUE; 268 break 2; 269 } 270 if ($stack[$brackets] != '(') { 271 $bracketMissmatsh = TRUE; 272 break 2; 273 } 274 break; 275 case ']' : 276 $brackets--; 277 if ($brackets<0) { 278 $bracketMisscount = TRUE; 279 break 2; 280 } 281 if ($stack[$brackets] != '[') { 282 $bracketMissmatsh = TRUE; 283 break 2; 284 } 285 break; 286 } 287 } 288 // Check whether we had a valid number of brackets. 289 if ($brackets != 0) $bracketMisscount = TRUE; 290 if ($bracketMisscount || $bracketMissmatsh) { 291 return FALSE; 292 } 293 return TRUE; 294 } 295 296 /** 297 * Looks for a string within another string -- BUT the search-string must be located *outside* of any brackets. 298 * 299 * This method looks for a string within another string. Brackets in the 300 * string the method is looking through will be respected, which means that 301 * only if the string the method is looking for is located outside of 302 * brackets, the search will be successful. 303 * 304 * @param $term (string) String in which the search shall take place. 305 * @param $expression (string) String that should be searched. 306 * @return (int) This method returns -1 if no string was found, 307 * otherwise the offset at which the string was found. 308 */ 309 function _searchString($term, $expression) { 310 $bracketCounter = 0; // Record where we are in the brackets. 311 $leng = strlen($term); 312 $exprLeng = strlen($expression); 313 for ($i=0; $i<$leng; $i++) { 314 $char = $term[$i]; 315 if ($char=='(' || $char=='[') { 316 $bracketCounter++; 317 continue; 318 } 319 elseif ($char==')' || $char==']') { 320 $bracketCounter--; 321 } 322 if ($bracketCounter == 0) { 323 // Check whether we can find the expression at this index. 324 if (substr($term, $i, $exprLeng) == $expression) return $i; 325 } 326 } 327 // Nothing was found. 328 return (-1); 329 } 330 331 /** 332 * Split a string by a searator-string -- BUT the separator-string must be located *outside* of any brackets. 333 * 334 * Returns an array of strings, each of which is a substring of string formed 335 * by splitting it on boundaries formed by the string separator. 336 * 337 * @param $separator (string) String that should be searched. 338 * @param $term (string) String in which the search shall take place. 339 * @return (array) see above 340 */ 341 function _bracketExplode($separator, $term) { 342 // Note that it doesn't make sense for $separator to itself contain (,),[ or ], 343 // but as this is a private function we should be ok. 344 $resultArr = array(); 345 $bracketCounter = 0; // Record where we are in the brackets. 346 do { // BEGIN try block 347 // Check if any separator is in the term 348 $sepLeng = strlen($separator); 349 if (strpos($term, $separator)===FALSE) { // no separator found so end now 350 $resultArr[] = $term; 351 break; // try-block 352 } 353 354 // Make a substitute separator out of 'unused chars'. 355 $substituteSep = str_repeat(chr(2), $sepLeng); 356 357 // Now determine the first bracket '(' or '['. 358 $tmp1 = strpos($term, '('); 359 $tmp2 = strpos($term, '['); 360 if ($tmp1===FALSE) { 361 $startAt = (int)$tmp2; 362 } elseif ($tmp2===FALSE) { 363 $startAt = (int)$tmp1; 364 } else { 365 $startAt = min($tmp1, $tmp2); 366 } 367 368 // Get prefix string part before the first bracket. 369 $preStr = substr($term, 0, $startAt); 370 // Substitute separator in prefix string. 371 $preStr = str_replace($separator, $substituteSep, $preStr); 372 373 // Now get the rest-string (postfix string) 374 $postStr = substr($term, $startAt); 375 // Go all the way through the rest-string. 376 $strLeng = strlen($postStr); 377 for ($i=0; $i < $strLeng; $i++) { 378 $char = $postStr[$i]; 379 // Spot (,),[,] and modify our bracket counter. Note there is an 380 // assumption here that you don't have a string(with[mis)matched]brackets. 381 // This should be ok as the dodgy string will be detected elsewhere. 382 if ($char=='(' || $char=='[') { 383 $bracketCounter++; 384 continue; 385 } 386 elseif ($char==')' || $char==']') { 387 $bracketCounter--; 388 } 389 // If no brackets surround us check for separator 390 if ($bracketCounter == 0) { 391 // Check whether we can find the expression starting at this index. 392 if ((substr($postStr, $i, $sepLeng) == $separator)) { 393 // Substitute the found separator 394 for ($j=0; $j<$sepLeng; $j++) { 395 $postStr[$i+$j] = $substituteSep[$j]; 396 } 397 } 398 } 399 } 400 // Now explod using the substitute separator as key. 401 $resultArr = explode($substituteSep, $preStr . $postStr); 402 } while (FALSE); // End try block 403 // Return the results that we found. May be a array with 1 entry. 404 return $resultArr; 405 } 406 407 /** 408 * Split a string at it's groups, ie bracketed expressions 409 * 410 * Returns an array of strings, when concatenated together would produce the original 411 * string. ie a(b)cde(f)(g) would map to: 412 * array ('a', '(b)', cde', '(f)', '(g)') 413 * 414 * @param $string (string) The string to process 415 * @param $open (string) The substring for the open of a group 416 * @param $close (string) The substring for the close of a group 417 * @return (array) The parsed string, see above 418 */ 419 function _getEndGroups($string, $open='[', $close=']') { 420 // Note that it doesn't make sense for $separator to itself contain (,),[ or ], 421 // but as this is a private function we should be ok. 422 $resultArr = array(); 423 do { // BEGIN try block 424 // Check if we have both an open and a close tag 425 if (empty($open) and empty($close)) { // no separator found so end now 426 $resultArr[] = $string; 427 break; // try-block 428 } 429 430 if (empty($string)) { 431 $resultArr[] = $string; 432 break; // try-block 433 } 434 435 436 while (!empty($string)) { 437 // Now determine the first bracket '(' or '['. 438 $openPos = strpos($string, $open); 439 $closePos = strpos($string, $close); 440 if ($openPos===FALSE || $closePos===FALSE) { 441 // Oh, no more groups to be found then. Quit 442 $resultArr[] = $string; 443 break; 444 } 445 446 // Sanity check 447 if ($openPos > $closePos) { 448 // Malformed string, dump the rest and quit. 449 $resultArr[] = $string; 450 break; 451 } 452 453 // Get prefix string part before the first bracket. 454 $preStr = substr($string, 0, $openPos); 455 // This is the first string that will go in our output 456 if (!empty($preStr)) 457 $resultArr[] = $preStr; 458 459 // Skip over what we've proceed, including the open char 460 $string = substr($string, $openPos + 1 - strlen($string)); 461 462 // Find the next open char and adjust our close char 463 //echo "close: $closePos\nopen: $openPos\n\n"; 464 $closePos -= $openPos + 1; 465 $openPos = strpos($string, $open); 466 //echo "close: $closePos\nopen: $openPos\n\n"; 467 468 // While we have found nesting... 469 while ($openPos && $closePos && ($closePos > $openPos)) { 470 // Find another close pos after the one we are looking at 471 $closePos = strpos($string, $close, $closePos + 1); 472 // And skip our open 473 $openPos = strpos($string, $open, $openPos + 1); 474 } 475 //echo "close: $closePos\nopen: $openPos\n\n"; 476 477 // If we now have a close pos, then it's the end of the group. 478 if ($closePos === FALSE) { 479 // We didn't... so bail dumping what was left 480 $resultArr[] = $open.$string; 481 break; 482 } 483 484 // We did, so we can extract the group 485 $resultArr[] = $open.substr($string, 0, $closePos + 1); 486 // Skip what we have processed 487 $string = substr($string, $closePos + 1); 488 } 489 } while (FALSE); // End try block 490 // Return the results that we found. May be a array with 1 entry. 491 return $resultArr; 492 } 493 494 /** 495 * Retrieves a substring before a delimiter. 496 * 497 * This method retrieves everything from a string before a given delimiter, 498 * not including the delimiter. 499 * 500 * @param $string (string) String, from which the substring should be extracted. 501 * @param $delimiter (string) String containing the delimiter to use. 502 * @return (string) Substring from the original string before the delimiter. 503 * @see _afterstr() 504 */ 505 function _prestr(&$string, $delimiter, $offset=0) { 506 // Return the substring. 507 $offset = ($offset<0) ? 0 : $offset; 508 $pos = strpos($string, $delimiter, $offset); 509 if ($pos===FALSE) return $string; else return substr($string, 0, $pos); 510 } 511 512 /** 513 * Retrieves a substring after a delimiter. 514 * 515 * This method retrieves everything from a string after a given delimiter, 516 * not including the delimiter. 517 * 518 * @param $string (string) String, from which the substring should be extracted. 519 * @param $delimiter (string) String containing the delimiter to use. 520 * @return (string) Substring from the original string after the delimiter. 521 * @see _prestr() 522 */ 523 function _afterstr($string, $delimiter, $offset=0) { 524 $offset = ($offset<0) ? 0 : $offset; 525 // Return the substring. 526 return substr($string, strpos($string, $delimiter, $offset) + strlen($delimiter)); 527 } 528 529 //----------------------------------------------------------------------------------------- 530 // XPathBase ------ Debug Stuff ------ 531 //----------------------------------------------------------------------------------------- 532 533 /** 534 * Alter the verbose (error) level reporting. 535 * 536 * Pass an int. >0 to turn on, 0 to turn off. The higher the number, the 537 * higher the level of verbosity. By default, the class has a verbose level 538 * of 1. 539 * 540 * @param $levelOfVerbosity (int) default is 1 = on 541 */ 542 function setVerbose($levelOfVerbosity = 1) { 543 $level = -1; 544 if ($levelOfVerbosity === TRUE) { 545 $level = 1; 546 } elseif ($levelOfVerbosity === FALSE) { 547 $level = 0; 548 } elseif (is_numeric($levelOfVerbosity)) { 549 $level = $levelOfVerbosity; 550 } 551 if ($level >= 0) $this->properties['verboseLevel'] = $levelOfVerbosity; 552 } 553 554 /** 555 * Returns the last occured error message. 556 * 557 * @access public 558 * @return string (may be empty if there was no error at all) 559 * @see _setLastError(), _lastError 560 */ 561 function getLastError() { 562 return $this->_lastError; 563 } 564 565 /** 566 * Creates a textual error message and sets it. 567 * 568 * example: 'XPath error in THIS_FILE_NAME:LINE. Message: YOUR_MESSAGE'; 569 * 570 * I don't think the message should include any markup because not everyone wants to debug 571 * into the browser window. 572 * 573 * You should call _displayError() rather than _setLastError() if you would like the message, 574 * dependant on their verbose settings, echoed to the screen. 575 * 576 * @param $message (string) a textual error message default is '' 577 * @param $line (int) the line number where the error occured, use __LINE__ 578 * @see getLastError() 579 */ 580 function _setLastError($message='', $line='-', $file='-') { 581 $this->_lastError = 'XPath error in ' . basename($file) . ':' . $line . '. Message: ' . $message; 582 } 583 584 /** 585 * Displays an error message. 586 * 587 * This method displays an error messages depending on the users verbose settings 588 * and sets the last error message. 589 * 590 * If also possibly stops the execution of the script. 591 * ### Terminate should not be allowed --fab. Should it?? N.S. 592 * 593 * @param $message (string) Error message to be displayed. 594 * @param $lineNumber (int) line number given by __LINE__ 595 * @param $terminate (bool) (default TURE) End the execution of this script. 596 */ 597 function _displayError($message, $lineNumber='-', $file='-', $terminate=TRUE) { 598 // Display the error message. 599 $err = '<b>XPath error in '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n"; 600 $this->_setLastError($message, $lineNumber, $file); 601 if (($this->properties['verboseLevel'] > 0) OR ($terminate)) echo $err; 602 // End the execution of this script. 603 if ($terminate) exit; 604 } 605 606 /** 607 * Displays a diagnostic message 608 * 609 * This method displays an error messages 610 * 611 * @param $message (string) Error message to be displayed. 612 * @param $lineNumber (int) line number given by __LINE__ 613 */ 614 function _displayMessage($message, $lineNumber='-', $file='-') { 615 // Display the error message. 616 $err = '<b>XPath message from '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n"; 617 if ($this->properties['verboseLevel'] > 0) echo $err; 618 } 619 620 /** 621 * Called to begin the debug run of a function. 622 * 623 * This method starts a <DIV><PRE> tag so that the entry to this function 624 * is clear to the debugging user. Call _closeDebugFunction() at the 625 * end of the function to create a clean box round the function call. 626 * 627 * @author Nigel Swinson <nigelswinson@users.sourceforge.net> 628 * @author Sam Blum <bs_php@infeer.com> 629 * @param $functionName (string) the name of the function we are beginning to debug 630 * @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise 631 * @return (array) the output from the microtime() function. 632 * @see _closeDebugFunction() 633 */ 634 function _beginDebugFunction($functionName, $bDebugFlag) { 635 if ($bDebugFlag) { 636 $fileName = basename(__FILE__); 637 static $color = array('green','blue','red','lime','fuchsia', 'aqua'); 638 static $colIndex = -1; 639 $colIndex++; 640 echo '<div style="clear:both" align="left"> '; 641 echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">'; 642 echo '<a style="float:right;margin:5px" name="'.$this->iDebugNextLinkNumber.'Open" href="#'.$this->iDebugNextLinkNumber.'Close">Function Close '.$this->iDebugNextLinkNumber.'</a>'; 643 echo "<STRONG>{$fileName} : {$functionName}</STRONG>"; 644 echo '<hr style="clear:both">'; 645 array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber); 646 $this->iDebugNextLinkNumber++; 647 } 648 649 if ($this->bClassProfiling) 650 $this->_ProfBegin($FunctionName); 651 652 return TRUE; 653 } 654 655 /** 656 * Called to end the debug run of a function. 657 * 658 * This method ends a <DIV><PRE> block and reports the time since $aStartTime 659 * is clear to the debugging user. 660 * 661 * @author Nigel Swinson <nigelswinson@users.sourceforge.net> 662 * @param $functionName (string) the name of the function we are beginning to debug 663 * @param $return_value (mixed) the return value from the function call that 664 * we are debugging 665 * @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise 666 */ 667 function _closeDebugFunction($functionName, $returnValue = "", $bDebugFlag) { 668 if ($bDebugFlag) { 669 echo "<hr>"; 670 $iOpenLinkNumber = array_pop($this->aDebugOpenLinks); 671 echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>'; 672 if (isSet($returnValue)) { 673 if (is_array($returnValue)) 674 echo "Return Value: ".print_r($returnValue)."\n"; 675 else if (is_numeric($returnValue)) 676 echo "Return Value: ".(string)$returnValue."\n"; 677 else if (is_bool($returnValue)) 678 echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n"; 679 else 680 echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n"; 681 } 682 echo '<br style="clear:both">'; 683 echo " \n</pre></div>"; 684 } 685 686 if ($this->bClassProfiling) 687 $this->_ProfEnd($FunctionName); 688 689 return TRUE; 690 } 691 692 /** 693 * Profile begin call 694 */ 695 function _ProfBegin($sonFuncName) { 696 static $entryTmpl = array ( 'start' => array(), 697 'recursiveCount' => 0, 698 'totTime' => 0, 699 'callCount' => 0 ); 700 $now = explode(' ', microtime()); 701 702 if (empty($this->callStack)) { 703 $fatherFuncName = ''; 704 } 705 else { 706 $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1]; 707 $fatherEntry = &$this->profile[$fatherFuncName]; 708 } 709 $this->callStack[] = $sonFuncName; 710 711 if (!isSet($this->profile[$sonFuncName])) { 712 $this->profile[$sonFuncName] = $entryTmpl; 713 } 714 715 $sonEntry = &$this->profile[$sonFuncName]; 716 $sonEntry['callCount']++; 717 // if we call the t's the same function let the time run, otherwise sum up 718 if ($fatherFuncName == $sonFuncName) { 719 $sonEntry['recursiveCount']++; 720 } 721 if (!empty($fatherFuncName)) { 722 $last = $fatherEntry['start']; 723 $fatherEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 ); 724 $fatherEntry['start'] = 0; 725 } 726 $sonEntry['start'] = explode(' ', microtime()); 727 } 728 729 /** 730 * Profile end call 731 */ 732 function _ProfEnd($sonFuncName) { 733 $now = explode(' ', microtime()); 734 735 array_pop($this->callStack); 736 if (empty($this->callStack)) { 737 $fatherFuncName = ''; 738 } 739 else { 740 $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1]; 741 $fatherEntry = &$this->profile[$fatherFuncName]; 742 } 743 $sonEntry = &$this->profile[$sonFuncName]; 744 if (empty($sonEntry)) { 745 echo "ERROR in profEnd(): '$funcNam' not in list. Seams it was never started ;o)"; 746 } 747 748 $last = $sonEntry['start']; 749 $sonEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 ); 750 $sonEntry['start'] = 0; 751 if (!empty($fatherEntry)) $fatherEntry['start'] = explode(' ', microtime()); 752 } 753 754 /** 755 * Show profile gathered so far as HTML table 756 */ 757 function _ProfileToHtml() { 758 $sortArr = array(); 759 if (empty($this->profile)) return ''; 760 reset($this->profile); 761 while (list($funcName) = each($this->profile)) { 762 $sortArrKey[] = $this->profile[$funcName]['totTime']; 763 $sortArrVal[] = $funcName; 764 } 765 //echo '<pre>';var_dump($sortArrVal);echo '</pre>'; 766 array_multisort ($sortArrKey, SORT_DESC, $sortArrVal ); 767 //echo '<pre>';var_dump($sortArrVal);echo '</pre>'; 768 769 $totTime = 0; 770 $size = sizeOf($sortArrVal); 771 for ($i=0; $i<$size; $i++) { 772 $funcName = &$sortArrVal[$i]; 773 $totTime += $this->profile[$funcName]['totTime']; 774 } 775 $out = '<table border="1">'; 776 $out .='<tr align="center" bgcolor="#bcd6f1"><th>Function</th><th> % </th><th>Total [ms]</th><th># Call</th><th>[ms] per Call</th><th># Recursive</th></tr>'; 777 for ($i=0; $i<$size; $i++) { 778 $funcName = &$sortArrVal[$i]; 779 $row = &$this->profile[$funcName]; 780 $procent = round($row['totTime']*100/$totTime); 781 if ($procent>20) $bgc = '#ff8080'; 782 elseif ($procent>15) $bgc = '#ff9999'; 783 elseif ($procent>10) $bgc = '#ffcccc'; 784 elseif ($procent>5) $bgc = '#ffffcc'; 785 else $bgc = '#66ff99'; 786 787 $out .="<tr align='center' bgcolor='{$bgc}'>"; 788 $out .='<td>'. $funcName .'</td><td>'. $procent .'% '.'</td><td>'. $row['totTime']/10 .'</td><td>'. $row['callCount'] .'</td><td>'. round($row['totTime']/10/$row['callCount'],2) .'</td><td>'. $row['recursiveCount'].'</td>'; 789 $out .='</tr>'; 790 } 791 $out .= '</table> Total Time [' . $totTime/10 .'ms]' ; 792 793 echo $out; 794 return TRUE; 795 } 796 797 /** 798 * Echo an XPath context for diagnostic purposes 799 * 800 * @param $context (array) An XPath context 801 */ 802 function _printContext($context) { 803 echo "{$context['nodePath']}({$context['pos']}/{$context['size']})"; 804 } 805 806 /** 807 * This is a debug helper function. It dumps the node-tree as HTML 808 * 809 * *QUICK AND DIRTY*. Needs some polishing. 810 * 811 * @param $node (array) A node 812 * @param $indent (string) (optional, default=''). For internal recursive calls. 813 */ 814 function _treeDump($node, $indent = '') { 815 $out = ''; 816 817 // Get rid of recursion 818 $parentName = empty($node['parentNode']) ? "SUPER ROOT" : $node['parentNode']['name']; 819 unset($node['parentNode']); 820 $node['parentNode'] = $parentName ; 821 822 $out .= "NODE[{$node['name']}]\n"; 823 824 foreach($node as $key => $val) { 825 if ($key === 'childNodes') continue; 826 if (is_Array($val)) { 827 $out .= $indent . " [{$key}]\n" . arrayToStr($val, $indent . ' '); 828 } else { 829 $out .= $indent . " [{$key}] => '{$val}' \n"; 830 } 831 } 832 833 if (!empty($node['childNodes'])) { 834 $out .= $indent . " ['childNodes'] (Size = ".sizeOf($node['childNodes']).")\n"; 835 foreach($node['childNodes'] as $key => $childNode) { 836 $out .= $indent . " [$key] => " . $this->_treeDump($childNode, $indent . ' ') . "\n"; 837 } 838 } 839 840 if (empty($indent)) { 841 return "<pre>" . htmlspecialchars($out) . "</pre>"; 842 } 843 return $out; 844 } 845 } // END OF CLASS XPathBase 846 847 848 /************************************************************************************************ 849 * =============================================================================================== 850 * X P a t h E n g i n e - Class 851 * =============================================================================================== 852 ************************************************************************************************/ 853 854 class XPathEngine extends XPathBase { 855 856 // List of supported XPath axes. 857 // What a stupid idea from W3C to take axes name containing a '-' (dash) 858 // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator. 859 // We will then do the same on the users Xpath querys 860 // -sibling => _sibling 861 // -or- => _or_ 862 // 863 // This array contains a list of all valid axes that can be evaluated in an 864 // XPath query. 865 var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 'descendant', 866 'descendant_or_self', 'following', 'following_sibling', 867 'namespace', 'parent', 'preceding', 'preceding_sibling', 'self' 868 ); 869 870 // List of supported XPath functions. 871 // What a stupid idea from W3C to take function name containing a '-' (dash) 872 // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator. 873 // We will then do the same on the users Xpath querys 874 // starts-with => starts_with 875 // substring-before => substring_before 876 // substring-after => substring_after 877 // string-length => string_length 878 // 879 // This array contains a list of all valid functions that can be evaluated 880 // in an XPath query. 881 var $functions = array ( 'last', 'position', 'count', 'id', 'name', 882 'string', 'concat', 'starts_with', 'contains', 'substring_before', 883 'substring_after', 'substring', 'string_length', 'normalize_space', 'translate', 884 'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor', 885 'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' ); 886 887 // List of supported XPath operators. 888 // 889 // This array contains a list of all valid operators that can be evaluated 890 // in a predicate of an XPath query. The list is ordered by the 891 // precedence of the operators (lowest precedence first). 892 var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>', 893 '+', '-', '*', ' div ', ' mod ', ' | '); 894 895 // List of literals from the xPath string. 896 var $axPathLiterals = array(); 897 898 // The index and tree that is created during the analysis of an XML source. 899 var $nodeIndex = array(); 900 var $nodeRoot = array(); 901 var $emptyNode = array( 902 'name' => '', // The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO' 903 'attributes' => array(), // The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa') 904 'childNodes' => array(), // Array of pointers to child nodes. 905 'textParts' => array(), // Array of text parts between the cilderen E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd') 906 'parentNode' => NULL, // Pointer to parent node or NULL if this node is the 'super root' 907 //-- *!* Following vars are set by the indexer and is for optimisation only *!* 908 'depth' => 0, // The tag depth (or tree level) starting with the root tag at 0. 909 'pos' => 0, // Is the zero-based position this node has in the parents 'childNodes'-list. 910 'contextPos' => 1, // Is the one-based position this node has by counting the siblings tags (tags with same name) 911 'xpath' => '' // Is the abs. XPath to this node. 912 ); 913 var $_indexIsDirty = FALSE; 914 915 916 // These variable used during the parse XML source 917 var $nodeStack = array(); // The elements that we have still to close. 918 var $parseStackIndex = 0; // The current element of the nodeStack[] that we are adding to while 919 // parsing an XML source. Corresponds to the depth of the xml node. 920 // in our input data. 921 var $parseOptions = array(); // Used to set the PHP's XML parser options (see xml_parser_set_option) 922 var $parsedTextLocation = ''; // A reference to where we have to put char data collected during XML parsing 923 var $parsInCData = 0 ; // Is >0 when we are inside a CDATA section. 924 var $parseSkipWhiteCache = 0; // A cache of the skip whitespace parse option to speed up the parse. 925 926 // This is the array of error strings, to keep consistency. 927 var $errorStrings = array( 928 'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* describe a node in the xml document.", 929 'NoNodeMatch' => "The supplied xPath-query '%s' does not match *any* node in the xml document.", 930 'RootNodeAlreadyExists' => "An xml document may have only one root node." 931 ); 932 933 /** 934 * Constructor 935 * 936 * Optionally you may call this constructor with the XML-filename to parse and the 937 * XML option vector. Each of the entries in the option vector will be passed to 938 * xml_parser_set_option(). 939 * 940 * A option vector sample: 941 * $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, 942 * XML_OPTION_SKIP_WHITE => TRUE); 943 * 944 * @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, 945 * <optionID>=><value>, ...). See PHP's 946 * xml_parser_set_option() docu for a list of possible 947 * options. 948 * @see importFromFile(), importFromString(), setXmlOptions() 949 */ 950 function XPathEngine($userXmlOptions=array()) { 951 parent::XPathBase(); 952 // Default to not folding case 953 $this->parseOptions[XML_OPTION_CASE_FOLDING] = FALSE; 954 // And not skipping whitespace 955 $this->parseOptions[XML_OPTION_SKIP_WHITE] = FALSE; 956 957 // Now merge in the overrides. 958 // Don't use PHP's array_merge! 959 if (is_array($userXmlOptions)) { 960 foreach($userXmlOptions as $key => $val) $this->parseOptions[$key] = $val; 961 } 962 } 963 964 /** 965 * Resets the object so it's able to take a new xml sting/file 966 * 967 * Constructing objects is slow. If you can, reuse ones that you have used already 968 * by using this reset() function. 969 */ 970 function reset() { 971 parent::reset(); 972 $this->properties['xmlFile'] = ''; 973 $this->parseStackIndex = 0; 974 $this->parsedTextLocation = ''; 975 $this->parsInCData = 0; 976 $this->nodeIndex = array(); 977 $this->nodeRoot = array(); 978 $this->nodeStack = array(); 979 $this->aLiterals = array(); 980 $this->_indexIsDirty = FALSE; 981 } 982 983 984 //----------------------------------------------------------------------------------------- 985 // XPathEngine ------ Get / Set Stuff ------ 986 //----------------------------------------------------------------------------------------- 987 988 /** 989 * Returns the property/ies you want. 990 * 991 * if $param is not given, all properties will be returned in a hash. 992 * 993 * @param $param (string) the property you want the value of, or NULL for all the properties 994 * @return (mixed) string OR hash of all params, or NULL on an unknown parameter. 995 */ 996 function getProperties($param=NULL) { 997 $this->properties['hasContent'] = !empty($this->nodeRoot); 998 $this->properties['caseFolding'] = $this->parseOptions[XML_OPTION_CASE_FOLDING]; 999 $this->properties['skipWhiteSpaces'] = $this->parseOptions[XML_OPTION_SKIP_WHITE]; 1000 1001 if (empty($param)) return $this->properties; 1002 1003 if (isSet($this->properties[$param])) { 1004 return $this->properties[$param]; 1005 } else { 1006 return NULL; 1007 } 1008 } 1009 1010 /** 1011 * Set an xml_parser_set_option() 1012 * 1013 * @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE) 1014 * @param $value (int) The option value. 1015 * @see XML parser functions in PHP doc 1016 */ 1017 function setXmlOption($optionID, $value) { 1018 if (!is_numeric($optionID)) return; 1019 $this->parseOptions[$optionID] = $value; 1020 } 1021 1022 /** 1023 * Sets a number of xml_parser_set_option()s 1024 * 1025 * @param $userXmlOptions (array) An array of parser options. 1026 * @see setXmlOption 1027 */ 1028 function setXmlOptions($userXmlOptions=array()) { 1029 if (!is_array($userXmlOptions)) return; 1030 foreach($userXmlOptions as $key => $val) { 1031 $this->setXmlOption($key, $val); 1032 } 1033 } 1034 1035 /** 1036 * Alternative way to control whether case-folding is enabled for this XML parser. 1037 * 1038 * Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE) 1039 * 1040 * When it comes to XML, case-folding simply means uppercasing all tag- 1041 * and attribute-names (NOT the content) if set to TRUE. Note if you 1042 * have this option set, then your XPath queries will also be case folded 1043 * for you. 1044 * 1045 * @param $onOff (bool) (default TRUE) 1046 * @see XML parser functions in PHP doc 1047 */ 1048 function setCaseFolding($onOff=TRUE) { 1049 $this->parseOptions[XML_OPTION_CASE_FOLDING] = $onOff; 1050 } 1051 1052 /** 1053 * Alternative way to control whether skip-white-spaces is enabled for this XML parser. 1054 * 1055 * Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE) 1056 * 1057 * When it comes to XML, skip-white-spaces will trim the tag content. 1058 * An XML file with no whitespace will be faster to process, but will make 1059 * your data less human readable when you come to write it out. 1060 * 1061 * Running with this option on will slow the class down, so if you want to 1062 * speed up your XML, then run it through once skipping white-spaces, then 1063 * write out the new version of your XML without whitespace, then use the 1064 * new XML file with skip whitespaces turned off. 1065 * 1066 * @param $onOff (bool) (default TRUE) 1067 * @see XML parser functions in PHP doc 1068 */ 1069 function setSkipWhiteSpaces($onOff=TRUE) { 1070 $this->parseOptions[XML_OPTION_SKIP_WHITE] = $onOff; 1071 } 1072 1073 /** 1074 * Get the node defined by the $absoluteXPath. 1075 * 1076 * @param $absoluteXPath (string) (optional, default is 'super-root') xpath to the node. 1077 * @return (array) The node, or FALSE if the node wasn't found. 1078 */ 1079 function &getNode($absoluteXPath='') { 1080 if ($absoluteXPath==='/') $absoluteXPath = ''; 1081 if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE; 1082 if ($this->_indexIsDirty) $this->reindexNodeTree(); 1083 return $this->nodeIndex[$absoluteXPath]; 1084 } 1085 1086 /** 1087 * Get a the content of a node text part or node attribute. 1088 * 1089 * If the absolute Xpath references an attribute (Xpath ends with @ or attribute::), 1090 * then the text value of that node-attribute is returned. 1091 * Otherwise the Xpath is referencing a text part of the node. This can be either a 1092 * direct reference to a text part (Xpath ends with text()[<nr>]) or indirect reference 1093 * (a simple abs. Xpath to a node). 1094 * 1) Direct Reference (xpath ends with text()[<part-number>]): 1095 * If the 'part-number' is omitted, the first text-part is assumed; starting by 1. 1096 * Negative numbers are allowed, where -1 is the last text-part a.s.o. 1097 * 2) Indirect Reference (a simple abs. Xpath to a node): 1098 * Default is to return the *whole text*; that is the concated text-parts of the matching 1099 * node. (NOTE that only in this case you'll only get a copy and changes to the returned 1100 * value wounld have no effect). Optionally you may pass a parameter 1101 * $textPartNr to define the text-part you want; starting by 1. 1102 * Negative numbers are allowed, where -1 is the last text-part a.s.o. 1103 * 1104 * NOTE I : The returned value can be fetched by reference 1105 * E.g. $text =& wholeText(). If you wish to modify the text. 1106 * NOTE II: text-part numbers out of range will return FALSE 1107 * SIDENOTE:The function name is a suggestion from W3C in the XPath specification level 3. 1108 * 1109 * @param $absoluteXPath (string) xpath to the node (See above). 1110 * @param $textPartNr (int) If referring to a node, specifies which text part 1111 * to query. 1112 * @return (&string) A *reference* to the text if the node that the other 1113 * parameters describe or FALSE if the node is not found. 1114 */ 1115 function &wholeText($absoluteXPath, $textPartNr=NULL) { 1116 $status = FALSE; 1117 $text = NULL; 1118 if ($this->_indexIsDirty) $this->reindexNodeTree(); 1119 1120 do { // try-block 1121 if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, $matches)) { 1122 $absoluteXPath = $matches[1]; 1123 $attribute = $matches[3]; 1124 if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) { 1125 $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE); 1126 break; // try-block 1127 } 1128 $text =& $this->nodeIndex[$absoluteXPath]['attributes'][$attribute]; 1129 $status = TRUE; 1130 break; // try-block 1131 } 1132 1133 // Xpath contains a 'text()'-function, thus goes right to a text node. If so interpret the Xpath. 1134 if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, $matches)) { 1135 $absoluteXPath = $matches[1]; 1136 1137 if (!isSet($this->nodeIndex[$absoluteXPath])) { 1138 $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE); 1139 break; // try-block 1140 } 1141 1142 // Get the amount of the text parts in the node. 1143 $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']); 1144 1145 // default to the first text node if a text node was not specified 1146 $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1; 1147 1148 // Support negative indexes like -1 === last a.s.o. 1149 if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1; 1150 if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) { 1151 $this->_displayError("The $absoluteXPath/text()[$textPartNr] value isn't a NODE in this document.", __LINE__, __FILE__, FALSE); 1152 break; // try-block 1153 } 1154 $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 1]; 1155 $status = TRUE; 1156 break; // try-block 1157 } 1158 1159 // At this point we have been given an xpath with neither a 'text()' nor 'attribute::' axis at the end 1160 // So we assume a get to text is wanted and use the optioanl fallback parameters $textPartNr 1161 1162 if (!isSet($this->nodeIndex[$absoluteXPath])) { 1163 $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE); 1164 break; // try-block 1165 } 1166 1167 // Get the amount of the text parts in the node. 1168 $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']); 1169 1170 // If $textPartNr == NULL we return a *copy* of the whole concated text-parts 1171 if (is_null($textPartNr)) { 1172 unset($text); 1173 $text = implode('', $this->nodeIndex[$absoluteXPath]['textParts']); 1174 $status = TRUE; 1175 break; // try-block 1176 } 1177 1178 // Support negative indexes like -1 === last a.s.o. 1179 if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1; 1180 if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) { 1181 $this->_displayError("The $absoluteXPath has no text part at pos [$textPartNr] (Note: text parts start with 1).", __LINE__, __FILE__, FALSE); 1182 break; // try-block 1183 } 1184 $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr -1]; 1185 $status = TRUE; 1186 } while (FALSE); // END try-block 1187 1188 if (!$status) return FALSE; 1189 return $text; 1190 } 1191 1192 /** 1193 * Obtain the string value of an object 1194 * 1195 * http://www.w3.org/TR/xpath#dt-string-value 1196 * 1197 * "For every type of node, there is a way of determining a string-value for a node of that type. 1198 * For some types of node, the string-value is part of the node; for other types of node, the 1199 * string-value is computed from the string-value of descendant nodes." 1200 * 1201 * @param $node (node) The node we have to convert 1202 * @return (string) The string value of the node. "" if the object has no evaluatable 1203 * string value 1204 */ 1205 function _stringValue($node) { 1206 // Decode the entitites and then add the resulting literal string into our array. 1207 return $this->_addLiteral($this->decodeEntities($this->wholeText($node))); 1208 } 1209 1210 //----------------------------------------------------------------------------------------- 1211 // XPathEngine ------ Export the XML Document ------ 1212 //----------------------------------------------------------------------------------------- 1213 1214 /** 1215 * Returns the containing XML as marked up HTML with specified nodes hi-lighted 1216 * 1217 * @param $absoluteXPath (string) The address of the node you would like to export. 1218 * If empty the whole document will be exported. 1219 * @param $hilighXpathList (array) A list of nodes that you would like to highlight 1220 * @return (mixed) The Xml document marked up as HTML so that it can 1221 * be viewed in a browser, including any XML headers. 1222 * FALSE on error. 1223 * @see _export() 1224 */ 1225 function exportAsHtml($absoluteXPath='', $hilightXpathList=array()) { 1226 $htmlString = $this->_export($absoluteXPath, $xmlHeader=NULL, $hilightXpathList); 1227 if (!$htmlString) return FALSE; 1228 return "<pre>\n" . $htmlString . "\n</pre>"; 1229 } 1230 1231 /** 1232 * Given a context this function returns the containing XML 1233 * 1234 * @param $absoluteXPath (string) The address of the node you would like to export. 1235 * If empty the whole document will be exported. 1236 * @param $xmlHeader (array) The string that you would like to appear before 1237 * the XML content. ie before the <root></root>. If you 1238 * do not specify this argument, the xmlHeader that was 1239 * found in the parsed xml file will be used instead. 1240 * @return (mixed) The Xml fragment/document, suitable for writing 1241 * out to an .xml file or as part of a larger xml file, or 1242 * FALSE on error. 1243 * @see _export() 1244 */ 1245 function exportAsXml($absoluteXPath='', $xmlHeader=NULL) { 1246 $this->hilightXpathList = NULL; 1247 return $this->_export($absoluteXPath, $xmlHeader); 1248 } 1249 1250 /** 1251 * Generates a XML string with the content of the current document and writes it to a file. 1252 * 1253 * Per default includes a <?xml ...> tag at the start of the data too. 1254 * 1255 * @param $fileName (string) 1256 * @param $absoluteXPath (string) The path to the parent node you want(see text above) 1257 * @param $xmlHeader (array) The string that you would like to appear before 1258 * the XML content. ie before the <root></root>. If you 1259 * do not specify this argument, the xmlHeader that was 1260 * found in the parsed xml file will be used instead. 1261 * @return (string) The returned string contains well-formed XML data 1262 * or FALSE on error. 1263 * @see exportAsXml(), exportAsHtml() 1264 */ 1265 function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) { 1266 $status = FALSE; 1267 do { // try-block 1268 if (!($hFile = fopen($fileName, "wb"))) { // Did we open the file ok? 1269 $errStr = "Failed to open the $fileName xml file."; 1270 break; // try-block 1271 } 1272 1273 if ($this->properties['OS_supports_flock']) { 1274 if (!flock($hFile, LOCK_EX + LOCK_NB)) { // Lock the file 1275 $errStr = "Couldn't get an exclusive lock on the $fileName file."; 1276 break; // try-block 1277 } 1278 } 1279 if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) { 1280 $errStr = "Export failed"; 1281 break; // try-block 1282 } 1283 1284 $iBytesWritten = fwrite($hFile, $xmlOut); 1285 if ($iBytesWritten != strlen($xmlOut)) { 1286 $errStr = "Write error when writing back the $fileName file."; 1287 break; // try-block 1288 } 1289 1290 // Flush and unlock the file 1291 @fflush($hFile); 1292 $status = TRUE; 1293 } while(FALSE); 1294 1295 @flock($hFile, LOCK_UN); 1296 @fclose($hFile); 1297 // Sanity check the produced file. 1298 clearstatcache(); 1299 if (filesize($fileName) < strlen($xmlOut)) { 1300 $errStr = "Write error when writing back the $fileName file."; 1301 $status = FALSE; 1302 } 1303 1304 if (!$status) $this->_displayError($errStr, __LINE__, __FILE__, FALSE); 1305 return $status; 1306 } 1307 1308 /** 1309 * Generates a XML string with the content of the current document. 1310 * 1311 * This is the start for extracting the XML-data from the node-tree. We do some preperations 1312 * and then call _InternalExport() to fetch the main XML-data. You optionally may pass 1313 * xpath to any node that will then be used as top node, to extract XML-parts of the 1314 * document. Default is '', meaning to extract the whole document. 1315 * 1316 * You also may pass a 'xmlHeader' (usually something like <?xml version="1.0"? > that will 1317 * overwrite any other 'xmlHeader', if there was one in the original source. If there 1318 * wasn't one in the original source, and you still don't specify one, then it will 1319 * use a default of <?xml version="1.0"? > 1320 * Finaly, when exporting to HTML, you may pass a vector xPaths you want to hi-light. 1321 * The hi-lighted tags and attributes will receive a nice color. 1322 * 1323 * NOTE I : The output can have 2 formats: 1324 * a) If "skip white spaces" is/was set. (Not Recommended - slower) 1325 * The output is formatted by adding indenting and carriage returns. 1326 * b) If "skip white spaces" is/was *NOT* set. 1327 * 'as is'. No formatting is done. The output should the same as the 1328 * the original parsed XML source. 1329 * 1330 * @param $absoluteXPath (string) (optional, default is root) The node we choose as top-node 1331 * @param $xmlHeader (string) (optional) content before <root/> (see text above) 1332 * @param $hilightXpath (array) (optional) a vector of xPaths to nodes we wat to 1333 * hi-light (see text above) 1334 * @return (mixed) The xml string, or FALSE on error. 1335 */ 1336 function _export($absoluteXPath='', $xmlHeader=NULL, $hilightXpathList='') { 1337 // Check whether a root node is given. 1338 if (empty($absoluteXpath)) $absoluteXpath = ''; 1339 if ($absoluteXpath == '/') $absoluteXpath = ''; 1340 if ($this->_indexIsDirty) $this->reindexNodeTree(); 1341 if (!isSet($this->nodeIndex[$absoluteXpath])) { 1342 // If the $absoluteXpath was '' and it didn't exist, then the document is empty 1343 // and we can safely return ''. 1344 if ($absoluteXpath == '') return ''; 1345 $this->_displayError("The given xpath '{$absoluteXpath}' isn't a node in this document.", __LINE__, __FILE__, FALSE); 1346 return FALSE; 1347 } 1348 1349 $this->hilightXpathList = $hilightXpathList; 1350 $this->indentStep = ' '; 1351 $hilightIsActive = is_array($hilightXpathList); 1352 if ($hilightIsActive) { 1353 $this->indentStep = ' '; 1354 } 1355 1356 // Cache this now 1357 $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE; 1358 1359 /////////////////////////////////////// 1360 // Get the starting node and begin with the header 1361 1362 // Get the start node. The super root is a special case. 1363 $startNode = NULL; 1364 if (empty($absoluteXPath)) { 1365 $superRoot = $this->nodeIndex['']; 1366 // If they didn't specify an xml header, use the one in the object 1367 if (is_null($xmlHeader)) { 1368 $xmlHeader = $this->parseSkipWhiteCache ? trim($superRoot['textParts'][0]) : $superRoot['textParts'][0]; 1369 // If we still don't have an XML header, then use a suitable default 1370 if (empty($xmlHeader)) { 1371 $xmlHeader = '<?xml version="1.0"?>'; 1372 } 1373 } 1374 1375 if (isSet($superRoot['childNodes'][0])) $startNode = $superRoot['childNodes'][0]; 1376 } else { 1377 $startNode = $this->nodeIndex[$absoluteXPath]; 1378 } 1379 1380 if (!empty($xmlHeader)) { 1381 $xmlOut = $this->parseSkipWhiteCache ? $xmlHeader."\n" : $xmlHeader; 1382 } else { 1383 $xmlOut = ''; 1384 } 1385 1386 /////////////////////////////////////// 1387 // Output the document. 1388 1389 if (($xmlOut .= $this->_InternalExport($startNode)) === FALSE) { 1390 return FALSE; 1391 } 1392 1393 /////////////////////////////////////// 1394 1395 // Convert our markers to hi-lights. 1396 if ($hilightIsActive) { 1397 $from = array('<', '>', chr(2), chr(3)); 1398 $to = array('<', '>', '<font color="#FF0000"><b>', '</b></font>'); 1399 $xmlOut = str_replace($from, $to, $xmlOut); 1400 } 1401 return $xmlOut; 1402 } 1403 1404 /** 1405 * Export the xml document starting at the named node. 1406 * 1407 * @param $node (node) The node we have to start exporting from 1408 * @return (string) The string representation of the node. 1409 */ 1410 function _InternalExport($node) { 1411 $ThisFunctionName = '_InternalExport'; 1412 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 1413 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 1414 if ($bDebugThisFunction) { 1415 echo "Exporting node: ".$node['xpath']."<br>\n"; 1416 } 1417 1418 //////////////////////////////// 1419 1420 // Quick out. 1421 if (empty($node)) return ''; 1422 1423 // The output starts as empty. 1424 $xmlOut = ''; 1425 // This loop will output the text before the current child of a parent then the 1426 // current child. Where the child is a short tag we output the child, then move 1427 // onto the next child. Where the child is not a short tag, we output the open tag, 1428 // then queue up on currentParentStack[] the child. 1429 // 1430 // When we run out of children, we then output the last text part, and close the 1431 // 'parent' tag before popping the stack and carrying on. 1432 // 1433 // To illustrate, the numbers in this xml file indicate what is output on each 1434 // pass of the while loop: 1435 // 1436 // 1 1437 // <1>2 1438 // <2>3 1439 // <3/>4 1440 // </4>5 1441 // <5/>6 1442 // </6> 1443 1444 // Although this is neater done using recursion, there's a 33% performance saving 1445 // to be gained by using this stack mechanism. 1446 1447 // Only add CR's if "skip white spaces" was set. Otherwise leave as is. 1448 $CR = ($this->parseSkipWhiteCache) ? "\n" : ''; 1449 $currentIndent = ''; 1450 $hilightIsActive = is_array($this->hilightXpathList); 1451 1452 // To keep track of where we are in the document we use a node stack. The node 1453 // stack has the following parallel entries: 1454 // 'Parent' => (array) A copy of the parent node that who's children we are 1455 // exporting 1456 // 'ChildIndex' => (array) The child index of the corresponding parent that we 1457 // are currently exporting. 1458 // 'Highlighted'=> (bool) If we are highlighting this node. Only relevant if 1459 // the hilight is active. 1460 1461 // Setup our node stack. The loop is designed to output children of a parent, 1462 // not the parent itself, so we must put the parent on as the starting point. 1463 $nodeStack['Parent'] = array($node['parentNode']); 1464 // And add the childpos of our node in it's parent to our "child index stack". 1465 $nodeStack['ChildIndex'] = array($node['pos']); 1466 // We start at 0. 1467 $nodeStackIndex = 0; 1468 1469 // We have not to output text before/after our node, so blank it. We will recover it 1470 // later 1471 $OldPreceedingStringValue = $nodeStack['Parent'][0]['textParts'][$node['pos']]; 1472 $OldPreceedingStringRef =& $nodeStack['Parent'][0]['textParts'][$node['pos']]; 1473 $OldPreceedingStringRef = ""; 1474 $currentXpath = ""; 1475 1476 // While we still have data on our stack 1477 while ($nodeStackIndex >= 0) { 1478 // Count the children and get a copy of the current child. 1479 $iChildCount = count($nodeStack['Parent'][$nodeStackIndex]['childNodes']); 1480 $currentChild = $nodeStack['ChildIndex'][$nodeStackIndex]; 1481 // Only do the auto indenting if the $parseSkipWhiteCache flag was set. 1482 if ($this->parseSkipWhiteCache) 1483 $currentIndent = str_repeat($this->indentStep, $nodeStackIndex); 1484 1485 if ($bDebugThisFunction) 1486 echo "Exporting child ".($currentChild+1)." of node {$nodeStack['Parent'][$nodeStackIndex]['xpath']}\n"; 1487 1488 /////////////////////////////////////////// 1489 // Add the text before our child. 1490 1491 // Add the text part before the current child 1492 $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild]; 1493 if (isSet($tmpTxt) AND ($tmpTxt!="")) { 1494 // Only add CR indent if there were children 1495 if ($iChildCount) 1496 $xmlOut .= $CR.$currentIndent; 1497 // Hilight if necessary. 1498 $highlightStart = $highlightEnd = ''; 1499 if ($hilightIsActive) { 1500 $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+1).']'; 1501 if (in_array($currentXpath, $this->hilightXpathList)) { 1502 // Yes we hilight 1503 $highlightStart = chr(2); 1504 $highlightEnd = chr(3); 1505 } 1506 } 1507 $xmlOut .= $highlightStart.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild].$highlightEnd; 1508 } 1509 if ($iChildCount && $nodeStackIndex) $xmlOut .= $CR; 1510 1511 /////////////////////////////////////////// 1512 1513 // Are there any more children? 1514 if ($iChildCount <= $currentChild) { 1515 // Nope, so output the last text before the closing tag 1516 $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1]; 1517 if (isSet($tmpTxt) AND ($tmpTxt!="")) { 1518 // Hilight if necessary. 1519 $highlightStart = $highlightEnd = ''; 1520 if ($hilightIsActive) { 1521 $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+2).']'; 1522 if (in_array($currentXpath, $this->hilightXpathList)) { 1523 // Yes we hilight 1524 $highlightStart = chr(2); 1525 $highlightEnd = chr(3); 1526 } 1527 } 1528 $xmlOut .= $highlightStart 1529 .$currentIndent.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1].$CR 1530 .$highlightEnd; 1531 } 1532 1533 // Now close this tag, as we are finished with this child. 1534 1535 // Potentially output an (slightly smaller indent). 1536 if ($this->parseSkipWhiteCache 1537 && count($nodeStack['Parent'][$nodeStackIndex]['childNodes'])) { 1538 $xmlOut .= str_repeat($this->indentStep, $nodeStackIndex - 1); 1539 } 1540 1541 // Check whether the xml-tag is to be hilighted. 1542 $highlightStart = $highlightEnd = ''; 1543 if ($hilightIsActive) { 1544 $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath']; 1545 if (in_array($currentXpath, $this->hilightXpathList)) { 1546 // Yes we hilight 1547 $highlightStart = chr(2); 1548 $highlightEnd = chr(3); 1549 } 1550 } 1551 $xmlOut .= $highlightStart 1552 .'</'.$nodeStack['Parent'][$nodeStackIndex]['name'].'>' 1553 .$highlightEnd; 1554 // Decrement the $nodeStackIndex to go back to the next unfinished parent. 1555 $nodeStackIndex--; 1556 1557 // If the index is 0 we are finished exporting the last node, as we may have been 1558 // exporting an internal node. 1559 if ($nodeStackIndex == 0) break; 1560 1561 // Indicate to the parent that we are finished with this child. 1562 $nodeStack['ChildIndex'][$nodeStackIndex]++; 1563 1564 continue; 1565 } 1566 1567 /////////////////////////////////////////// 1568 // Ok, there are children still to process. 1569 1570 // Queue up the next child (I can copy because I won't modify and copying is faster.) 1571 $nodeStack['Parent'][$nodeStackIndex + 1] = $nodeStack['Parent'][$nodeStackIndex]['childNodes'][$currentChild]; 1572 1573 // Work out if it is a short child tag. 1574 $iGrandChildCount = count($nodeStack['Parent'][$nodeStackIndex + 1]['childNodes']); 1575 $shortGrandChild = (($iGrandChildCount == 0) AND (implode('',$nodeStack['Parent'][$nodeStackIndex + 1]['textParts'])=='')); 1576 1577 /////////////////////////////////////////// 1578 // Assemble the attribute string first. 1579 $attrStr = ''; 1580 foreach($nodeStack['Parent'][$nodeStackIndex + 1]['attributes'] as $key=>$val) { 1581 // Should we hilight the attribute? 1582 if ($hilightIsActive AND in_array($currentXpath.'/attribute::'.$key, $this->hilightXpathList)) { 1583 $hiAttrStart = chr(2); 1584 $hiAttrEnd = chr(3); 1585 } else { 1586 $hiAttrStart = $hiAttrEnd = ''; 1587 } 1588 $attrStr .= ' '.$hiAttrStart.$key.'="'.$val.'"'.$hiAttrEnd; 1589 } 1590 1591 /////////////////////////////////////////// 1592 // Work out what goes before and after the tag content 1593 1594 $beforeTagContent = $currentIndent; 1595 if ($shortGrandChild) $afterTagContent = '/>'; 1596 else $afterTagContent = '>'; 1597 1598 // Check whether the xml-tag is to be hilighted. 1599 if ($hilightIsActive) { 1600 $currentXpath = $nodeStack['Parent'][$nodeStackIndex + 1]['xpath']; 1601 if (in_array($currentXpath, $this->hilightXpathList)) { 1602 // Yes we hilight 1603 $beforeTagContent .= chr(2); 1604 $afterTagContent .= chr(3); 1605 } 1606 } 1607 $beforeTagContent .= '<'; 1608 // if ($shortGrandChild) $afterTagContent .= $CR; 1609 1610 /////////////////////////////////////////// 1611 // Output the tag 1612 1613 $xmlOut .= $beforeTagContent 1614 .$nodeStack['Parent'][$nodeStackIndex + 1]['name'].$attrStr 1615 .$afterTagContent; 1616 1617 /////////////////////////////////////////// 1618 // Carry on. 1619 1620 // If it is a short tag, then we've already done this child, we just move to the next 1621 if ($shortGrandChild) { 1622 // Move to the next child, we need not go deeper in the tree. 1623 $nodeStack['ChildIndex'][$nodeStackIndex]++; 1624 // But if we are just exporting the one node we'd go no further. 1625 if ($nodeStackIndex == 0) break; 1626 } else { 1627 // Else queue up the child going one deeper in the stack 1628 $nodeStackIndex++; 1629 // Start with it's first child 1630 $nodeStack['ChildIndex'][$nodeStackIndex] = 0; 1631 } 1632 } 1633 1634 $result = $xmlOut; 1635 1636 // Repair what we "undid" 1637 $OldPreceedingStringRef = $OldPreceedingStringValue; 1638 1639 //////////////////////////////////////////// 1640 1641 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 1642 1643 return $result; 1644 } 1645 1646 //----------------------------------------------------------------------------------------- 1647 // XPathEngine ------ Import the XML Source ------ 1648 //----------------------------------------------------------------------------------------- 1649 1650 /** 1651 * Reads a file or URL and parses the XML data. 1652 * 1653 * Parse the XML source and (upon success) store the information into an internal structure. 1654 * 1655 * @param $fileName (string) Path and name (or URL) of the file to be read and parsed. 1656 * @return (bool) TRUE on success, FALSE on failure (check getLastError()) 1657 * @see importFromString(), getLastError(), 1658 */ 1659 function importFromFile($fileName) { 1660 $status = FALSE; 1661 $errStr = ''; 1662 do { // try-block 1663 // Remember file name. Used in error output to know in which file it happend 1664 $this->properties['xmlFile'] = $fileName; 1665 // If we already have content, then complain. 1666 if (!empty($this->nodeRoot)) { 1667 $errStr = 'Called when this object already contains xml data. Use reset().'; 1668 break; // try-block 1669 } 1670 // The the source is an url try to fetch it. 1671 if (preg_match(';^http(s)?://;', $fileName)) { 1672 // Read the content of the url...this is really prone to errors, and we don't really 1673 // check for too many here...for now, suppressing both possible warnings...we need 1674 // to check if we get a none xml page or something of that nature in the future 1675 $xmlString = @implode('', @file($fileName)); 1676 if (!empty($xmlString)) { 1677 $status = TRUE; 1678 } else { 1679 $errStr = "The url '{$fileName}' could not be found or read."; 1680 } 1681 break; // try-block 1682 } 1683 1684 // Reaching this point we're dealing with a real file (not an url). Check if the file exists and is readable. 1685 if (!is_readable($fileName)) { // Read the content from the file 1686 $errStr = "File '{$fileName}' could not be found or read."; 1687 break; // try-block 1688 } 1689 if (is_dir($fileName)) { 1690 $errStr = "'{$fileName}' is a directory."; 1691 break; // try-block 1692 } 1693 // Read the file 1694 if (!($fp = @fopen($fileName, 'rb'))) { 1695 $errStr = "Failed to open '{$fileName}' for read."; 1696 break; // try-block 1697 } 1698 $xmlString = fread($fp, filesize($fileName)); 1699 @fclose($fp); 1700 1701 $status = TRUE; 1702 } while (FALSE); 1703 1704 if (!$status) { 1705 $this->_displayError('In importFromFile(): '. $errStr, __LINE__, __FILE__, FALSE); 1706 return FALSE; 1707 } 1708 return $this->importFromString($xmlString); 1709 } 1710 1711 /** 1712 * Reads a string and parses the XML data. 1713 * 1714 * Parse the XML source and (upon success) store the information into an internal structure. 1715 * If a parent xpath is given this means that XML data is to be *appended* to that parent. 1716 * 1717 * ### If a function uses setLastError(), then say in the function header that getLastError() is useful. 1718 * 1719 * @param $xmlString (string) Name of the string to be read and parsed. 1720 * @param $absoluteParentPath (string) Node to append data too (see above) 1721 * @return (bool) TRUE on success, FALSE on failure 1722 * (check getLastError()) 1723 */ 1724 function importFromString($xmlString, $absoluteParentPath = '') { 1725 $ThisFunctionName = 'importFromString'; 1726 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 1727 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 1728 if ($bDebugThisFunction) { 1729 echo "Importing from string of length ".strlen($xmlString)." to node '$absoluteParentPath'\n<br>"; 1730 echo "Parser options:\n<br>"; 1731 print_r($this->parseOptions); 1732 } 1733 1734 $status = FALSE; 1735 $errStr = ''; 1736 do { // try-block 1737 // If we already have content, then complain. 1738 if (!empty($this->nodeRoot) AND empty($absoluteParentPath)) { 1739 $errStr = 'Called when this object already contains xml data. Use reset() or pass the parent Xpath as 2ed param to where tie data will append.'; 1740 break; // try-block 1741 } 1742 // Check whether content has been read. 1743 if (empty($xmlString)) { 1744 // Nothing to do!! 1745 $status = TRUE; 1746 // If we were importing to root, build a blank root. 1747 if (empty($absoluteParentPath)) { 1748 $this->_createSuperRoot(); 1749 } 1750 $this->reindexNodeTree(); 1751 // $errStr = 'This xml document (string) was empty'; 1752 break; // try-block 1753 } else { 1754 $xmlString = $this->_translateAmpersand($xmlString); 1755 } 1756 1757 // Restart our node index with a root entry. 1758 $nodeStack = array(); 1759 $this->parseStackIndex = 0; 1760 1761 // If a parent xpath is given this means that XML data is to be *appended* to that parent. 1762 if (!empty($absoluteParentPath)) { 1763 // Check if parent exists 1764 if (!isSet($this->nodeIndex[$absoluteParentPath])) { 1765 $errStr = "You tried to append XML data to a parent '$absoluteParentPath' that does not exist."; 1766 break; // try-block 1767 } 1768 // Add it as the starting point in our array. 1769 $this->nodeStack[0] =& $this->nodeIndex[$absoluteParentPath]; 1770 } else { 1771 // Build a 'super-root' 1772 $this->_createSuperRoot(); 1773 // Put it in as the start of our node stack. 1774 $this->nodeStack[0] =& $this->nodeRoot; 1775 } 1776 1777 // Point our text buffer reference at the next text part of the root 1778 $this->parsedTextLocation =& $this->nodeStack[0]['textParts'][]; 1779 $this->parsInCData = 0; 1780 // We cache this now. 1781 $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE; 1782 1783 // Create an XML parser. 1784 $parser = xml_parser_create(); 1785 // Set default XML parser options. 1786 if (is_array($this->parseOptions)) { 1787 foreach($this->parseOptions as $key => $val) { 1788 xml_parser_set_option($parser, $key, $val); 1789 } 1790 } 1791 1792 // Set the object and the element handlers for the XML parser. 1793 xml_set_object($parser, $this); 1794 xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement'); 1795 xml_set_character_data_handler($parser, '_handleCharacterData'); 1796 xml_set_default_handler($parser, '_handleDefaultData'); 1797 xml_set_processing_instruction_handler($parser, '_handlePI'); 1798 1799 // Parse the XML source and on error generate an error message. 1800 if (!xml_parse($parser, $xmlString, TRUE)) { 1801 $source = empty($this->properties['xmlFile']) ? 'string' : 'file ' . basename($this->properties['xmlFile']) . "'"; 1802 $errStr = "XML error in given {$source} on line ". 1803 xml_get_current_line_number($parser). ' column '. xml_get_current_column_number($parser) . 1804 '. Reason:'. xml_error_string(xml_get_error_code($parser)); 1805 break; // try-block 1806 } 1807 1808 // Free the parser. 1809 @xml_parser_free($parser); 1810 // And we don't need this any more. 1811 $this->nodeStack = array(); 1812 1813 $this->reindexNodeTree(); 1814 1815 if ($bDebugThisFunction) { 1816 print_r(array_keys($this->nodeIndex)); 1817 } 1818 1819 $status = TRUE; 1820 } while (FALSE); 1821 1822 if (!$status) { 1823 $this->_displayError('In importFromString(): '. $errStr, __LINE__, __FILE__, FALSE); 1824 $bResult = FALSE; 1825 } else { 1826 $bResult = TRUE; 1827 } 1828 1829 //////////////////////////////////////////// 1830 1831 $this->_closeDebugFunction($ThisFunctionName, $bResult, $bDebugThisFunction); 1832 1833 return $bResult; 1834 } 1835 1836 1837 //----------------------------------------------------------------------------------------- 1838 // XPathEngine ------ XML Handlers ------ 1839 //----------------------------------------------------------------------------------------- 1840 1841 /** 1842 * Handles opening XML tags while parsing. 1843 * 1844 * While parsing a XML document for each opening tag this method is 1845 * called. It'll add the tag found to the tree of document nodes. 1846 * 1847 * @param $parser (int) Handler for accessing the current XML parser. 1848 * @param $name (string) Name of the opening tag found in the document. 1849 * @param $attributes (array) Associative array containing a list of 1850 * all attributes of the tag found in the document. 1851 * @see _handleEndElement(), _handleCharacterData() 1852 */ 1853 function _handleStartElement($parser, $nodeName, $attributes) { 1854 if (empty($nodeName)) { 1855 $this->_displayError('XML error in file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__); 1856 return; 1857 } 1858 1859 // Trim accumulated text if necessary. 1860 if ($this->parseSkipWhiteCache) { 1861 $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']); 1862 $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation); 1863 } 1864 1865 if ($this->bDebugXmlParse) { 1866 echo "<blockquote>" . htmlspecialchars("Start node: <".$nodeName . ">")."<br>"; 1867 echo "Appended to stack entry: $this->parseStackIndex<br>\n"; 1868 echo "Text part before element is: ".htmlspecialchars($this->parsedTextLocation); 1869 /* 1870 echo "<pre>"; 1871 $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']); 1872 for ($i = 0; $i < $dataPartsCount; $i++) { 1873 echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n"; 1874 } 1875 echo "</pre>"; 1876 */ 1877 } 1878 1879 // Add a node and set path to current. 1880 if (!$this->_internalAppendChild($this->parseStackIndex, $nodeName)) { 1881 $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__); 1882 return; 1883 } 1884 1885 // We will have gone one deeper then in the stack. 1886 $this->parseStackIndex++; 1887 1888 // Point our parseTxtBuffer reference at the new node. 1889 $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][0]; 1890 1891 // Set the attributes. 1892 if (!empty($attributes)) { 1893 if ($this->bDebugXmlParse) { 1894 echo 'Attributes: <br>'; 1895 print_r($attributes); 1896 echo '<br>'; 1897 } 1898 $this->nodeStack[$this->parseStackIndex]['attributes'] = $attributes; 1899 } 1900 } 1901 1902 /** 1903 * Handles closing XML tags while parsing. 1904 * 1905 * While parsing a XML document for each closing tag this method is called. 1906 * 1907 * @param $parser (int) Handler for accessing the current XML parser. 1908 * @param $name (string) Name of the closing tag found in the document. 1909 * @see _handleStartElement(), _handleCharacterData() 1910 */ 1911 function _handleEndElement($parser, $name) { 1912 if (($this->parsedTextLocation=='') 1913 && empty($this->nodeStack[$this->parseStackIndex]['textParts'])) { 1914 // We reach this point when parsing a tag of format <foo/>. The 'textParts'-array 1915 // should stay empty and not have an empty string in it. 1916 } else { 1917 // Trim accumulated text if necessary. 1918 if ($this->parseSkipWhiteCache) { 1919 $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']); 1920 $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation); 1921 } 1922 } 1923 1924 if ($this->bDebugXmlParse) { 1925 echo "Text part after element is: ".htmlspecialchars($this->parsedTextLocation)."<br>\n"; 1926 echo htmlspecialchars("Parent:<{$this->parseStackIndex}>, End-node:</$name> '".$this->parsedTextLocation) . "'<br>Text nodes:<pre>\n"; 1927 $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']); 1928 for ($i = 0; $i < $dataPartsCount; $i++) { 1929 echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n"; 1930 } 1931 var_dump($this->nodeStack[$this->parseStackIndex]['textParts']); 1932 echo "</pre></blockquote>\n"; 1933 } 1934 1935 // Jump back to the parent element. 1936 $this->parseStackIndex--; 1937 1938 // Set our reference for where we put any more whitespace 1939 $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][]; 1940 1941 // Note we leave the entry in the stack, as it will get blanked over by the next element 1942 // at this level. The safe thing to do would be to remove it too, but in the interests 1943 // of performance, we will not bother, as were it to be a problem, then it would be an 1944 // internal bug anyway. 1945 if ($this->parseStackIndex < 0) { 1946 $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__); 1947 return; 1948 } 1949 } 1950 1951 /** 1952 * Handles character data while parsing. 1953 * 1954 * While parsing a XML document for each character data this method 1955 * is called. It'll add the character data to the document tree. 1956 * 1957 * @param $parser (int) Handler for accessing the current XML parser. 1958 * @param $text (string) Character data found in the document. 1959 * @see _handleStartElement(), _handleEndElement() 1960 */ 1961 function _handleCharacterData($parser, $text) { 1962 1963 if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, $reverse=TRUE); 1964 1965 if ($this->bDebugXmlParse) echo "Handling character data: '".htmlspecialchars($text)."'<br>"; 1966 if ($this->parseSkipWhiteCache AND !empty($text) AND !$this->parsInCData) { 1967 // Special case CR. CR always comes in a separate data. Trans. it to '' or ' '. 1968 // If txtBuffer is already ending with a space use '' otherwise ' '. 1969 $bufferHasEndingSpace = (empty($this->parsedTextLocation) OR substr($this->parsedTextLocation, -1) === ' ') ? TRUE : FALSE; 1970 if ($text=="\n") { 1971 $text = $bufferHasEndingSpace ? '' : ' '; 1972 } else { 1973 if ($bufferHasEndingSpace) { 1974 $text = ltrim(preg_replace('/\s+/', ' ', $text)); 1975 } else { 1976 $text = preg_replace('/\s+/', ' ', $text); 1977 } 1978 } 1979 if ($this->bDebugXmlParse) echo "'Skip white space' is ON. reduced to : '" .htmlspecialchars($text) . "'<br>"; 1980 } 1981 $this->parsedTextLocation .= $text; 1982 } 1983 1984 /** 1985 * Default handler for the XML parser. 1986 * 1987 * While parsing a XML document for string not caught by one of the other 1988 * handler functions, we end up here. 1989 * 1990 * @param $parser (int) Handler for accessing the current XML parser. 1991 * @param $text (string) Character data found in the document. 1992 * @see _handleStartElement(), _handleEndElement() 1993 */ 1994 function _handleDefaultData($parser, $text) { 1995 do { // try-block 1996 if (!strcmp($text, '<![CDATA[')) { 1997 $this->parsInCData++; 1998 } elseif (!strcmp($text, ']]>')) { 1999 $this->parsInCData--; 2000 if ($this->parsInCData < 0) $this->parsInCData = 0; 2001 } 2002 $this->parsedTextLocation .= $this->_translateAmpersand($text, $reverse=TRUE); 2003 if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>"; 2004 break; // try-block 2005 } while (FALSE); // END try-block 2006 } 2007 2008 /** 2009 * Handles processing instruction (PI) 2010 * 2011 * A processing instruction has the following format: 2012 * <? target data ? > e.g. <? dtd version="1.0" ? > 2013 * 2014 * Currently I have no bether idea as to left it 'as is' and treat the PI data as normal 2015 * text (and adding the surrounding PI-tags <? ? >). 2016 * 2017 * @param $parser (int) Handler for accessing the current XML parser. 2018 * @param $target (string) Name of the PI target. E.g. XML, PHP, DTD, ... 2019 * @param $data (string) Associative array containing a list of 2020 * @see PHP's manual "xml_set_processing_instruction_handler" 2021 */ 2022 function _handlePI($parser, $target, $data) { 2023 //echo("pi data=".$data."end"); exit; 2024 $data = $this->_translateAmpersand($data, $reverse=TRUE); 2025 $this->parsedTextLocation .= "<?{$target} {$data}?>"; 2026 return TRUE; 2027 } 2028 2029 //----------------------------------------------------------------------------------------- 2030 // XPathEngine ------ Node Tree Stuff ------ 2031 //----------------------------------------------------------------------------------------- 2032 2033 /** 2034 * Creates a super root node. 2035 */ 2036 function _createSuperRoot() { 2037 // Build a 'super-root' 2038 $this->nodeRoot = $this->emptyNode; 2039 $this->nodeRoot['name'] = ''; 2040 $this->nodeRoot['parentNode'] = NULL; 2041 $this->nodeIndex[''] =& $this->nodeRoot; 2042 } 2043 2044 /** 2045 * Adds a new node to the XML document tree during xml parsing. 2046 * 2047 * This method adds a new node to the tree of nodes of the XML document 2048 * being handled by this class. The new node is created according to the 2049 * parameters passed to this method. This method is a much watered down 2050 * version of appendChild(), used in parsing an xml file only. 2051 * 2052 * It is assumed that adding starts with root and progresses through the 2053 * document in parse order. New nodes must have a corresponding parent. And 2054 * once we have read the </> tag for the element we will never need to add 2055 * any more data to that node. Otherwise the add will be ignored or fail. 2056 * 2057 * The function is faciliated by a nodeStack, which is an array of nodes that 2058 * we have yet to close. 2059 * 2060 * @param $stackParentIndex (int) The index into the nodeStack[] of the parent 2061 * node to which the new node should be added as 2062 * a child. *READONLY* 2063 * @param $nodeName (string) Name of the new node. *READONLY* 2064 * @return (bool) TRUE if we successfully added a new child to 2065 * the node stack at index $stackParentIndex + 1, 2066 * FALSE on error. 2067 */ 2068 function _internalAppendChild($stackParentIndex, $nodeName) { 2069 // This call is likely to be executed thousands of times, so every 0.01ms counts. 2070 // If you want to debug this function, you'll have to comment the stuff back in 2071 //$bDebugThisFunction = FALSE; 2072 2073 /* 2074 $ThisFunctionName = '_internalAppendChild'; 2075 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 2076 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 2077 if ($bDebugThisFunction) { 2078 echo "Current Node (parent-index) and the child to append : '{$stackParentIndex}' + '{$nodeName}' \n<br>"; 2079 } 2080 */ 2081 ////////////////////////////////////// 2082 2083 if (!isSet($this->nodeStack[$stackParentIndex])) { 2084 $errStr = "Invalid parent. You tried to append the tag '{$nodeName}' to an non-existing parent in our node stack '{$stackParentIndex}'."; 2085 $this->_displayError('In _internalAppendChild(): '. $errStr, __LINE__, __FILE__, FALSE); 2086 2087 /* 2088 $this->_closeDebugFunction($ThisFunctionName, FALSE, $bDebugThisFunction); 2089 */ 2090 2091 return FALSE; 2092 } 2093 2094 // Retrieve the parent node from the node stack. This is the last node at that 2095 // depth that we have yet to close. This is where we should add the text/node. 2096 $parentNode =& $this->nodeStack[$stackParentIndex]; 2097 2098 // Brand new node please 2099 $newChildNode = $this->emptyNode; 2100 2101 // Save the vital information about the node. 2102 $newChildNode['name'] = $nodeName; 2103 $parentNode['childNodes'][] =& $newChildNode; 2104 2105 // Add to our node stack 2106 $this->nodeStack[$stackParentIndex + 1] =& $newChildNode; 2107 2108 /* 2109 if ($bDebugThisFunction) { 2110 echo "The new node received index: '".($stackParentIndex + 1)."'\n"; 2111 foreach($this->nodeStack as $key => $val) echo "$key => ".$val['name']."\n"; 2112 } 2113 $this->_closeDebugFunction($ThisFunctionName, TRUE, $bDebugThisFunction); 2114 */ 2115 2116 return TRUE; 2117 } 2118 2119 /** 2120 * Update nodeIndex and every node of the node-tree. 2121 * 2122 * Call after you have finished any tree modifications other wise a match with 2123 * an xPathQuery will produce wrong results. The $this->nodeIndex[] is recreated 2124 * and every nodes optimization data is updated. The optimization data is all the 2125 * data that is duplicate information, would just take longer to find. Child nodes 2126 * with value NULL are removed from the tree. 2127 * 2128 * By default the modification functions in this component will automatically re-index 2129 * the nodes in the tree. Sometimes this is not the behaver you want. To surpress the 2130 * reindex, set the functions $autoReindex to FALSE and call reindexNodeTree() at the 2131 * end of your changes. This sometimes leads to better code (and less CPU overhead). 2132 * 2133 * Sample: 2134 * ======= 2135 * Given the xml is <AAA><B/>.<B/>.<B/></AAA> | Goal is <AAA>.<B/>.</AAA> (Delete B[1] and B[3]) 2136 * $xPathSet = $xPath->match('//B'); # Will result in array('/AAA[1]/B[1]', '/AAA[1]/B[2]', '/AAA[1]/B[3]'); 2137 * Three ways to do it. 2138 * 1) Top-Down (with auto reindexing) - Safe, Slow and you get easily mix up with the the changing node index 2139 * removeChild('/AAA[1]/B[1]'); // B[1] removed, thus all B[n] become B[n-1] !! 2140 * removeChild('/AAA[1]/B[2]'); // Now remove B[2] (That originaly was B[3]) 2141 * 2) Bottom-Up (with auto reindexing) - Safe, Slow and the changing node index (caused by auto-reindex) can be ignored. 2142 * for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 2143 * if ($i==1) continue; 2144 * removeChild($xPathSet[$i]); 2145 * } 2146 * 3) // Top-down (with *NO* auto reindexing) - Fast, Safe as long as you call reindexNodeTree() 2147 * foreach($xPathSet as $xPath) { 2148 * // Specify no reindexing 2149 * if ($xPath == $xPathSet[1]) continue; 2150 * removeChild($xPath, $autoReindex=FALSE); 2151 * // The object is now in a slightly inconsistent state. 2152 * } 2153 * // Finally do the reindex and the object is consistent again 2154 * reindexNodeTree(); 2155 * 2156 * @return (bool) TRUE on success, FALSE otherwise. 2157 * @see _recursiveReindexNodeTree() 2158 */ 2159 function reindexNodeTree() { 2160 //return; 2161 $this->_indexIsDirty = FALSE; 2162 $this->nodeIndex = array(); 2163 $this->nodeIndex[''] =& $this->nodeRoot; 2164 // Quick out for when the tree has no data. 2165 if (empty($this->nodeRoot)) return TRUE; 2166 return $this->_recursiveReindexNodeTree(''); 2167 } 2168 2169 2170 /** 2171 * Create the ids that are accessable through the generate-id() function 2172 */ 2173 function _generate_ids() { 2174 // If we have generated them already, then bail. 2175 if (isset($this->nodeIndex['']['generate_id'])) return; 2176 2177 // keys generated are the string 'id0' . hexatridecimal-based (0..9,a-z) index 2178 $aNodeIndexes = array_keys($this->nodeIndex); 2179 $idNumber = 0; 2180 foreach($aNodeIndexes as $index => $key) { 2181 // $this->nodeIndex[$key]['generated_id'] = 'id' . base_convert($index,10,36); 2182 // Skip attribute and text nodes. 2183 // ### Currently don't support attribute and text nodes. 2184 if (strstr($key, 'text()') !== FALSE) continue; 2185 if (strstr($key, 'attribute::') !== FALSE) continue; 2186 $this->nodeIndex[$key]['generated_id'] = 'idPhpXPath' . $idNumber; 2187 2188 // Make the id's sequential so that we can test predictively. 2189 $idNumber++; 2190 } 2191 } 2192 2193 /** 2194 * Here's where the work is done for reindexing (see reindexNodeTree) 2195 * 2196 * @param $absoluteParentPath (string) the xPath to the parent node 2197 * @return (bool) TRUE on success, FALSE otherwise. 2198 * @see reindexNodeTree() 2199 */ 2200 function _recursiveReindexNodeTree($absoluteParentPath) { 2201 $parentNode =& $this->nodeIndex[$absoluteParentPath]; 2202 2203 // Check for any 'dead' child nodes first and concate the text parts if found. 2204 for ($iChildIndex=sizeOf($parentNode['childNodes'])-1; $iChildIndex>=0; $iChildIndex--) { 2205 // Check if the child node still exits (it may have been removed). 2206 if (!empty($parentNode['childNodes'][$iChildIndex])) continue; 2207 // Child node was removed. We got to merge the text parts then. 2208 $parentNode['textParts'][$iChildIndex] .= $parentNode['textParts'][$iChildIndex+1]; 2209 array_splice($parentNode['textParts'], $iChildIndex+1, 1); 2210 array_splice($parentNode['childNodes'], $iChildIndex, 1); 2211 } 2212 2213 // Now start a reindex. 2214 $contextHash = array(); 2215 $childSize = sizeOf($parentNode['childNodes']); 2216 2217 // If there are no children, we have to treat this specially: 2218 if ($childSize == 0) { 2219 // Add a dummy text node. 2220 $this->nodeIndex[$absoluteParentPath.'/text()[1]'] =& $parentNode; 2221 } else { 2222 for ($iChildIndex=0; $iChildIndex<$childSize; $iChildIndex++) { 2223 $childNode =& $parentNode['childNodes'][$iChildIndex]; 2224 // Make sure that there is a text-part in front of every node. (May be empty) 2225 if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = ''; 2226 // Count the nodes with same name (to determine their context position) 2227 $childName = $childNode['name']; 2228 if (empty($contextHash[$childName])) { 2229 $contextPos = $contextHash[$childName] = 1; 2230 } else { 2231 $contextPos = ++$contextHash[$childName]; 2232 } 2233 // Make the node-index hash 2234 $newPath = $absoluteParentPath . '/' . $childName . '['.$contextPos.']'; 2235 2236 // ### Note ultimately we will end up supporting text nodes as actual nodes. 2237 2238 // Preceed with a dummy entry for the text node. 2239 $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+1).']'] =& $childNode; 2240 // Then the node itself 2241 $this->nodeIndex[$newPath] =& $childNode; 2242 2243 // Now some dummy nodes for each of the attribute nodes. 2244 $iAttributeCount = sizeOf($childNode['attributes']); 2245 if ($iAttributeCount > 0) { 2246 $aAttributesNames = array_keys($childNode['attributes']); 2247 for ($iAttributeIndex = 0; $iAttributeIndex < $iAttributeCount; $iAttributeIndex++) { 2248 $attribute = $aAttributesNames[$iAttributeIndex]; 2249 $newAttributeNode = $this->emptyNode; 2250 $newAttributeNode['name'] = $attribute; 2251 $newAttributeNode['textParts'] = array($childNode['attributes'][$attribute]); 2252 $newAttributeNode['contextPos'] = $iAttributeIndex; 2253 $newAttributeNode['xpath'] = "$newPath/attribute::$attribute"; 2254 $newAttributeNode['parentNode'] =& $childNode; 2255 $newAttributeNode['depth'] =& $parentNode['depth'] + 2; 2256 // Insert the node as a master node, not a reference, otherwise there will be 2257 // variable "bleeding". 2258 $this->nodeIndex["$newPath/attribute::$attribute"] = $newAttributeNode; 2259 } 2260 } 2261 2262 // Update the node info (optimisation) 2263 $childNode['parentNode'] =& $parentNode; 2264 $childNode['depth'] = $parentNode['depth'] + 1; 2265 $childNode['pos'] = $iChildIndex; 2266 $childNode['contextPos'] = $contextHash[$childName]; 2267 $childNode['xpath'] = $newPath; 2268 $this->_recursiveReindexNodeTree($newPath); 2269 2270 // Follow with a dummy entry for the text node. 2271 $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+2).']'] =& $childNode; 2272 } 2273 2274 // Make sure that their is a text-part after the last node. 2275 if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = ''; 2276 } 2277 2278 return TRUE; 2279 } 2280 2281 /** 2282 * Clone a node and it's child nodes. 2283 * 2284 * NOTE: If the node has children you *MUST* use the reference operator! 2285 * E.g. $clonedNode =& cloneNode($node); 2286 * Otherwise the children will not point back to the parent, they will point 2287 * back to your temporary variable instead. 2288 * 2289 * @param $node (mixed) Either a node (hash array) or an abs. Xpath to a node in 2290 * the current doc 2291 * @return (&array) A node and it's child nodes. 2292 */ 2293 function &cloneNode($node, $recursive=FALSE) { 2294 if (is_string($node) AND isSet($this->nodeIndex[$node])) { 2295 $node = $this->nodeIndex[$node]; 2296 } 2297 // Copy the text-parts () 2298 $textParts = $node['textParts']; 2299 $node['textParts'] = array(); 2300 foreach ($textParts as $key => $val) { 2301 $node['textParts'][] = $val; 2302 } 2303 2304 $childSize = sizeOf($node['childNodes']); 2305 for ($i=0; $i<$childSize; $i++) { 2306 $childNode =& $this->cloneNode($node['childNodes'][$i], TRUE); // copy child 2307 $node['childNodes'][$i] =& $childNode; // reference the copy 2308 $childNode['parentNode'] =& $node; // child references the parent. 2309 } 2310 2311 if (!$recursive) { 2312 //$node['childNodes'][0]['parentNode'] = null; 2313 //print "<pre>"; 2314 //var_dump($node); 2315 } 2316 return $node; 2317 } 2318 2319 2320 /** Nice to have but __sleep() has a bug. 2321 (2002-2 PHP V4.1. See bug #15350) 2322 2323 /** 2324 * PHP cals this function when you call PHP's serialize. 2325 * 2326 * It prevents cyclic referencing, which is why print_r() of an XPath object doesn't work. 2327 * 2328 function __sleep() { 2329 // Destroy recursive pointers 2330 $keys = array_keys($this->nodeIndex); 2331 $size = sizeOf($keys); 2332 for ($i=0; $i<$size; $i++) { 2333 unset($this->nodeIndex[$keys[$i]]['parentNode']); 2334 } 2335 unset($this->nodeIndex); 2336 } 2337 2338 /** 2339 * PHP cals this function when you call PHP's unserialize. 2340 * 2341 * It reindexes the node-tree 2342 * 2343 function __wakeup() { 2344 $this->reindexNodeTree(); 2345 } 2346 2347 */ 2348 2349 //----------------------------------------------------------------------------------------- 2350 // XPath ------ XPath Query / Evaluation Handlers ------ 2351 //----------------------------------------------------------------------------------------- 2352 2353 /** 2354 * Matches (evaluates) an XPath query 2355 * 2356 * This method tries to evaluate an XPath query by parsing it. A XML source must 2357 * have been imported before this method is able to work. 2358 * 2359 * @param $xPathQuery (string) XPath query to be evaluated. 2360 * @param $baseXPath (string) (default is super-root) XPath query to a single document node, 2361 * from which the XPath query should start evaluating. 2362 * @return (mixed) The result of the XPath expression. Either: 2363 * node-set (an ordered collection of absolute references to nodes without duplicates) 2364 * boolean (true or false) 2365 * number (a floating-point number) 2366 * string (a sequence of UCS characters) 2367 */ 2368 function match($xPathQuery, $baseXPath='') { 2369 if ($this->_indexIsDirty) $this->reindexNodeTree(); 2370 2371 // Replace a double slashes, because they'll cause problems otherwise. 2372 static $slashes2descendant = array( 2373 '//@' => '/descendant_or_self::*/attribute::', 2374 '//' => '/descendant_or_self::node()/', 2375 '/@' => '/attribute::'); 2376 // Stupid idea from W3C to take axes name containing a '-' (dash) !!! 2377 // We replace the '-' with '_' to avoid the conflict with the minus operator. 2378 static $dash2underscoreHash = array( 2379 '-sibling' => '_sibling', 2380 '-or-' => '_or_', 2381 'starts-with' => 'starts_with', 2382 'substring-before' => 'substring_before', 2383 'substring-after' => 'substring_after', 2384 'string-length' => 'string_length', 2385 'normalize-space' => 'normalize_space', 2386 'x-lower' => 'x_lower', 2387 'x-upper' => 'x_upper', 2388 'generate-id' => 'generate_id'); 2389 2390 if (empty($xPathQuery)) return array(); 2391 2392 // Special case for when document is empty. 2393 if (empty($this->nodeRoot)) return array(); 2394 2395 if (!isSet($this->nodeIndex[$baseXPath])) { 2396 $xPathSet = $this->_resolveXPathQuery($baseXPath,'match'); 2397 if (sizeOf($xPathSet) !== 1) { 2398 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 2399 return FALSE; 2400 } 2401 $baseXPath = $xPathSet[0]; 2402 } 2403 2404 // We should possibly do a proper syntactical parse, but instead we will cheat and just 2405 // remove any literals that could make things very difficult for us, and replace them with 2406 // special tags. Then we can treat the xPathQuery much more easily as JUST "syntax". Provided 2407 // there are no literals in the string, then we can guarentee that most of the operators and 2408 // syntactical elements are indeed elements and not just part of a literal string. 2409 $processedxPathQuery = $this->_removeLiterals($xPathQuery); 2410 2411 // Replace a double slashes, and '-' (dash) in axes names. 2412 $processedxPathQuery = strtr($processedxPathQuery, $slashes2descendant); 2413 $processedxPathQuery = strtr($processedxPathQuery, $dash2underscoreHash); 2414 2415 // Build the context 2416 $context = array('nodePath' => $baseXPath, 'pos' => 1, 'size' => 1); 2417 2418 // The primary syntactic construct in XPath is the expression. 2419 $result = $this->_evaluateExpr($processedxPathQuery, $context); 2420 2421 // We might have been returned a string.. If so convert back to a literal 2422 $literalString = $this->_asLiteral($result); 2423 if ($literalString != FALSE) return $literalString; 2424 else return $result; 2425 } 2426 2427 /** 2428 * Alias for the match function 2429 * 2430 * @see match() 2431 */ 2432 function evaluate($xPathQuery, $baseXPath='') { 2433 return $this->match($xPathQuery, $baseXPath); 2434 } 2435 2436 /** 2437 * Parse out the literals of an XPath expression. 2438 * 2439 * Instead of doing a full lexical parse, we parse out the literal strings, and then 2440 * Treat the sections of the string either as parts of XPath or literal strings. So 2441 * this function replaces each literal it finds with a literal reference, and then inserts 2442 * the reference into an array of strings that we can access. The literals can be accessed 2443 * later from the literals associative array. 2444 * 2445 * Example: 2446 * XPathExpr = /AAA[@CCC = "hello"]/BBB[DDD = 'world'] 2447 * => literals: array("hello", "world") 2448 * return value: /AAA[@CCC = $1]/BBB[DDD = $2] 2449 * 2450 * Note: This does not interfere with the VariableReference syntactical element, as these 2451 * elements must not start with a number. 2452 * 2453 * @param $xPathQuery (string) XPath expression to be processed 2454 * @return (string) The XPath expression without the literals. 2455 * 2456 */ 2457 function _removeLiterals($xPathQuery) { 2458 // What comes first? A " or a '? 2459 if (!preg_match(":^([^\"']*)([\"'].*)$:", $xPathQuery, $aMatches)) { 2460 // No " or ' means no more literals. 2461 return $xPathQuery; 2462 } 2463 2464 $result = $aMatches[1]; 2465 $remainder = $aMatches[2]; 2466 // What kind of literal? 2467 if (preg_match(':^"([^"]*)"(.*)$:', $remainder, $aMatches)) { 2468 // A "" literal. 2469 $literal = $aMatches[1]; 2470 $remainder = $aMatches[2]; 2471 } else if (preg_match(":^'([^']*)'(.*)$:", $remainder, $aMatches)) { 2472 // A '' literal. 2473 $literal = $aMatches[1]; 2474 $remainder = $aMatches[2]; 2475 } else { 2476 $this->_displayError("The '$xPathQuery' argument began a literal, but did not close it.", __LINE__, __FILE__); 2477 } 2478 2479 // Store the literal 2480 $literalNumber = count($this->axPathLiterals); 2481 $this->axPathLiterals[$literalNumber] = $literal; 2482 $result .= '$'.$literalNumber; 2483 return $result.$this->_removeLiterals($remainder); 2484 } 2485 2486 /** 2487 * Returns the given string as a literal reference. 2488 * 2489 * @param $string (string) The string that we are processing 2490 * @return (mixed) The literal string. FALSE if the string isn't a literal reference. 2491 */ 2492 function _asLiteral($string) { 2493 if (empty($string)) return FALSE; 2494 if (empty($string[0])) return FALSE; 2495 if ($string[0] == '$') { 2496 $remainder = substr($string, 1); 2497 if (is_numeric($remainder)) { 2498 // We have a string reference then. 2499 $stringNumber = (int)$remainder; 2500 if ($stringNumber >= count($this->axPathLiterals)) { 2501 $this->_displayError("Internal error. Found a string reference that we didn't set in xPathQuery: '$xPathQuery'.", __LINE__, __FILE__); 2502 return FALSE; 2503 } 2504 return $this->axPathLiterals[$stringNumber]; 2505 } 2506 } 2507 2508 // It's not a reference then. 2509 return FALSE; 2510 } 2511 2512 /** 2513 * Adds a literal to our array of literals 2514 * 2515 * In order to make sure we don't interpret literal strings as XPath expressions, we have to 2516 * encode literal strings so that we know that they are not XPaths. 2517 * 2518 * @param $string (string) The literal string that we need to store for future access 2519 * @return (mixed) A reference string to this literal. 2520 */ 2521 function _addLiteral($string) { 2522 // Store the literal 2523 $literalNumber = count($this->axPathLiterals); 2524 $this->axPathLiterals[$literalNumber] = $string; 2525 $result = '$'.$literalNumber; 2526 return $result; 2527 } 2528 2529 /** 2530 * Look for operators in the expression 2531 * 2532 * Parses through the given expression looking for operators. If found returns 2533 * the operands and the operator in the resulting array. 2534 * 2535 * @param $xPathQuery (string) XPath query to be evaluated. 2536 * @return (array) If an operator is found, it returns an array containing 2537 * information about the operator. If no operator is found 2538 * then it returns an empty array. If an operator is found, 2539 * but has invalid operands, it returns FALSE. 2540 * The resulting array has the following entries: 2541 * 'operator' => The string version of operator that was found, 2542 * trimmed for whitespace 2543 * 'left operand' => The left operand, or empty if there was no 2544 * left operand for this operator. 2545 * 'right operand' => The right operand, or empty if there was no 2546 * right operand for this operator. 2547 */ 2548 function _GetOperator($xPathQuery) { 2549 $position = 0; 2550 $operator = ''; 2551 2552 // The results of this function can easily be cached. 2553 static $aResultsCache = array(); 2554 if (isset($aResultsCache[$xPathQuery])) { 2555 return $aResultsCache[$xPathQuery]; 2556 } 2557 2558 // Run through all operators and try to find one. 2559 $opSize = sizeOf($this->operators); 2560 for ($i=0; $i<$opSize; $i++) { 2561 // Pick an operator to try. 2562 $operator = $this->operators[$i]; 2563 // Quickcheck. If not present don't wast time searching 'the hard way' 2564 if (strpos($xPathQuery, $operator)===FALSE) continue; 2565 // Special check 2566 $position = $this->_searchString($xPathQuery, $operator); 2567 // Check whether a operator was found. 2568 if ($position <= 0 ) continue; 2569 2570 // Check whether it's the equal operator. 2571 if ($operator == '=') { 2572 // Also look for other operators containing the equal sign. 2573 switch ($xPathQuery[$position-1]) { 2574 case '<' : 2575 $position--; 2576 $operator = '<='; 2577 break; 2578 case '>' : 2579 $position--; 2580 $operator = '>='; 2581 break; 2582 case '!' : 2583 $position--; 2584 $operator = '!='; 2585 break; 2586 default: 2587 // It's a pure = operator then. 2588 } 2589 break; 2590 } 2591 2592 if ($operator == '*') { 2593 // http://www.w3.org/TR/xpath#exprlex: 2594 // "If there is a preceding token and the preceding token is not one of @, ::, (, [, 2595 // or an Operator, then a * must be recognized as a MultiplyOperator and an NCName must 2596 // be recognized as an OperatorName." 2597 2598 // Get some substrings. 2599 $character = substr($xPathQuery, $position - 1, 1); 2600 2601 // Check whether it's a multiply operator or a name test. 2602 if (strchr('/@:([', $character) != FALSE) { 2603 // Don't use the operator. 2604 $position = -1; 2605 continue; 2606 } else { 2607 // The operator is good. Lets use it. 2608 break; 2609 } 2610 } 2611 2612 // Extremely annoyingly, we could have a node name like "for-each" and we should not 2613 // parse this as a "-" operator. So if the first char of the right operator is alphabetic, 2614 // then this is NOT an interger operator. 2615 if (strchr('-+*', $operator) != FALSE) { 2616 $rightOperand = trim(substr($xPathQuery, $position + strlen($operator))); 2617 if (strlen($rightOperand) > 1) { 2618 if (preg_match(':^\D$:', $rightOperand[0])) { 2619 // Don't use the operator. 2620 $position = -1; 2621 continue; 2622 } else { 2623 // The operator is good. Lets use it. 2624 break; 2625 } 2626 } 2627 } 2628 2629 // The operator must be good then :o) 2630 break; 2631 2632 } // end while each($this->operators) 2633 2634 // Did we find an operator? 2635 if ($position == -1) { 2636 $aResultsCache[$xPathQuery] = array(); 2637 return array(); 2638 } 2639 2640 ///////////////////////////////////////////// 2641 // Get the operands 2642 2643 // Get the left and the right part of the expression. 2644 $leftOperand = trim(substr($xPathQuery, 0, $position)); 2645 $rightOperand = trim(substr($xPathQuery, $position + strlen($operator))); 2646 2647 // Remove whitespaces. 2648 $leftOperand = trim($leftOperand); 2649 $rightOperand = trim($rightOperand); 2650 2651 ///////////////////////////////////////////// 2652 // Check the operands. 2653 2654 if ($leftOperand == '') { 2655 $aResultsCache[$xPathQuery] = FALSE; 2656 return FALSE; 2657 } 2658 2659 if ($rightOperand == '') { 2660 $aResultsCache[$xPathQuery] = FALSE; 2661 return FALSE; 2662 } 2663 2664 // Package up and return what we found. 2665 $aResult = array('operator' => $operator, 2666 'left operand' => $leftOperand, 2667 'right operand' => $rightOperand); 2668 2669 $aResultsCache[$xPathQuery] = $aResult; 2670 2671 return $aResult; 2672 } 2673 2674 /** 2675 * Evaluates an XPath PrimaryExpr 2676 * 2677 * http://www.w3.org/TR/xpath#section-Basics 2678 * 2679 * [15] PrimaryExpr ::= VariableReference 2680 * | '(' Expr ')' 2681 * | Literal 2682 * | Number 2683 * | FunctionCall 2684 * 2685 * @param $xPathQuery (string) XPath query to be evaluated. 2686 * @param $context (array) The context from which to evaluate 2687 * @param $results (mixed) If the expression could be parsed and evaluated as one of these 2688 * syntactical elements, then this will be either: 2689 * - node-set (an ordered collection of nodes without duplicates) 2690 * - boolean (true or false) 2691 * - number (a floating-point number) 2692 * - string (a sequence of UCS characters) 2693 * @return (string) An empty string if the query was successfully parsed and 2694 * evaluated, else a string containing the reason for failing. 2695 * @see evaluate() 2696 */ 2697 function _evaluatePrimaryExpr($xPathQuery, $context, &$result) { 2698 $ThisFunctionName = '_evaluatePrimaryExpr'; 2699 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 2700 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 2701 if ($bDebugThisFunction) { 2702 echo "Path: $xPathQuery\n"; 2703 echo "Context:"; 2704 $this->_printContext($context); 2705 echo "\n"; 2706 } 2707 2708 // Certain expressions will never be PrimaryExpr, so to speed up processing, cache the 2709 // results we do find from this function. 2710 static $aResultsCache = array(); 2711 2712 // Do while false loop 2713 $error = ""; 2714 // If the result is independant of context, then we can cache the result and speed this function 2715 // up on future calls. 2716 $bCacheableResult = FALSE; 2717 do { 2718 if (isset($aResultsCache[$xPathQuery])) { 2719 $error = $aResultsCache[$xPathQuery]['Error']; 2720 $result = $aResultsCache[$xPathQuery]['Result']; 2721 break; 2722 } 2723 2724 // VariableReference 2725 // ### Not supported. 2726 2727 // Is it a number? 2728 // | Number 2729 if (is_numeric($xPathQuery)) { 2730 $result = doubleval($xPathQuery); 2731 $bCacheableResult = TRUE; 2732 break; 2733 } 2734 2735 // If it starts with $, and the remainder is a number, then it's a string. 2736 // | Literal 2737 $literal = $this->_asLiteral($xPathQuery); 2738 if ($literal !== FALSE) { 2739 $result = $xPathQuery; 2740 $bCacheableResult = TRUE; 2741 break; 2742 } 2743 2744 // Is it a function? 2745 // | FunctionCall 2746 { 2747 // Check whether it's all wrapped in a function. will be like count(.*) where .* is anything 2748 // text() will try to be matched here, so just explicitly ignore it 2749 $regex = ":^([^\(\)\[\]/]*)\s*\((.*)\)$:U"; 2750 if (preg_match($regex, $xPathQuery, $aMatch) && $xPathQuery != "text()") { 2751 $function = $aMatch[1]; 2752 $data = $aMatch[2]; 2753 // It is possible that we will get "a() or b()" which will match as function "a" with 2754 // arguments ") or b(" which is clearly wrong... _bracketsCheck() should catch this. 2755 if ($this->_bracketsCheck($data)) { 2756 if (in_array($function, $this->functions)) { 2757 if ($bDebugThisFunction) echo "XPathExpr: $xPathQuery is a $function() function call:\n"; 2758 $result = $this->_evaluateFunction($function, $data, $context); 2759 break; 2760 } 2761 } 2762 } 2763 } 2764 2765 // Is it a bracketed expression? 2766 // | '(' Expr ')' 2767 // If it is surrounded by () then trim the brackets 2768 $bBrackets = FALSE; 2769 if (preg_match(":^\((.*)\):", $xPathQuery, $aMatches)) { 2770 // Do not keep trimming off the () as we could have "(() and ())" 2771 $bBrackets = TRUE; 2772 $xPathQuery = $aMatches[1]; 2773 } 2774 2775 if ($bBrackets) { 2776 // Must be a Expr then. 2777 $result = $this->_evaluateExpr($xPathQuery, $context); 2778 break; 2779 } 2780 2781 // Can't be a PrimaryExpr then. 2782 $error = "Expression is not a PrimaryExpr"; 2783 $bCacheableResult = TRUE; 2784 } while (FALSE); 2785 ////////////////////////////////////////////// 2786 2787 // If possible, cache the result. 2788 if ($bCacheableResult) { 2789 $aResultsCache[$xPathQuery]['Error'] = $error; 2790 $aResultsCache[$xPathQuery]['Result'] = $result; 2791 } 2792 2793 $this->_closeDebugFunction($ThisFunctionName, array('result' => $result, 'error' => $error), $bDebugThisFunction); 2794 2795 // Return the result. 2796 return $error; 2797 } 2798 2799 /** 2800 * Evaluates an XPath Expr 2801 * 2802 * $this->evaluate() is the entry point and does some inits, while this 2803 * function is called recursive internaly for every sub-xPath expresion we find. 2804 * It handles the following syntax, and calls evaluatePathExpr if it finds that none 2805 * of this grammer applies. 2806 * 2807 * http://www.w3.org/TR/xpath#section-Basics 2808 * 2809 * [14] Expr ::= OrExpr 2810 * [21] OrExpr ::= AndExpr 2811 * | OrExpr 'or' AndExpr 2812 * [22] AndExpr ::= EqualityExpr 2813 * | AndExpr 'and' EqualityExpr 2814 * [23] EqualityExpr ::= RelationalExpr 2815 * | EqualityExpr '=' RelationalExpr 2816 * | EqualityExpr '!=' RelationalExpr 2817 * [24] RelationalExpr ::= AdditiveExpr 2818 * | RelationalExpr '<' AdditiveExpr 2819 * | RelationalExpr '>' AdditiveExpr 2820 * | RelationalExpr '<=' AdditiveExpr 2821 * | RelationalExpr '>=' AdditiveExpr 2822 * [25] AdditiveExpr ::= MultiplicativeExpr 2823 * | AdditiveExpr '+' MultiplicativeExpr 2824 * | AdditiveExpr '-' MultiplicativeExpr 2825 * [26] MultiplicativeExpr ::= UnaryExpr 2826 * | MultiplicativeExpr MultiplyOperator UnaryExpr 2827 * | MultiplicativeExpr 'div' UnaryExpr 2828 * | MultiplicativeExpr 'mod' UnaryExpr 2829 * [27] UnaryExpr ::= UnionExpr 2830 * | '-' UnaryExpr 2831 * [18] UnionExpr ::= PathExpr 2832 * | UnionExpr '|' PathExpr 2833 * 2834 * NOTE: The effect of the above grammar is that the order of precedence is 2835 * (lowest precedence first): 2836 * 1) or 2837 * 2) and 2838 * 3) =, != 2839 * 4) <=, <, >=, > 2840 * 5) +, - 2841 * 6) *, div, mod 2842 * 7) - (negate) 2843 * 8) | 2844 * 2845 * @param $xPathQuery (string) XPath query to be evaluated. 2846 * @param $context (array) An associative array the describes the context from which 2847 * to evaluate the XPath Expr. Contains three members: 2848 * 'nodePath' => The absolute XPath expression to the context node 2849 * 'size' => The context size 2850 * 'pos' => The context position 2851 * @return (mixed) The result of the XPath expression. Either: 2852 * node-set (an ordered collection of nodes without duplicates) 2853 * boolean (true or false) 2854 * number (a floating-point number) 2855 * string (a sequence of UCS characters) 2856 * @see evaluate() 2857 */ 2858 function _evaluateExpr($xPathQuery, $context) { 2859 $ThisFunctionName = '_evaluateExpr'; 2860 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 2861 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 2862 if ($bDebugThisFunction) { 2863 echo "Path: $xPathQuery\n"; 2864 echo "Context:"; 2865 $this->_printContext($context); 2866 echo "\n"; 2867 } 2868 2869 // Numpty check 2870 if (!isset($xPathQuery) || ($xPathQuery == '')) { 2871 $this->_displayError("The \$xPathQuery argument must have a value.", __LINE__, __FILE__); 2872 return FALSE; 2873 } 2874 2875 // At the top level we deal with booleans. Only if the Expr is just an AdditiveExpr will 2876 // the result not be a boolean. 2877 // 2878 // 2879 // Between these syntactical elements we get PathExprs. 2880 2881 // Do while false loop 2882 do { 2883 static $aKnownPathExprCache = array(); 2884 2885 if (isset($aKnownPathExprCache[$xPathQuery])) { 2886 if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n"; 2887 $result = $this->_evaluatePathExpr($xPathQuery, $context); 2888 break; 2889 } 2890 2891 // Check for operators first, as we could have "() op ()" and the PrimaryExpr will try to 2892 // say that that is an Expr called ") op (" 2893 // Set the default position and the type of the operator. 2894 $aOperatorInfo = $this->_GetOperator($xPathQuery); 2895 2896 // An expression can be one of these, and we should catch these "first" as they are most common 2897 if (empty($aOperatorInfo)) { 2898 $error = $this->_evaluatePrimaryExpr($xPathQuery, $context, $result); 2899 if (empty($error)) { 2900 // It could be parsed as a PrimaryExpr, so look no further :o) 2901 break; 2902 } 2903 } 2904 2905 // Check whether an operator was found. 2906 if (empty($aOperatorInfo)) { 2907 if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n"; 2908 $aKnownPathExprCache[$xPathQuery] = TRUE; 2909 // No operator. Means we have a PathExpr then. Go to the next level. 2910 $result = $this->_evaluatePathExpr($xPathQuery, $context); 2911 break; 2912 } 2913 2914 if ($bDebugThisFunction) { echo "\nFound and operator:"; print_r($aOperatorInfo); }//LEFT:[$leftOperand] oper:[$operator] RIGHT:[$rightOperand]"; 2915 2916 $operator = $aOperatorInfo['operator']; 2917 2918 ///////////////////////////////////////////// 2919 // Recursively process the operator 2920 2921 // Check the kind of operator. 2922 switch ($operator) { 2923 case ' or ': 2924 case ' and ': 2925 $operatorType = 'Boolean'; 2926 break; 2927 case '+': 2928 case '-': 2929 case '*': 2930 case ' div ': 2931 case ' mod ': 2932 $operatorType = 'Integer'; 2933 break; 2934 case ' | ': 2935 $operatorType = 'NodeSet'; 2936 break; 2937 case '<=': 2938 case '<': 2939 case '>=': 2940 case '>': 2941 case '=': 2942 case '!=': 2943 $operatorType = 'Multi'; 2944 break; 2945 default: 2946 $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__); 2947 } 2948 2949 if ($bDebugThisFunction) echo "\nOperator is a [$operator]($operatorType operator)"; 2950 2951 ///////////////////////////////////////////// 2952 // Evaluate the operands 2953 2954 // Evaluate the left part. 2955 if ($bDebugThisFunction) echo "\nEvaluating LEFT:[{$aOperatorInfo['left operand']}]\n"; 2956 $left = $this->_evaluateExpr($aOperatorInfo['left operand'], $context); 2957 if ($bDebugThisFunction) {echo "{$aOperatorInfo['left operand']} evals as:\n"; print_r($left); } 2958 2959 // If it is a boolean operator, it's possible we don't need to evaluate the right part. 2960 2961 // Only evaluate the right part if we need to. 2962 $right = ''; 2963 if ($operatorType == 'Boolean') { 2964 // Is the left part false? 2965 $left = $this->_handleFunction_boolean($left, $context); 2966 if (!$left and ($operator == ' and ')) { 2967 $result = FALSE; 2968 break; 2969 } else if ($left and ($operator == ' or ')) { 2970 $result = TRUE; 2971 break; 2972 } 2973 } 2974 2975 // Evaluate the right part 2976 if ($bDebugThisFunction) echo "\nEvaluating RIGHT:[{$aOperatorInfo['right operand']}]\n"; 2977 $right = $this->_evaluateExpr($aOperatorInfo['right operand'], $context); 2978 if ($bDebugThisFunction) {echo "{$aOperatorInfo['right operand']} evals as:\n"; print_r($right); echo "\n";} 2979 2980 ///////////////////////////////////////////// 2981 // Combine the operands 2982 2983 // If necessary, work out how to treat the multi operators 2984 if ($operatorType != 'Multi') { 2985 $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context); 2986 } else { 2987 // http://www.w3.org/TR/xpath#booleans 2988 // If both objects to be compared are node-sets, then the comparison will be true if and 2989 // only if there is a node in the first node-set and a node in the second node-set such 2990 // that the result of performing the comparison on the string-values of the two nodes is 2991 // true. 2992 // 2993 // If one object to be compared is a node-set and the other is a number, then the 2994 // comparison will be true if and only if there is a node in the node-set such that the 2995 // result of performing the comparison on the number to be compared and on the result of 2996 // converting the string-value of that node to a number using the number function is true. 2997 // 2998 // If one object to be compared is a node-set and the other is a string, then the comparison 2999 // will be true if and only if there is a node in the node-set such that the result of performing 3000 // the comparison on the string-value of the node and the other string is true. 3001 // 3002 // If one object to be compared is a node-set and the other is a boolean, then the comparison 3003 // will be true if and only if the result of performing the comparison on the boolean and on 3004 // the result of converting the node-set to a boolean using the boolean function is true. 3005 if (is_array($left) || is_array($right)) { 3006 if ($bDebugThisFunction) echo "As one of the operands is an array, we will need to loop\n"; 3007 if (is_array($left) && is_array($right)) { 3008 $operatorType = 'String'; 3009 } elseif (is_numeric($left) || is_numeric($right)) { 3010 $operatorType = 'Integer'; 3011 } elseif (is_bool($left)) { 3012 $operatorType = 'Boolean'; 3013 $right = $this->_handleFunction_boolean($right, $context); 3014 } elseif (is_bool($right)) { 3015 $operatorType = 'Boolean'; 3016 $left = $this->_handleFunction_boolean($left, $context); 3017 } else { 3018 $operatorType = 'String'; 3019 } 3020 if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n"; 3021 // Turn both operands into arrays to simplify logic 3022 $aLeft = $left; 3023 $aRight = $right; 3024 if (!is_array($aLeft)) $aLeft = array($aLeft); 3025 if (!is_array($aRight)) $aRight = array($aRight); 3026 $result = FALSE; 3027 if (!empty($aLeft)) { 3028 foreach ($aLeft as $leftItem) { 3029 if (empty($aRight)) break; 3030 // If the item is from a node set, we should evaluate it's string-value 3031 if (is_array($left)) { 3032 if ($bDebugThisFunction) echo "\tObtaining string-value of LHS:$leftItem as it's from a nodeset\n"; 3033 $leftItem = $this->_stringValue($leftItem); 3034 } 3035 foreach ($aRight as $rightItem) { 3036 // If the item is from a node set, we should evaluate it's string-value 3037 if (is_array($right)) { 3038 if ($bDebugThisFunction) echo "\tObtaining string-value of RHS:$rightItem as it's from a nodeset\n"; 3039 $rightItem = $this->_stringValue($rightItem); 3040 } 3041 3042 if ($bDebugThisFunction) echo "\tEvaluating $leftItem $operator $rightItem\n"; 3043 $result = $this->_evaluateOperator($leftItem, $operator, $rightItem, $operatorType, $context); 3044 if ($result === TRUE) break; 3045 } 3046 if ($result === TRUE) break; 3047 } 3048 } 3049 } 3050 // When neither object to be compared is a node-set and the operator is = or !=, then the 3051 // objects are compared by converting them to a common type as follows and then comparing 3052 // them. 3053 // 3054 // If at least one object to be compared is a boolean, then each object to be compared 3055 // is converted to a boolean as if by applying the boolean function. 3056 // 3057 // Otherwise, if at least one object to be compared is a number, then each object to be 3058 // compared is converted to a number as if by applying the number function. 3059 // 3060 // Otherwise, both objects to be compared are converted to strings as if by applying 3061 // the string function. 3062 // 3063 // The = comparison will be true if and only if the objects are equal; the != comparison 3064 // will be true if and only if the objects are not equal. Numbers are compared for equality 3065 // according to IEEE 754 [IEEE 754]. Two booleans are equal if either both are true or 3066 // both are false. Two strings are equal if and only if they consist of the same sequence 3067 // of UCS characters. 3068 else { 3069 if (is_bool($left) || is_bool($right)) { 3070 $operatorType = 'Boolean'; 3071 } elseif (is_numeric($left) || is_numeric($right)) { 3072 $operatorType = 'Integer'; 3073 } else { 3074 $operatorType = 'String'; 3075 } 3076 if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n"; 3077 $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context); 3078 } 3079 } 3080 3081 } while (FALSE); 3082 ////////////////////////////////////////////// 3083 3084 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3085 3086 // Return the result. 3087 return $result; 3088 } 3089 3090 /** 3091 * Evaluate the result of an operator whose operands have been evaluated 3092 * 3093 * If the operator type is not "NodeSet", then neither the left or right operators 3094 * will be node sets, as the processing when one or other is an array is complex, 3095 * and should be handled by the caller. 3096 * 3097 * @param $left (mixed) The left operand 3098 * @param $right (mixed) The right operand 3099 * @param $operator (string) The operator to use to combine the operands 3100 * @param $operatorType (string) The type of the operator. Either 'Boolean', 3101 * 'Integer', 'String', or 'NodeSet' 3102 * @param $context (array) The context from which to evaluate 3103 * @return (mixed) The result of the XPath expression. Either: 3104 * node-set (an ordered collection of nodes without duplicates) 3105 * boolean (true or false) 3106 * number (a floating-point number) 3107 * string (a sequence of UCS characters) 3108 */ 3109 function _evaluateOperator($left, $operator, $right, $operatorType, $context) { 3110 $ThisFunctionName = '_evaluateOperator'; 3111 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3112 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3113 if ($bDebugThisFunction) { 3114 echo "left: $left\n"; 3115 echo "right: $right\n"; 3116 echo "operator: $operator\n"; 3117 echo "operator type: $operatorType\n"; 3118 } 3119 3120 // Do while false loop 3121 do { 3122 // Handle the operator depending on the operator type. 3123 switch ($operatorType) { 3124 case 'Boolean': 3125 { 3126 // Boolify the arguments. (The left arg is already a bool) 3127 $right = $this->_handleFunction_boolean($right, $context); 3128 switch ($operator) { 3129 case '=': // Compare the two results. 3130 $result = (bool)($left == $right); 3131 break; 3132 case ' or ': // Return the two results connected by an 'or'. 3133 $result = (bool)( $left or $right ); 3134 break; 3135 case ' and ': // Return the two results connected by an 'and'. 3136 $result = (bool)( $left and $right ); 3137 break; 3138 case '!=': // Check whether the two results are not equal. 3139 $result = (bool)( $left != $right ); 3140 break; 3141 default: 3142 $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__); 3143 } 3144 } 3145 break; 3146 case 'Integer': 3147 { 3148 // Convert both left and right operands into numbers. 3149 if (empty($left) && ($operator == '-')) { 3150 // There may be no left operator if the op is '-' 3151 $left = 0; 3152 } else { 3153 $left = $this->_handleFunction_number($left, $context); 3154 } 3155 $right = $this->_handleFunction_number($right, $context); 3156 if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n"; 3157 switch ($operator) { 3158 case '=': // Compare the two results. 3159 $result = (bool)($left == $right); 3160 break; 3161 case '!=': // Compare the two results. 3162 $result = (bool)($left != $right); 3163 break; 3164 case '+': // Return the result by adding one result to the other. 3165 $result = $left + $right; 3166 break; 3167 case '-': // Return the result by decrease one result by the other. 3168 $result = $left - $right; 3169 break; 3170 case '*': // Return a multiplication of the two results. 3171 $result = $left * $right; 3172 break; 3173 case ' div ': // Return a division of the two results. 3174 $result = $left / $right; 3175 break; 3176 case ' mod ': // Return a modulo division of the two results. 3177 $result = $left % $right; 3178 break; 3179 case '<=': // Compare the two results. 3180 $result = (bool)( $left <= $right ); 3181 break; 3182 case '<': // Compare the two results. 3183 $result = (bool)( $left < $right ); 3184 break; 3185 case '>=': // Compare the two results. 3186 $result = (bool)( $left >= $right ); 3187 break; 3188 case '>': // Compare the two results. 3189 $result = (bool)( $left > $right ); 3190 break; 3191 default: 3192 $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__); 3193 } 3194 } 3195 break; 3196 case 'NodeSet': 3197 // Add the nodes to the result set 3198 $result = array_merge($left, $right); 3199 // Remove duplicated nodes. 3200 $result = array_unique($result); 3201 3202 // Preserve doc order if there was more than one query. 3203 if (count($result) > 1) { 3204 $result = $this->_sortByDocOrder($result); 3205 } 3206 break; 3207 case 'String': 3208 $left = $this->_handleFunction_string($left, $context); 3209 $right = $this->_handleFunction_string($right, $context); 3210 if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n"; 3211 switch ($operator) { 3212 case '=': // Compare the two results. 3213 $result = (bool)($left == $right); 3214 break; 3215 case '!=': // Compare the two results. 3216 $result = (bool)($left != $right); 3217 break; 3218 default: 3219 $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__); 3220 } 3221 break; 3222 default: 3223 $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__); 3224 } 3225 } while (FALSE); 3226 3227 ////////////////////////////////////////////// 3228 3229 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3230 3231 // Return the result. 3232 return $result; 3233 } 3234 3235 /** 3236 * Evaluates an XPath PathExpr 3237 * 3238 * It handles the following syntax: 3239 * 3240 * http://www.w3.org/TR/xpath#node-sets 3241 * http://www.w3.org/TR/xpath#NT-LocationPath 3242 * http://www.w3.org/TR/xpath#path-abbrev 3243 * http://www.w3.org/TR/xpath#NT-Step 3244 * 3245 * [19] PathExpr ::= LocationPath 3246 * | FilterExpr 3247 * | FilterExpr '/' RelativeLocationPath 3248 * | FilterExpr '//' RelativeLocationPath 3249 * [20] FilterExpr ::= PrimaryExpr 3250 * | FilterExpr Predicate 3251 * [1] LocationPath ::= RelativeLocationPath 3252 * | AbsoluteLocationPath 3253 * [2] AbsoluteLocationPath ::= '/' RelativeLocationPath? 3254 * | AbbreviatedAbsoluteLocationPath 3255 * [3] RelativeLocationPath ::= Step 3256 * | RelativeLocationPath '/' Step 3257 * | AbbreviatedRelativeLocationPath 3258 * [4] Step ::= AxisSpecifier NodeTest Predicate* 3259 * | AbbreviatedStep 3260 * [5] AxisSpecifier ::= AxisName '::' 3261 * | AbbreviatedAxisSpecifier 3262 * [10] AbbreviatedAbsoluteLocationPath 3263 * ::= '//' RelativeLocationPath 3264 * [11] AbbreviatedRelativeLocationPath 3265 * ::= RelativeLocationPath '//' Step 3266 * [12] AbbreviatedStep ::= '.' 3267 * | '..' 3268 * [13] AbbreviatedAxisSpecifier 3269 * ::= '@'? 3270 * 3271 * If you expand all the abbreviated versions, then the grammer simplifies to: 3272 * 3273 * [19] PathExpr ::= RelativeLocationPath 3274 * | '/' RelativeLocationPath? 3275 * | FilterExpr 3276 * | FilterExpr '/' RelativeLocationPath 3277 * [20] FilterExpr ::= PrimaryExpr 3278 * | FilterExpr Predicate 3279 * [3] RelativeLocationPath ::= Step 3280 * | RelativeLocationPath '/' Step 3281 * [4] Step ::= AxisName '::' NodeTest Predicate* 3282 * 3283 * Conceptually you can say that we should split by '/' and try to treat the parts 3284 * as steps, and if that fails then try to treat it as a PrimaryExpr. 3285 * 3286 * @param $PathExpr (string) PathExpr syntactical element 3287 * @param $context (array) The context from which to evaluate 3288 * @return (mixed) The result of the XPath expression. Either: 3289 * node-set (an ordered collection of nodes without duplicates) 3290 * boolean (true or false) 3291 * number (a floating-point number) 3292 * string (a sequence of UCS characters) 3293 * @see evaluate() 3294 */ 3295 function _evaluatePathExpr($PathExpr, $context) { 3296 $ThisFunctionName = '_evaluatePathExpr'; 3297 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3298 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3299 if ($bDebugThisFunction) { 3300 echo "PathExpr: $PathExpr\n"; 3301 echo "Context:"; 3302 $this->_printContext($context); 3303 echo "\n"; 3304 } 3305 3306 // Numpty check 3307 if (empty($PathExpr)) { 3308 $this->_displayError("The \$PathExpr argument must have a value.", __LINE__, __FILE__); 3309 return FALSE; 3310 } 3311 ////////////////////////////////////////////// 3312 3313 // Parsing the expression into steps is a cachable operation as it doesn't depend on the context 3314 static $aResultsCache = array(); 3315 3316 if (isset($aResultsCache[$PathExpr])) { 3317 $steps = $aResultsCache[$PathExpr]; 3318 } else { 3319 // Note that we have used $this->slashes2descendant to simplify this logic, so the 3320 // "Abbreviated" paths basically never exist as '//' never exists. 3321 3322 // mini syntax check 3323 if (!$this->_bracketsCheck($PathExpr)) { 3324 $this->_displayError('While parsing an XPath query, in the PathExpr "' . 3325 $PathExpr. 3326 '", there was an invalid number of brackets or a bracket mismatch.', __LINE__, __FILE__); 3327 } 3328 // Save the current path. 3329 $this->currentXpathQuery = $PathExpr; 3330 // Split the path at every slash *outside* a bracket. 3331 $steps = $this->_bracketExplode('/', $PathExpr); 3332 if ($bDebugThisFunction) { echo "<hr>Split the path '$PathExpr' at every slash *outside* a bracket.\n "; print_r($steps); } 3333 // Check whether the first element is empty. 3334 if (empty($steps[0])) { 3335 // Remove the first and empty element. It's a starting '//'. 3336 array_shift($steps); 3337 } 3338 $aResultsCache[$PathExpr] = $steps; 3339 } 3340 3341 // Start to evaluate the steps. 3342 // ### Consider implementing an evaluateSteps() function that removes recursion from 3343 // evaluateStep() 3344 $result = $this->_evaluateStep($steps, $context); 3345 3346 // Preserve doc order if there was more than one result 3347 if (count($result) > 1) { 3348 $result = $this->_sortByDocOrder($result); 3349 } 3350 ////////////////////////////////////////////// 3351 3352 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3353 3354 // Return the result. 3355 return $result; 3356 } 3357 3358 /** 3359 * Sort an xPathSet by doc order. 3360 * 3361 * @param $xPathSet (array) Array of full paths to nodes that need to be sorted 3362 * @return (array) Array containing the same contents as $xPathSet, but 3363 * with the contents in doc order 3364 */ 3365 function _sortByDocOrder($xPathSet) { 3366 $ThisFunctionName = '_sortByDocOrder'; 3367 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3368 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3369 if ($bDebugThisFunction) { 3370 echo "_sortByDocOrder(xPathSet:[".count($xPathSet)."])"; 3371 echo "xPathSet:\n"; 3372 print_r($xPathSet); 3373 echo "<hr>\n"; 3374 } 3375 ////////////////////////////////////////////// 3376 3377 $aResult = array(); 3378 3379 // Spot some common shortcuts. 3380 if (count($xPathSet) < 1) { 3381 $aResult = $xPathSet; 3382 } else { 3383 // Build an array of doc-pos indexes. 3384 $aDocPos = array(); 3385 $nodeCount = count($this->nodeIndex); 3386 $aPaths = array_keys($this->nodeIndex); 3387 if ($bDebugThisFunction) { 3388 echo "searching for path indices in array_keys(this->nodeIndex)...\n"; 3389 //print_r($aPaths); 3390 } 3391 3392 // The last index we found. In general the elements will be in groups 3393 // that are themselves in order. 3394 $iLastIndex = 0; 3395 foreach ($xPathSet as $path) { 3396 // Cycle round the nodes, starting at the last index, looking for the path. 3397 $foundNode = FALSE; 3398 for ($iIndex = $iLastIndex; $iIndex < $nodeCount + $iLastIndex; $iIndex++) { 3399 $iThisIndex = $iIndex % $nodeCount; 3400 if (!strcmp($aPaths[$iThisIndex],$path)) { 3401 // we have found the doc-position index of the path 3402 $aDocPos[] = $iThisIndex; 3403 $iLastIndex = $iThisIndex; 3404 $foundNode = TRUE; 3405 break; 3406 } 3407 } 3408 if ($bDebugThisFunction) { 3409 if (!$foundNode) 3410 echo "Error: $path not found in \$this->nodeIndex\n"; 3411 else 3412 echo "Found node after ".($iIndex - $iLastIndex)." iterations\n"; 3413 } 3414 } 3415 // Now count the number of doc pos we have and the number of results and 3416 // confirm that we have the same number of each. 3417 $iDocPosCount = count($aDocPos); 3418 $iResultCount = count($xPathSet); 3419 if ($iDocPosCount != $iResultCount) { 3420 if ($bDebugThisFunction) { 3421 echo "count(\$aDocPos)=$iDocPosCount; count(\$result)=$iResultCount\n"; 3422 print_r(array_keys($this->nodeIndex)); 3423 } 3424 $this->_displayError('Results from _InternalEvaluate() are corrupt. '. 3425 'Do you need to call reindexNodeTree()?', __LINE__, __FILE__); 3426 } 3427 3428 // Now sort the indexes. 3429 sort($aDocPos); 3430 3431 // And now convert back to paths. 3432 $iPathCount = count($aDocPos); 3433 for ($iIndex = 0; $iIndex < $iPathCount; $iIndex++) { 3434 $aResult[] = $aPaths[$aDocPos[$iIndex]]; 3435 } 3436 } 3437 3438 // Our result from the function is this array. 3439 $result = $aResult; 3440 3441 ////////////////////////////////////////////// 3442 3443 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3444 3445 // Return the result. 3446 return $result; 3447 } 3448 3449 /** 3450 * Evaluate a step from a XPathQuery expression at a specific contextPath. 3451 * 3452 * Steps are the arguments of a XPathQuery when divided by a '/'. A contextPath is a 3453 * absolute XPath (or vector of XPaths) to a starting node(s) from which the step should 3454 * be evaluated. 3455 * 3456 * @param $steps (array) Vector containing the remaining steps of the current 3457 * XPathQuery expression. 3458 * @param $context (array) The context from which to evaluate 3459 * @return (array) Vector of absolute XPath's as a result of the step 3460 * evaluation. The results will not necessarily be in doc order 3461 * @see _evaluatePathExpr() 3462 */ 3463 function _evaluateStep($steps, $context) { 3464 $ThisFunctionName = '_evaluateStep'; 3465 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3466 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3467 if ($bDebugThisFunction) { 3468 echo "Context:"; 3469 $this->_printContext($context); 3470 echo "\n"; 3471 echo "Steps: "; 3472 print_r($steps); 3473 echo "<hr>\n"; 3474 } 3475 ////////////////////////////////////////////// 3476 3477 $result = array(); // Create an empty array for saving the abs. XPath's found. 3478 3479 $contextPaths = array(); // Create an array to save the new contexts. 3480 $step = trim(array_shift($steps)); // Get this step. 3481 if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n"; 3482 3483 $axis = $this->_getAxis($step); // Get the axis of the current step. 3484 3485 // If there was no axis, then it must be a PrimaryExpr 3486 if ($axis == FALSE) { 3487 if ($bDebugThisFunction) echo __LINE__.":Step is not an axis but a PrimaryExpr\n"; 3488 // ### This isn't correct, as the result of this function might not be a node set. 3489 $error = $this->_evaluatePrimaryExpr($step, $context, $contextPaths); 3490 if (!empty($error)) { 3491 $this->_displayError("Expression failed to parse as PrimaryExpr because: $error" 3492 , __LINE__, __FILE__, FALSE); 3493 } 3494 } else { 3495 if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; print_r($axis); echo "\n";} 3496 $method = '_handleAxis_' . $axis['axis']; // Create the name of the method. 3497 3498 // Check whether the axis handler is defined. If not display an error message. 3499 if (!method_exists($this, $method)) { 3500 $this->_displayError('While parsing an XPath query, the axis ' . 3501 $axis['axis'] . ' could not be handled, because this version does not support this axis.', __LINE__, __FILE__); 3502 } 3503 if ($bDebugThisFunction) echo __LINE__.":Calling user method $method\n"; 3504 3505 // Perform an axis action. 3506 $contextPaths = $this->$method($axis, $context['nodePath']); 3507 if ($bDebugThisFunction) { echo __LINE__.":We found these contexts from this step:\n"; print_r( $contextPaths ); echo "\n";} 3508 } 3509 3510 // Check whether there are predicates. 3511 if (count($contextPaths) > 0 && count($axis['predicate']) > 0) { 3512 if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by predicate...\n"; 3513 3514 // Check whether each node fits the predicates. 3515 $contextPaths = $this->_checkPredicates($contextPaths, $axis['predicate']); 3516 } 3517 3518 // Check whether there are more steps left. 3519 if (count($steps) > 0) { 3520 if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given the context of the first step...\n"; 3521 3522 // Continue the evaluation of the next steps. 3523 3524 // Run through the array. 3525 $size = sizeOf($contextPaths); 3526 for ($pos=0; $pos<$size; $pos++) { 3527 // Build new context 3528 $newContext = array('nodePath' => $contextPaths[$pos], 'size' => $size, 'pos' => $pos + 1); 3529 if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the {$contextPaths[$pos]} context...\n"; 3530 // Call this method for this single path. 3531 $xPathSetNew = $this->_evaluateStep($steps, $newContext); 3532 if ($bDebugThisFunction) {echo "New results for this context:\n"; print_r($xPathSetNew);} 3533 $result = array_merge($result, $xPathSetNew); 3534 } 3535 3536 // Remove duplicated nodes. 3537 $result = array_unique($result); 3538 } else { 3539 $result = $contextPaths; // Save the found contexts. 3540 } 3541 3542 ////////////////////////////////////////////// 3543 3544 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3545 3546 // Return the result. 3547 return $result; 3548 } 3549 3550 /** 3551 * Checks whether a node matches predicates. 3552 * 3553 * This method checks whether a list of nodes passed to this method match 3554 * a given list of predicates. 3555 * 3556 * @param $xPathSet (array) Array of full paths of all nodes to be tested. 3557 * @param $predicates (array) Array of predicates to use. 3558 * @return (array) Vector of absolute XPath's that match the given predicates. 3559 * @see _evaluateStep() 3560 */ 3561 function _checkPredicates($xPathSet, $predicates) { 3562 $ThisFunctionName = '_checkPredicates'; 3563 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3564 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3565 if ($bDebugThisFunction) { 3566 echo "XPathSet:"; 3567 print_r($xPathSet); 3568 echo "Predicates:"; 3569 print_r($predicates); 3570 echo "<hr>"; 3571 } 3572 ////////////////////////////////////////////// 3573 // Create an empty set of nodes. 3574 $result = array(); 3575 3576 // Run through all predicates. 3577 $pSize = sizeOf($predicates); 3578 for ($j=0; $j<$pSize; $j++) { 3579 $predicate = $predicates[$j]; 3580 if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n"; 3581 3582 // This will contain all the nodes that match this predicate 3583 $aNewSet = array(); 3584 3585 // Run through all nodes. 3586 $contextSize = count($xPathSet); 3587 for ($contextPos=0; $contextPos<$contextSize; $contextPos++) { 3588 $xPath = $xPathSet[$contextPos]; 3589 3590 // Build the context for this predicate 3591 $context = array('nodePath' => $xPath, 'size' => $contextSize, 'pos' => $contextPos + 1); 3592 3593 // Check whether the predicate is just an number. 3594 if (preg_match('/^\d+$/', $predicate)) { 3595 if ($bDebugThisFunction) echo "Taking short cut and calling _handleFunction_position() directly.\n"; 3596 // Take a short cut. If it is just a position, then call 3597 // _handleFunction_position() directly. 70% of the 3598 // time this will be the case. ## N.S 3599 // $check = (bool) ($predicate == $context['pos']); 3600 $check = (bool) ($predicate == $this->_handleFunction_position('', $context)); 3601 } else { 3602 // Else do the predicate check the long and through way. 3603 $check = $this->_evaluateExpr($predicate, $context); 3604 } 3605 if ($bDebugThisFunction) { 3606 echo "Evaluating the predicate returned "; 3607 var_dump($check); 3608 echo "\n"; 3609 } 3610 3611 if (is_int($check)) { // Check whether it's an integer. 3612 // Check whether it's the current position. 3613 $check = (bool) ($check == $this->_handleFunction_position('', $context)); 3614 } else { 3615 $check = (bool) ($this->_handleFunction_boolean($check, $context)); 3616 // if ($bDebugThisFunction) {echo $this->_handleFunction_string($check, $context);} 3617 } 3618 3619 if ($bDebugThisFunction) echo "Node $xPath matches predicate $predicate: " . (($check) ? "TRUE" : "FALSE") ."\n"; 3620 3621 // Do we add it? 3622 if ($check) $aNewSet[] = $xPath; 3623 } 3624 3625 // Use the newly filtered list. 3626 $xPathSet = $aNewSet; 3627 3628 if ($bDebugThisFunction) {echo "Node set now contains : "; print_r($xPathSet); } 3629 } 3630 3631 $result = $xPathSet; 3632 3633 ////////////////////////////////////////////// 3634 3635 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3636 3637 // Return the array of nodes. 3638 return $result; 3639 } 3640 3641 /** 3642 * Evaluates an XPath function 3643 * 3644 * This method evaluates a given XPath function with its arguments on a 3645 * specific node of the document. 3646 * 3647 * @param $function (string) Name of the function to be evaluated. 3648 * @param $arguments (string) String containing the arguments being 3649 * passed to the function. 3650 * @param $context (array) The context from which to evaluate 3651 * @return (mixed) This method returns the result of the evaluation of 3652 * the function. Depending on the function the type of the 3653 * return value can be different. 3654 * @see evaluate() 3655 */ 3656 function _evaluateFunction($function, $arguments, $context) { 3657 $ThisFunctionName = '_evaluateFunction'; 3658 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 3659 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 3660 if ($bDebugThisFunction) { 3661 if (is_array($arguments)) { 3662 echo "Arguments:\n"; 3663 print_r($arguments); 3664 } else { 3665 echo "Arguments: $arguments\n"; 3666 } 3667 echo "Context:"; 3668 $this->_printContext($context); 3669 echo "\n"; 3670 echo "<hr>\n"; 3671 } 3672 ///////////////////////////////////// 3673 // Remove whitespaces. 3674 $function = trim($function); 3675 $arguments = trim($arguments); 3676 // Create the name of the function handling function. 3677 $method = '_handleFunction_'. $function; 3678 3679 // Check whether the function handling function is available. 3680 if (!method_exists($this, $method)) { 3681 // Display an error message. 3682 $this->_displayError("While parsing an XPath query, ". 3683 "the function \"$function\" could not be handled, because this ". 3684 "version does not support this function.", __LINE__, __FILE__); 3685 } 3686 if ($bDebugThisFunction) echo "Calling function $method($arguments)\n"; 3687 3688 // Return the result of the function. 3689 $result = $this->$method($arguments, $context); 3690 3691 ////////////////////////////////////////////// 3692 // Return the nodes found. 3693 3694 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 3695 3696 // Return the result. 3697 return $result; 3698 } 3699 3700 /** 3701 * Checks whether a node matches a node-test. 3702 * 3703 * This method checks whether a node in the document matches a given node-test. 3704 * A node test is something like text(), node(), or an element name. 3705 * 3706 * @param $contextPath (string) Full xpath of the node, which should be tested for 3707 * matching the node-test. 3708 * @param $nodeTest (string) String containing the node-test for the node. 3709 * @return (boolean) This method returns TRUE if the node matches the 3710 * node-test, otherwise FALSE. 3711 * @see evaluate() 3712 */ 3713 function _checkNodeTest($contextPath, $nodeTest) { 3714 // Empty node test means that it must match 3715 if (empty($nodeTest)) return TRUE; 3716 3717 if ($nodeTest == '*') { 3718 // * matches all element nodes. 3719 return (!preg_match(':/[^/]+\(\)\[\d+\]$:U', $contextPath)); 3720 } 3721 elseif (preg_match('/^[\w-:\.]+$/', $nodeTest)) { 3722 // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name 3723 // The real spec for what constitutes whitespace is quite elaborate, and 3724 // we currently just hope that "\w" catches them all. In reality it should 3725 // start with a letter too, not a number, but we've just left it simple. 3726 // It's just a node name test. It should end with "/$nodeTest[x]" 3727 return (preg_match('"/'.$nodeTest.'\[\d+\]$"', $contextPath)); 3728 } 3729 elseif (preg_match('/\(/U', $nodeTest)) { // Check whether it's a function. 3730 // Get the type of function to use. 3731 $function = $this->_prestr($nodeTest, '('); 3732 // Check whether the node fits the method. 3733 switch ($function) { 3734 case 'node': // Add this node to the list of nodes. 3735 return TRUE; 3736 case 'text': // Check whether the node has some text. 3737 $tmp = implode('', $this->nodeIndex[$contextPath]['textParts']); 3738 if (!empty($tmp)) { 3739 return TRUE; // Add this node to the list of nodes. 3740 } 3741 break; 3742 /******** NOT supported (yet?) 3743 case 'comment': // Check whether the node has some comment. 3744 if (!empty($this->nodeIndex[$contextPath]['comment'])) { 3745 return TRUE; // Add this node to the list of nodes. 3746 } 3747 break; 3748 case 'processing-instruction': 3749 $literal = $this->_afterstr($axis['node-test'], '('); // Get the literal argument. 3750 $literal = substr($literal, 0, strlen($literal) - 1); // Cut the literal. 3751 3752 // Check whether a literal was given. 3753 if (!empty($literal)) { 3754 // Check whether the node's processing instructions are matching the literals given. 3755 if ($this->nodeIndex[$context]['processing-instructions'] == $literal) { 3756 return TRUE; // Add this node to the node-set. 3757 } 3758 } else { 3759 // Check whether the node has processing instructions. 3760 if (!empty($this->nodeIndex[$contextPath]['processing-instructions'])) { 3761 return TRUE; // Add this node to the node-set. 3762 } 3763 } 3764 break; 3765 ***********/ 3766 default: // Display an error message. 3767 $this->_displayError('While parsing an XPath query there was an undefined function called "' . 3768 str_replace($function, '<b>'.$function.'</b>', $this->currentXpathQuery) .'"', __LINE__, __FILE__); 3769 } 3770 } 3771 else { // Display an error message. 3772 $this->_displayError("While parsing the XPath query \"{$this->currentXpathQuery}\" ". 3773 "an empty and therefore invalid node-test has been found.", __LINE__, __FILE__, FALSE); 3774 } 3775 3776 return FALSE; // Don't add this context. 3777 } 3778 3779 //----------------------------------------------------------------------------------------- 3780 // XPath ------ XPath AXIS Handlers ------ 3781 //----------------------------------------------------------------------------------------- 3782 3783 /** 3784 * Retrieves axis information from an XPath query step. 3785 * 3786 * This method tries to extract the name of the axis and its node-test 3787 * from a given step of an XPath query at a given node. If it can't parse 3788 * the step, then we treat it as a PrimaryExpr. 3789 * 3790 * [4] Step ::= AxisSpecifier NodeTest Predicate* 3791 * | AbbreviatedStep 3792 * [5] AxisSpecifier ::= AxisName '::' 3793 * | AbbreviatedAxisSpecifier 3794 * [12] AbbreviatedStep ::= '.' 3795 * | '..' 3796 * [13] AbbreviatedAxisSpecifier 3797 * ::= '@'? 3798 * 3799 * [7] NodeTest ::= NameTest 3800 * | NodeType '(' ')' 3801 * | 'processing-instruction' '(' Literal ')' 3802 * [37] NameTest ::= '*' 3803 * | NCName ':' '*' 3804 * | QName 3805 * [38] NodeType ::= 'comment' 3806 * | 'text' 3807 * | 'processing-instruction' 3808 * | 'node' 3809 * 3810 * @param $step (string) String containing a step of an XPath query. 3811 * @return (array) Contains information about the axis found in the step, or FALSE 3812 * if the string isn't a valid step. 3813 * @see _evaluateStep() 3814 */ 3815 function _getAxis($step) { 3816 // The results of this function are very cachable, as it is completely independant of context. 3817 static $aResultsCache = array(); 3818 3819 // Create an array to save the axis information. 3820 $axis = array( 3821 'axis' => '', 3822 'node-test' => '', 3823 'predicate' => array() 3824 ); 3825 3826 $cacheKey = $step; 3827 do { // parse block 3828 $parseBlock = 1; 3829 3830 if (isset($aResultsCache[$cacheKey])) { 3831 return $aResultsCache[$cacheKey]; 3832 } else { 3833 // We have some danger of causing recursion here if we refuse to parse a step as having an 3834 // axis, and demand it be treated as a PrimaryExpr. So if we are going to fail, make sure 3835 // we record what we tried, so that we can catch to see if it comes straight back. 3836 $guess = array( 3837 'axis' => 'child', 3838 'node-test' => $step, 3839 'predicate' => array()); 3840 $aResultsCache[$cacheKey] = $guess; 3841 } 3842 3843 /////////////////////////////////////////////////// 3844 // Spot the steps that won't come with an axis 3845 3846 // Check whether the step is empty or only self. 3847 if (empty($step) OR ($step == '.') OR ($step == 'current()')) { 3848 // Set it to the default value. 3849 $step = '.'; 3850 $axis['axis'] = 'self'; 3851 $axis['node-test'] = '*'; 3852 break $parseBlock; 3853 } 3854 3855 if ($step == '..') { 3856 // Select the parent axis. 3857 $axis['axis'] = 'parent'; 3858 $axis['node-test'] = '*'; 3859 break $parseBlock; 3860 } 3861 3862 /////////////////////////////////////////////////// 3863 // Pull off the predicates 3864 3865 // Check whether there are predicates and add the predicate to the list 3866 // of predicates without []. Get contents of every [] found. 3867 $groups = $this->_getEndGroups($step); 3868 //print_r($groups); 3869 $groupCount = count($groups); 3870 while (($groupCount > 0) && ($groups[$groupCount - 1][0] == '[')) { 3871 // Remove the [] and add the predicate to the top of the list 3872 $predicate = substr($groups[$groupCount - 1], 1, -1); 3873 array_unshift($axis['predicate'], $predicate); 3874 // Pop a group off the end of the list 3875 array_pop($groups); 3876 $groupCount--; 3877 } 3878 3879 // Finally stick the rest back together and this is the rest of our step 3880 if ($groupCount > 0) { 3881 $step = implode('', $groups); 3882 } 3883 3884 /////////////////////////////////////////////////// 3885 // Pull off the axis 3886 3887 // Check for abbreviated syntax 3888 if ($step[0] == '@') { 3889 // Use the attribute axis and select the attribute. 3890 $axis['axis'] = 'attribute'; 3891 $step = substr($step, 1); 3892 } else { 3893 // Check whether the axis is given in plain text. 3894 if (preg_match("/^([^:]*)::(.*)$/", $step, $match)) { 3895 // Split the step to extract axis and node-test. 3896 $axis['axis'] = $match[1]; 3897 $step = $match[2]; 3898 } else { 3899 // The default axis is child 3900 $axis['axis'] = 'child'; 3901 } 3902 } 3903 3904 /////////////////////////////////////////////////// 3905 // Process the rest which will either a node test, or else this isn't a step. 3906 3907 // Check whether is an abbreviated syntax. 3908 if ($step == '*') { 3909 // Use the child axis and select all children. 3910 $axis['node-test'] = '*'; 3911 break $parseBlock; 3912 } 3913 3914 // ### I'm pretty sure our current handling of cdata is a fudge, and we should 3915 // really do this better, but leave this as is for now. 3916 if ($step == "text()") { 3917 // Handle the text node 3918 $axis["node-test"] = "cdata"; 3919 break $parseBlock; 3920 } 3921 3922 // There are a few node tests that we match verbatim. 3923 if ($step == "node()" 3924 || $step == "comment()" 3925 || $step == "text()" 3926 || $step == "processing-instruction") { 3927 $axis["node-test"] = $step; 3928 break $parseBlock; 3929 } 3930 3931 // processing-instruction() is allowed to take an argument, but if it does, the argument 3932 // is a literal, which we will have parsed out to $[number]. 3933 if (preg_match(":processing-instruction\(\$\d*\):", $step)) { 3934 $axis["node-test"] = $step; 3935 break $parseBlock; 3936 } 3937 3938 // The only remaining way this can be a step, is if the remaining string is a simple name 3939 // or else a :* name. 3940 // http://www.w3.org/TR/xpath#NT-NameTest 3941 // NameTest ::= '*' 3942 // | NCName ':' '*' 3943 // | QName 3944 // QName ::= (Prefix ':')? LocalPart 3945 // Prefix ::= NCName 3946 // LocalPart ::= NCName 3947 // 3948 // ie 3949 // NameTest ::= '*' 3950 // | NCName ':' '*' 3951 // | (NCName ':')? NCName 3952 // NCName ::= (Letter | '_') (NCNameChar)* 3953 $NCName = "[a-zA-Z_][\w\.\-_]*"; 3954 if (preg_match("/^$NCName:$NCName$/", $step) 3955 || preg_match("/^$NCName:*$/", $step)) { 3956 $axis['node-test'] = $step; 3957 if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) { 3958 // Case in-sensitive 3959 $axis['node-test'] = strtoupper($axis['node-test']); 3960 } 3961 // Not currently recursing 3962 $LastFailedStep = ''; 3963 $LastFailedContext = ''; 3964 break $parseBlock; 3965 } 3966 3967 // It's not a node then, we must treat it as a PrimaryExpr 3968 // Check for recursion 3969 if ($LastFailedStep == $step) { 3970 $this->_displayError('Recursion detected while parsing an XPath query, in the step ' . 3971 str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) 3972 , __LINE__, __FILE__, FALSE); 3973 $axis['node-test'] = $step; 3974 } else { 3975 $LastFailedStep = $step; 3976 $axis = FALSE; 3977 } 3978 3979 } while(FALSE); // end parse block 3980 3981 // Check whether it's a valid axis. 3982 if ($axis !== FALSE) { 3983 if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) { 3984 // Display an error message. 3985 $this->_displayError('While parsing an XPath query, in the step ' . 3986 str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) . 3987 ' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__, __FILE__, FALSE); 3988 } 3989 } 3990 3991 // Cache the real axis information 3992 $aResultsCache[$cacheKey] = $axis; 3993 3994 // Return the axis information. 3995 return $axis; 3996 } 3997 3998 3999 /** 4000 * Handles the XPath child axis. 4001 * 4002 * This method handles the XPath child axis. It essentially filters out the 4003 * children to match the name specified after the '/'. 4004 * 4005 * @param $axis (array) Array containing information about the axis. 4006 * @param $contextPath (string) xpath to starting node from which the axis should 4007 * be processed. 4008 * @return (array) A vector containing all nodes that were found, during 4009 * the evaluation of the axis. 4010 * @see evaluate() 4011 */ 4012 function _handleAxis_child($axis, $contextPath) { 4013 $xPathSet = array(); // Create an empty node-set to hold the results of the child matches 4014 if ($axis["node-test"] == "cdata") { 4015 if (!isSet($this->nodeIndex[$contextPath]['textParts']) ) return ''; 4016 $tSize = sizeOf($this->nodeIndex[$contextPath]['textParts']); 4017 for ($i=1; $i<=$tSize; $i++) { 4018 $xPathSet[] = $contextPath . '/text()['.$i.']'; 4019 } 4020 } 4021 else { 4022 // Get a list of all children. 4023 $allChildren = $this->nodeIndex[$contextPath]['childNodes']; 4024 4025 // Run through all children in the order they where set. 4026 $cSize = sizeOf($allChildren); 4027 for ($i=0; $i<$cSize; $i++) { 4028 $childPath = $contextPath .'/'. $allChildren[$i]['name'] .'['. $allChildren[$i]['contextPos'] .']'; 4029 $textChildPath = $contextPath.'/text()['.($i + 1).']'; 4030 // Check the text node 4031 if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check 4032 $xPathSet[] = $textChildPath; // Add the child to the node-set. 4033 } 4034 // Check the actual node 4035 if ($this->_checkNodeTest($childPath, $axis['node-test'])) { // node test check 4036 $xPathSet[] = $childPath; // Add the child to the node-set. 4037 } 4038 } 4039 4040 // Finally there will be one more text node to try 4041 $textChildPath = $contextPath.'/text()['.($cSize + 1).']'; 4042 // Check the text node 4043 if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check 4044 $xPathSet[] = $textChildPath; // Add the child to the node-set. 4045 } 4046 } 4047 return $xPathSet; // Return the nodeset. 4048 } 4049 4050 /** 4051 * Handles the XPath parent axis. 4052 * 4053 * @param $axis (array) Array containing information about the axis. 4054 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4055 * @return (array) A vector containing all nodes that were found, during the 4056 * evaluation of the axis. 4057 * @see evaluate() 4058 */ 4059 function _handleAxis_parent($axis, $contextPath) { 4060 $xPathSet = array(); // Create an empty node-set. 4061 4062 // Check whether the parent matches the node-test. 4063 $parentPath = $this->getParentXPath($contextPath); 4064 if ($this->_checkNodeTest($parentPath, $axis['node-test'])) { 4065 $xPathSet[] = $parentPath; // Add this node to the list of nodes. 4066 } 4067 return $xPathSet; // Return the nodeset. 4068 } 4069 4070 /** 4071 * Handles the XPath attribute axis. 4072 * 4073 * @param $axis (array) Array containing information about the axis. 4074 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4075 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4076 * @see evaluate() 4077 */ 4078 function _handleAxis_attribute($axis, $contextPath) { 4079 $xPathSet = array(); // Create an empty node-set. 4080 4081 // Check whether all nodes should be selected. 4082 $nodeAttr = $this->nodeIndex[$contextPath]['attributes']; 4083 if ($axis['node-test'] == '*' 4084 || $axis['node-test'] == 'node()') { 4085 foreach($nodeAttr as $key=>$dummy) { // Run through the attributes. 4086 $xPathSet[] = $contextPath.'/attribute::'.$key; // Add this node to the node-set. 4087 } 4088 } 4089 elseif (isset($nodeAttr[$axis['node-test']])) { 4090 $xPathSet[] = $contextPath . '/attribute::'. $axis['node-test']; // Add this node to the node-set. 4091 } 4092 return $xPathSet; // Return the nodeset. 4093 } 4094 4095 /** 4096 * Handles the XPath self axis. 4097 * 4098 * @param $axis (array) Array containing information about the axis. 4099 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4100 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4101 * @see evaluate() 4102 */ 4103 function _handleAxis_self($axis, $contextPath) { 4104 $xPathSet = array(); // Create an empty node-set. 4105 4106 // Check whether the context match the node-test. 4107 if ($this->_checkNodeTest($contextPath, $axis['node-test'])) { 4108 $xPathSet[] = $contextPath; // Add this node to the node-set. 4109 } 4110 return $xPathSet; // Return the nodeset. 4111 } 4112 4113 /** 4114 * Handles the XPath descendant axis. 4115 * 4116 * @param $axis (array) Array containing information about the axis. 4117 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4118 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4119 * @see evaluate() 4120 */ 4121 function _handleAxis_descendant($axis, $contextPath) { 4122 $xPathSet = array(); // Create an empty node-set. 4123 4124 // Get a list of all children. 4125 $allChildren = $this->nodeIndex[$contextPath]['childNodes']; 4126 4127 // Run through all children in the order they where set. 4128 $cSize = sizeOf($allChildren); 4129 for ($i=0; $i<$cSize; $i++) { 4130 $childPath = $allChildren[$i]['xpath']; 4131 // Check whether the child matches the node-test. 4132 if ($this->_checkNodeTest($childPath, $axis['node-test'])) { 4133 $xPathSet[] = $childPath; // Add the child to the list of nodes. 4134 } 4135 // Recurse to the next level. 4136 $xPathSet = array_merge($xPathSet, $this->_handleAxis_descendant($axis, $childPath)); 4137 } 4138 return $xPathSet; // Return the nodeset. 4139 } 4140 4141 /** 4142 * Handles the XPath ancestor axis. 4143 * 4144 * @param $axis (array) Array containing information about the axis. 4145 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4146 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4147 * @see evaluate() 4148 */ 4149 function _handleAxis_ancestor($axis, $contextPath) { 4150 $xPathSet = array(); // Create an empty node-set. 4151 4152 $parentPath = $this->getParentXPath($contextPath); // Get the parent of the current node. 4153 4154 // Check whether the parent isn't super-root. 4155 if (!empty($parentPath)) { 4156 // Check whether the parent matches the node-test. 4157 if ($this->_checkNodeTest($parentPath, $axis['node-test'])) { 4158 $xPathSet[] = $parentPath; // Add the parent to the list of nodes. 4159 } 4160 // Handle all other ancestors. 4161 $xPathSet = array_merge($this->_handleAxis_ancestor($axis, $parentPath), $xPathSet); 4162 } 4163 return $xPathSet; // Return the nodeset. 4164 } 4165 4166 /** 4167 * Handles the XPath namespace axis. 4168 * 4169 * @param $axis (array) Array containing information about the axis. 4170 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4171 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4172 * @see evaluate() 4173 */ 4174 function _handleAxis_namespace($axis, $contextPath) { 4175 $this->_displayError("The axis 'namespace is not suported'", __LINE__, __FILE__, FALSE); 4176 } 4177 4178 /** 4179 * Handles the XPath following axis. 4180 * 4181 * @param $axis (array) Array containing information about the axis. 4182 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4183 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4184 * @see evaluate() 4185 */ 4186 function _handleAxis_following($axis, $contextPath) { 4187 $xPathSet = array(); // Create an empty node-set. 4188 4189 do { // try-block 4190 $node = $this->nodeIndex[$contextPath]; // Get the current node 4191 $position = $node['pos']; // Get the current tree position. 4192 $parent = $node['parentNode']; 4193 // Check if there is a following sibling at all; if not end. 4194 if ($position >= sizeOf($parent['childNodes'])) break; // try-block 4195 // Build the starting abs. XPath 4196 $startXPath = $parent['childNodes'][$position+1]['xpath']; 4197 // Run through all nodes of the document. 4198 $nodeKeys = array_keys($this->nodeIndex); 4199 $nodeSize = sizeOf($nodeKeys); 4200 for ($k=0; $k<$nodeSize; $k++) { 4201 if ($nodeKeys[$k] == $startXPath) break; // Check whether this is the starting abs. XPath 4202 } 4203 for (; $k<$nodeSize; $k++) { 4204 // Check whether the node fits the node-test. 4205 if ($this->_checkNodeTest($nodeKeys[$k], $axis['node-test'])) { 4206 $xPathSet[] = $nodeKeys[$k]; // Add the node to the list of nodes. 4207 } 4208 } 4209 } while(FALSE); 4210 return $xPathSet; // Return the nodeset. 4211 } 4212 4213 /** 4214 * Handles the XPath preceding axis. 4215 * 4216 * @param $axis (array) Array containing information about the axis. 4217 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4218 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4219 * @see evaluate() 4220 */ 4221 function _handleAxis_preceding($axis, $contextPath) { 4222 $xPathSet = array(); // Create an empty node-set. 4223 4224 // Run through all nodes of the document. 4225 foreach ($this->nodeIndex as $xPath=>$dummy) { 4226 if (empty($xPath)) continue; // skip super-Root 4227 4228 // Check whether this is the context node. 4229 if ($xPath == $contextPath) { 4230 break; // After this we won't look for more nodes. 4231 } 4232 if (!strncmp($xPath, $contextPath, strLen($xPath))) { 4233 continue; 4234 } 4235 // Check whether the node fits the node-test. 4236 if ($this->_checkNodeTest($xPath, $axis['node-test'])) { 4237 $xPathSet[] = $xPath; // Add the node to the list of nodes. 4238 } 4239 } 4240 return $xPathSet; // Return the nodeset. 4241 } 4242 4243 /** 4244 * Handles the XPath following-sibling axis. 4245 * 4246 * @param $axis (array) Array containing information about the axis. 4247 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4248 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4249 * @see evaluate() 4250 */ 4251 function _handleAxis_following_sibling($axis, $contextPath) { 4252 $xPathSet = array(); // Create an empty node-set. 4253 4254 // Get all children from the parent. 4255 $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath)); 4256 // Create a flag whether the context node was already found. 4257 $found = FALSE; 4258 4259 // Run through all siblings. 4260 $size = sizeOf($siblings); 4261 for ($i=0; $i<$size; $i++) { 4262 $sibling = $siblings[$i]; 4263 4264 // Check whether the context node was already found. 4265 if ($found) { 4266 // Check whether the sibling matches the node-test. 4267 if ($this->_checkNodeTest($sibling, $axis['node-test'])) { 4268 $xPathSet[] = $sibling; // Add the sibling to the list of nodes. 4269 } 4270 } 4271 // Check if we reached *this* context node. 4272 if ($sibling == $contextPath) { 4273 $found = TRUE; // Continue looking for other siblings. 4274 } 4275 } 4276 return $xPathSet; // Return the nodeset. 4277 } 4278 4279 /** 4280 * Handles the XPath preceding-sibling axis. 4281 * 4282 * @param $axis (array) Array containing information about the axis. 4283 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4284 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4285 * @see evaluate() 4286 */ 4287 function _handleAxis_preceding_sibling($axis, $contextPath) { 4288 $xPathSet = array(); // Create an empty node-set. 4289 4290 // Get all children from the parent. 4291 $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath)); 4292 4293 // Run through all siblings. 4294 $size = sizeOf($siblings); 4295 for ($i=0; $i<$size; $i++) { 4296 $sibling = $siblings[$i]; 4297 // Check whether this is the context node. 4298 if ($sibling == $contextPath) { 4299 break; // Don't continue looking for other siblings. 4300 } 4301 // Check whether the sibling matches the node-test. 4302 if ($this->_checkNodeTest($sibling, $axis['node-test'])) { 4303 $xPathSet[] = $sibling; // Add the sibling to the list of nodes. 4304 } 4305 } 4306 return $xPathSet; // Return the nodeset. 4307 } 4308 4309 /** 4310 * Handles the XPath descendant-or-self axis. 4311 * 4312 * @param $axis (array) Array containing information about the axis. 4313 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4314 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4315 * @see evaluate() 4316 */ 4317 function _handleAxis_descendant_or_self($axis, $contextPath) { 4318 $xPathSet = array(); // Create an empty node-set. 4319 4320 // Read the nodes. 4321 $xPathSet = array_merge( 4322 $this->_handleAxis_self($axis, $contextPath), 4323 $this->_handleAxis_descendant($axis, $contextPath) 4324 ); 4325 return $xPathSet; // Return the nodeset. 4326 } 4327 4328 /** 4329 * Handles the XPath ancestor-or-self axis. 4330 * 4331 * This method handles the XPath ancestor-or-self axis. 4332 * 4333 * @param $axis (array) Array containing information about the axis. 4334 * @param $contextPath (string) xpath to starting node from which the axis should be processed. 4335 * @return (array) A vector containing all nodes that were found, during the evaluation of the axis. 4336 * @see evaluate() 4337 */ 4338 function _handleAxis_ancestor_or_self ( $axis, $contextPath) { 4339 $xPathSet = array(); // Create an empty node-set. 4340 4341 // Read the nodes. 4342 $xPathSet = array_merge( 4343 $this->_handleAxis_ancestor($axis, $contextPath), 4344 $this->_handleAxis_self($axis, $contextPath) 4345 ); 4346 return $xPathSet; // Return the nodeset. 4347 } 4348 4349 4350 //----------------------------------------------------------------------------------------- 4351 // XPath ------ XPath FUNCTION Handlers ------ 4352 //----------------------------------------------------------------------------------------- 4353 4354 /** 4355 * Handles the XPath function last. 4356 * 4357 * @param $arguments (string) String containing the arguments that were passed to the function. 4358 * @param $context (array) The context from which to evaluate the function 4359 * @return (mixed) Depending on the type of function being processed 4360 * @see evaluate() 4361 */ 4362 function _handleFunction_last($arguments, $context) { 4363 return $context['size']; 4364 } 4365 4366 /** 4367 * Handles the XPath function position. 4368 * 4369 * @param $arguments (string) String containing the arguments that were passed to the function. 4370 * @param $context (array) The context from which to evaluate the function 4371 * @return (mixed) Depending on the type of function being processed 4372 * @see evaluate() 4373 */ 4374 function _handleFunction_position($arguments, $context) { 4375 return $context['pos']; 4376 } 4377 4378 /** 4379 * Handles the XPath function count. 4380 * 4381 * @param $arguments (string) String containing the arguments that were passed to the function. 4382 * @param $context (array) The context from which to evaluate the function 4383 * @return (mixed) Depending on the type of function being processed 4384 * @see evaluate() 4385 */ 4386 function _handleFunction_count($arguments, $context) { 4387 // Evaluate the argument of the method as an XPath and return the number of results. 4388 return count($this->_evaluateExpr($arguments, $context)); 4389 } 4390 4391 /** 4392 * Handles the XPath function id. 4393 * 4394 * @param $arguments (string) String containing the arguments that were passed to the function. 4395 * @param $context (array) The context from which to evaluate the function 4396 * @return (mixed) Depending on the type of function being processed 4397 * @see evaluate() 4398 */ 4399 function _handleFunction_id($arguments, $context) { 4400 $arguments = trim($arguments); // Trim the arguments. 4401 $arguments = explode(' ', $arguments); // Now split the arguments into an array. 4402 // Create a list of nodes. 4403 $resultXPaths = array(); 4404 // Run through all nodes of the document. 4405 $keys = array_keys($this->nodeIndex); 4406 $kSize = $sizeOf($keys); 4407 for ($i=0; $i<$kSize; $i++) { 4408 if (empty($keys[$i])) continue; // skip super-Root 4409 if (in_array($this->nodeIndex[$keys[$i]]['attributes']['id'], $arguments)) { 4410 $resultXPaths[] = $context['nodePath']; // Add this node to the list of nodes. 4411 } 4412 } 4413 return $resultXPaths; // Return the list of nodes. 4414 } 4415 4416 /** 4417 * Handles the XPath function name. 4418 * 4419 * @param $arguments (string) String containing the arguments that were passed to the function. 4420 * @param $context (array) The context from which to evaluate the function 4421 * @return (mixed) Depending on the type of function being processed 4422 * @see evaluate() 4423 */ 4424 function _handleFunction_name($arguments, $context) { 4425 // If the argument it omitted, it defaults to a node-set with the context node as its only member. 4426 if (empty($arguments)) { 4427 return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['name']); 4428 } 4429 4430 // Evaluate the argument to get a node set. 4431 $nodeSet = $this->_evaluateExpr($arguments, $context); 4432 if (!is_array($nodeSet)) return ''; 4433 if (count($nodeSet) < 1) return ''; 4434 if (!isset($this->nodeIndex[$nodeSet[0]])) return ''; 4435 // Return a reference to the name of the node. 4436 return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['name']); 4437 } 4438 4439 /** 4440 * Handles the XPath function string. 4441 * 4442 * http://www.w3.org/TR/xpath#section-String-Functions 4443 * 4444 * @param $arguments (string) String containing the arguments that were passed to the function. 4445 * @param $context (array) The context from which to evaluate the function 4446 * @return (mixed) Depending on the type of function being processed 4447 * @see evaluate() 4448 */ 4449 function _handleFunction_string($arguments, $context) { 4450 // Check what type of parameter is given 4451 if (is_array($arguments)) { 4452 // Get the value of the first result (which means we want to concat all the text...unless 4453 // a specific text() node has been given, and it will switch off to substringData 4454 if (!count($arguments)) $result = ''; 4455 else { 4456 $result = $this->_stringValue($arguments[0]); 4457 if (($literal = $this->_asLiteral($result)) !== FALSE) { 4458 $result = $literal; 4459 } 4460 } 4461 } 4462 // Is it a number string? 4463 elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR preg_match('/^\.[0-9]+$/', $arguments)) { 4464 // ### Note no support for NaN and Infinity. 4465 $number = doubleval($arguments); // Convert the digits to a number. 4466 $result = strval($number); // Return the number. 4467 } 4468 elseif (is_bool($arguments)) { // Check whether it's TRUE or FALSE and return as string. 4469 // ### Note that we used to return TRUE and FALSE which was incorrect according to the standard. 4470 if ($arguments === TRUE) { 4471 $result = 'true'; 4472 } else { 4473 $result = 'false'; 4474 } 4475 } 4476 elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) { 4477 return $literal; 4478 } 4479 elseif (!empty($arguments)) { 4480 // Spec says: 4481 // "An object of a type other than the four basic types is converted to a string in a way that 4482 // is dependent on that type." 4483 // Use the argument as an XPath. 4484 $result = $this->_evaluateExpr($arguments, $context); 4485 if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) { 4486 $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_string($result)", __LINE__, __FILE__, FALSE); 4487 return ''; 4488 } else { 4489 $result = $this->_handleFunction_string($result, $context); 4490 } 4491 } 4492 else { 4493 $result = ''; // Return an empty string. 4494 } 4495 return $result; 4496 } 4497 4498 /** 4499 * Handles the XPath function concat. 4500 * 4501 * @param $arguments (string) String containing the arguments that were passed to the function. 4502 * @param $context (array) The context from which to evaluate the function 4503 * @return (mixed) Depending on the type of function being processed 4504 * @see evaluate() 4505 */ 4506 function _handleFunction_concat($arguments, $context) { 4507 // Split the arguments. 4508 $arguments = explode(',', $arguments); 4509 // Run through each argument and evaluate it. 4510 $size = sizeof($arguments); 4511 for ($i=0; $i<$size; $i++) { 4512 $arguments[$i] = trim($arguments[$i]); // Trim each argument. 4513 // Evaluate it. 4514 $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context); 4515 } 4516 $arguments = implode('', $arguments); // Put the string together and return it. 4517 return $this->_addLiteral($arguments); 4518 } 4519 4520 /** 4521 * Handles the XPath function starts-with. 4522 * 4523 * @param $arguments (string) String containing the arguments that were passed to the function. 4524 * @param $context (array) The context from which to evaluate the function 4525 * @return (mixed) Depending on the type of function being processed 4526 * @see evaluate() 4527 */ 4528 function _handleFunction_starts_with($arguments, $context) { 4529 // Get the arguments. 4530 $first = trim($this->_prestr($arguments, ',')); 4531 $second = trim($this->_afterstr($arguments, ',')); 4532 // Evaluate each argument. 4533 $first = $this->_handleFunction_string($first, $context); 4534 $second = $this->_handleFunction_string($second, $context); 4535 // Check whether the first string starts with the second one. 4536 return (bool) ereg('^'.$second, $first); 4537 } 4538 4539 /** 4540 * Handles the XPath function contains. 4541 * 4542 * @param $arguments (string) String containing the arguments that were passed to the function. 4543 * @param $context (array) The context from which to evaluate the function 4544 * @return (mixed) Depending on the type of function being processed 4545 * @see evaluate() 4546 */ 4547 function _handleFunction_contains($arguments, $context) { 4548 // Get the arguments. 4549 $first = trim($this->_prestr($arguments, ',')); 4550 $second = trim($this->_afterstr($arguments, ',')); 4551 //echo "Predicate: $arguments First: ".$first." Second: ".$second."\n"; 4552 // Evaluate each argument. 4553 $first = $this->_handleFunction_string($first, $context); 4554 $second = $this->_handleFunction_string($second, $context); 4555 //echo $second.": ".$first."\n"; 4556 // If the search string is null, then the provided there is a value it will contain it as 4557 // it is considered that all strings contain the empty string. ## N.S. 4558 if ($second==='') return TRUE; 4559 // Check whether the first string starts with the second one. 4560 if (strpos($first, $second) === FALSE) { 4561 return FALSE; 4562 } else { 4563 return TRUE; 4564 } 4565 } 4566 4567 /** 4568 * Handles the XPath function substring-before. 4569 * 4570 * @param $arguments (string) String containing the arguments that were passed to the function. 4571 * @param $context (array) The context from which to evaluate the function 4572 * @return (mixed) Depending on the type of function being processed 4573 * @see evaluate() 4574 */ 4575 function _handleFunction_substring_before($arguments, $context) { 4576 // Get the arguments. 4577 $first = trim($this->_prestr($arguments, ',')); 4578 $second = trim($this->_afterstr($arguments, ',')); 4579 // Evaluate each argument. 4580 $first = $this->_handleFunction_string($first, $context); 4581 $second = $this->_handleFunction_string($second, $context); 4582 // Return the substring. 4583 return $this->_addLiteral($this->_prestr(strval($first), strval($second))); 4584 } 4585 4586 /** 4587 * Handles the XPath function substring-after. 4588 * 4589 * @param $arguments (string) String containing the arguments that were passed to the function. 4590 * @param $context (array) The context from which to evaluate the function 4591 * @return (mixed) Depending on the type of function being processed 4592 * @see evaluate() 4593 */ 4594 function _handleFunction_substring_after($arguments, $context) { 4595 // Get the arguments. 4596 $first = trim($this->_prestr($arguments, ',')); 4597 $second = trim($this->_afterstr($arguments, ',')); 4598 // Evaluate each argument. 4599 $first = $this->_handleFunction_string($first, $context); 4600 $second = $this->_handleFunction_string($second, $context); 4601 // Return the substring. 4602 return $this->_addLiteral($this->_afterstr(strval($first), strval($second))); 4603 } 4604 4605 /** 4606 * Handles the XPath function substring. 4607 * 4608 * @param $arguments (string) String containing the arguments that were passed to the function. 4609 * @param $context (array) The context from which to evaluate the function 4610 * @return (mixed) Depending on the type of function being processed 4611 * @see evaluate() 4612 */ 4613 function _handleFunction_substring($arguments, $context) { 4614 // Split the arguments. 4615 $arguments = explode(",", $arguments); 4616 $size = sizeOf($arguments); 4617 for ($i=0; $i<$size; $i++) { // Run through all arguments. 4618 $arguments[$i] = trim($arguments[$i]); // Trim the string. 4619 // Evaluate each argument. 4620 $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context); 4621 } 4622 // Check whether a third argument was given and return the substring.. 4623 if (!empty($arguments[2])) { 4624 return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1, $arguments[2])); 4625 } else { 4626 return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1)); 4627 } 4628 } 4629 4630 /** 4631 * Handles the XPath function string-length. 4632 * 4633 * @param $arguments (string) String containing the arguments that were passed to the function. 4634 * @param $context (array) The context from which to evaluate the function 4635 * @return (mixed) Depending on the type of function being processed 4636 * @see evaluate() 4637 */ 4638 function _handleFunction_string_length($arguments, $context) { 4639 $arguments = trim($arguments); // Trim the argument. 4640 // Evaluate the argument. 4641 $arguments = $this->_handleFunction_string($arguments, $context); 4642 return strlen(strval($arguments)); // Return the length of the string. 4643 } 4644 4645 /** 4646 * Handles the XPath function normalize-space. 4647 * 4648 * The normalize-space function returns the argument string with whitespace 4649 * normalized by stripping leading and trailing whitespace and replacing sequences 4650 * of whitespace characters by a single space. 4651 * If the argument is omitted, it defaults to the context node converted to a string, 4652 * in other words the string-value of the context node 4653 * 4654 * @param $arguments (string) String containing the arguments that were passed to the function. 4655 * @param $context (array) The context from which to evaluate the function 4656 * @return (stri)g trimed string 4657 * @see evaluate() 4658 */ 4659 function _handleFunction_normalize_space($arguments, $context) { 4660 if (empty($arguments)) { 4661 $arguments = $this->getParentXPath($context['nodePath']).'/'.$this->nodeIndex[$context['nodePath']]['name'].'['.$this->nodeIndex[$context['nodePath']]['contextPos'].']'; 4662 } else { 4663 $arguments = $this->_handleFunction_string($arguments, $context); 4664 } 4665 $arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments)); 4666 return $this->_addLiteral($arguments); 4667 } 4668 4669 /** 4670 * Handles the XPath function translate. 4671 * 4672 * @param $arguments (string) String containing the arguments that were passed to the function. 4673 * @param $context (array) The context from which to evaluate the function 4674 * @return (mixed) Depending on the type of function being processed 4675 * @see evaluate() 4676 */ 4677 function _handleFunction_translate($arguments, $context) { 4678 $arguments = explode(',', $arguments); // Split the arguments. 4679 $size = sizeOf($arguments); 4680 for ($i=0; $i<$size; $i++) { // Run through all arguments. 4681 $arguments[$i] = trim($arguments[$i]); // Trim the argument. 4682 // Evaluate the argument. 4683 $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context); 4684 } 4685 // Return the translated string. 4686 return $this->_addLiteral(strtr($arguments[0], $arguments[1], $arguments[2])); 4687 } 4688 4689 /** 4690 * Handles the XPath function boolean. 4691 * 4692 * http://www.w3.org/TR/xpath#section-Boolean-Functions 4693 * 4694 * @param $arguments (string) String containing the arguments that were passed to the function. 4695 * @param $context (array) The context from which to evaluate the function 4696 * @return (mixed) Depending on the type of function being processed 4697 * @see evaluate() 4698 */ 4699 function _handleFunction_boolean($arguments, $context) { 4700 if (empty($arguments)) { 4701 return FALSE; // Sorry, there were no arguments. 4702 } 4703 // a bool is dead obvious 4704 elseif (is_bool($arguments)) { 4705 return $arguments; 4706 } 4707 // a node-set is true if and only if it is non-empty 4708 elseif (is_array($arguments)) { 4709 return (count($arguments) > 0); 4710 } 4711 // a number is true if and only if it is neither positive or negative zero nor NaN 4712 // (Straight out of the XPath spec.. makes no sense?????) 4713 elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || preg_match('/^\.[0-9]+$/', $arguments)) { 4714 $number = doubleval($arguments); // Convert the digits to a number. 4715 // If number zero return FALSE else TRUE. 4716 if ($number == 0) return FALSE; else return TRUE; 4717 } 4718 // a string is true if and only if its length is non-zero 4719 elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) { 4720 return (strlen($literal) != 0); 4721 } 4722 // an object of a type other than the four basic types is converted to a boolean in a 4723 // way that is dependent on that type 4724 else { 4725 // Spec says: 4726 // "An object of a type other than the four basic types is converted to a number in a way 4727 // that is dependent on that type" 4728 // Try to evaluate the argument as an XPath. 4729 $result = $this->_evaluateExpr($arguments, $context); 4730 if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) { 4731 $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_boolean($result)", __LINE__, __FILE__, FALSE); 4732 return FALSE; 4733 } else { 4734 return $this->_handleFunction_boolean($result, $context); 4735 } 4736 } 4737 } 4738 4739 /** 4740 * Handles the XPath function not. 4741 * 4742 * @param $arguments (string) String containing the arguments that were passed to the function. 4743 * @param $context (array) The context from which to evaluate the function 4744 * @return (mixed) Depending on the type of function being processed 4745 * @see evaluate() 4746 */ 4747 function _handleFunction_not($arguments, $context) { 4748 // Return the negative value of the content of the brackets. 4749 $bArgResult = $this->_handleFunction_boolean($arguments, $context); 4750 //echo "Before inversion: ".($bArgResult?"TRUE":"FALSE")."\n"; 4751 return !$bArgResult; 4752 } 4753 4754 /** 4755 * Handles the XPath function TRUE. 4756 * 4757 * @param $arguments (string) String containing the arguments that were passed to the function. 4758 * @param $context (array) The context from which to evaluate the function 4759 * @return (mixed) Depending on the type of function being processed 4760 * @see evaluate() 4761 */ 4762 function _handleFunction_true($arguments, $context) { 4763 return TRUE; // Return TRUE. 4764 } 4765 4766 /** 4767 * Handles the XPath function FALSE. 4768 * 4769 * @param $arguments (string) String containing the arguments that were passed to the function. 4770 * @param $context (array) The context from which to evaluate the function 4771 * @return (mixed) Depending on the type of function being processed 4772 * @see evaluate() 4773 */ 4774 function _handleFunction_false($arguments, $context) { 4775 return FALSE; // Return FALSE. 4776 } 4777 4778 /** 4779 * Handles the XPath function lang. 4780 * 4781 * @param $arguments (string) String containing the arguments that were passed to the function. 4782 * @param $context (array) The context from which to evaluate the function 4783 * @return (mixed) Depending on the type of function being processed 4784 * @see evaluate() 4785 */ 4786 function _handleFunction_lang($arguments, $context) { 4787 $arguments = trim($arguments); // Trim the arguments. 4788 $currentNode = $this->nodeIndex[$context['nodePath']]; 4789 while (!empty($currentNode['name'])) { // Run through the ancestors. 4790 // Check whether the node has an language attribute. 4791 if (isSet($currentNode['attributes']['xml:lang'])) { 4792 // Check whether it's the language, the user asks for; if so return TRUE else FALSE 4793 return eregi('^'.$arguments, $currentNode['attributes']['xml:lang']); 4794 } 4795 $currentNode = $currentNode['parentNode']; // Move up to parent 4796 } // End while 4797 return FALSE; 4798 } 4799 4800 /** 4801 * Handles the XPath function number. 4802 * 4803 * http://www.w3.org/TR/xpath#section-Number-Functions 4804 * 4805 * @param $arguments (string) String containing the arguments that were passed to the function. 4806 * @param $context (array) The context from which to evaluate the function 4807 * @return (mixed) Depending on the type of function being processed 4808 * @see evaluate() 4809 */ 4810 function _handleFunction_number($arguments, $context) { 4811 // Check the type of argument. 4812 4813 // A string that is a number 4814 if (is_numeric($arguments)) { 4815 return doubleval($arguments); // Return the argument as a number. 4816 } 4817 // A bool 4818 elseif (is_bool($arguments)) { // Return TRUE/FALSE as a number. 4819 if ($arguments === TRUE) return 1; else return 0; 4820 } 4821 // A node set 4822 elseif (is_array($arguments)) { 4823 // Is converted to a string then handled like a string 4824 $string = $this->_handleFunction_string($arguments, $context); 4825 if (is_numeric($string)) 4826 return doubleval($string); 4827 } 4828 elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) { 4829 if (is_numeric($literal)) { 4830 return doubleval($literal); 4831 } else { 4832 // If we are to stick strictly to the spec, we should return NaN, but lets just 4833 // leave PHP to see if can do some dynamic conversion. 4834 return $literal; 4835 } 4836 } 4837 else { 4838 // Spec says: 4839 // "An object of a type other than the four basic types is converted to a number in a way 4840 // that is dependent on that type" 4841 // Try to evaluate the argument as an XPath. 4842 $result = $this->_evaluateExpr($arguments, $context); 4843 if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) { 4844 $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_number($result)", __LINE__, __FILE__, FALSE); 4845 return FALSE; 4846 } else { 4847 return $this->_handleFunction_number($result, $context); 4848 } 4849 } 4850 } 4851 4852 /** 4853 * Handles the XPath function sum. 4854 * 4855 * @param $arguments (string) String containing the arguments that were passed to the function. 4856 * @param $context (array) The context from which to evaluate the function 4857 * @return (mixed) Depending on the type of function being processed 4858 * @see evaluate() 4859 */ 4860 function _handleFunction_sum($arguments, $context) { 4861 $arguments = trim($arguments); // Trim the arguments. 4862 // Evaluate the arguments as an XPath query. 4863 $result = $this->_evaluateExpr($arguments, $context); 4864 $sum = 0; // Create a variable to save the sum. 4865 // The sum function expects a node set as an argument. 4866 if (is_array($result)) { 4867 // Run through all results. 4868 $size = sizeOf($result); 4869 for ($i=0; $i<$size; $i++) { 4870 $value = $this->_stringValue($result[$i], $context); 4871 if (($literal = $this->_asLiteral($value)) !== FALSE) { 4872 $value = $literal; 4873 } 4874 $sum += doubleval($value); // Add it to the sum. 4875 } 4876 } 4877 return $sum; // Return the sum. 4878 } 4879 4880 /** 4881 * Handles the XPath function floor. 4882 * 4883 * @param $arguments (string) String containing the arguments that were passed to the function. 4884 * @param $context (array) The context from which to evaluate the function 4885 * @return (mixed) Depending on the type of function being processed 4886 * @see evaluate() 4887 */ 4888 function _handleFunction_floor($arguments, $context) { 4889 if (!is_numeric($arguments)) { 4890 $arguments = $this->_handleFunction_number($arguments, $context); 4891 } 4892 $arguments = doubleval($arguments); // Convert the arguments to a number. 4893 return floor($arguments); // Return the result 4894 } 4895 4896 /** 4897 * Handles the XPath function ceiling. 4898 * 4899 * @param $arguments (string) String containing the arguments that were passed to the function. 4900 * @param $context (array) The context from which to evaluate the function 4901 * @return (mixed) Depending on the type of function being processed 4902 * @see evaluate() 4903 */ 4904 function _handleFunction_ceiling($arguments, $context) { 4905 if (!is_numeric($arguments)) { 4906 $arguments = $this->_handleFunction_number($arguments, $context); 4907 } 4908 $arguments = doubleval($arguments); // Convert the arguments to a number. 4909 return ceil($arguments); // Return the result 4910 } 4911 4912 /** 4913 * Handles the XPath function round. 4914 * 4915 * @param $arguments (string) String containing the arguments that were passed to the function. 4916 * @param $context (array) The context from which to evaluate the function 4917 * @return (mixed) Depending on the type of function being processed 4918 * @see evaluate() 4919 */ 4920 function _handleFunction_round($arguments, $context) { 4921 if (!is_numeric($arguments)) { 4922 $arguments = $this->_handleFunction_number($arguments, $context); 4923 } 4924 $arguments = doubleval($arguments); // Convert the arguments to a number. 4925 return round($arguments); // Return the result 4926 } 4927 4928 //----------------------------------------------------------------------------------------- 4929 // XPath ------ XPath Extension FUNCTION Handlers ------ 4930 //----------------------------------------------------------------------------------------- 4931 4932 /** 4933 * Handles the XPath function x-lower. 4934 * 4935 * lower case a string. 4936 * string x-lower(string) 4937 * 4938 * @param $arguments (string) String containing the arguments that were passed to the function. 4939 * @param $context (array) The context from which to evaluate the function 4940 * @return (mixed) Depending on the type of function being processed 4941 * @see evaluate() 4942 */ 4943 function _handleFunction_x_lower($arguments, $context) { 4944 // Evaluate the argument. 4945 $string = $this->_handleFunction_string($arguments, $context); 4946 // Return a reference to the lowercased string 4947 return $this->_addLiteral(strtolower(strval($string))); 4948 } 4949 4950 /** 4951 * Handles the XPath function x-upper. 4952 * 4953 * upper case a string. 4954 * string x-upper(string) 4955 * 4956 * @param $arguments (string) String containing the arguments that were passed to the function. 4957 * @param $context (array) The context from which to evaluate the function 4958 * @return (mixed) Depending on the type of function being processed 4959 * @see evaluate() 4960 */ 4961 function _handleFunction_x_upper($arguments, $context) { 4962 // Evaluate the argument. 4963 $string = $this->_handleFunction_string($arguments, $context); 4964 // Return a reference to the lowercased string 4965 return $this->_addLiteral(strtoupper(strval($string))); 4966 } 4967 4968 /** 4969 * Handles the XPath function generate-id. 4970 * 4971 * Produce a unique id for the first node of the node set. 4972 * 4973 * Example usage, produces an index of all the nodes in an .xml document, where the content of each 4974 * "section" is the exported node as XML. 4975 * 4976 * $aFunctions = $xPath->match('//'); 4977 * 4978 * foreach ($aFunctions as $Function) { 4979 * $id = $xPath->match("generate-id($Function)"); 4980 * echo "<a href='#$id'>$Function</a><br>"; 4981 * } 4982 * 4983 * foreach ($aFunctions as $Function) { 4984 * $id = $xPath->match("generate-id($Function)"); 4985 * echo "<h2 id='$id'>$Function</h2>"; 4986 * echo htmlspecialchars($xPath->exportAsXml($Function)); 4987 * } 4988 * 4989 * @param $arguments (string) String containing the arguments that were passed to the function. 4990 * @param $context (array) The context from which to evaluate the function 4991 * @return (mixed) Depending on the type of function being processed 4992 * @author Ricardo Garcia 4993 * @see evaluate() 4994 */ 4995 function _handleFunction_generate_id($arguments, $context) { 4996 // If the argument is omitted, it defaults to a node-set with the context node as its only member. 4997 if (is_string($arguments) && empty($arguments)) { 4998 // We need ids then 4999 $this->_generate_ids(); 5000 return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['generated_id']); 5001 } 5002 5003 // Evaluate the argument to get a node set. 5004 $nodeSet = $this->_evaluateExpr($arguments, $context); 5005 5006 if (!is_array($nodeSet)) return ''; 5007 if (count($nodeSet) < 1) return ''; 5008 if (!isset($this->nodeIndex[$nodeSet[0]])) return ''; 5009 // Return a reference to the name of the node. 5010 // We need ids then 5011 $this->_generate_ids(); 5012 return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['generated_id']); 5013 } 5014 5015 //----------------------------------------------------------------------------------------- 5016 // XPathEngine ------ Help Stuff ------ 5017 //----------------------------------------------------------------------------------------- 5018 5019 /** 5020 * Decodes the character set entities in the given string. 5021 * 5022 * This function is given for convenience, as all text strings or attributes 5023 * are going to come back to you with their entities still encoded. You can 5024 * use this function to remove these entites. 5025 * 5026 * It makes use of the get_html_translation_table(HTML_ENTITIES) php library 5027 * call, so is limited in the same ways. At the time of writing this seemed 5028 * be restricted to iso-8859-1 5029 * 5030 * ### Provide an option that will do this by default. 5031 * 5032 * @param $encodedData (mixed) The string or array that has entities you would like to remove 5033 * @param $reverse (bool) If TRUE entities will be encoded rather than decoded, ie 5034 * < to < rather than < to <. 5035 * @return (mixed) The string or array returned with entities decoded. 5036 */ 5037 function decodeEntities($encodedData, $reverse=FALSE) { 5038 static $aEncodeTbl; 5039 static $aDecodeTbl; 5040 // Get the translation entities, but we'll cache the result to enhance performance. 5041 if (empty($aDecodeTbl)) { 5042 // Get the translation entities. 5043 $aEncodeTbl = get_html_translation_table(HTML_ENTITIES); 5044 $aDecodeTbl = array_flip($aEncodeTbl); 5045 } 5046 5047 // If it's just a single string. 5048 if (!is_array($encodedData)) { 5049 if ($reverse) { 5050 return strtr($encodedData, $aEncodeTbl); 5051 } else { 5052 return strtr($encodedData, $aDecodeTbl); 5053 } 5054 } 5055 5056 $result = array(); 5057 foreach($encodedData as $string) { 5058 if ($reverse) { 5059 $result[] = strtr($string, $aEncodeTbl); 5060 } else { 5061 $result[] = strtr($string, $aDecodeTbl); 5062 } 5063 } 5064 5065 return $result; 5066 } 5067 5068 /** 5069 * Compare two nodes to see if they are equal (point to the same node in the doc) 5070 * 5071 * 2 nodes are considered equal if the absolute XPath is equal. 5072 * 5073 * @param $node1 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array) 5074 * @param $node2 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array) 5075 * @return (bool) TRUE if equal (see text above), FALSE if not (and on error). 5076 */ 5077 function equalNodes($node1, $node2) { 5078 $xPath_1 = is_string($node1) ? $node1 : $this->getNodePath($node1); 5079 $xPath_2 = is_string($node2) ? $node2 : $this->getNodePath($node2); 5080 return (strncasecmp ($xPath_1, $xPath_2, strLen($xPath_1)) == 0); 5081 } 5082 5083 /** 5084 * Get the absolute XPath of a node that is in a document tree. 5085 * 5086 * @param $node (array) A real tree-node (hash-array) 5087 * @return (string) The string path to the node or FALSE on error. 5088 */ 5089 function getNodePath($node) { 5090 if (!empty($node['xpath'])) return $node['xpath']; 5091 $pathInfo = array(); 5092 do { 5093 if (empty($node['name']) OR empty($node['parentNode'])) break; // End criteria 5094 $pathInfo[] = array('name' => $node['name'], 'contextPos' => $node['contextPos']); 5095 $node = $node['parentNode']; 5096 } while (TRUE); 5097 5098 $xPath = ''; 5099 for ($i=sizeOf($pathInfo)-1; $i>=0; $i--) { 5100 $xPath .= '/' . $pathInfo[$i]['name'] . '[' . $pathInfo[$i]['contextPos'] . ']'; 5101 } 5102 if (empty($xPath)) return FALSE; 5103 return $xPath; 5104 } 5105 5106 /** 5107 * Retrieves the absolute parent XPath query. 5108 * 5109 * The parents stored in the tree are only relative parents...but all the parent 5110 * information is stored in the XPath query itself...so instead we use a function 5111 * to extract the parent from the absolute Xpath query 5112 * 5113 * @param $childPath (string) String containing an absolute XPath query 5114 * @return (string) returns the absolute XPath of the parent 5115 */ 5116 function getParentXPath($absoluteXPath) { 5117 $lastSlashPos = strrpos($absoluteXPath, '/'); 5118 if ($lastSlashPos == 0) { // it's already the root path 5119 return ''; // 'super-root' 5120 } else { 5121 return (substr($absoluteXPath, 0, $lastSlashPos)); 5122 } 5123 } 5124 5125 /** 5126 * Returns TRUE if the given node has child nodes below it 5127 * 5128 * @param $absoluteXPath (string) full path of the potential parent node 5129 * @return (bool) TRUE if this node exists and has a child, FALSE otherwise 5130 */ 5131 function hasChildNodes($absoluteXPath) { 5132 if ($this->_indexIsDirty) $this->reindexNodeTree(); 5133 return (bool) (isSet($this->nodeIndex[$absoluteXPath]) 5134 AND sizeOf($this->nodeIndex[$absoluteXPath]['childNodes'])); 5135 } 5136 5137 /** 5138 * Translate all ampersands to it's literal entities '&' and back. 5139 * 5140 * I wasn't aware of this problem at first but it's important to understand why we do this. 5141 * At first you must know: 5142 * a) PHP's XML parser *translates* all entities to the equivalent char E.g. < is returned as '<' 5143 * b) PHP's XML parser (in V 4.1.0) has problems with most *literal* entities! The only one's that are 5144 * recognized are &, < > and ". *ALL* others (like © a.s.o.) cause an 5145 * XML_ERROR_UNDEFINED_ENTITY error. I reported this as bug at http://bugs.php.net/bug.php?id=15092 5146 * (It turned out not to be a 'real' bug, but one of those nice W3C-spec things). 5147 * 5148 * Forget position b) now. It's just for info. Because the way we will solve a) will also solve b) too. 5149 * 5150 * THE PROBLEM 5151 * To understand the problem, here a sample: 5152 * Given is the following XML: "<AAA> < > </AAA>" 5153 * Try to parse it and PHP's XML parser will fail with a XML_ERROR_UNDEFINED_ENTITY becaus of 5154 * the unknown litteral-entity ' '. (The numeric equivalent ' ' would work though). 5155 * Next try is to use the numeric equivalent 160 for ' ', thus "<AAA> <   > </AAA>" 5156 * The data we receive in the tag <AAA> is " < > ". So we get the *translated entities* and 5157 * NOT the 3 entities <   >. Thus, we will not even notice that there were entities at all! 5158 * In *most* cases we're not able to tell if the data was given as entity or as 'normal' char. 5159 * E.g. When receiving a quote or a single space were not able to tell if it was given as 'normal' char 5160 * or as or ". Thus we loose the entity-information of the XML-data! 5161 * 5162 * THE SOLUTION 5163 * The better solution is to keep the data 'as is' by replacing the '&' before parsing begins. 5164 * E.g. Taking the original input from above, this would result in "<AAA> &lt; &nbsp; &gt; </AAA>" 5165 * The data we receive now for the tag <AAA> is " < > ". and that's what we want. 5166 * 5167 * The bad thing is, that a global replace will also replace data in section that are NOT translated by the 5168 * PHP XML-parser. That is comments (<!-- -->), IP-sections (stuff between <? ? >) and CDATA-block too. 5169 * So all data comming from those sections must be reversed. This is done during the XML parse phase. 5170 * So: 5171 * a) Replacement of all '&' in the XML-source. 5172 * b) All data that is not char-data or in CDATA-block have to be reversed during the XML-parse phase. 5173 * 5174 * @param $xmlSource (string) The XML string 5175 * @return (string) The XML string with translated ampersands. 5176 */ 5177 function _translateAmpersand($xmlSource, $reverse=FALSE) { 5178 $PHP5 = (substr(phpversion(), 0, 1) == '5'); 5179 if ($PHP5) { 5180 //otherwise we receive &nbsp; instead of 5181 return $xmlSource; 5182 } else { 5183 return ($reverse ? str_replace('&', '&', $xmlSource) : str_replace('&', '&', $xmlSource)); 5184 } 5185 } 5186 5187 } // END OF CLASS XPathEngine 5188 5189 5190 /************************************************************************************************ 5191 * =============================================================================================== 5192 * X P a t h - Class 5193 * =============================================================================================== 5194 ************************************************************************************************/ 5195 5196 define('XPATH_QUERYHIT_ALL' , 1); 5197 define('XPATH_QUERYHIT_FIRST' , 2); 5198 define('XPATH_QUERYHIT_UNIQUE', 3); 5199 5200 class XPath extends XPathEngine { 5201 5202 /** 5203 * Constructor of the class 5204 * 5205 * Optionally you may call this constructor with the XML-filename to parse and the 5206 * XML option vector. A option vector sample: 5207 * $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, XML_OPTION_SKIP_WHITE => TRUE); 5208 * 5209 * @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, <optionID>=><value>, ...) 5210 * @param $fileName (string) (optional) Filename of XML file to load from. 5211 * It is recommended that you call importFromFile() 5212 * instead as you will get an error code. If the 5213 * import fails, the object will be set to FALSE. 5214 * @see parent::XPathEngine() 5215 */ 5216 function XPath($fileName='', $userXmlOptions=array()) { 5217 parent::XPathEngine($userXmlOptions); 5218 $this->properties['modMatch'] = XPATH_QUERYHIT_ALL; 5219 if ($fileName) { 5220 if (!$this->importFromFile($fileName)) { 5221 // Re-run the base constructor to "reset" the object. If the user has any sense, then 5222 // they will have created the object, and then explicitly called importFromFile(), giving 5223 // them the chance to catch and handle the error properly. 5224 parent::XPathEngine($userXmlOptions); 5225 } 5226 } 5227 } 5228 5229 /** 5230 * Resets the object so it's able to take a new xml sting/file 5231 * 5232 * Constructing objects is slow. If you can, reuse ones that you have used already 5233 * by using this reset() function. 5234 */ 5235 function reset() { 5236 parent::reset(); 5237 $this->properties['modMatch'] = XPATH_QUERYHIT_ALL; 5238 } 5239 5240 //----------------------------------------------------------------------------------------- 5241 // XPath ------ Get / Set Stuff ------ 5242 //----------------------------------------------------------------------------------------- 5243 5244 /** 5245 * Resolves and xPathQuery array depending on the property['modMatch'] 5246 * 5247 * Most of the modification functions of XPath will also accept a xPathQuery (instead 5248 * of an absolute Xpath). The only problem is that the query could match more the one 5249 * node. The question is, if the none, the fist or all nodes are to be modified. 5250 * The behaver can be set with setModMatch() 5251 * 5252 * @param $modMatch (int) One of the following: 5253 * - XPATH_QUERYHIT_ALL (default) 5254 * - XPATH_QUERYHIT_FIRST 5255 * - XPATH_QUERYHIT_UNIQUE // If the query matches more then one node. 5256 * @see _resolveXPathQuery() 5257 */ 5258 function setModMatch($modMatch = XPATH_QUERYHIT_ALL) { 5259 switch($modMatch) { 5260 case XPATH_QUERYHIT_UNIQUE : $this->properties['modMatch'] = XPATH_QUERYHIT_UNIQUE; break; 5261 case XPATH_QUERYHIT_FIRST: $this->properties['modMatch'] = XPATH_QUERYHIT_FIRST; break; 5262 default: $this->properties['modMatch'] = XPATH_QUERYHIT_ALL; 5263 } 5264 } 5265 5266 //----------------------------------------------------------------------------------------- 5267 // XPath ------ DOM Like Modification ------ 5268 //----------------------------------------------------------------------------------------- 5269 5270 //----------------------------------------------------------------------------------------- 5271 // XPath ------ Child (Node) Set/Get ------ 5272 //----------------------------------------------------------------------------------------- 5273 5274 /** 5275 * Retrieves the name(s) of a node or a group of document nodes. 5276 * 5277 * This method retrieves the names of a group of document nodes 5278 * specified in the argument. So if the argument was '/A[1]/B[2]' then it 5279 * would return 'B' if the node did exist in the tree. 5280 * 5281 * @param $xPathQuery (mixed) Array or single full document path(s) of the node(s), 5282 * from which the names should be retrieved. 5283 * @return (mixed) Array or single string of the names of the specified 5284 * nodes, or just the individual name. If the node did 5285 * not exist, then returns FALSE. 5286 */ 5287 function nodeName($xPathQuery) { 5288 if (is_array($xPathQuery)) { 5289 $xPathSet = $xPathQuery; 5290 } else { 5291 // Check for a valid xPathQuery 5292 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'nodeName'); 5293 } 5294 if (count($xPathSet) == 0) return FALSE; 5295 // For each node, get it's name 5296 $result = array(); 5297 foreach($xPathSet as $xPath) { 5298 $node = &$this->getNode($xPath); 5299 if (!$node) { 5300 // ### Fatal internal error?? 5301 continue; 5302 } 5303 $result[] = $node['name']; 5304 } 5305 // If just a single string, return string 5306 if (count($xPathSet) == 1) $result = $result[0]; 5307 // Return result. 5308 return $result; 5309 } 5310 5311 /** 5312 * Removes a node from the XML document. 5313 * 5314 * This method removes a node from the tree of nodes of the XML document. If the node 5315 * is a document node, all children of the node and its character data will be removed. 5316 * If the node is an attribute node, only this attribute will be removed, the node to which 5317 * the attribute belongs as well as its children will remain unmodified. 5318 * 5319 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5320 * Depending on setModMatch() one, none or multiple nodes are affected. 5321 * 5322 * @param $xPathQuery (string) xpath to the node (See note above). 5323 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5324 * the changes. A performance helper. See reindexNodeTree() 5325 * @return (bool) TRUE on success, FALSE on error; 5326 * @see setModMatch(), reindexNodeTree() 5327 */ 5328 function removeChild($xPathQuery, $autoReindex=TRUE) { 5329 $ThisFunctionName = 'removeChild'; 5330 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 5331 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 5332 if ($bDebugThisFunction) { 5333 echo "Node: $xPathQuery\n"; 5334 echo '<hr>'; 5335 } 5336 5337 $NULL = NULL; 5338 $status = FALSE; 5339 do { // try-block 5340 // Check for a valid xPathQuery 5341 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'removeChild'); 5342 if (sizeOf($xPathSet) === 0) { 5343 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 5344 break; // try-block 5345 } 5346 $mustReindex = FALSE; 5347 // Make chages from 'bottom-up'. In this manner the modifications will not affect itself. 5348 for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 5349 $absoluteXPath = $xPathSet[$i]; 5350 if (preg_match(';/attribute::;', $absoluteXPath)) { // Handle the case of an attribute node 5351 $xPath = $this->_prestr($absoluteXPath, '/attribute::'); // Get the path to the attribute node's parent. 5352 $attribute = $this->_afterstr($absoluteXPath, '/attribute::'); // Get the name of the attribute. 5353 unSet($this->nodeIndex[$xPath]['attributes'][$attribute]); // Unset the attribute 5354 if ($bDebugThisFunction) echo "We removed the attribute '$attribute' of node '$xPath'.\n"; 5355 continue; 5356 } 5357 // Otherwise remove the node by setting it to NULL. It will be removed on the next reindexNodeTree() call. 5358 $mustReindex = $autoReindex; 5359 // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc 5360 $this->_indexIsDirty = TRUE; 5361 5362 $theNode = $this->nodeIndex[$absoluteXPath]; 5363 $theNode['parentNode']['childNodes'][$theNode['pos']] =& $NULL; 5364 if ($bDebugThisFunction) echo "We removed the node '$absoluteXPath'.\n"; 5365 } 5366 // Reindex the node tree again 5367 if ($mustReindex) $this->reindexNodeTree(); 5368 $status = TRUE; 5369 } while(FALSE); 5370 5371 $this->_closeDebugFunction($ThisFunctionName, $status, $bDebugThisFunction); 5372 5373 return $status; 5374 } 5375 5376 /** 5377 * Replace a node with any data string. The $data is taken 1:1. 5378 * 5379 * This function will delete the node you define by $absoluteXPath (plus it's sub-nodes) and 5380 * substitute it by the string $text. Often used to push in not well formed HTML. 5381 * WARNING: 5382 * The $data is taken 1:1. 5383 * You are in charge that the data you enter is valid XML if you intend 5384 * to export and import the content again. 5385 * 5386 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5387 * Depending on setModMatch() one, none or multiple nodes are affected. 5388 * 5389 * @param $xPathQuery (string) xpath to the node (See note above). 5390 * @param $data (string) String containing the content to be set. *READONLY* 5391 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5392 * the changes. A performance helper. See reindexNodeTree() 5393 * @return (bool) TRUE on success, FALSE on error; 5394 * @see setModMatch(), replaceChild(), reindexNodeTree() 5395 */ 5396 function replaceChildByData($xPathQuery, $data, $autoReindex=TRUE) { 5397 $ThisFunctionName = 'replaceChildByData'; 5398 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 5399 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 5400 if ($bDebugThisFunction) { 5401 echo "Node: $xPathQuery\n"; 5402 } 5403 5404 $NULL = NULL; 5405 $status = FALSE; 5406 do { // try-block 5407 // Check for a valid xPathQuery 5408 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChildByData'); 5409 if (sizeOf($xPathSet) === 0) { 5410 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 5411 break; // try-block 5412 } 5413 $mustReindex = FALSE; 5414 // Make chages from 'bottom-up'. In this manner the modifications will not affect itself. 5415 for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 5416 $mustReindex = $autoReindex; 5417 // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc 5418 $this->_indexIsDirty = TRUE; 5419 5420 $absoluteXPath = $xPathSet[$i]; 5421 $theNode = $this->nodeIndex[$absoluteXPath]; 5422 $pos = $theNode['pos']; 5423 $theNode['parentNode']['textParts'][$pos] .= $data; 5424 $theNode['parentNode']['childNodes'][$pos] =& $NULL; 5425 if ($bDebugThisFunction) echo "We replaced the node '$absoluteXPath' with data.\n"; 5426 } 5427 // Reindex the node tree again 5428 if ($mustReindex) $this->reindexNodeTree(); 5429 $status = TRUE; 5430 } while(FALSE); 5431 5432 $this->_closeDebugFunction($ThisFunctionName, ($status) ? 'Success' : '!!! FAILD !!!', $bDebugThisFunction); 5433 5434 return $status; 5435 } 5436 5437 /** 5438 * Replace the node(s) that matches the xQuery with the passed node (or passed node-tree) 5439 * 5440 * If the passed node is a string it's assumed to be XML and replaceChildByXml() 5441 * will be called. 5442 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5443 * Depending on setModMatch() one, none or multiple nodes are affected. 5444 * 5445 * @param $xPathQuery (string) Xpath to the node being replaced. 5446 * @param $node (mixed) String or Array (Usually a String) 5447 * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>" 5448 * If array: A Node (can be a whole sub-tree) (See comment in header) 5449 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5450 * the changes. A performance helper. See reindexNodeTree() 5451 * @return (array) The last replaced $node (can be a whole sub-tree) 5452 * @see reindexNodeTree() 5453 */ 5454 function &replaceChild($xPathQuery, $node, $autoReindex=TRUE) { 5455 $NULL = NULL; 5456 if (is_string($node)) { 5457 if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error. 5458 return array(); 5459 } else { 5460 if (!($node = $this->_xml2Document($node))) return FALSE; 5461 } 5462 } 5463 5464 // Special case if it's 'super root'. We then have to take the child node == top node 5465 if (empty($node['parentNode'])) $node = $node['childNodes'][0]; 5466 5467 $status = FALSE; 5468 do { // try-block 5469 // Check for a valid xPathQuery 5470 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChild'); 5471 if (sizeOf($xPathSet) === 0) { 5472 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 5473 break; // try-block 5474 } 5475 $mustReindex = FALSE; 5476 5477 // Make chages from 'bottom-up'. In this manner the modifications will not affect itself. 5478 for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 5479 $mustReindex = $autoReindex; 5480 // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc 5481 $this->_indexIsDirty = TRUE; 5482 5483 $absoluteXPath = $xPathSet[$i]; 5484 $childNode =& $this->nodeIndex[$absoluteXPath]; 5485 $parentNode =& $childNode['parentNode']; 5486 $childNode['parentNode'] =& $NULL; 5487 $childPos = $childNode['pos']; 5488 $parentNode['childNodes'][$childPos] =& $this->cloneNode($node); 5489 } 5490 if ($mustReindex) $this->reindexNodeTree(); 5491 $status = TRUE; 5492 } while(FALSE); 5493 5494 if (!$status) return FALSE; 5495 return $childNode; 5496 } 5497 5498 /** 5499 * Insert passed node (or passed node-tree) at the node(s) that matches the xQuery. 5500 * 5501 * With parameters you can define if the 'hit'-node is shifted to the right or left 5502 * and if it's placed before of after the text-part. 5503 * Per derfault the 'hit'-node is shifted to the right and the node takes the place 5504 * the of the 'hit'-node. 5505 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5506 * Depending on setModMatch() one, none or multiple nodes are affected. 5507 * 5508 * E.g. Following is given: AAA[1] 5509 * / \ 5510 * ..BBB[1]..BBB[2] .. 5511 * 5512 * a) insertChild('/AAA[1]/BBB[2]', <node CCC>) 5513 * b) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE) 5514 * c) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE, $afterText=FALSE) 5515 * 5516 * a) b) c) 5517 * AAA[1] AAA[1] AAA[1] 5518 * / | \ / | \ / | \ 5519 * ..BBB[1]..CCC[1]BBB[2].. ..BBB[1]..BBB[2]..CCC[1] ..BBB[1]..BBB[2]CCC[1].. 5520 * 5521 * #### Do a complete review of the "(optional)" tag after several arguments. 5522 * 5523 * @param $xPathQuery (string) Xpath to the node to append. 5524 * @param $node (mixed) String or Array (Usually a String) 5525 * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>" 5526 * If array: A Node (can be a whole sub-tree) (See comment in header) 5527 * @param $shiftRight (bool) (optional, default=TRUE) Shift the target node to the right. 5528 * @param $afterText (bool) (optional, default=TRUE) Insert after the text. 5529 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5530 * the changes. A performance helper. See reindexNodeTree() 5531 * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly 5532 * appended nodes. That is: Array of paths if more then 1 node was added or 5533 * a single path string if only one node was added. 5534 * NOTE: If autoReindex is FALSE, then we can't return the *complete* path 5535 * as the exact doc-pos isn't available without reindexing. In that case we leave 5536 * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2] 5537 * @see appendChildByXml(), reindexNodeTree() 5538 */ 5539 function insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText=TRUE, $autoReindex=TRUE) { 5540 if (is_string($node)) { 5541 if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error. 5542 return FALSE; 5543 } else { 5544 if (!($node = $this->_xml2Document($node))) return FALSE; 5545 } 5546 } 5547 5548 // Special case if it's 'super root'. We then have to take the child node == top node 5549 if (empty($node['parentNode'])) $node = $node['childNodes'][0]; 5550 5551 // Check for a valid xPathQuery 5552 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'insertChild'); 5553 if (sizeOf($xPathSet) === 0) { 5554 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 5555 return FALSE; 5556 } 5557 $mustReindex = FALSE; 5558 $newNodes = array(); 5559 $result = array(); 5560 // Make chages from 'bottom-up'. In this manner the modifications will not affect itself. 5561 for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 5562 $absoluteXPath = $xPathSet[$i]; 5563 $childNode =& $this->nodeIndex[$absoluteXPath]; 5564 $parentNode =& $childNode['parentNode']; 5565 5566 // We can't insert at the super root or at the root. 5567 if (empty($absoluteXPath) || (!$parentNode['parentNode'])) { 5568 $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE); 5569 return FALSE; 5570 } 5571 5572 $mustReindex = $autoReindex; 5573 // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc 5574 $this->_indexIsDirty = TRUE; 5575 5576 //Special case: It not possible to add siblings to the top node. 5577 if (empty($parentNode['name'])) continue; 5578 $newNode =& $this->cloneNode($node); 5579 $pos = $shiftRight ? $childNode['pos'] : $childNode['pos']+1; 5580 $parentNode['childNodes'] = array_merge( 5581 array_slice($parentNode['childNodes'], 0, $pos), 5582 array(&$newNode), 5583 array_slice($parentNode['childNodes'], $pos) 5584 ); 5585 $pos += $afterText ? 1 : 0; 5586 $parentNode['textParts'] = array_merge( 5587 array_slice($parentNode['textParts'], 0, $pos), 5588 array(''), 5589 array_slice($parentNode['textParts'], $pos) 5590 ); 5591 5592 // We are going from bottom to top, but the user will want results from top to bottom. 5593 if ($mustReindex) { 5594 // We'll have to wait till after the reindex to get the full path to this new node. 5595 $newNodes[] = &$newNode; 5596 } else { 5597 // If we are reindexing the tree later, then we can't return the user any 5598 // useful results, so we just return them the count. 5599 $newNodePath = $parentNode['xpath'].'/'.$newNode['name']; 5600 array_unshift($result, $newNodePath); 5601 } 5602 } 5603 if ($mustReindex) { 5604 $this->reindexNodeTree(); 5605 // Now we must fill in the result array. Because until now we did not 5606 // know what contextpos our newly added entries had, just their pos within 5607 // the siblings. 5608 foreach ($newNodes as $newNode) { 5609 array_unshift($result, $newNode['xpath']); 5610 } 5611 } 5612 if (count($result) == 1) $result = $result[0]; 5613 return $result; 5614 } 5615 5616 /** 5617 * Appends a child to anothers children. 5618 * 5619 * If you intend to do a lot of appending, you should leave autoIndex as FALSE 5620 * and then call reindexNodeTree() when you are finished all the appending. 5621 * 5622 * @param $xPathQuery (string) Xpath to the node to append to. 5623 * @param $node (mixed) String or Array (Usually a String) 5624 * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>" 5625 * If array: A Node (can be a whole sub-tree) (See comment in header) 5626 * @param $afterText (bool) (optional, default=FALSE) Insert after the text. 5627 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5628 * the changes. A performance helper. See reindexNodeTree() 5629 * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly 5630 * appended nodes. That is: Array of paths if more then 1 node was added or 5631 * a single path string if only one node was added. 5632 * NOTE: If autoReindex is FALSE, then we can't return the *complete* path 5633 * as the exact doc-pos isn't available without reindexing. In that case we leave 5634 * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2] 5635 * @see insertChild(), reindexNodeTree() 5636 */ 5637 function appendChild($xPathQuery, $node, $afterText=FALSE, $autoReindex=TRUE) { 5638 if (is_string($node)) { 5639 if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error. 5640 return FALSE; 5641 } else { 5642 if (!($node = $this->_xml2Document($node))) return FALSE; 5643 } 5644 } 5645 5646 // Special case if it's 'super root'. We then have to take the child node == top node 5647 if (empty($node['parentNode'])) $node = $node['childNodes'][0]; 5648 5649 // Check for a valid xPathQuery 5650 $xPathSet = $this->_resolveXPathQueryForNodeMod($xPathQuery, 'appendChild'); 5651 if (sizeOf($xPathSet) === 0) return FALSE; 5652 5653 $mustReindex = FALSE; 5654 $newNodes = array(); 5655 $result = array(); 5656 // Make chages from 'bottom-up'. In this manner the modifications will not affect itself. 5657 for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) { 5658 $mustReindex = $autoReindex; 5659 // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc 5660 $this->_indexIsDirty = TRUE; 5661 5662 $absoluteXPath = $xPathSet[$i]; 5663 $parentNode =& $this->nodeIndex[$absoluteXPath]; 5664 $newNode =& $this->cloneNode($node); 5665 $parentNode['childNodes'][] =& $newNode; 5666 $pos = count($parentNode['textParts']); 5667 $pos -= $afterText ? 0 : 1; 5668 $parentNode['textParts'] = array_merge( 5669 array_slice($parentNode['textParts'], 0, $pos), 5670 array(''), 5671 array_slice($parentNode['textParts'], $pos) 5672 ); 5673 // We are going from bottom to top, but the user will want results from top to bottom. 5674 if ($mustReindex) { 5675 // We'll have to wait till after the reindex to get the full path to this new node. 5676 $newNodes[] = &$newNode; 5677 } else { 5678 // If we are reindexing the tree later, then we can't return the user any 5679 // useful results, so we just return them the count. 5680 array_unshift($result, "$absoluteXPath/{$newNode['name']}"); 5681 } 5682 } 5683 if ($mustReindex) { 5684 $this->reindexNodeTree(); 5685 // Now we must fill in the result array. Because until now we did not 5686 // know what contextpos our newly added entries had, just their pos within 5687 // the siblings. 5688 foreach ($newNodes as $newNode) { 5689 array_unshift($result, $newNode['xpath']); 5690 } 5691 } 5692 if (count($result) == 1) $result = $result[0]; 5693 return $result; 5694 } 5695 5696 /** 5697 * Inserts a node before the reference node with the same parent. 5698 * 5699 * If you intend to do a lot of appending, you should leave autoIndex as FALSE 5700 * and then call reindexNodeTree() when you are finished all the appending. 5701 * 5702 * @param $xPathQuery (string) Xpath to the node to insert new node before 5703 * @param $node (mixed) String or Array (Usually a String) 5704 * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>" 5705 * If array: A Node (can be a whole sub-tree) (See comment in header) 5706 * @param $afterText (bool) (optional, default=FLASE) Insert after the text. 5707 * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect 5708 * the changes. A performance helper. See reindexNodeTree() 5709 * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly 5710 * appended nodes. That is: Array of paths if more then 1 node was added or 5711 * a single path string if only one node was added. 5712 * NOTE: If autoReindex is FALSE, then we can't return the *complete* path 5713 * as the exact doc-pos isn't available without reindexing. In that case we leave 5714 * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2] 5715 * @see reindexNodeTree() 5716 */ 5717 function insertBefore($xPathQuery, $node, $afterText=TRUE, $autoReindex=TRUE) { 5718 return $this->insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText, $autoReindex); 5719 } 5720 5721 5722 //----------------------------------------------------------------------------------------- 5723 // XPath ------ Attribute Set/Get ------ 5724 //----------------------------------------------------------------------------------------- 5725 5726 /** 5727 * Retrieves a dedecated attribute value or a hash-array of all attributes of a node. 5728 * 5729 * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results 5730 * to *one* xpath. If the second param $attrName is not set, a hash-array of all attributes 5731 * of that node is returned. 5732 * 5733 * Optionally you may pass an attrubute name in $attrName and the function will return the 5734 * string value of that attribute. 5735 * 5736 * @param $absoluteXPath (string) Full xpath OR a xpath-query that results to *one* xpath. 5737 * @param $attrName (string) (Optional) The name of the attribute. See above. 5738 * @return (mixed) hash-array or a string of attributes depending if the 5739 * parameter $attrName was set (see above). FALSE if the 5740 * node or attribute couldn't be found. 5741 * @see setAttribute(), removeAttribute() 5742 */ 5743 function getAttributes($absoluteXPath, $attrName=NULL) { 5744 // Numpty check 5745 if (!isSet($this->nodeIndex[$absoluteXPath])) { 5746 $xPathSet = $this->_resolveXPathQuery($absoluteXPath,'getAttributes'); 5747 if (empty($xPathSet)) return FALSE; 5748 // only use the first entry 5749 $absoluteXPath = $xPathSet[0]; 5750 } 5751 if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) { 5752 // Case in-sensitive 5753 $attrName = strtoupper($attrName); 5754 } 5755 5756 // Return the complete list or just the desired element 5757 if (is_null($attrName)) { 5758 return $this->nodeIndex[$absoluteXPath]['attributes']; 5759 } elseif (isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attrName])) { 5760 return $this->nodeIndex[$absoluteXPath]['attributes'][$attrName]; 5761 } 5762 return FALSE; 5763 } 5764 5765 /** 5766 * Set attributes of a node(s). 5767 * 5768 * This method sets a number single attributes. An existing attribute is overwritten (default) 5769 * with the new value, but setting the last param to FALSE will prevent overwritten. 5770 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5771 * Depending on setModMatch() one, none or multiple nodes are affected. 5772 * 5773 * @param $xPathQuery (string) xpath to the node (See note above). 5774 * @param $name (string) Attribute name. 5775 * @param $value (string) Attribute value. 5776 * @param $overwrite (bool) If the attribute is already set we overwrite it (see text above) 5777 * @return (bool) TRUE on success, FALSE on failure. 5778 * @see getAttributes(), removeAttribute() 5779 */ 5780 function setAttribute($xPathQuery, $name, $value, $overwrite=TRUE) { 5781 return $this->setAttributes($xPathQuery, array($name => $value), $overwrite); 5782 } 5783 5784 /** 5785 * Version of setAttribute() that sets multiple attributes to node(s). 5786 * 5787 * This method sets a number of attributes. Existing attributes are overwritten (default) 5788 * with the new values, but setting the last param to FALSE will prevent overwritten. 5789 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5790 * Depending on setModMatch() one, none or multiple nodes are affected. 5791 * 5792 * @param $xPathQuery (string) xpath to the node (See note above). 5793 * @param $attributes (array) associative array of attributes to set. 5794 * @param $overwrite (bool) If the attributes are already set we overwrite them (see text above) 5795 * @return (bool) TRUE on success, FALSE otherwise 5796 * @see setAttribute(), getAttributes(), removeAttribute() 5797 */ 5798 function setAttributes($xPathQuery, $attributes, $overwrite=TRUE) { 5799 $status = FALSE; 5800 do { // try-block 5801 // The attributes parameter should be an associative array. 5802 if (!is_array($attributes)) break; // try-block 5803 5804 // Check for a valid xPathQuery 5805 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'setAttributes'); 5806 foreach($xPathSet as $absoluteXPath) { 5807 // Add the attributes to the node. 5808 $theNode =& $this->nodeIndex[$absoluteXPath]; 5809 if (empty($theNode['attributes'])) { 5810 $this->nodeIndex[$absoluteXPath]['attributes'] = $attributes; 5811 } else { 5812 $theNode['attributes'] = $overwrite ? array_merge($theNode['attributes'],$attributes) : array_merge($attributes, $theNode['attributes']); 5813 } 5814 } 5815 $status = TRUE; 5816 } while(FALSE); // END try-block 5817 5818 return $status; 5819 } 5820 5821 /** 5822 * Removes an attribute of a node(s). 5823 * 5824 * This method removes *ALL* attributres per default unless the second parameter $attrList is set. 5825 * $attrList can be either a single attr-name as string OR a vector of attr-names as array. 5826 * E.g. 5827 * removeAttribute(<xPath>); # will remove *ALL* attributes. 5828 * removeAttribute(<xPath>, 'A'); # will only remove attributes called 'A'. 5829 * removeAttribute(<xPath>, array('A_1','A_2')); # will remove attribute 'A_1' and 'A_2'. 5830 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5831 * Depending on setModMatch() one, none or multiple nodes are affected. 5832 * 5833 * @param $xPathQuery (string) xpath to the node (See note above). 5834 * @param $attrList (mixed) (optional) if not set will delete *all* (see text above) 5835 * @return (bool) TRUE on success, FALSE if the node couldn't be found 5836 * @see getAttributes(), setAttribute() 5837 */ 5838 function removeAttribute($xPathQuery, $attrList=NULL) { 5839 // Check for a valid xPathQuery 5840 $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'removeAttribute'); 5841 5842 if (!empty($attrList) AND is_string($attrList)) $attrList = array($attrList); 5843 if (!is_array($attrList)) return FALSE; 5844 5845 foreach($xPathSet as $absoluteXPath) { 5846 // If the attribute parameter wasn't set then remove all the attributes 5847 if ($attrList[0] === NULL) { 5848 $this->nodeIndex[$absoluteXPath]['attributes'] = array(); 5849 continue; 5850 } 5851 // Remove all the elements in the array then. 5852 foreach($attrList as $name) { 5853 unset($this->nodeIndex[$absoluteXPath]['attributes'][$name]); 5854 } 5855 } 5856 return TRUE; 5857 } 5858 5859 //----------------------------------------------------------------------------------------- 5860 // XPath ------ Text Set/Get ------ 5861 //----------------------------------------------------------------------------------------- 5862 5863 /** 5864 * Retrieve all the text from a node as a single string. 5865 * 5866 * Sample 5867 * Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA> 5868 * Return of getData('/AA[1]') would be: " This is sometext " 5869 * The first param $xPathQuery must be a valid xpath OR a xpath-query that 5870 * results to *one* xpath. 5871 * 5872 * @param $xPathQuery (string) xpath to the node - resolves to *one* xpath. 5873 * @return (mixed) The returned string (see above), FALSE if the node 5874 * couldn't be found or is not unique. 5875 * @see getDataParts() 5876 */ 5877 function getData($xPathQuery) { 5878 $aDataParts = $this->getDataParts($xPathQuery); 5879 if ($aDataParts === FALSE) return FALSE; 5880 return implode('', $aDataParts); 5881 } 5882 5883 /** 5884 * Retrieve all the text from a node as a vector of strings 5885 * 5886 * Where each element of the array was interrupted by a non-text child element. 5887 * 5888 * Sample 5889 * Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA> 5890 * Return of getDataParts('/AA[1]') would be: array([0]=>' This ', [1]=>'is ', [2]=>' some', [3]=>'text '); 5891 * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results 5892 * to *one* xpath. 5893 * 5894 * @param $xPathQuery (string) xpath to the node - resolves to *one* xpath. 5895 * @return (mixed) The returned array (see above), or FALSE if node is not 5896 * found or is not unique. 5897 * @see getData() 5898 */ 5899 function getDataParts($xPathQuery) { 5900 // Resolve xPath argument 5901 $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'getDataParts'); 5902 if (1 !== ($setSize=count($xPathSet))) { 5903 $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery) . "Not unique xpath-query, matched {$setSize}-times.", __LINE__, __FILE__, FALSE); 5904 return FALSE; 5905 } 5906 $absoluteXPath = $xPathSet[0]; 5907 // Is it an attribute node? 5908 if (preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches)) { 5909 $absoluteXPath = $matches[1]; 5910 $attribute = $matches[2]; 5911 if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) { 5912 $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE); 5913 continue; 5914 } 5915 return array($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]); 5916 } else if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) { 5917 $absoluteXPath = $matches[1]; 5918 $textPartNr = $matches[2]; 5919 return array($this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr]); 5920 } else { 5921 return $this->nodeIndex[$absoluteXPath]['textParts']; 5922 } 5923 } 5924 5925 /** 5926 * Retrieves a sub string of a text-part OR attribute-value. 5927 * 5928 * This method retrieves the sub string of a specific text-part OR (if the 5929 * $absoluteXPath references an attribute) the the sub string of the attribute value. 5930 * If no 'direct referencing' is used (Xpath ends with text()[<part-number>]), then 5931 * the first text-part of the node ist returned (if exsiting). 5932 * 5933 * @param $absoluteXPath (string) Xpath to the node (See note above). 5934 * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr()) 5935 * @param $count (number) (optional, default is ALL) Character count (Just like PHP's substr()) 5936 * @return (mixed) The sub string, FALSE if not found or on error 5937 * @see XPathEngine::wholeText(), PHP's substr() 5938 */ 5939 function substringData($absoluteXPath, $offset = 0, $count = NULL) { 5940 if (!($text = $this->wholeText($absoluteXPath))) return FALSE; 5941 if (is_null($count)) { 5942 return substr($text, $offset); 5943 } else { 5944 return substr($text, $offset, $count); 5945 } 5946 } 5947 5948 /** 5949 * Replace a sub string of a text-part OR attribute-value. 5950 * 5951 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5952 * Depending on setModMatch() one, none or multiple nodes are affected. 5953 * 5954 * @param $xPathQuery (string) xpath to the node (See note above). 5955 * @param $replacement (string) The string to replace with. 5956 * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace ()) 5957 * @param $count (number) (optional, default is 0=ALL) Character count (Just like PHP's substr_replace()) 5958 * @param $textPartNr (int) (optional) (see _getTextSet() ) 5959 * @return (bool) The new string value on success, FALSE if not found or on error 5960 * @see substringData() 5961 */ 5962 function replaceData($xPathQuery, $replacement, $offset = 0, $count = 0, $textPartNr=1) { 5963 if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE; 5964 $tSize=sizeOf($textSet); 5965 for ($i=0; $i<$tSize; $i++) { 5966 if ($count) { 5967 $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset, $count); 5968 } else { 5969 $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset); 5970 } 5971 } 5972 return TRUE; 5973 } 5974 5975 /** 5976 * Insert a sub string in a text-part OR attribute-value. 5977 * 5978 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5979 * Depending on setModMatch() one, none or multiple nodes are affected. 5980 * 5981 * @param $xPathQuery (string) xpath to the node (See note above). 5982 * @param $data (string) The string to replace with. 5983 * @param $offset (int) (optional, default is 0) Offset at which to insert the data. 5984 * @return (bool) The new string on success, FALSE if not found or on error 5985 * @see replaceData() 5986 */ 5987 function insertData($xPathQuery, $data, $offset=0) { 5988 return $this->replaceData($xPathQuery, $data, $offset, 0); 5989 } 5990 5991 /** 5992 * Append text data to the end of the text for an attribute OR node text-part. 5993 * 5994 * This method adds content to a node. If it's an attribute node, then 5995 * the value of the attribute will be set, otherwise the passed data will append to 5996 * character data of the node text-part. Per default the first text-part is taken. 5997 * 5998 * NOTE: When passing a xpath-query instead of an abs. Xpath. 5999 * Depending on setModMatch() one, none or multiple nodes are affected. 6000 * 6001 * @param $xPathQuery (string) to the node(s) (See note above). 6002 * @param $data (string) String containing the content to be added. 6003 * @param $textPartNr (int) (optional, default is 1) (see _getTextSet()) 6004 * @return (bool) TRUE on success, otherwise FALSE 6005 * @see _getTextSet() 6006 */ 6007 function appendData($xPathQuery, $data, $textPartNr=1) { 6008 if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE; 6009 $tSize=sizeOf($textSet); 6010 for ($i=0; $i<$tSize; $i++) { 6011 $textSet[$i] .= $data; 6012 } 6013 return TRUE; 6014 } 6015 6016 /** 6017 * Delete the data of a node. 6018 * 6019 * This method deletes content of a node. If it's an attribute node, then 6020 * the value of the attribute will be removed, otherwise the node text-part. 6021 * will be deleted. Per default the first text-part is deleted. 6022 * 6023 * NOTE: When passing a xpath-query instead of an abs. Xpath. 6024 * Depending on setModMatch() one, none or multiple nodes are affected. 6025 * 6026 * @param $xPathQuery (string) to the node(s) (See note above). 6027 * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace()) 6028 * @param $count (number) (optional, default is 0=ALL) Character count. (Just like PHP's substr_replace()) 6029 * @param $textPartNr (int) (optional, default is 0) the text part to delete (see _getTextSet()) 6030 * @return (bool) TRUE on success, otherwise FALSE 6031 * @see _getTextSet() 6032 */ 6033 function deleteData($xPathQuery, $offset=0, $count=0, $textPartNr=1) { 6034 if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE; 6035 $tSize=sizeOf($textSet); 6036 for ($i=0; $i<$tSize; $i++) { 6037 if (!$count) 6038 $textSet[$i] = ""; 6039 else 6040 $textSet[$i] = substr_replace($textSet[$i],'', $offset, $count); 6041 } 6042 return TRUE; 6043 } 6044 6045 //----------------------------------------------------------------------------------------- 6046 // XPath ------ Help Stuff ------ 6047 //----------------------------------------------------------------------------------------- 6048 6049 /** 6050 * Parse the XML to a node-tree. A so called 'document' 6051 * 6052 * @param $xmlString (string) The string to turn into a document node. 6053 * @return (&array) a node-tree 6054 */ 6055 function &_xml2Document($xmlString) { 6056 $xmlOptions = array( 6057 XML_OPTION_CASE_FOLDING => $this->getProperties('caseFolding'), 6058 XML_OPTION_SKIP_WHITE => $this->getProperties('skipWhiteSpaces') 6059 ); 6060 $xmlParser =& new XPathEngine($xmlOptions); 6061 $xmlParser->setVerbose($this->properties['verboseLevel']); 6062 // Parse the XML string 6063 if (!$xmlParser->importFromString($xmlString)) { 6064 $this->_displayError($xmlParser->getLastError(), __LINE__, __FILE__, FALSE); 6065 return FALSE; 6066 } 6067 return $xmlParser->getNode('/'); 6068 } 6069 6070 /** 6071 * Get a reference-list to node text part(s) or node attribute(s). 6072 * 6073 * If the Xquery references an attribute(s) (Xquery ends with attribute::), 6074 * then the text value of the node-attribute(s) is/are returned. 6075 * Otherwise the Xquery is referencing to text part(s) of node(s). This can be either a 6076 * direct reference to text part(s) (Xquery ends with text()[<nr>]) or indirect reference 6077 * (a simple Xquery to node(s)). 6078 * 1) Direct Reference (Xquery ends with text()[<part-number>]): 6079 * If the 'part-number' is omitted, the first text-part is assumed; starting by 1. 6080 * Negative numbers are allowed, where -1 is the last text-part a.s.o. 6081 * 2) Indirect Reference (a simple Xquery to node(s)): 6082 * Default is to return the first text part(s). Optionally you may pass a parameter 6083 * $textPartNr to define the text-part you want; starting by 1. 6084 * Negative numbers are allowed, where -1 is the last text-part a.s.o. 6085 * 6086 * NOTE I : The returned vector is a set of references to the text parts / attributes. 6087 * This is handy, if you wish to modify the contents. 6088 * NOTE II: text-part numbers out of range will not be in the list 6089 * NOTE III:Instead of an absolute xpath you may also pass a xpath-query. 6090 * Depending on setModMatch() one, none or multiple nodes are affected. 6091 * 6092 * @param $xPathQuery (string) xpath to the node (See note above). 6093 * @param $textPartNr (int) String containing the content to be set. 6094 * @return (mixed) A vector of *references* to the text that match, or 6095 * FALSE on error 6096 * @see XPathEngine::wholeText() 6097 */ 6098 function _getTextSet($xPathQuery, $textPartNr=1) { 6099 $ThisFunctionName = '_getTextSet'; 6100 $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions); 6101 $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction); 6102 if ($bDebugThisFunction) { 6103 echo "Node: $xPathQuery\n"; 6104 echo "Text Part Number: $textPartNr\n"; 6105 echo "<hr>"; 6106 } 6107 6108 $status = FALSE; 6109 $funcName = '_getTextSet'; 6110 $textSet = array(); 6111 6112 do { // try-block 6113 // Check if it's a Xpath reference to an attribut(s). Xpath ends with attribute::) 6114 if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, $matches)) { 6115 $xPathQuery = $matches[1]; 6116 $attribute = $matches[3]; 6117 // Quick out 6118 if (isSet($this->nodeIndex[$xPathQuery])) { 6119 $xPathSet[] = $xPathQuery; 6120 } else { 6121 // Try to evaluate the absoluteXPath (since it seems to be an Xquery and not an abs. Xpath) 6122 $xPathSet = $this->_resolveXPathQuery("$xPathQuery/attribute::$attribute", $funcName); 6123 } 6124 foreach($xPathSet as $absoluteXPath) { 6125 preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches); 6126 $absoluteXPath = $matches[1]; 6127 $attribute = $matches[2]; 6128 if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) { 6129 $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE); 6130 continue; 6131 } 6132 $textSet[] =& $this->nodes[$absoluteXPath]['attributes'][$attribute]; 6133 } 6134 $status = TRUE; 6135 break; // try-block 6136 } 6137 6138 // Check if it's a Xpath reference direct to a text-part(s). (xpath ends with text()[<part-number>]) 6139 if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) { 6140 $xPathQuery = $matches[1]; 6141 // default to the first text node if a text node was not specified 6142 $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1; 6143 // Quick check 6144 if (isSet($this->nodeIndex[$xPathQuery])) { 6145 $xPathSet[] = $xPathQuery; 6146 } else { 6147 // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath) 6148 $xPathSet = $this->_resolveXPathQuery("$xPathQuery/text()[$textPartNr]", $funcName); 6149 } 6150 } 6151 else { 6152 // At this point we have been given an xpath with neither a 'text()' or 'attribute::' axis at the end 6153 // So this means to get the text-part of the node. If parameter $textPartNr was not set, use the last 6154 // text-part. 6155 if (isSet($this->nodeIndex[$xPathQuery])) { 6156 $xPathSet[] = $xPathQuery; 6157 } else { 6158 // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath) 6159 $xPathSet = $this->_resolveXPathQuery($xPathQuery, $funcName); 6160 } 6161 } 6162 6163 if ($bDebugThisFunction) { 6164 echo "Looking up paths for:\n"; 6165 print_r($xPathSet); 6166 } 6167 6168 // Now fetch all text-parts that match. (May be 0,1 or many) 6169 foreach($xPathSet as $absoluteXPath) { 6170 unset($text); 6171 if ($text =& $this->wholeText($absoluteXPath, $textPartNr)) { 6172 $textSet[] =& $text; 6173 } else { 6174 // The node does not yet have any text, so we have to add a '' string so that 6175 // if we insert or replace to it, then we'll actually have something to op on. 6176 $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1] = ''; 6177 $textSet[] =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1]; 6178 } 6179 } 6180 6181 $status = TRUE; 6182 } while (FALSE); // END try-block 6183 6184 if (!$status) $result = FALSE; 6185 else $result = $textSet; 6186 6187 $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction); 6188 6189 return $result; 6190 } 6191 6192 6193 /** 6194 * Resolves an xPathQuery vector for a node op for modification 6195 * 6196 * It is possible to create a brand new object, and try to append and insert nodes 6197 * into it, so this is a version of _resolveXPathQuery() that will autocreate the 6198 * super root if it detects that it is not present and the $xPathQuery is empty. 6199 * 6200 * Also it demands that there be at least one node returned, and displays a suitable 6201 * error message if the returned xPathSet does not contain any nodes. 6202 * 6203 * @param $xPathQuery (string) An xpath query targeting a single node. If empty() 6204 * returns the root node and auto creates the root node 6205 * if it doesn't exist. 6206 * @param $function (string) The function in which this check was called 6207 * @return (array) Vector of $absoluteXPath's (May be empty) 6208 * @see _resolveXPathQuery() 6209 */ 6210 function _resolveXPathQueryForNodeMod($xPathQuery, $functionName) { 6211 $xPathSet = array(); 6212 if (empty($xPathQuery)) { 6213 // You can append even if the root node doesn't exist. 6214 if (!isset($this->nodeIndex[$xPathQuery])) $this->_createSuperRoot(); 6215 $xPathSet[] = ''; 6216 // However, you can only append to the super root, if there isn't already a root entry. 6217 $rootNodes = $this->_resolveXPathQuery('/*','appendChild'); 6218 if (count($rootNodes) !== 0) { 6219 $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE); 6220 return array(); 6221 } 6222 } else { 6223 $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild'); 6224 if (sizeOf($xPathSet) === 0) { 6225 $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE); 6226 return array(); 6227 } 6228 } 6229 return $xPathSet; 6230 } 6231 6232 /** 6233 * Resolves an xPathQuery vector depending on the property['modMatch'] 6234 * 6235 * To: 6236 * - all matches, 6237 * - the first 6238 * - none (If the query matches more then one node.) 6239 * see setModMatch() for details 6240 * 6241 * @param $xPathQuery (string) An xpath query targeting a single node. If empty() 6242 * returns the root node (if it exists). 6243 * @param $function (string) The function in which this check was called 6244 * @return (array) Vector of $absoluteXPath's (May be empty) 6245 * @see setModMatch() 6246 */ 6247 function _resolveXPathQuery($xPathQuery, $function) { 6248 $xPathSet = array(); 6249 do { // try-block 6250 if (isSet($this->nodeIndex[$xPathQuery])) { 6251 $xPathSet[] = $xPathQuery; 6252 break; // try-block 6253 } 6254 if (empty($xPathQuery)) break; // try-block 6255 if (substr($xPathQuery, -1) === '/') break; // If the xPathQuery ends with '/' then it cannot be a good query. 6256 // If this xPathQuery is not absolute then attempt to evaluate it 6257 $xPathSet = $this->match($xPathQuery); 6258 6259 $resultSize = sizeOf($xPathSet); 6260 switch($this->properties['modMatch']) { 6261 case XPATH_QUERYHIT_UNIQUE : 6262 if ($resultSize >1) { 6263 $xPathSet = array(); 6264 if ($this->properties['verboseLevel']) $this->_displayError("Canceled function '{$function}'. The query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_UNIQUE.", __LINE__, __FILE__, FALSE); 6265 } 6266 break; 6267 case XPATH_QUERYHIT_FIRST : 6268 if ($resultSize >1) { 6269 $xPathSet = array($xPathSet[0]); 6270 if ($this->properties['verboseLevel']) $this->_displayError("Only modified first node in function '{$function}' because the query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_FIRST.", __LINE__, __FILE__, FALSE); 6271 } 6272 break; 6273 default: ; // DO NOTHING 6274 } 6275 } while (FALSE); 6276 6277 if ($this->properties['verboseLevel'] >= 2) $this->_displayMessage("'{$xPathQuery}' parameter from '{$function}' returned the following nodes: ".(count($xPathSet)?implode('<br>', $xPathSet):'[none]'), __LINE__, __FILE__); 6278 return $xPathSet; 6279 } 6280 } // END OF CLASS XPath 6281 6282 // ----------------------------------------------------------------------------------------- 6283 // ----------------------------------------------------------------------------------------- 6284 // ----------------------------------------------------------------------------------------- 6285 // ----------------------------------------------------------------------------------------- 6286 6287 /************************************************************************************************** 6288 // Usage Sample: 6289 // ------------- 6290 // Following code will give you an idea how to work with PHP.XPath. It's a working sample 6291 // to help you get started. :o) 6292 // Take the comment tags away and run this file. 6293 **************************************************************************************************/ 6294 6295 /** 6296 * Produces a short title line. 6297 */ 6298 function _title($title) { 6299 echo "<br><hr><b>" . htmlspecialchars($title) . "</b><hr>\n"; 6300 } 6301 6302 $self = isSet($_SERVER) ? $_SERVER['PHP_SELF'] : $PHP_SELF; 6303 if (basename($self) == 'XPath.class.php') { 6304 // The sampe source: 6305 $q = '?'; 6306 $xmlSource = <<< EOD 6307 <{$q}Process_Instruction test="© All right reserved" {$q}> 6308 <AAA foo="bar"> ,,1,, 6309 ..1.. <![CDATA[ bla bla 6310 newLine blo blo ]]> 6311 <BBB foo="bar"> 6312 ..2.. 6313 </BBB>..3..<CC/> ..4..</AAA> 6314 EOD; 6315 6316 // The sample code: 6317 $xmlOptions = array(XML_OPTION_CASE_FOLDING => TRUE, XML_OPTION_SKIP_WHITE => TRUE); 6318 $xPath =& new XPath(FALSE, $xmlOptions); 6319 //$xPath->bDebugXmlParse = TRUE; 6320 if (!$xPath->importFromString($xmlSource)) { echo $xPath->getLastError(); exit; } 6321 6322 _title("Following was imported:"); 6323 echo $xPath->exportAsHtml(); 6324 6325 _title("Get some content"); 6326 echo "Last text part in <AAA>: '" . $xPath->wholeText('/AAA[1]', -1) ."'<br>\n"; 6327 echo "All the text in <AAA>: '" . $xPath->wholeText('/AAA[1]') ."'<br>\n"; 6328 echo "The attibute value in <BBB> using getAttributes('/AAA[1]/BBB[1]', 'FOO'): '" . $xPath->getAttributes('/AAA[1]', 'FOO') ."'<br>\n"; 6329 echo "The attibute value in <BBB> using getData('/AAA[1]/@FOO'): '" . $xPath->getData('/AAA[1]/@FOO') ."'<br>\n"; 6330 6331 _title("Append some additional XML below /AAA/BBB:"); 6332 $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node </CCC>', $afterText=FALSE); 6333 $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node </CCC>', $afterText=TRUE); 6334 $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node </CCC>', $afterText=TRUE); 6335 echo $xPath->exportAsHtml(); 6336 6337 _title("Insert some additional XML below <AAA>:"); 6338 $xPath->reindexNodeTree(); 6339 $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 1. Insert new node </BB>', $shiftRight=TRUE, $afterText=TRUE); 6340 $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 2. Insert new node </BB>', $shiftRight=FALSE, $afterText=TRUE); 6341 $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 3. Insert new node </BB>', $shiftRight=FALSE, $afterText=FALSE); 6342 echo $xPath->exportAsHtml(); 6343 6344 _title("Replace the last <BB> node with new XML data '<DDD> Replaced last BB </DDD>':"); 6345 $xPath->reindexNodeTree(); 6346 $xPath->replaceChild('/AAA[1]/BB[last()]', '<DDD> Replaced last BB </DDD>', $afterText=FALSE); 6347 echo $xPath->exportAsHtml(); 6348 6349 _title("Replace second <BB> node with normal text"); 6350 $xPath->reindexNodeTree(); 6351 $xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"'); 6352 echo $xPath->exportAsHtml(); 6353 } 6354 6355 ?>
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
| Généré le : Sun Feb 25 17:20:01 2007 | par Balluche grâce à PHPXref 0.7 |