PageRenderTime 137ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 1ms

/modules/System/includes/XPath.class.php

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