/modules/System/includes/XPath.class.php
PHP | 6355 lines | 3144 code | 532 blank | 2679 comment | 670 complexity | e34a8630fd3d4a3a20450f2d4e8e5fe2 MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0
- <?php
- /**
- * Php.XPath
- *
- * +======================================================================================================+
- * | A php class for searching an XML document using XPath, and making modifications using a DOM
- * | style API. Does not require the DOM XML PHP library.
- * |
- * +======================================================================================================+
- * | What Is XPath:
- * | --------------
- * | - "What SQL is for a relational database, XPath is for an XML document." -- Sam Blum
- * | - "The primary purpose of XPath is to address parts of an XML document. In support of this
- * | primary purpose, it also provides basic facilities for manipulting it." -- W3C
- * |
- * | XPath in action and a very nice intro is under:
- * | http://www.zvon.org/xxl/XPathTutorial/General/examples.html
- * | Specs Can be found under:
- * | http://www.w3.org/TR/xpath W3C XPath Recommendation
- * | http://www.w3.org/TR/xpath20 W3C XPath Recommendation
- * |
- * | NOTE: Most of the XPath-spec has been realized, but not all. Usually this should not be
- * | problem as the missing part is either rarely used or it's simpler to do with PHP itself.
- * +------------------------------------------------------------------------------------------------------+
- * | Requires PHP version 4.0.5 and up
- * +------------------------------------------------------------------------------------------------------+
- * | Main Active Authors:
- * | --------------------
- * | Nigel Swinson <nigelswinson@users.sourceforge.net>
- * | Started around 2001-07, saved phpxml from near death and renamed to Php.XPath
- * | Restructured XPath code to stay in line with XPath spec.
- * | Sam Blum <bs_php@infeer.com>
- * | Started around 2001-09 1st major restruct (V2.0) and testbench initiator.
- * | 2nd (V3.0) major rewrite in 2002-02
- * | Daniel Allen <bigredlinux@yahoo.com>
- * | Started around 2001-10 working to make Php.XPath adhere to specs
- * | Main Former Author: Michael P. Mehl <mpm@phpxml.org>
- * | Inital creator of V 1.0. Stoped activities around 2001-03
- * +------------------------------------------------------------------------------------------------------+
- * | Code Structure:
- * | --------------_
- * | The class is split into 3 main objects. To keep usability easy all 3
- * | objects are in this file (but may be split in 3 file in future).
- * | +-------------+
- * | | XPathBase | XPathBase holds general and debugging functions.
- * | +------+------+
- * | v
- * | +-------------+ XPathEngine is the implementation of the W3C XPath spec. It contains the
- * | | XPathEngine | XML-import (parser), -export and can handle xPathQueries. It's a fully
- * | +------+------+ functional class but has no functions to modify the XML-document (see following).
- * | v
- * | +-------------+
- * | | XPath | XPath extends the functionality with actions to modify the XML-document.
- * | +-------------+ We tryed to implement a DOM - like interface.
- * +------------------------------------------------------------------------------------------------------+
- * | Usage:
- * | ------
- * | Scroll to the end of this php file and you will find a short sample code to get you started
- * +------------------------------------------------------------------------------------------------------+
- * | Glossary:
- * | ---------
- * | To understand how to use the functions and to pass the right parameters, read following:
- * |
- * | Document: (full node tree, XML-tree)
- * | After a XML-source has been imported and parsed, it's stored as a tree of nodes sometimes
- * | refered to as 'document'.
- * |
- * | AbsoluteXPath: (xPath, xPathSet)
- * | A absolute XPath is a string. It 'points' to *one* node in the XML-document. We use the
- * | term 'absolute' to emphasise that it is not an xPath-query (see xPathQuery). A valid xPath
- * | has the form like '/AAA[1]/BBB[2]/CCC[1]'. Usually functions that require a node (see Node)
- * | will also accept an abs. XPath.
- * |
- * | Node: (node, nodeSet, node-tree)
- * | Some funtions require or return a node (or a whole node-tree). Nodes are only used with the
- * | XPath-interface and have an internal structure. Every node in a XML document has a unique
- * | corresponding abs. xPath. That's why public functions that accept a node, will usually also
- * | accept a abs. xPath (a string) 'pointing' to an existing node (see absolutXPath).
- * |
- * | XPathQuery: (xquery, query)
- * | A xPath-query is a string that is matched against the XML-document. The result of the match
- * | is a xPathSet (vector of xPath's). It's always possible to pass a single absoluteXPath
- * | instead of a xPath-query. A valid xPathQuery could look like this:
- * | '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to learn more).
- * |
- * |
- * +------------------------------------------------------------------------------------------------------+
- * | Internals:
- * | ----------
- * | - The Node Tree
- * | -------------
- * | A central role of the package is how the XML-data is stored. The whole data is in a node-tree.
- * | A node can be seen as the equvalent to a tag in the XML soure with some extra info.
- * | For instance the following XML
- * | <AAA foo="x">***<BBB/><CCC/>**<BBB/>*</AAA>
- * | Would produce folowing node-tree:
- * | 'super-root' <-- $nodeRoot (Very handy)
- * | |
- * | 'depth' 0 AAA[1] <-- top node. The 'textParts' of this node would be
- * | / | \ 'textParts' => array('***','','**','*')
- * | 'depth' 1 BBB[1] CCC[1] BBB[2] (NOTE: Is always size of child nodes+1)
- * | - The Node
- * | --------
- * | The node itself is an structure desiged mainly to be used in connection with the interface of PHP.XPath.
- * | That means it's possible for functions to return a sub-node-tree that can be used as input of an other
- * | PHP.XPath function.
- * |
- * | The main structure of a node is:
- * | $node = array(
- * | 'name' => '', # The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
- * | 'attributes' => array(), # The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
- * | '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')
- * | 'childNodes' => array(), # Array of refences (pointers) to child nodes.
- * |
- * | For optimisation reasions some additional data is stored in the node too:
- * | 'parentNode' => NULL # Reference (pointer) to the parent node (or NULL if it's 'super root')
- * | 'depth' => 0, # The tag depth (or tree level) starting with the root tag at 0.
- * | 'pos' => 0, # Is the zero-based position this node has in the parent's 'childNodes'-list.
- * | 'contextPos' => 1, # Is the one-based position this node has by counting the siblings tags (tags with same name)
- * | 'xpath' => '' # Is the abs. XPath to this node.
- * | 'generated_id'=> '' # The id returned for this node by generate-id() (attribute and text nodes not supported)
- * |
- * | - The NodeIndex
- * | -------------
- * | Every node in the tree has an absolute XPath. E.g '/AAA[1]/BBB[2]' the $nodeIndex is a hash array
- * | to all the nodes in the node-tree. The key used is the absolute XPath (a string).
- * |
- * +------------------------------------------------------------------------------------------------------+
- * | License:
- * | --------
- * | The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License");
- * | you may not use this file except in compliance with the License. You may obtain a copy of the
- * | License at http://www.mozilla.org/MPL/
- * |
- * | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY
- * | OF ANY KIND, either express or implied. See the License for the specific language governing
- * | rights and limitations under the License.
- * |
- * | The Original Code is <phpXML/>.
- * |
- * | The Initial Developer of the Original Code is Michael P. Mehl. Portions created by Michael
- * | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved.
- * |
- * | Contributor(s): N.Swinson / S.Blum / D.Allen
- * |
- * | Alternatively, the contents of this file may be used under the terms of either of the GNU
- * | General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public
- * | License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the
- * | LGPL License are applicable instead of those above. If you wish to allow use of your version
- * | of this file only under the terms of the GPL or the LGPL License and not to allow others to
- * | use your version of this file under the MPL, indicate your decision by deleting the
- * | provisions above and replace them with the notice and other provisions required by the
- * | GPL or the LGPL License. If you do not delete the provisions above, a recipient may use
- * | your version of this file under either the MPL, the GPL or the LGPL License.
- * |
- * +======================================================================================================+
- *
- * @author S.Blum / N.Swinson / D.Allen / (P.Mehl)
- * @link http://sourceforge.net/projects/phpxpath/
- * @version 3.5
- * @CVS $Id: XPath.class.php,v 1.9 2005/11/16 17:26:05 bigmichi1 Exp $
- */
- // Include guard, protects file being included twice
- $ConstantName = 'INCLUDED_'.strtoupper(__FILE__);
- if (defined($ConstantName)) return;
- define($ConstantName,1, TRUE);
- /************************************************************************************************
- * ===============================================================================================
- * X P a t h B a s e - Class
- * ===============================================================================================
- ************************************************************************************************/
- class XPathBase {
- var $_lastError;
-
- // As debugging of the xml parse is spread across several functions, we need to make this a member.
- var $bDebugXmlParse = FALSE;
- // do we want to do profiling?
- var $bClassProfiling = FALSE;
- // Used to help navigate through the begin/end debug calls
- var $iDebugNextLinkNumber = 1;
- var $aDebugOpenLinks = array();
- var $aDebugFunctions = array(
- //'_evaluatePrimaryExpr',
- //'_evaluateExpr',
- //'_evaluateStep',
- //'_checkPredicates',
- //'_evaluateFunction',
- //'_evaluateOperator',
- //'_evaluatePathExpr',
- );
- /**
- * Constructor
- */
- function XPathBase() {
- # $this->bDebugXmlParse = TRUE;
- $this->properties['verboseLevel'] = 1; // 0=silent, 1 and above produce verbose output (an echo to screen).
-
- if (!isSet($_ENV)) { // Note: $_ENV introduced in 4.1.0. In earlier versions, use $HTTP_ENV_VARS.
- $_ENV = $GLOBALS['HTTP_ENV_VARS'];
- }
-
- // Windows 95/98 do not support file locking. Detecting OS (Operation System) and setting the
- // properties['OS_supports_flock'] to FALSE if win 95/98 is detected.
- // This will surpress the file locking error reported from win 98 users when exportToFile() is called.
- // May have to add more OS's to the list in future (Macs?).
- // ### Note that it's only the FAT and NFS file systems that are really a problem. NTFS and
- // the latest php libs do support flock()
- $_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS';
- switch ($_ENV['OS']) {
- case 'Windows_95':
- case 'Windows_98':
- case 'Unknown OS':
- // should catch Mac OS X compatible environment
- if (!empty($_SERVER['SERVER_SOFTWARE'])
- && preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) {
- // fall-through
- } else {
- $this->properties['OS_supports_flock'] = FALSE;
- break;
- }
- default:
- $this->properties['OS_supports_flock'] = TRUE;
- }
- }
-
-
- /**
- * Resets the object so it's able to take a new xml sting/file
- *
- * Constructing objects is slow. If you can, reuse ones that you have used already
- * by using this reset() function.
- */
- function reset() {
- $this->_lastError = '';
- }
-
- //-----------------------------------------------------------------------------------------
- // XPathBase ------ Helpers ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * This method checks the right amount and match of brackets
- *
- * @param $term (string) String in which is checked.
- * @return (bool) TRUE: OK / FALSE: KO
- */
- function _bracketsCheck($term) {
- $leng = strlen($term);
- $brackets = 0;
- $bracketMisscount = $bracketMissmatsh = FALSE;
- $stack = array();
- for ($i=0; $i<$leng; $i++) {
- switch ($term[$i]) {
- case '(' :
- case '[' :
- $stack[$brackets] = $term[$i];
- $brackets++;
- break;
- case ')':
- $brackets--;
- if ($brackets<0) {
- $bracketMisscount = TRUE;
- break 2;
- }
- if ($stack[$brackets] != '(') {
- $bracketMissmatsh = TRUE;
- break 2;
- }
- break;
- case ']' :
- $brackets--;
- if ($brackets<0) {
- $bracketMisscount = TRUE;
- break 2;
- }
- if ($stack[$brackets] != '[') {
- $bracketMissmatsh = TRUE;
- break 2;
- }
- break;
- }
- }
- // Check whether we had a valid number of brackets.
- if ($brackets != 0) $bracketMisscount = TRUE;
- if ($bracketMisscount || $bracketMissmatsh) {
- return FALSE;
- }
- return TRUE;
- }
-
- /**
- * Looks for a string within another string -- BUT the search-string must be located *outside* of any brackets.
- *
- * This method looks for a string within another string. Brackets in the
- * string the method is looking through will be respected, which means that
- * only if the string the method is looking for is located outside of
- * brackets, the search will be successful.
- *
- * @param $term (string) String in which the search shall take place.
- * @param $expression (string) String that should be searched.
- * @return (int) This method returns -1 if no string was found,
- * otherwise the offset at which the string was found.
- */
- function _searchString($term, $expression) {
- $bracketCounter = 0; // Record where we are in the brackets.
- $leng = strlen($term);
- $exprLeng = strlen($expression);
- for ($i=0; $i<$leng; $i++) {
- $char = $term[$i];
- if ($char=='(' || $char=='[') {
- $bracketCounter++;
- continue;
- }
- elseif ($char==')' || $char==']') {
- $bracketCounter--;
- }
- if ($bracketCounter == 0) {
- // Check whether we can find the expression at this index.
- if (substr($term, $i, $exprLeng) == $expression) return $i;
- }
- }
- // Nothing was found.
- return (-1);
- }
-
- /**
- * Split a string by a searator-string -- BUT the separator-string must be located *outside* of any brackets.
- *
- * Returns an array of strings, each of which is a substring of string formed
- * by splitting it on boundaries formed by the string separator.
- *
- * @param $separator (string) String that should be searched.
- * @param $term (string) String in which the search shall take place.
- * @return (array) see above
- */
- function _bracketExplode($separator, $term) {
- // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
- // but as this is a private function we should be ok.
- $resultArr = array();
- $bracketCounter = 0; // Record where we are in the brackets.
- do { // BEGIN try block
- // Check if any separator is in the term
- $sepLeng = strlen($separator);
- if (strpos($term, $separator)===FALSE) { // no separator found so end now
- $resultArr[] = $term;
- break; // try-block
- }
-
- // Make a substitute separator out of 'unused chars'.
- $substituteSep = str_repeat(chr(2), $sepLeng);
-
- // Now determine the first bracket '(' or '['.
- $tmp1 = strpos($term, '(');
- $tmp2 = strpos($term, '[');
- if ($tmp1===FALSE) {
- $startAt = (int)$tmp2;
- } elseif ($tmp2===FALSE) {
- $startAt = (int)$tmp1;
- } else {
- $startAt = min($tmp1, $tmp2);
- }
-
- // Get prefix string part before the first bracket.
- $preStr = substr($term, 0, $startAt);
- // Substitute separator in prefix string.
- $preStr = str_replace($separator, $substituteSep, $preStr);
-
- // Now get the rest-string (postfix string)
- $postStr = substr($term, $startAt);
- // Go all the way through the rest-string.
- $strLeng = strlen($postStr);
- for ($i=0; $i < $strLeng; $i++) {
- $char = $postStr[$i];
- // Spot (,),[,] and modify our bracket counter. Note there is an
- // assumption here that you don't have a string(with[mis)matched]brackets.
- // This should be ok as the dodgy string will be detected elsewhere.
- if ($char=='(' || $char=='[') {
- $bracketCounter++;
- continue;
- }
- elseif ($char==')' || $char==']') {
- $bracketCounter--;
- }
- // If no brackets surround us check for separator
- if ($bracketCounter == 0) {
- // Check whether we can find the expression starting at this index.
- if ((substr($postStr, $i, $sepLeng) == $separator)) {
- // Substitute the found separator
- for ($j=0; $j<$sepLeng; $j++) {
- $postStr[$i+$j] = $substituteSep[$j];
- }
- }
- }
- }
- // Now explod using the substitute separator as key.
- $resultArr = explode($substituteSep, $preStr . $postStr);
- } while (FALSE); // End try block
- // Return the results that we found. May be a array with 1 entry.
- return $resultArr;
- }
- /**
- * Split a string at it's groups, ie bracketed expressions
- *
- * Returns an array of strings, when concatenated together would produce the original
- * string. ie a(b)cde(f)(g) would map to:
- * array ('a', '(b)', cde', '(f)', '(g)')
- *
- * @param $string (string) The string to process
- * @param $open (string) The substring for the open of a group
- * @param $close (string) The substring for the close of a group
- * @return (array) The parsed string, see above
- */
- function _getEndGroups($string, $open='[', $close=']') {
- // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
- // but as this is a private function we should be ok.
- $resultArr = array();
- do { // BEGIN try block
- // Check if we have both an open and a close tag
- if (empty($open) and empty($close)) { // no separator found so end now
- $resultArr[] = $string;
- break; // try-block
- }
- if (empty($string)) {
- $resultArr[] = $string;
- break; // try-block
- }
-
- while (!empty($string)) {
- // Now determine the first bracket '(' or '['.
- $openPos = strpos($string, $open);
- $closePos = strpos($string, $close);
- if ($openPos===FALSE || $closePos===FALSE) {
- // Oh, no more groups to be found then. Quit
- $resultArr[] = $string;
- break;
- }
- // Sanity check
- if ($openPos > $closePos) {
- // Malformed string, dump the rest and quit.
- $resultArr[] = $string;
- break;
- }
- // Get prefix string part before the first bracket.
- $preStr = substr($string, 0, $openPos);
- // This is the first string that will go in our output
- if (!empty($preStr))
- $resultArr[] = $preStr;
- // Skip over what we've proceed, including the open char
- $string = substr($string, $openPos + 1 - strlen($string));
- // Find the next open char and adjust our close char
- //echo "close: $closePos\nopen: $openPos\n\n";
- $closePos -= $openPos + 1;
- $openPos = strpos($string, $open);
- //echo "close: $closePos\nopen: $openPos\n\n";
- // While we have found nesting...
- while ($openPos && $closePos && ($closePos > $openPos)) {
- // Find another close pos after the one we are looking at
- $closePos = strpos($string, $close, $closePos + 1);
- // And skip our open
- $openPos = strpos($string, $open, $openPos + 1);
- }
- //echo "close: $closePos\nopen: $openPos\n\n";
- // If we now have a close pos, then it's the end of the group.
- if ($closePos === FALSE) {
- // We didn't... so bail dumping what was left
- $resultArr[] = $open.$string;
- break;
- }
- // We did, so we can extract the group
- $resultArr[] = $open.substr($string, 0, $closePos + 1);
- // Skip what we have processed
- $string = substr($string, $closePos + 1);
- }
- } while (FALSE); // End try block
- // Return the results that we found. May be a array with 1 entry.
- return $resultArr;
- }
-
- /**
- * Retrieves a substring before a delimiter.
- *
- * This method retrieves everything from a string before a given delimiter,
- * not including the delimiter.
- *
- * @param $string (string) String, from which the substring should be extracted.
- * @param $delimiter (string) String containing the delimiter to use.
- * @return (string) Substring from the original string before the delimiter.
- * @see _afterstr()
- */
- function _prestr(&$string, $delimiter, $offset=0) {
- // Return the substring.
- $offset = ($offset<0) ? 0 : $offset;
- $pos = strpos($string, $delimiter, $offset);
- if ($pos===FALSE) return $string; else return substr($string, 0, $pos);
- }
-
- /**
- * Retrieves a substring after a delimiter.
- *
- * This method retrieves everything from a string after a given delimiter,
- * not including the delimiter.
- *
- * @param $string (string) String, from which the substring should be extracted.
- * @param $delimiter (string) String containing the delimiter to use.
- * @return (string) Substring from the original string after the delimiter.
- * @see _prestr()
- */
- function _afterstr($string, $delimiter, $offset=0) {
- $offset = ($offset<0) ? 0 : $offset;
- // Return the substring.
- return substr($string, strpos($string, $delimiter, $offset) + strlen($delimiter));
- }
-
- //-----------------------------------------------------------------------------------------
- // XPathBase ------ Debug Stuff ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Alter the verbose (error) level reporting.
- *
- * Pass an int. >0 to turn on, 0 to turn off. The higher the number, the
- * higher the level of verbosity. By default, the class has a verbose level
- * of 1.
- *
- * @param $levelOfVerbosity (int) default is 1 = on
- */
- function setVerbose($levelOfVerbosity = 1) {
- $level = -1;
- if ($levelOfVerbosity === TRUE) {
- $level = 1;
- } elseif ($levelOfVerbosity === FALSE) {
- $level = 0;
- } elseif (is_numeric($levelOfVerbosity)) {
- $level = $levelOfVerbosity;
- }
- if ($level >= 0) $this->properties['verboseLevel'] = $levelOfVerbosity;
- }
-
- /**
- * Returns the last occured error message.
- *
- * @access public
- * @return string (may be empty if there was no error at all)
- * @see _setLastError(), _lastError
- */
- function getLastError() {
- return $this->_lastError;
- }
-
- /**
- * Creates a textual error message and sets it.
- *
- * example: 'XPath error in THIS_FILE_NAME:LINE. Message: YOUR_MESSAGE';
- *
- * I don't think the message should include any markup because not everyone wants to debug
- * into the browser window.
- *
- * You should call _displayError() rather than _setLastError() if you would like the message,
- * dependant on their verbose settings, echoed to the screen.
- *
- * @param $message (string) a textual error message default is ''
- * @param $line (int) the line number where the error occured, use __LINE__
- * @see getLastError()
- */
- function _setLastError($message='', $line='-', $file='-') {
- $this->_lastError = 'XPath error in ' . basename($file) . ':' . $line . '. Message: ' . $message;
- }
-
- /**
- * Displays an error message.
- *
- * This method displays an error messages depending on the users verbose settings
- * and sets the last error message.
- *
- * If also possibly stops the execution of the script.
- * ### Terminate should not be allowed --fab. Should it?? N.S.
- *
- * @param $message (string) Error message to be displayed.
- * @param $lineNumber (int) line number given by __LINE__
- * @param $terminate (bool) (default TURE) End the execution of this script.
- */
- function _displayError($message, $lineNumber='-', $file='-', $terminate=TRUE) {
- // Display the error message.
- $err = '<b>XPath error in '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
- $this->_setLastError($message, $lineNumber, $file);
- if (($this->properties['verboseLevel'] > 0) OR ($terminate)) echo $err;
- // End the execution of this script.
- if ($terminate) exit;
- }
- /**
- * Displays a diagnostic message
- *
- * This method displays an error messages
- *
- * @param $message (string) Error message to be displayed.
- * @param $lineNumber (int) line number given by __LINE__
- */
- function _displayMessage($message, $lineNumber='-', $file='-') {
- // Display the error message.
- $err = '<b>XPath message from '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
- if ($this->properties['verboseLevel'] > 0) echo $err;
- }
-
- /**
- * Called to begin the debug run of a function.
- *
- * This method starts a <DIV><PRE> tag so that the entry to this function
- * is clear to the debugging user. Call _closeDebugFunction() at the
- * end of the function to create a clean box round the function call.
- *
- * @author Nigel Swinson <nigelswinson@users.sourceforge.net>
- * @author Sam Blum <bs_php@infeer.com>
- * @param $functionName (string) the name of the function we are beginning to debug
- * @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise
- * @return (array) the output from the microtime() function.
- * @see _closeDebugFunction()
- */
- function _beginDebugFunction($functionName, $bDebugFlag) {
- if ($bDebugFlag) {
- $fileName = basename(__FILE__);
- static $color = array('green','blue','red','lime','fuchsia', 'aqua');
- static $colIndex = -1;
- $colIndex++;
- echo '<div style="clear:both" align="left"> ';
- echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">';
- echo '<a style="float:right;margin:5px" name="'.$this->iDebugNextLinkNumber.'Open" href="#'.$this->iDebugNextLinkNumber.'Close">Function Close '.$this->iDebugNextLinkNumber.'</a>';
- echo "<STRONG>{$fileName} : {$functionName}</STRONG>";
- echo '<hr style="clear:both">';
- array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber);
- $this->iDebugNextLinkNumber++;
- }
- if ($this->bClassProfiling)
- $this->_ProfBegin($FunctionName);
- return TRUE;
- }
-
- /**
- * Called to end the debug run of a function.
- *
- * This method ends a <DIV><PRE> block and reports the time since $aStartTime
- * is clear to the debugging user.
- *
- * @author Nigel Swinson <nigelswinson@users.sourceforge.net>
- * @param $functionName (string) the name of the function we are beginning to debug
- * @param $return_value (mixed) the return value from the function call that
- * we are debugging
- * @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise
- */
- function _closeDebugFunction($functionName, $returnValue = "", $bDebugFlag) {
- if ($bDebugFlag) {
- echo "<hr>";
- $iOpenLinkNumber = array_pop($this->aDebugOpenLinks);
- echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>';
- if (isSet($returnValue)) {
- if (is_array($returnValue))
- echo "Return Value: ".print_r($returnValue)."\n";
- else if (is_numeric($returnValue))
- echo "Return Value: ".(string)$returnValue."\n";
- else if (is_bool($returnValue))
- echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n";
- else
- echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n";
- }
- echo '<br style="clear:both">';
- echo " \n</pre></div>";
- }
-
- if ($this->bClassProfiling)
- $this->_ProfEnd($FunctionName);
- return TRUE;
- }
-
- /**
- * Profile begin call
- */
- function _ProfBegin($sonFuncName) {
- static $entryTmpl = array ( 'start' => array(),
- 'recursiveCount' => 0,
- 'totTime' => 0,
- 'callCount' => 0 );
- $now = explode(' ', microtime());
- if (empty($this->callStack)) {
- $fatherFuncName = '';
- }
- else {
- $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
- $fatherEntry = &$this->profile[$fatherFuncName];
- }
- $this->callStack[] = $sonFuncName;
- if (!isSet($this->profile[$sonFuncName])) {
- $this->profile[$sonFuncName] = $entryTmpl;
- }
- $sonEntry = &$this->profile[$sonFuncName];
- $sonEntry['callCount']++;
- // if we call the t's the same function let the time run, otherwise sum up
- if ($fatherFuncName == $sonFuncName) {
- $sonEntry['recursiveCount']++;
- }
- if (!empty($fatherFuncName)) {
- $last = $fatherEntry['start'];
- $fatherEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
- $fatherEntry['start'] = 0;
- }
- $sonEntry['start'] = explode(' ', microtime());
- }
- /**
- * Profile end call
- */
- function _ProfEnd($sonFuncName) {
- $now = explode(' ', microtime());
- array_pop($this->callStack);
- if (empty($this->callStack)) {
- $fatherFuncName = '';
- }
- else {
- $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
- $fatherEntry = &$this->profile[$fatherFuncName];
- }
- $sonEntry = &$this->profile[$sonFuncName];
- if (empty($sonEntry)) {
- echo "ERROR in profEnd(): '$funcNam' not in list. Seams it was never started ;o)";
- }
- $last = $sonEntry['start'];
- $sonEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
- $sonEntry['start'] = 0;
- if (!empty($fatherEntry)) $fatherEntry['start'] = explode(' ', microtime());
- }
- /**
- * Show profile gathered so far as HTML table
- */
- function _ProfileToHtml() {
- $sortArr = array();
- if (empty($this->profile)) return '';
- reset($this->profile);
- while (list($funcName) = each($this->profile)) {
- $sortArrKey[] = $this->profile[$funcName]['totTime'];
- $sortArrVal[] = $funcName;
- }
- //echo '<pre>';var_dump($sortArrVal);echo '</pre>';
- array_multisort ($sortArrKey, SORT_DESC, $sortArrVal );
- //echo '<pre>';var_dump($sortArrVal);echo '</pre>';
- $totTime = 0;
- $size = sizeOf($sortArrVal);
- for ($i=0; $i<$size; $i++) {
- $funcName = &$sortArrVal[$i];
- $totTime += $this->profile[$funcName]['totTime'];
- }
- $out = '<table border="1">';
- $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>';
- for ($i=0; $i<$size; $i++) {
- $funcName = &$sortArrVal[$i];
- $row = &$this->profile[$funcName];
- $procent = round($row['totTime']*100/$totTime);
- if ($procent>20) $bgc = '#ff8080';
- elseif ($procent>15) $bgc = '#ff9999';
- elseif ($procent>10) $bgc = '#ffcccc';
- elseif ($procent>5) $bgc = '#ffffcc';
- else $bgc = '#66ff99';
- $out .="<tr align='center' bgcolor='{$bgc}'>";
- $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>';
- $out .='</tr>';
- }
- $out .= '</table> Total Time [' . $totTime/10 .'ms]' ;
- echo $out;
- return TRUE;
- }
- /**
- * Echo an XPath context for diagnostic purposes
- *
- * @param $context (array) An XPath context
- */
- function _printContext($context) {
- echo "{$context['nodePath']}({$context['pos']}/{$context['size']})";
- }
-
- /**
- * This is a debug helper function. It dumps the node-tree as HTML
- *
- * *QUICK AND DIRTY*. Needs some polishing.
- *
- * @param $node (array) A node
- * @param $indent (string) (optional, default=''). For internal recursive calls.
- */
- function _treeDump($node, $indent = '') {
- $out = '';
-
- // Get rid of recursion
- $parentName = empty($node['parentNode']) ? "SUPER ROOT" : $node['parentNode']['name'];
- unset($node['parentNode']);
- $node['parentNode'] = $parentName ;
-
- $out .= "NODE[{$node['name']}]\n";
-
- foreach($node as $key => $val) {
- if ($key === 'childNodes') continue;
- if (is_Array($val)) {
- $out .= $indent . " [{$key}]\n" . arrayToStr($val, $indent . ' ');
- } else {
- $out .= $indent . " [{$key}] => '{$val}' \n";
- }
- }
-
- if (!empty($node['childNodes'])) {
- $out .= $indent . " ['childNodes'] (Size = ".sizeOf($node['childNodes']).")\n";
- foreach($node['childNodes'] as $key => $childNode) {
- $out .= $indent . " [$key] => " . $this->_treeDump($childNode, $indent . ' ') . "\n";
- }
- }
-
- if (empty($indent)) {
- return "<pre>" . htmlspecialchars($out) . "</pre>";
- }
- return $out;
- }
- } // END OF CLASS XPathBase
- /************************************************************************************************
- * ===============================================================================================
- * X P a t h E n g i n e - Class
- * ===============================================================================================
- ************************************************************************************************/
- class XPathEngine extends XPathBase {
-
- // List of supported XPath axes.
- // What a stupid idea from W3C to take axes name containing a '-' (dash)
- // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
- // We will then do the same on the users Xpath querys
- // -sibling => _sibling
- // -or- => _or_
- //
- // This array contains a list of all valid axes that can be evaluated in an
- // XPath query.
- var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 'descendant',
- 'descendant_or_self', 'following', 'following_sibling',
- 'namespace', 'parent', 'preceding', 'preceding_sibling', 'self'
- );
-
- // List of supported XPath functions.
- // What a stupid idea from W3C to take function name containing a '-' (dash)
- // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
- // We will then do the same on the users Xpath querys
- // starts-with => starts_with
- // substring-before => substring_before
- // substring-after => substring_after
- // string-length => string_length
- //
- // This array contains a list of all valid functions that can be evaluated
- // in an XPath query.
- var $functions = array ( 'last', 'position', 'count', 'id', 'name',
- 'string', 'concat', 'starts_with', 'contains', 'substring_before',
- 'substring_after', 'substring', 'string_length', 'normalize_space', 'translate',
- 'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor',
- 'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' );
-
- // List of supported XPath operators.
- //
- // This array contains a list of all valid operators that can be evaluated
- // in a predicate of an XPath query. The list is ordered by the
- // precedence of the operators (lowest precedence first).
- var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
- '+', '-', '*', ' div ', ' mod ', ' | ');
- // List of literals from the xPath string.
- var $axPathLiterals = array();
-
- // The index and tree that is created during the analysis of an XML source.
- var $nodeIndex = array();
- var $nodeRoot = array();
- var $emptyNode = array(
- 'name' => '', // The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
- 'attributes' => array(), // The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
- 'childNodes' => array(), // Array of pointers to child nodes.
- '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')
- 'parentNode' => NULL, // Pointer to parent node or NULL if this node is the 'super root'
- //-- *!* Following vars are set by the indexer and is for optimisation only *!*
- 'depth' => 0, // The tag depth (or tree level) starting with the root tag at 0.
- 'pos' => 0, // Is the zero-based position this node has in the parents 'childNodes'-list.
- 'contextPos' => 1, // Is the one-based position this node has by counting the siblings tags (tags with same name)
- 'xpath' => '' // Is the abs. XPath to this node.
- );
- var $_indexIsDirty = FALSE;
-
- // These variable used during the parse XML source
- var $nodeStack = array(); // The elements that we have still to close.
- var $parseStackIndex = 0; // The current element of the nodeStack[] that we are adding to while
- // parsing an XML source. Corresponds to the depth of the xml node.
- // in our input data.
- var $parseOptions = array(); // Used to set the PHP's XML parser options (see xml_parser_set_option)
- var $parsedTextLocation = ''; // A reference to where we have to put char data collected during XML parsing
- var $parsInCData = 0 ; // Is >0 when we are inside a CDATA section.
- var $parseSkipWhiteCache = 0; // A cache of the skip whitespace parse option to speed up the parse.
- // This is the array of error strings, to keep consistency.
- var $errorStrings = array(
- 'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* describe a node in the xml document.",
- 'NoNodeMatch' => "The supplied xPath-query '%s' does not match *any* node in the xml document.",
- 'RootNodeAlreadyExists' => "An xml document may have only one root node."
- );
-
- /**
- * Constructor
- *
- * Optionally you may call this constructor with the XML-filename to parse and the
- * XML option vector. Each of the entries in the option vector will be passed to
- * xml_parser_set_option().
- *
- * A option vector sample:
- * $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE,
- * XML_OPTION_SKIP_WHITE => TRUE);
- *
- * @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>,
- * <optionID>=><value>, ...). See PHP's
- * xml_parser_set_option() docu for a list of possible
- * options.
- * @see importFromFile(), importFromString(), setXmlOptions()
- */
- function XPathEngine($userXmlOptions=array()) {
- parent::XPathBase();
- // Default to not folding case
- $this->parseOptions[XML_OPTION_CASE_FOLDING] = FALSE;
- // And not skipping whitespace
- $this->parseOptions[XML_OPTION_SKIP_WHITE] = FALSE;
-
- // Now merge in the overrides.
- // Don't use PHP's array_merge!
- if (is_array($userXmlOptions)) {
- foreach($userXmlOptions as $key => $val) $this->parseOptions[$key] = $val;
- }
- }
-
- /**
- * Resets the object so it's able to take a new xml sting/file
- *
- * Constructing objects is slow. If you can, reuse ones that you have used already
- * by using this reset() function.
- */
- function reset() {
- parent::reset();
- $this->properties['xmlFile'] = '';
- $this->parseStackIndex = 0;
- $this->parsedTextLocation = '';
- $this->parsInCData = 0;
- $this->nodeIndex = array();
- $this->nodeRoot = array();
- $this->nodeStack = array();
- $this->aLiterals = array();
- $this->_indexIsDirty = FALSE;
- }
-
-
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ Get / Set Stuff ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Returns the property/ies you want.
- *
- * if $param is not given, all properties will be returned in a hash.
- *
- * @param $param (string) the property you want the value of, or NULL for all the properties
- * @return (mixed) string OR hash of all params, or NULL on an unknown parameter.
- */
- function getProperties($param=NULL) {
- $this->properties['hasContent'] = !empty($this->nodeRoot);
- $this->properties['caseFolding'] = $this->parseOptions[XML_OPTION_CASE_FOLDING];
- $this->properties['skipWhiteSpaces'] = $this->parseOptions[XML_OPTION_SKIP_WHITE];
-
- if (empty($param)) return $this->properties;
-
- if (isSet($this->properties[$param])) {
- return $this->properties[$param];
- } else {
- return NULL;
- }
- }
-
- /**
- * Set an xml_parser_set_option()
- *
- * @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE)
- * @param $value (int) The option value.
- * @see XML parser functions in PHP doc
- */
- function setXmlOption($optionID, $value) {
- if (!is_numeric($optionID)) return;
- $this->parseOptions[$optionID] = $value;
- }
- /**
- * Sets a number of xml_parser_set_option()s
- *
- * @param $userXmlOptions (array) An array of parser options.
- * @see setXmlOption
- */
- function setXmlOptions($userXmlOptions=array()) {
- if (!is_array($userXmlOptions)) return;
- foreach($userXmlOptions as $key => $val) {
- $this->setXmlOption($key, $val);
- }
- }
-
- /**
- * Alternative way to control whether case-folding is enabled for this XML parser.
- *
- * Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE)
- *
- * When it comes to XML, case-folding simply means uppercasing all tag-
- * and attribute-names (NOT the content) if set to TRUE. Note if you
- * have this option set, then your XPath queries will also be case folded
- * for you.
- *
- * @param $onOff (bool) (default TRUE)
- * @see XML parser functions in PHP doc
- */
- function setCaseFolding($onOff=TRUE) {
- $this->parseOptions[XML_OPTION_CASE_FOLDING] = $onOff;
- }
-
- /**
- * Alternative way to control whether skip-white-spaces is enabled for this XML parser.
- *
- * Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE)
- *
- * When it comes to XML, skip-white-spaces will trim the tag content.
- * An XML file with no whitespace will be faster to process, but will make
- * your data less human readable when you come to write it out.
- *
- * Running with this option on will slow the class down, so if you want to
- * speed up your XML, then run it through once skipping white-spaces, then
- * write out the new version of your XML without whitespace, then use the
- * new XML file with skip whitespaces turned off.
- *
- * @param $onOff (bool) (default TRUE)
- * @see XML parser functions in PHP doc
- */
- function setSkipWhiteSpaces($onOff=TRUE) {
- $this->parseOptions[XML_OPTION_SKIP_WHITE] = $onOff;
- }
-
- /**
- * Get the node defined by the $absoluteXPath.
- *
- * @param $absoluteXPath (string) (optional, default is 'super-root') xpath to the node.
- * @return (array) The node, or FALSE if the node wasn't found.
- */
- function &getNode($absoluteXPath='') {
- if ($absoluteXPath==='/') $absoluteXPath = '';
- if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE;
- if ($this->_indexIsDirty) $this->reindexNodeTree();
- return $this->nodeIndex[$absoluteXPath];
- }
- /**
- * Get a the content of a node text part or node attribute.
- *
- * If the absolute Xpath references an attribute (Xpath ends with @ or attribute::),
- * then the text value of that node-attribute is returned.
- * Otherwise the Xpath is referencing a text part of the node. This can be either a
- * direct reference to a text part (Xpath ends with text()[<nr>]) or indirect reference
- * (a simple abs. Xpath to a node).
- * 1) Direct Reference (xpath ends with text()[<part-number>]):
- * If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
- * Negative numbers are allowed, where -1 is the last text-part a.s.o.
- * 2) Indirect Reference (a simple abs. Xpath to a node):
- * Default is to return the *whole text*; that is the concated text-parts of the matching
- * node. (NOTE that only in this case you'll only get a copy and changes to the returned
- * value wounld have no effect). Optionally you may pass a parameter
- * $textPartNr to define the text-part you want; starting by 1.
- * Negative numbers are allowed, where -1 is the last text-part a.s.o.
- *
- * NOTE I : The returned value can be fetched by reference
- * E.g. $text =& wholeText(). If you wish to modify the text.
- * NOTE II: text-part numbers out of range will return FALSE
- * SIDENOTE:The function name is a suggestion from W3C in the XPath specification level 3.
- *
- * @param $absoluteXPath (string) xpath to the node (See above).
- * @param $textPartNr (int) If referring to a node, specifies which text part
- * to query.
- * @return (&string) A *reference* to the text if the node that the other
- * parameters describe or FALSE if the node is not found.
- */
- function &wholeText($absoluteXPath, $textPartNr=NULL) {
- $status = FALSE;
- $text = NULL;
- if ($this->_indexIsDirty) $this->reindexNodeTree();
-
- do { // try-block
- if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, $matches)) {
- $absoluteXPath = $matches[1];
- $attribute = $matches[3];
- if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
- $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $text =& $this->nodeIndex[$absoluteXPath]['attributes'][$attribute];
- $status = TRUE;
- break; // try-block
- }
-
- // Xpath contains a 'text()'-function, thus goes right to a text node. If so interpret the Xpath.
- if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, $matches)) {
- $absoluteXPath = $matches[1];
-
- if (!isSet($this->nodeIndex[$absoluteXPath])) {
- $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- // Get the amount of the text parts in the node.
- $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
- // default to the first text node if a text node was not specified
- $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
- // Support negative indexes like -1 === last a.s.o.
- if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
- if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
- $this->_displayError("The $absoluteXPath/text()[$textPartNr] value isn't a NODE in this document.", __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 1];
- $status = TRUE;
- break; // try-block
- }
-
- // At this point we have been given an xpath with neither a 'text()' nor 'attribute::' axis at the end
- // So we assume a get to text is wanted and use the optioanl fallback parameters $textPartNr
-
- if (!isSet($this->nodeIndex[$absoluteXPath])) {
- $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- // Get the amount of the text parts in the node.
- $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
- // If $textPartNr == NULL we return a *copy* of the whole concated text-parts
- if (is_null($textPartNr)) {
- unset($text);
- $text = implode('', $this->nodeIndex[$absoluteXPath]['textParts']);
- $status = TRUE;
- break; // try-block
- }
-
- // Support negative indexes like -1 === last a.s.o.
- if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
- if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
- $this->_displayError("The $absoluteXPath has no text part at pos [$textPartNr] (Note: text parts start with 1).", __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr -1];
- $status = TRUE;
- } while (FALSE); // END try-block
-
- if (!$status) return FALSE;
- return $text;
- }
- /**
- * Obtain the string value of an object
- *
- * http://www.w3.org/TR/xpath#dt-string-value
- *
- * "For every type of node, there is a way of determining a string-value for a node of that type.
- * For some types of node, the string-value is part of the node; for other types of node, the
- * string-value is computed from the string-value of descendant nodes."
- *
- * @param $node (node) The node we have to convert
- * @return (string) The string value of the node. "" if the object has no evaluatable
- * string value
- */
- function _stringValue($node) {
- // Decode the entitites and then add the resulting literal string into our array.
- return $this->_addLiteral($this->decodeEntities($this->wholeText($node)));
- }
-
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ Export the XML Document ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Returns the containing XML as marked up HTML with specified nodes hi-lighted
- *
- * @param $absoluteXPath (string) The address of the node you would like to export.
- * If empty the whole document will be exported.
- * @param $hilighXpathList (array) A list of nodes that you would like to highlight
- * @return (mixed) The Xml document marked up as HTML so that it can
- * be viewed in a browser, including any XML headers.
- * FALSE on error.
- * @see _export()
- */
- function exportAsHtml($absoluteXPath='', $hilightXpathList=array()) {
- $htmlString = $this->_export($absoluteXPath, $xmlHeader=NULL, $hilightXpathList);
- if (!$htmlString) return FALSE;
- return "<pre>\n" . $htmlString . "\n</pre>";
- }
-
- /**
- * Given a context this function returns the containing XML
- *
- * @param $absoluteXPath (string) The address of the node you would like to export.
- * If empty the whole document will be exported.
- * @param $xmlHeader (array) The string that you would like to appear before
- * the XML content. ie before the <root></root>. If you
- * do not specify this argument, the xmlHeader that was
- * found in the parsed xml file will be used instead.
- * @return (mixed) The Xml fragment/document, suitable for writing
- * out to an .xml file or as part of a larger xml file, or
- * FALSE on error.
- * @see _export()
- */
- function exportAsXml($absoluteXPath='', $xmlHeader=NULL) {
- $this->hilightXpathList = NULL;
- return $this->_export($absoluteXPath, $xmlHeader);
- }
-
- /**
- * Generates a XML string with the content of the current document and writes it to a file.
- *
- * Per default includes a <?xml ...> tag at the start of the data too.
- *
- * @param $fileName (string)
- * @param $absoluteXPath (string) The path to the parent node you want(see text above)
- * @param $xmlHeader (array) The string that you would like to appear before
- * the XML content. ie before the <root></root>. If you
- * do not specify this argument, the xmlHeader that was
- * found in the parsed xml file will be used instead.
- * @return (string) The returned string contains well-formed XML data
- * or FALSE on error.
- * @see exportAsXml(), exportAsHtml()
- */
- function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) {
- $status = FALSE;
- do { // try-block
- if (!($hFile = fopen($fileName, "wb"))) { // Did we open the file ok?
- $errStr = "Failed to open the $fileName xml file.";
- break; // try-block
- }
-
- if ($this->properties['OS_supports_flock']) {
- if (!flock($hFile, LOCK_EX + LOCK_NB)) { // Lock the file
- $errStr = "Couldn't get an exclusive lock on the $fileName file.";
- break; // try-block
- }
- }
- if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) {
- $errStr = "Export failed";
- break; // try-block
- }
-
- $iBytesWritten = fwrite($hFile, $xmlOut);
- if ($iBytesWritten != strlen($xmlOut)) {
- $errStr = "Write error when writing back the $fileName file.";
- break; // try-block
- }
-
- // Flush and unlock the file
- @fflush($hFile);
- $status = TRUE;
- } while(FALSE);
-
- @flock($hFile, LOCK_UN);
- @fclose($hFile);
- // Sanity check the produced file.
- clearstatcache();
- if (filesize($fileName) < strlen($xmlOut)) {
- $errStr = "Write error when writing back the $fileName file.";
- $status = FALSE;
- }
-
- if (!$status) $this->_displayError($errStr, __LINE__, __FILE__, FALSE);
- return $status;
- }
- /**
- * Generates a XML string with the content of the current document.
- *
- * This is the start for extracting the XML-data from the node-tree. We do some preperations
- * and then call _InternalExport() to fetch the main XML-data. You optionally may pass
- * xpath to any node that will then be used as top node, to extract XML-parts of the
- * document. Default is '', meaning to extract the whole document.
- *
- * You also may pass a 'xmlHeader' (usually something like <?xml version="1.0"? > that will
- * overwrite any other 'xmlHeader', if there was one in the original source. If there
- * wasn't one in the original source, and you still don't specify one, then it will
- * use a default of <?xml version="1.0"? >
- * Finaly, when exporting to HTML, you may pass a vector xPaths you want to hi-light.
- * The hi-lighted tags and attributes will receive a nice color.
- *
- * NOTE I : The output can have 2 formats:
- * a) If "skip white spaces" is/was set. (Not Recommended - slower)
- * The output is formatted by adding indenting and carriage returns.
- * b) If "skip white spaces" is/was *NOT* set.
- * 'as is'. No formatting is done. The output should the same as the
- * the original parsed XML source.
- *
- * @param $absoluteXPath (string) (optional, default is root) The node we choose as top-node
- * @param $xmlHeader (string) (optional) content before <root/> (see text above)
- * @param $hilightXpath (array) (optional) a vector of xPaths to nodes we wat to
- * hi-light (see text above)
- * @return (mixed) The xml string, or FALSE on error.
- */
- function _export($absoluteXPath='', $xmlHeader=NULL, $hilightXpathList='') {
- // Check whether a root node is given.
- if (empty($absoluteXpath)) $absoluteXpath = '';
- if ($absoluteXpath == '/') $absoluteXpath = '';
- if ($this->_indexIsDirty) $this->reindexNodeTree();
- if (!isSet($this->nodeIndex[$absoluteXpath])) {
- // If the $absoluteXpath was '' and it didn't exist, then the document is empty
- // and we can safely return ''.
- if ($absoluteXpath == '') return '';
- $this->_displayError("The given xpath '{$absoluteXpath}' isn't a node in this document.", __LINE__, __FILE__, FALSE);
- return FALSE;
- }
-
- $this->hilightXpathList = $hilightXpathList;
- $this->indentStep = ' ';
- $hilightIsActive = is_array($hilightXpathList);
- if ($hilightIsActive) {
- $this->indentStep = ' ';
- }
-
- // Cache this now
- $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
- ///////////////////////////////////////
- // Get the starting node and begin with the header
- // Get the start node. The super root is a special case.
- $startNode = NULL;
- if (empty($absoluteXPath)) {
- $superRoot = $this->nodeIndex[''];
- // If they didn't specify an xml header, use the one in the object
- if (is_null($xmlHeader)) {
- $xmlHeader = $this->parseSkipWhiteCache ? trim($superRoot['textParts'][0]) : $superRoot['textParts'][0];
- // If we still don't have an XML header, then use a suitable default
- if (empty($xmlHeader)) {
- $xmlHeader = '<?xml version="1.0"?>';
- }
- }
- if (isSet($superRoot['childNodes'][0])) $startNode = $superRoot['childNodes'][0];
- } else {
- $startNode = $this->nodeIndex[$absoluteXPath];
- }
- if (!empty($xmlHeader)) {
- $xmlOut = $this->parseSkipWhiteCache ? $xmlHeader."\n" : $xmlHeader;
- } else {
- $xmlOut = '';
- }
- ///////////////////////////////////////
- // Output the document.
- if (($xmlOut .= $this->_InternalExport($startNode)) === FALSE) {
- return FALSE;
- }
-
- ///////////////////////////////////////
- // Convert our markers to hi-lights.
- if ($hilightIsActive) {
- $from = array('<', '>', chr(2), chr(3));
- $to = array('<', '>', '<font color="#FF0000"><b>', '</b></font>');
- $xmlOut = str_replace($from, $to, $xmlOut);
- }
- return $xmlOut;
- }
- /**
- * Export the xml document starting at the named node.
- *
- * @param $node (node) The node we have to start exporting from
- * @return (string) The string representation of the node.
- */
- function _InternalExport($node) {
- $ThisFunctionName = '_InternalExport';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Exporting node: ".$node['xpath']."<br>\n";
- }
- ////////////////////////////////
- // Quick out.
- if (empty($node)) return '';
- // The output starts as empty.
- $xmlOut = '';
- // This loop will output the text before the current child of a parent then the
- // current child. Where the child is a short tag we output the child, then move
- // onto the next child. Where the child is not a short tag, we output the open tag,
- // then queue up on currentParentStack[] the child.
- //
- // When we run out of children, we then output the last text part, and close the
- // 'parent' tag before popping the stack and carrying on.
- //
- // To illustrate, the numbers in this xml file indicate what is output on each
- // pass of the while loop:
- //
- // 1
- // <1>2
- // <2>3
- // <3/>4
- // </4>5
- // <5/>6
- // </6>
- // Although this is neater done using recursion, there's a 33% performance saving
- // to be gained by using this stack mechanism.
- // Only add CR's if "skip white spaces" was set. Otherwise leave as is.
- $CR = ($this->parseSkipWhiteCache) ? "\n" : '';
- $currentIndent = '';
- $hilightIsActive = is_array($this->hilightXpathList);
- // To keep track of where we are in the document we use a node stack. The node
- // stack has the following parallel entries:
- // 'Parent' => (array) A copy of the parent node that who's children we are
- // exporting
- // 'ChildIndex' => (array) The child index of the corresponding parent that we
- // are currently exporting.
- // 'Highlighted'=> (bool) If we are highlighting this node. Only relevant if
- // the hilight is active.
- // Setup our node stack. The loop is designed to output children of a parent,
- // not the parent itself, so we must put the parent on as the starting point.
- $nodeStack['Parent'] = array($node['parentNode']);
- // And add the childpos of our node in it's parent to our "child index stack".
- $nodeStack['ChildIndex'] = array($node['pos']);
- // We start at 0.
- $nodeStackIndex = 0;
- // We have not to output text before/after our node, so blank it. We will recover it
- // later
- $OldPreceedingStringValue = $nodeStack['Parent'][0]['textParts'][$node['pos']];
- $OldPreceedingStringRef =& $nodeStack['Parent'][0]['textParts'][$node['pos']];
- $OldPreceedingStringRef = "";
- $currentXpath = "";
- // While we still have data on our stack
- while ($nodeStackIndex >= 0) {
- // Count the children and get a copy of the current child.
- $iChildCount = count($nodeStack['Parent'][$nodeStackIndex]['childNodes']);
- $currentChild = $nodeStack['ChildIndex'][$nodeStackIndex];
- // Only do the auto indenting if the $parseSkipWhiteCache flag was set.
- if ($this->parseSkipWhiteCache)
- $currentIndent = str_repeat($this->indentStep, $nodeStackIndex);
- if ($bDebugThisFunction)
- echo "Exporting child ".($currentChild+1)." of node {$nodeStack['Parent'][$nodeStackIndex]['xpath']}\n";
- ///////////////////////////////////////////
- // Add the text before our child.
- // Add the text part before the current child
- $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild];
- if (isSet($tmpTxt) AND ($tmpTxt!="")) {
- // Only add CR indent if there were children
- if ($iChildCount)
- $xmlOut .= $CR.$currentIndent;
- // Hilight if necessary.
- $highlightStart = $highlightEnd = '';
- if ($hilightIsActive) {
- $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+1).']';
- if (in_array($currentXpath, $this->hilightXpathList)) {
- // Yes we hilight
- $highlightStart = chr(2);
- $highlightEnd = chr(3);
- }
- }
- $xmlOut .= $highlightStart.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild].$highlightEnd;
- }
- if ($iChildCount && $nodeStackIndex) $xmlOut .= $CR;
- ///////////////////////////////////////////
- // Are there any more children?
- if ($iChildCount <= $currentChild) {
- // Nope, so output the last text before the closing tag
- $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1];
- if (isSet($tmpTxt) AND ($tmpTxt!="")) {
- // Hilight if necessary.
- $highlightStart = $highlightEnd = '';
- if ($hilightIsActive) {
- $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+2).']';
- if (in_array($currentXpath, $this->hilightXpathList)) {
- // Yes we hilight
- $highlightStart = chr(2);
- $highlightEnd = chr(3);
- }
- }
- $xmlOut .= $highlightStart
- .$currentIndent.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1].$CR
- .$highlightEnd;
- }
- // Now close this tag, as we are finished with this child.
- // Potentially output an (slightly smaller indent).
- if ($this->parseSkipWhiteCache
- && count($nodeStack['Parent'][$nodeStackIndex]['childNodes'])) {
- $xmlOut .= str_repeat($this->indentStep, $nodeStackIndex - 1);
- }
- // Check whether the xml-tag is to be hilighted.
- $highlightStart = $highlightEnd = '';
- if ($hilightIsActive) {
- $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'];
- if (in_array($currentXpath, $this->hilightXpathList)) {
- // Yes we hilight
- $highlightStart = chr(2);
- $highlightEnd = chr(3);
- }
- }
- $xmlOut .= $highlightStart
- .'</'.$nodeStack['Parent'][$nodeStackIndex]['name'].'>'
- .$highlightEnd;
- // Decrement the $nodeStackIndex to go back to the next unfinished parent.
- $nodeStackIndex--;
- // If the index is 0 we are finished exporting the last node, as we may have been
- // exporting an internal node.
- if ($nodeStackIndex == 0) break;
- // Indicate to the parent that we are finished with this child.
- $nodeStack['ChildIndex'][$nodeStackIndex]++;
- continue;
- }
- ///////////////////////////////////////////
- // Ok, there are children still to process.
- // Queue up the next child (I can copy because I won't modify and copying is faster.)
- $nodeStack['Parent'][$nodeStackIndex + 1] = $nodeStack['Parent'][$nodeStackIndex]['childNodes'][$currentChild];
- // Work out if it is a short child tag.
- $iGrandChildCount = count($nodeStack['Parent'][$nodeStackIndex + 1]['childNodes']);
- $shortGrandChild = (($iGrandChildCount == 0) AND (implode('',$nodeStack['Parent'][$nodeStackIndex + 1]['textParts'])==''));
- ///////////////////////////////////////////
- // Assemble the attribute string first.
- $attrStr = '';
- foreach($nodeStack['Parent'][$nodeStackIndex + 1]['attributes'] as $key=>$val) {
- // Should we hilight the attribute?
- if ($hilightIsActive AND in_array($currentXpath.'/attribute::'.$key, $this->hilightXpathList)) {
- $hiAttrStart = chr(2);
- $hiAttrEnd = chr(3);
- } else {
- $hiAttrStart = $hiAttrEnd = '';
- }
- $attrStr .= ' '.$hiAttrStart.$key.'="'.$val.'"'.$hiAttrEnd;
- }
- ///////////////////////////////////////////
- // Work out what goes before and after the tag content
- $beforeTagContent = $currentIndent;
- if ($shortGrandChild) $afterTagContent = '/>';
- else $afterTagContent = '>';
- // Check whether the xml-tag is to be hilighted.
- if ($hilightIsActive) {
- $currentXpath = $nodeStack['Parent'][$nodeStackIndex + 1]['xpath'];
- if (in_array($currentXpath, $this->hilightXpathList)) {
- // Yes we hilight
- $beforeTagContent .= chr(2);
- $afterTagContent .= chr(3);
- }
- }
- $beforeTagContent .= '<';
- // if ($shortGrandChild) $afterTagContent .= $CR;
-
- ///////////////////////////////////////////
- // Output the tag
- $xmlOut .= $beforeTagContent
- .$nodeStack['Parent'][$nodeStackIndex + 1]['name'].$attrStr
- .$afterTagContent;
- ///////////////////////////////////////////
- // Carry on.
- // If it is a short tag, then we've already done this child, we just move to the next
- if ($shortGrandChild) {
- // Move to the next child, we need not go deeper in the tree.
- $nodeStack['ChildIndex'][$nodeStackIndex]++;
- // But if we are just exporting the one node we'd go no further.
- if ($nodeStackIndex == 0) break;
- } else {
- // Else queue up the child going one deeper in the stack
- $nodeStackIndex++;
- // Start with it's first child
- $nodeStack['ChildIndex'][$nodeStackIndex] = 0;
- }
- }
- $result = $xmlOut;
- // Repair what we "undid"
- $OldPreceedingStringRef = $OldPreceedingStringValue;
- ////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- return $result;
- }
-
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ Import the XML Source ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Reads a file or URL and parses the XML data.
- *
- * Parse the XML source and (upon success) store the information into an internal structure.
- *
- * @param $fileName (string) Path and name (or URL) of the file to be read and parsed.
- * @return (bool) TRUE on success, FALSE on failure (check getLastError())
- * @see importFromString(), getLastError(),
- */
- function importFromFile($fileName) {
- $status = FALSE;
- $errStr = '';
- do { // try-block
- // Remember file name. Used in error output to know in which file it happend
- $this->properties['xmlFile'] = $fileName;
- // If we already have content, then complain.
- if (!empty($this->nodeRoot)) {
- $errStr = 'Called when this object already contains xml data. Use reset().';
- break; // try-block
- }
- // The the source is an url try to fetch it.
- if (preg_match(';^http(s)?://;', $fileName)) {
- // Read the content of the url...this is really prone to errors, and we don't really
- // check for too many here...for now, suppressing both possible warnings...we need
- // to check if we get a none xml page or something of that nature in the future
- $xmlString = @implode('', @file($fileName));
- if (!empty($xmlString)) {
- $status = TRUE;
- } else {
- $errStr = "The url '{$fileName}' could not be found or read.";
- }
- break; // try-block
- }
-
- // Reaching this point we're dealing with a real file (not an url). Check if the file exists and is readable.
- if (!is_readable($fileName)) { // Read the content from the file
- $errStr = "File '{$fileName}' could not be found or read.";
- break; // try-block
- }
- if (is_dir($fileName)) {
- $errStr = "'{$fileName}' is a directory.";
- break; // try-block
- }
- // Read the file
- if (!($fp = @fopen($fileName, 'rb'))) {
- $errStr = "Failed to open '{$fileName}' for read.";
- break; // try-block
- }
- $xmlString = fread($fp, filesize($fileName));
- @fclose($fp);
-
- $status = TRUE;
- } while (FALSE);
-
- if (!$status) {
- $this->_displayError('In importFromFile(): '. $errStr, __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- return $this->importFromString($xmlString);
- }
-
- /**
- * Reads a string and parses the XML data.
- *
- * Parse the XML source and (upon success) store the information into an internal structure.
- * If a parent xpath is given this means that XML data is to be *appended* to that parent.
- *
- * ### If a function uses setLastError(), then say in the function header that getLastError() is useful.
- *
- * @param $xmlString (string) Name of the string to be read and parsed.
- * @param $absoluteParentPath (string) Node to append data too (see above)
- * @return (bool) TRUE on success, FALSE on failure
- * (check getLastError())
- */
- function importFromString($xmlString, $absoluteParentPath = '') {
- $ThisFunctionName = 'importFromString';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Importing from string of length ".strlen($xmlString)." to node '$absoluteParentPath'\n<br>";
- echo "Parser options:\n<br>";
- print_r($this->parseOptions);
- }
- $status = FALSE;
- $errStr = '';
- do { // try-block
- // If we already have content, then complain.
- if (!empty($this->nodeRoot) AND empty($absoluteParentPath)) {
- $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.';
- break; // try-block
- }
- // Check whether content has been read.
- if (empty($xmlString)) {
- // Nothing to do!!
- $status = TRUE;
- // If we were importing to root, build a blank root.
- if (empty($absoluteParentPath)) {
- $this->_createSuperRoot();
- }
- $this->reindexNodeTree();
- // $errStr = 'This xml document (string) was empty';
- break; // try-block
- } else {
- $xmlString = $this->_translateAmpersand($xmlString);
- }
-
- // Restart our node index with a root entry.
- $nodeStack = array();
- $this->parseStackIndex = 0;
- // If a parent xpath is given this means that XML data is to be *appended* to that parent.
- if (!empty($absoluteParentPath)) {
- // Check if parent exists
- if (!isSet($this->nodeIndex[$absoluteParentPath])) {
- $errStr = "You tried to append XML data to a parent '$absoluteParentPath' that does not exist.";
- break; // try-block
- }
- // Add it as the starting point in our array.
- $this->nodeStack[0] =& $this->nodeIndex[$absoluteParentPath];
- } else {
- // Build a 'super-root'
- $this->_createSuperRoot();
- // Put it in as the start of our node stack.
- $this->nodeStack[0] =& $this->nodeRoot;
- }
- // Point our text buffer reference at the next text part of the root
- $this->parsedTextLocation =& $this->nodeStack[0]['textParts'][];
- $this->parsInCData = 0;
- // We cache this now.
- $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
-
- // Create an XML parser.
- $parser = xml_parser_create();
- // Set default XML parser options.
- if (is_array($this->parseOptions)) {
- foreach($this->parseOptions as $key => $val) {
- xml_parser_set_option($parser, $key, $val);
- }
- }
-
- // Set the object and the element handlers for the XML parser.
- xml_set_object($parser, $this);
- xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
- xml_set_character_data_handler($parser, '_handleCharacterData');
- xml_set_default_handler($parser, '_handleDefaultData');
- xml_set_processing_instruction_handler($parser, '_handlePI');
-
- // Parse the XML source and on error generate an error message.
- if (!xml_parse($parser, $xmlString, TRUE)) {
- $source = empty($this->properties['xmlFile']) ? 'string' : 'file ' . basename($this->properties['xmlFile']) . "'";
- $errStr = "XML error in given {$source} on line ".
- xml_get_current_line_number($parser). ' column '. xml_get_current_column_number($parser) .
- '. Reason:'. xml_error_string(xml_get_error_code($parser));
- break; // try-block
- }
-
- // Free the parser.
- @xml_parser_free($parser);
- // And we don't need this any more.
- $this->nodeStack = array();
- $this->reindexNodeTree();
- if ($bDebugThisFunction) {
- print_r(array_keys($this->nodeIndex));
- }
- $status = TRUE;
- } while (FALSE);
-
- if (!$status) {
- $this->_displayError('In importFromString(): '. $errStr, __LINE__, __FILE__, FALSE);
- $bResult = FALSE;
- } else {
- $bResult = TRUE;
- }
- ////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $bResult, $bDebugThisFunction);
- return $bResult;
- }
-
-
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ XML Handlers ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Handles opening XML tags while parsing.
- *
- * While parsing a XML document for each opening tag this method is
- * called. It'll add the tag found to the tree of document nodes.
- *
- * @param $parser (int) Handler for accessing the current XML parser.
- * @param $name (string) Name of the opening tag found in the document.
- * @param $attributes (array) Associative array containing a list of
- * all attributes of the tag found in the document.
- * @see _handleEndElement(), _handleCharacterData()
- */
- function _handleStartElement($parser, $nodeName, $attributes) {
- if (empty($nodeName)) {
- $this->_displayError('XML error in file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
- return;
- }
- // Trim accumulated text if necessary.
- if ($this->parseSkipWhiteCache) {
- $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
- $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
- }
- if ($this->bDebugXmlParse) {
- echo "<blockquote>" . htmlspecialchars("Start node: <".$nodeName . ">")."<br>";
- echo "Appended to stack entry: $this->parseStackIndex<br>\n";
- echo "Text part before element is: ".htmlspecialchars($this->parsedTextLocation);
- /*
- echo "<pre>";
- $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
- for ($i = 0; $i < $dataPartsCount; $i++) {
- echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
- }
- echo "</pre>";
- */
- }
- // Add a node and set path to current.
- if (!$this->_internalAppendChild($this->parseStackIndex, $nodeName)) {
- $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
- return;
- }
- // We will have gone one deeper then in the stack.
- $this->parseStackIndex++;
- // Point our parseTxtBuffer reference at the new node.
- $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][0];
-
- // Set the attributes.
- if (!empty($attributes)) {
- if ($this->bDebugXmlParse) {
- echo 'Attributes: <br>';
- print_r($attributes);
- echo '<br>';
- }
- $this->nodeStack[$this->parseStackIndex]['attributes'] = $attributes;
- }
- }
-
- /**
- * Handles closing XML tags while parsing.
- *
- * While parsing a XML document for each closing tag this method is called.
- *
- * @param $parser (int) Handler for accessing the current XML parser.
- * @param $name (string) Name of the closing tag found in the document.
- * @see _handleStartElement(), _handleCharacterData()
- */
- function _handleEndElement($parser, $name) {
- if (($this->parsedTextLocation=='')
- && empty($this->nodeStack[$this->parseStackIndex]['textParts'])) {
- // We reach this point when parsing a tag of format <foo/>. The 'textParts'-array
- // should stay empty and not have an empty string in it.
- } else {
- // Trim accumulated text if necessary.
- if ($this->parseSkipWhiteCache) {
- $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
- $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
- }
- }
- if ($this->bDebugXmlParse) {
- echo "Text part after element is: ".htmlspecialchars($this->parsedTextLocation)."<br>\n";
- echo htmlspecialchars("Parent:<{$this->parseStackIndex}>, End-node:</$name> '".$this->parsedTextLocation) . "'<br>Text nodes:<pre>\n";
- $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
- for ($i = 0; $i < $dataPartsCount; $i++) {
- echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
- }
- var_dump($this->nodeStack[$this->parseStackIndex]['textParts']);
- echo "</pre></blockquote>\n";
- }
- // Jump back to the parent element.
- $this->parseStackIndex--;
- // Set our reference for where we put any more whitespace
- $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][];
- // Note we leave the entry in the stack, as it will get blanked over by the next element
- // at this level. The safe thing to do would be to remove it too, but in the interests
- // of performance, we will not bother, as were it to be a problem, then it would be an
- // internal bug anyway.
- if ($this->parseStackIndex < 0) {
- $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
- return;
- }
- }
-
- /**
- * Handles character data while parsing.
- *
- * While parsing a XML document for each character data this method
- * is called. It'll add the character data to the document tree.
- *
- * @param $parser (int) Handler for accessing the current XML parser.
- * @param $text (string) Character data found in the document.
- * @see _handleStartElement(), _handleEndElement()
- */
- function _handleCharacterData($parser, $text) {
-
- if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, $reverse=TRUE);
-
- if ($this->bDebugXmlParse) echo "Handling character data: '".htmlspecialchars($text)."'<br>";
- if ($this->parseSkipWhiteCache AND !empty($text) AND !$this->parsInCData) {
- // Special case CR. CR always comes in a separate data. Trans. it to '' or ' '.
- // If txtBuffer is already ending with a space use '' otherwise ' '.
- $bufferHasEndingSpace = (empty($this->parsedTextLocation) OR substr($this->parsedTextLocation, -1) === ' ') ? TRUE : FALSE;
- if ($text=="\n") {
- $text = $bufferHasEndingSpace ? '' : ' ';
- } else {
- if ($bufferHasEndingSpace) {
- $text = ltrim(preg_replace('/\s+/', ' ', $text));
- } else {
- $text = preg_replace('/\s+/', ' ', $text);
- }
- }
- if ($this->bDebugXmlParse) echo "'Skip white space' is ON. reduced to : '" .htmlspecialchars($text) . "'<br>";
- }
- $this->parsedTextLocation .= $text;
- }
-
- /**
- * Default handler for the XML parser.
- *
- * While parsing a XML document for string not caught by one of the other
- * handler functions, we end up here.
- *
- * @param $parser (int) Handler for accessing the current XML parser.
- * @param $text (string) Character data found in the document.
- * @see _handleStartElement(), _handleEndElement()
- */
- function _handleDefaultData($parser, $text) {
- do { // try-block
- if (!strcmp($text, '<![CDATA[')) {
- $this->parsInCData++;
- } elseif (!strcmp($text, ']]>')) {
- $this->parsInCData--;
- if ($this->parsInCData < 0) $this->parsInCData = 0;
- }
- $this->parsedTextLocation .= $this->_translateAmpersand($text, $reverse=TRUE);
- if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>";
- break; // try-block
- } while (FALSE); // END try-block
- }
-
- /**
- * Handles processing instruction (PI)
- *
- * A processing instruction has the following format:
- * <? target data ? > e.g. <? dtd version="1.0" ? >
- *
- * Currently I have no bether idea as to left it 'as is' and treat the PI data as normal
- * text (and adding the surrounding PI-tags <? ? >).
- *
- * @param $parser (int) Handler for accessing the current XML parser.
- * @param $target (string) Name of the PI target. E.g. XML, PHP, DTD, ...
- * @param $data (string) Associative array containing a list of
- * @see PHP's manual "xml_set_processing_instruction_handler"
- */
- function _handlePI($parser, $target, $data) {
- //echo("pi data=".$data."end"); exit;
- $data = $this->_translateAmpersand($data, $reverse=TRUE);
- $this->parsedTextLocation .= "<?{$target} {$data}?>";
- return TRUE;
- }
-
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ Node Tree Stuff ------
- //-----------------------------------------------------------------------------------------
- /**
- * Creates a super root node.
- */
- function _createSuperRoot() {
- // Build a 'super-root'
- $this->nodeRoot = $this->emptyNode;
- $this->nodeRoot['name'] = '';
- $this->nodeRoot['parentNode'] = NULL;
- $this->nodeIndex[''] =& $this->nodeRoot;
- }
- /**
- * Adds a new node to the XML document tree during xml parsing.
- *
- * This method adds a new node to the tree of nodes of the XML document
- * being handled by this class. The new node is created according to the
- * parameters passed to this method. This method is a much watered down
- * version of appendChild(), used in parsing an xml file only.
- *
- * It is assumed that adding starts with root and progresses through the
- * document in parse order. New nodes must have a corresponding parent. And
- * once we have read the </> tag for the element we will never need to add
- * any more data to that node. Otherwise the add will be ignored or fail.
- *
- * The function is faciliated by a nodeStack, which is an array of nodes that
- * we have yet to close.
- *
- * @param $stackParentIndex (int) The index into the nodeStack[] of the parent
- * node to which the new node should be added as
- * a child. *READONLY*
- * @param $nodeName (string) Name of the new node. *READONLY*
- * @return (bool) TRUE if we successfully added a new child to
- * the node stack at index $stackParentIndex + 1,
- * FALSE on error.
- */
- function _internalAppendChild($stackParentIndex, $nodeName) {
- // This call is likely to be executed thousands of times, so every 0.01ms counts.
- // If you want to debug this function, you'll have to comment the stuff back in
- //$bDebugThisFunction = FALSE;
-
- /*
- $ThisFunctionName = '_internalAppendChild';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Current Node (parent-index) and the child to append : '{$stackParentIndex}' + '{$nodeName}' \n<br>";
- }
- */
- //////////////////////////////////////
- if (!isSet($this->nodeStack[$stackParentIndex])) {
- $errStr = "Invalid parent. You tried to append the tag '{$nodeName}' to an non-existing parent in our node stack '{$stackParentIndex}'.";
- $this->_displayError('In _internalAppendChild(): '. $errStr, __LINE__, __FILE__, FALSE);
- /*
- $this->_closeDebugFunction($ThisFunctionName, FALSE, $bDebugThisFunction);
- */
- return FALSE;
- }
- // Retrieve the parent node from the node stack. This is the last node at that
- // depth that we have yet to close. This is where we should add the text/node.
- $parentNode =& $this->nodeStack[$stackParentIndex];
-
- // Brand new node please
- $newChildNode = $this->emptyNode;
-
- // Save the vital information about the node.
- $newChildNode['name'] = $nodeName;
- $parentNode['childNodes'][] =& $newChildNode;
-
- // Add to our node stack
- $this->nodeStack[$stackParentIndex + 1] =& $newChildNode;
- /*
- if ($bDebugThisFunction) {
- echo "The new node received index: '".($stackParentIndex + 1)."'\n";
- foreach($this->nodeStack as $key => $val) echo "$key => ".$val['name']."\n";
- }
- $this->_closeDebugFunction($ThisFunctionName, TRUE, $bDebugThisFunction);
- */
- return TRUE;
- }
-
- /**
- * Update nodeIndex and every node of the node-tree.
- *
- * Call after you have finished any tree modifications other wise a match with
- * an xPathQuery will produce wrong results. The $this->nodeIndex[] is recreated
- * and every nodes optimization data is updated. The optimization data is all the
- * data that is duplicate information, would just take longer to find. Child nodes
- * with value NULL are removed from the tree.
- *
- * By default the modification functions in this component will automatically re-index
- * the nodes in the tree. Sometimes this is not the behaver you want. To surpress the
- * reindex, set the functions $autoReindex to FALSE and call reindexNodeTree() at the
- * end of your changes. This sometimes leads to better code (and less CPU overhead).
- *
- * Sample:
- * =======
- * Given the xml is <AAA><B/>.<B/>.<B/></AAA> | Goal is <AAA>.<B/>.</AAA> (Delete B[1] and B[3])
- * $xPathSet = $xPath->match('//B'); # Will result in array('/AAA[1]/B[1]', '/AAA[1]/B[2]', '/AAA[1]/B[3]');
- * Three ways to do it.
- * 1) Top-Down (with auto reindexing) - Safe, Slow and you get easily mix up with the the changing node index
- * removeChild('/AAA[1]/B[1]'); // B[1] removed, thus all B[n] become B[n-1] !!
- * removeChild('/AAA[1]/B[2]'); // Now remove B[2] (That originaly was B[3])
- * 2) Bottom-Up (with auto reindexing) - Safe, Slow and the changing node index (caused by auto-reindex) can be ignored.
- * for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- * if ($i==1) continue;
- * removeChild($xPathSet[$i]);
- * }
- * 3) // Top-down (with *NO* auto reindexing) - Fast, Safe as long as you call reindexNodeTree()
- * foreach($xPathSet as $xPath) {
- * // Specify no reindexing
- * if ($xPath == $xPathSet[1]) continue;
- * removeChild($xPath, $autoReindex=FALSE);
- * // The object is now in a slightly inconsistent state.
- * }
- * // Finally do the reindex and the object is consistent again
- * reindexNodeTree();
- *
- * @return (bool) TRUE on success, FALSE otherwise.
- * @see _recursiveReindexNodeTree()
- */
- function reindexNodeTree() {
- //return;
- $this->_indexIsDirty = FALSE;
- $this->nodeIndex = array();
- $this->nodeIndex[''] =& $this->nodeRoot;
- // Quick out for when the tree has no data.
- if (empty($this->nodeRoot)) return TRUE;
- return $this->_recursiveReindexNodeTree('');
- }
-
- /**
- * Create the ids that are accessable through the generate-id() function
- */
- function _generate_ids() {
- // If we have generated them already, then bail.
- if (isset($this->nodeIndex['']['generate_id'])) return;
- // keys generated are the string 'id0' . hexatridecimal-based (0..9,a-z) index
- $aNodeIndexes = array_keys($this->nodeIndex);
- $idNumber = 0;
- foreach($aNodeIndexes as $index => $key) {
- // $this->nodeIndex[$key]['generated_id'] = 'id' . base_convert($index,10,36);
- // Skip attribute and text nodes.
- // ### Currently don't support attribute and text nodes.
- if (strstr($key, 'text()') !== FALSE) continue;
- if (strstr($key, 'attribute::') !== FALSE) continue;
- $this->nodeIndex[$key]['generated_id'] = 'idPhpXPath' . $idNumber;
- // Make the id's sequential so that we can test predictively.
- $idNumber++;
- }
- }
- /**
- * Here's where the work is done for reindexing (see reindexNodeTree)
- *
- * @param $absoluteParentPath (string) the xPath to the parent node
- * @return (bool) TRUE on success, FALSE otherwise.
- * @see reindexNodeTree()
- */
- function _recursiveReindexNodeTree($absoluteParentPath) {
- $parentNode =& $this->nodeIndex[$absoluteParentPath];
-
- // Check for any 'dead' child nodes first and concate the text parts if found.
- for ($iChildIndex=sizeOf($parentNode['childNodes'])-1; $iChildIndex>=0; $iChildIndex--) {
- // Check if the child node still exits (it may have been removed).
- if (!empty($parentNode['childNodes'][$iChildIndex])) continue;
- // Child node was removed. We got to merge the text parts then.
- $parentNode['textParts'][$iChildIndex] .= $parentNode['textParts'][$iChildIndex+1];
- array_splice($parentNode['textParts'], $iChildIndex+1, 1);
- array_splice($parentNode['childNodes'], $iChildIndex, 1);
- }
- // Now start a reindex.
- $contextHash = array();
- $childSize = sizeOf($parentNode['childNodes']);
- // If there are no children, we have to treat this specially:
- if ($childSize == 0) {
- // Add a dummy text node.
- $this->nodeIndex[$absoluteParentPath.'/text()[1]'] =& $parentNode;
- } else {
- for ($iChildIndex=0; $iChildIndex<$childSize; $iChildIndex++) {
- $childNode =& $parentNode['childNodes'][$iChildIndex];
- // Make sure that there is a text-part in front of every node. (May be empty)
- if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
- // Count the nodes with same name (to determine their context position)
- $childName = $childNode['name'];
- if (empty($contextHash[$childName])) {
- $contextPos = $contextHash[$childName] = 1;
- } else {
- $contextPos = ++$contextHash[$childName];
- }
- // Make the node-index hash
- $newPath = $absoluteParentPath . '/' . $childName . '['.$contextPos.']';
- // ### Note ultimately we will end up supporting text nodes as actual nodes.
- // Preceed with a dummy entry for the text node.
- $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+1).']'] =& $childNode;
- // Then the node itself
- $this->nodeIndex[$newPath] =& $childNode;
- // Now some dummy nodes for each of the attribute nodes.
- $iAttributeCount = sizeOf($childNode['attributes']);
- if ($iAttributeCount > 0) {
- $aAttributesNames = array_keys($childNode['attributes']);
- for ($iAttributeIndex = 0; $iAttributeIndex < $iAttributeCount; $iAttributeIndex++) {
- $attribute = $aAttributesNames[$iAttributeIndex];
- $newAttributeNode = $this->emptyNode;
- $newAttributeNode['name'] = $attribute;
- $newAttributeNode['textParts'] = array($childNode['attributes'][$attribute]);
- $newAttributeNode['contextPos'] = $iAttributeIndex;
- $newAttributeNode['xpath'] = "$newPath/attribute::$attribute";
- $newAttributeNode['parentNode'] =& $childNode;
- $newAttributeNode['depth'] =& $parentNode['depth'] + 2;
- // Insert the node as a master node, not a reference, otherwise there will be
- // variable "bleeding".
- $this->nodeIndex["$newPath/attribute::$attribute"] = $newAttributeNode;
- }
- }
- // Update the node info (optimisation)
- $childNode['parentNode'] =& $parentNode;
- $childNode['depth'] = $parentNode['depth'] + 1;
- $childNode['pos'] = $iChildIndex;
- $childNode['contextPos'] = $contextHash[$childName];
- $childNode['xpath'] = $newPath;
- $this->_recursiveReindexNodeTree($newPath);
- // Follow with a dummy entry for the text node.
- $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+2).']'] =& $childNode;
- }
- // Make sure that their is a text-part after the last node.
- if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
- }
- return TRUE;
- }
-
- /**
- * Clone a node and it's child nodes.
- *
- * NOTE: If the node has children you *MUST* use the reference operator!
- * E.g. $clonedNode =& cloneNode($node);
- * Otherwise the children will not point back to the parent, they will point
- * back to your temporary variable instead.
- *
- * @param $node (mixed) Either a node (hash array) or an abs. Xpath to a node in
- * the current doc
- * @return (&array) A node and it's child nodes.
- */
- function &cloneNode($node, $recursive=FALSE) {
- if (is_string($node) AND isSet($this->nodeIndex[$node])) {
- $node = $this->nodeIndex[$node];
- }
- // Copy the text-parts ()
- $textParts = $node['textParts'];
- $node['textParts'] = array();
- foreach ($textParts as $key => $val) {
- $node['textParts'][] = $val;
- }
-
- $childSize = sizeOf($node['childNodes']);
- for ($i=0; $i<$childSize; $i++) {
- $childNode =& $this->cloneNode($node['childNodes'][$i], TRUE); // copy child
- $node['childNodes'][$i] =& $childNode; // reference the copy
- $childNode['parentNode'] =& $node; // child references the parent.
- }
-
- if (!$recursive) {
- //$node['childNodes'][0]['parentNode'] = null;
- //print "<pre>";
- //var_dump($node);
- }
- return $node;
- }
-
-
- /** Nice to have but __sleep() has a bug.
- (2002-2 PHP V4.1. See bug #15350)
-
- /**
- * PHP cals this function when you call PHP's serialize.
- *
- * It prevents cyclic referencing, which is why print_r() of an XPath object doesn't work.
- *
- function __sleep() {
- // Destroy recursive pointers
- $keys = array_keys($this->nodeIndex);
- $size = sizeOf($keys);
- for ($i=0; $i<$size; $i++) {
- unset($this->nodeIndex[$keys[$i]]['parentNode']);
- }
- unset($this->nodeIndex);
- }
-
- /**
- * PHP cals this function when you call PHP's unserialize.
- *
- * It reindexes the node-tree
- *
- function __wakeup() {
- $this->reindexNodeTree();
- }
-
- */
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ XPath Query / Evaluation Handlers ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Matches (evaluates) an XPath query
- *
- * This method tries to evaluate an XPath query by parsing it. A XML source must
- * have been imported before this method is able to work.
- *
- * @param $xPathQuery (string) XPath query to be evaluated.
- * @param $baseXPath (string) (default is super-root) XPath query to a single document node,
- * from which the XPath query should start evaluating.
- * @return (mixed) The result of the XPath expression. Either:
- * node-set (an ordered collection of absolute references to nodes without duplicates)
- * boolean (true or false)
- * number (a floating-point number)
- * string (a sequence of UCS characters)
- */
- function match($xPathQuery, $baseXPath='') {
- if ($this->_indexIsDirty) $this->reindexNodeTree();
-
- // Replace a double slashes, because they'll cause problems otherwise.
- static $slashes2descendant = array(
- '//@' => '/descendant_or_self::*/attribute::',
- '//' => '/descendant_or_self::node()/',
- '/@' => '/attribute::');
- // Stupid idea from W3C to take axes name containing a '-' (dash) !!!
- // We replace the '-' with '_' to avoid the conflict with the minus operator.
- static $dash2underscoreHash = array(
- '-sibling' => '_sibling',
- '-or-' => '_or_',
- 'starts-with' => 'starts_with',
- 'substring-before' => 'substring_before',
- 'substring-after' => 'substring_after',
- 'string-length' => 'string_length',
- 'normalize-space' => 'normalize_space',
- 'x-lower' => 'x_lower',
- 'x-upper' => 'x_upper',
- 'generate-id' => 'generate_id');
-
- if (empty($xPathQuery)) return array();
- // Special case for when document is empty.
- if (empty($this->nodeRoot)) return array();
- if (!isSet($this->nodeIndex[$baseXPath])) {
- $xPathSet = $this->_resolveXPathQuery($baseXPath,'match');
- if (sizeOf($xPathSet) !== 1) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- $baseXPath = $xPathSet[0];
- }
- // We should possibly do a proper syntactical parse, but instead we will cheat and just
- // remove any literals that could make things very difficult for us, and replace them with
- // special tags. Then we can treat the xPathQuery much more easily as JUST "syntax". Provided
- // there are no literals in the string, then we can guarentee that most of the operators and
- // syntactical elements are indeed elements and not just part of a literal string.
- $processedxPathQuery = $this->_removeLiterals($xPathQuery);
-
- // Replace a double slashes, and '-' (dash) in axes names.
- $processedxPathQuery = strtr($processedxPathQuery, $slashes2descendant);
- $processedxPathQuery = strtr($processedxPathQuery, $dash2underscoreHash);
- // Build the context
- $context = array('nodePath' => $baseXPath, 'pos' => 1, 'size' => 1);
- // The primary syntactic construct in XPath is the expression.
- $result = $this->_evaluateExpr($processedxPathQuery, $context);
- // We might have been returned a string.. If so convert back to a literal
- $literalString = $this->_asLiteral($result);
- if ($literalString != FALSE) return $literalString;
- else return $result;
- }
- /**
- * Alias for the match function
- *
- * @see match()
- */
- function evaluate($xPathQuery, $baseXPath='') {
- return $this->match($xPathQuery, $baseXPath);
- }
- /**
- * Parse out the literals of an XPath expression.
- *
- * Instead of doing a full lexical parse, we parse out the literal strings, and then
- * Treat the sections of the string either as parts of XPath or literal strings. So
- * this function replaces each literal it finds with a literal reference, and then inserts
- * the reference into an array of strings that we can access. The literals can be accessed
- * later from the literals associative array.
- *
- * Example:
- * XPathExpr = /AAA[@CCC = "hello"]/BBB[DDD = 'world']
- * => literals: array("hello", "world")
- * return value: /AAA[@CCC = $1]/BBB[DDD = $2]
- *
- * Note: This does not interfere with the VariableReference syntactical element, as these
- * elements must not start with a number.
- *
- * @param $xPathQuery (string) XPath expression to be processed
- * @return (string) The XPath expression without the literals.
- *
- */
- function _removeLiterals($xPathQuery) {
- // What comes first? A " or a '?
- if (!preg_match(":^([^\"']*)([\"'].*)$:", $xPathQuery, $aMatches)) {
- // No " or ' means no more literals.
- return $xPathQuery;
- }
-
- $result = $aMatches[1];
- $remainder = $aMatches[2];
- // What kind of literal?
- if (preg_match(':^"([^"]*)"(.*)$:', $remainder, $aMatches)) {
- // A "" literal.
- $literal = $aMatches[1];
- $remainder = $aMatches[2];
- } else if (preg_match(":^'([^']*)'(.*)$:", $remainder, $aMatches)) {
- // A '' literal.
- $literal = $aMatches[1];
- $remainder = $aMatches[2];
- } else {
- $this->_displayError("The '$xPathQuery' argument began a literal, but did not close it.", __LINE__, __FILE__);
- }
- // Store the literal
- $literalNumber = count($this->axPathLiterals);
- $this->axPathLiterals[$literalNumber] = $literal;
- $result .= '$'.$literalNumber;
- return $result.$this->_removeLiterals($remainder);
- }
- /**
- * Returns the given string as a literal reference.
- *
- * @param $string (string) The string that we are processing
- * @return (mixed) The literal string. FALSE if the string isn't a literal reference.
- */
- function _asLiteral($string) {
- if (empty($string)) return FALSE;
- if (empty($string[0])) return FALSE;
- if ($string[0] == '$') {
- $remainder = substr($string, 1);
- if (is_numeric($remainder)) {
- // We have a string reference then.
- $stringNumber = (int)$remainder;
- if ($stringNumber >= count($this->axPathLiterals)) {
- $this->_displayError("Internal error. Found a string reference that we didn't set in xPathQuery: '$xPathQuery'.", __LINE__, __FILE__);
- return FALSE;
- }
- return $this->axPathLiterals[$stringNumber];
- }
- }
- // It's not a reference then.
- return FALSE;
- }
-
- /**
- * Adds a literal to our array of literals
- *
- * In order to make sure we don't interpret literal strings as XPath expressions, we have to
- * encode literal strings so that we know that they are not XPaths.
- *
- * @param $string (string) The literal string that we need to store for future access
- * @return (mixed) A reference string to this literal.
- */
- function _addLiteral($string) {
- // Store the literal
- $literalNumber = count($this->axPathLiterals);
- $this->axPathLiterals[$literalNumber] = $string;
- $result = '$'.$literalNumber;
- return $result;
- }
- /**
- * Look for operators in the expression
- *
- * Parses through the given expression looking for operators. If found returns
- * the operands and the operator in the resulting array.
- *
- * @param $xPathQuery (string) XPath query to be evaluated.
- * @return (array) If an operator is found, it returns an array containing
- * information about the operator. If no operator is found
- * then it returns an empty array. If an operator is found,
- * but has invalid operands, it returns FALSE.
- * The resulting array has the following entries:
- * 'operator' => The string version of operator that was found,
- * trimmed for whitespace
- * 'left operand' => The left operand, or empty if there was no
- * left operand for this operator.
- * 'right operand' => The right operand, or empty if there was no
- * right operand for this operator.
- */
- function _GetOperator($xPathQuery) {
- $position = 0;
- $operator = '';
- // The results of this function can easily be cached.
- static $aResultsCache = array();
- if (isset($aResultsCache[$xPathQuery])) {
- return $aResultsCache[$xPathQuery];
- }
- // Run through all operators and try to find one.
- $opSize = sizeOf($this->operators);
- for ($i=0; $i<$opSize; $i++) {
- // Pick an operator to try.
- $operator = $this->operators[$i];
- // Quickcheck. If not present don't wast time searching 'the hard way'
- if (strpos($xPathQuery, $operator)===FALSE) continue;
- // Special check
- $position = $this->_searchString($xPathQuery, $operator);
- // Check whether a operator was found.
- if ($position <= 0 ) continue;
- // Check whether it's the equal operator.
- if ($operator == '=') {
- // Also look for other operators containing the equal sign.
- switch ($xPathQuery[$position-1]) {
- case '<' :
- $position--;
- $operator = '<=';
- break;
- case '>' :
- $position--;
- $operator = '>=';
- break;
- case '!' :
- $position--;
- $operator = '!=';
- break;
- default:
- // It's a pure = operator then.
- }
- break;
- }
- if ($operator == '*') {
- // http://www.w3.org/TR/xpath#exprlex:
- // "If there is a preceding token and the preceding token is not one of @, ::, (, [,
- // or an Operator, then a * must be recognized as a MultiplyOperator and an NCName must
- // be recognized as an OperatorName."
- // Get some substrings.
- $character = substr($xPathQuery, $position - 1, 1);
-
- // Check whether it's a multiply operator or a name test.
- if (strchr('/@:([', $character) != FALSE) {
- // Don't use the operator.
- $position = -1;
- continue;
- } else {
- // The operator is good. Lets use it.
- break;
- }
- }
- // Extremely annoyingly, we could have a node name like "for-each" and we should not
- // parse this as a "-" operator. So if the first char of the right operator is alphabetic,
- // then this is NOT an interger operator.
- if (strchr('-+*', $operator) != FALSE) {
- $rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
- if (strlen($rightOperand) > 1) {
- if (preg_match(':^\D$:', $rightOperand[0])) {
- // Don't use the operator.
- $position = -1;
- continue;
- } else {
- // The operator is good. Lets use it.
- break;
- }
- }
- }
- // The operator must be good then :o)
- break;
- } // end while each($this->operators)
- // Did we find an operator?
- if ($position == -1) {
- $aResultsCache[$xPathQuery] = array();
- return array();
- }
- /////////////////////////////////////////////
- // Get the operands
- // Get the left and the right part of the expression.
- $leftOperand = trim(substr($xPathQuery, 0, $position));
- $rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
-
- // Remove whitespaces.
- $leftOperand = trim($leftOperand);
- $rightOperand = trim($rightOperand);
- /////////////////////////////////////////////
- // Check the operands.
- if ($leftOperand == '') {
- $aResultsCache[$xPathQuery] = FALSE;
- return FALSE;
- }
- if ($rightOperand == '') {
- $aResultsCache[$xPathQuery] = FALSE;
- return FALSE;
- }
- // Package up and return what we found.
- $aResult = array('operator' => $operator,
- 'left operand' => $leftOperand,
- 'right operand' => $rightOperand);
- $aResultsCache[$xPathQuery] = $aResult;
- return $aResult;
- }
- /**
- * Evaluates an XPath PrimaryExpr
- *
- * http://www.w3.org/TR/xpath#section-Basics
- *
- * [15] PrimaryExpr ::= VariableReference
- * | '(' Expr ')'
- * | Literal
- * | Number
- * | FunctionCall
- *
- * @param $xPathQuery (string) XPath query to be evaluated.
- * @param $context (array) The context from which to evaluate
- * @param $results (mixed) If the expression could be parsed and evaluated as one of these
- * syntactical elements, then this will be either:
- * - node-set (an ordered collection of nodes without duplicates)
- * - boolean (true or false)
- * - number (a floating-point number)
- * - string (a sequence of UCS characters)
- * @return (string) An empty string if the query was successfully parsed and
- * evaluated, else a string containing the reason for failing.
- * @see evaluate()
- */
- function _evaluatePrimaryExpr($xPathQuery, $context, &$result) {
- $ThisFunctionName = '_evaluatePrimaryExpr';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Path: $xPathQuery\n";
- echo "Context:";
- $this->_printContext($context);
- echo "\n";
- }
- // Certain expressions will never be PrimaryExpr, so to speed up processing, cache the
- // results we do find from this function.
- static $aResultsCache = array();
-
- // Do while false loop
- $error = "";
- // If the result is independant of context, then we can cache the result and speed this function
- // up on future calls.
- $bCacheableResult = FALSE;
- do {
- if (isset($aResultsCache[$xPathQuery])) {
- $error = $aResultsCache[$xPathQuery]['Error'];
- $result = $aResultsCache[$xPathQuery]['Result'];
- break;
- }
- // VariableReference
- // ### Not supported.
- // Is it a number?
- // | Number
- if (is_numeric($xPathQuery)) {
- $result = doubleval($xPathQuery);
- $bCacheableResult = TRUE;
- break;
- }
- // If it starts with $, and the remainder is a number, then it's a string.
- // | Literal
- $literal = $this->_asLiteral($xPathQuery);
- if ($literal !== FALSE) {
- $result = $xPathQuery;
- $bCacheableResult = TRUE;
- break;
- }
- // Is it a function?
- // | FunctionCall
- {
- // Check whether it's all wrapped in a function. will be like count(.*) where .* is anything
- // text() will try to be matched here, so just explicitly ignore it
- $regex = ":^([^\(\)\[\]/]*)\s*\((.*)\)$:U";
- if (preg_match($regex, $xPathQuery, $aMatch) && $xPathQuery != "text()") {
- $function = $aMatch[1];
- $data = $aMatch[2];
- // It is possible that we will get "a() or b()" which will match as function "a" with
- // arguments ") or b(" which is clearly wrong... _bracketsCheck() should catch this.
- if ($this->_bracketsCheck($data)) {
- if (in_array($function, $this->functions)) {
- if ($bDebugThisFunction) echo "XPathExpr: $xPathQuery is a $function() function call:\n";
- $result = $this->_evaluateFunction($function, $data, $context);
- break;
- }
- }
- }
- }
- // Is it a bracketed expression?
- // | '(' Expr ')'
- // If it is surrounded by () then trim the brackets
- $bBrackets = FALSE;
- if (preg_match(":^\((.*)\):", $xPathQuery, $aMatches)) {
- // Do not keep trimming off the () as we could have "(() and ())"
- $bBrackets = TRUE;
- $xPathQuery = $aMatches[1];
- }
- if ($bBrackets) {
- // Must be a Expr then.
- $result = $this->_evaluateExpr($xPathQuery, $context);
- break;
- }
- // Can't be a PrimaryExpr then.
- $error = "Expression is not a PrimaryExpr";
- $bCacheableResult = TRUE;
- } while (FALSE);
- //////////////////////////////////////////////
- // If possible, cache the result.
- if ($bCacheableResult) {
- $aResultsCache[$xPathQuery]['Error'] = $error;
- $aResultsCache[$xPathQuery]['Result'] = $result;
- }
- $this->_closeDebugFunction($ThisFunctionName, array('result' => $result, 'error' => $error), $bDebugThisFunction);
- // Return the result.
- return $error;
- }
- /**
- * Evaluates an XPath Expr
- *
- * $this->evaluate() is the entry point and does some inits, while this
- * function is called recursive internaly for every sub-xPath expresion we find.
- * It handles the following syntax, and calls evaluatePathExpr if it finds that none
- * of this grammer applies.
- *
- * http://www.w3.org/TR/xpath#section-Basics
- *
- * [14] Expr ::= OrExpr
- * [21] OrExpr ::= AndExpr
- * | OrExpr 'or' AndExpr
- * [22] AndExpr ::= EqualityExpr
- * | AndExpr 'and' EqualityExpr
- * [23] EqualityExpr ::= RelationalExpr
- * | EqualityExpr '=' RelationalExpr
- * | EqualityExpr '!=' RelationalExpr
- * [24] RelationalExpr ::= AdditiveExpr
- * | RelationalExpr '<' AdditiveExpr
- * | RelationalExpr '>' AdditiveExpr
- * | RelationalExpr '<=' AdditiveExpr
- * | RelationalExpr '>=' AdditiveExpr
- * [25] AdditiveExpr ::= MultiplicativeExpr
- * | AdditiveExpr '+' MultiplicativeExpr
- * | AdditiveExpr '-' MultiplicativeExpr
- * [26] MultiplicativeExpr ::= UnaryExpr
- * | MultiplicativeExpr MultiplyOperator UnaryExpr
- * | MultiplicativeExpr 'div' UnaryExpr
- * | MultiplicativeExpr 'mod' UnaryExpr
- * [27] UnaryExpr ::= UnionExpr
- * | '-' UnaryExpr
- * [18] UnionExpr ::= PathExpr
- * | UnionExpr '|' PathExpr
- *
- * NOTE: The effect of the above grammar is that the order of precedence is
- * (lowest precedence first):
- * 1) or
- * 2) and
- * 3) =, !=
- * 4) <=, <, >=, >
- * 5) +, -
- * 6) *, div, mod
- * 7) - (negate)
- * 8) |
- *
- * @param $xPathQuery (string) XPath query to be evaluated.
- * @param $context (array) An associative array the describes the context from which
- * to evaluate the XPath Expr. Contains three members:
- * 'nodePath' => The absolute XPath expression to the context node
- * 'size' => The context size
- * 'pos' => The context position
- * @return (mixed) The result of the XPath expression. Either:
- * node-set (an ordered collection of nodes without duplicates)
- * boolean (true or false)
- * number (a floating-point number)
- * string (a sequence of UCS characters)
- * @see evaluate()
- */
- function _evaluateExpr($xPathQuery, $context) {
- $ThisFunctionName = '_evaluateExpr';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Path: $xPathQuery\n";
- echo "Context:";
- $this->_printContext($context);
- echo "\n";
- }
- // Numpty check
- if (!isset($xPathQuery) || ($xPathQuery == '')) {
- $this->_displayError("The \$xPathQuery argument must have a value.", __LINE__, __FILE__);
- return FALSE;
- }
- // At the top level we deal with booleans. Only if the Expr is just an AdditiveExpr will
- // the result not be a boolean.
- //
- //
- // Between these syntactical elements we get PathExprs.
- // Do while false loop
- do {
- static $aKnownPathExprCache = array();
- if (isset($aKnownPathExprCache[$xPathQuery])) {
- if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
- $result = $this->_evaluatePathExpr($xPathQuery, $context);
- break;
- }
- // Check for operators first, as we could have "() op ()" and the PrimaryExpr will try to
- // say that that is an Expr called ") op ("
- // Set the default position and the type of the operator.
- $aOperatorInfo = $this->_GetOperator($xPathQuery);
- // An expression can be one of these, and we should catch these "first" as they are most common
- if (empty($aOperatorInfo)) {
- $error = $this->_evaluatePrimaryExpr($xPathQuery, $context, $result);
- if (empty($error)) {
- // It could be parsed as a PrimaryExpr, so look no further :o)
- break;
- }
- }
- // Check whether an operator was found.
- if (empty($aOperatorInfo)) {
- if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
- $aKnownPathExprCache[$xPathQuery] = TRUE;
- // No operator. Means we have a PathExpr then. Go to the next level.
- $result = $this->_evaluatePathExpr($xPathQuery, $context);
- break;
- }
- if ($bDebugThisFunction) { echo "\nFound and operator:"; print_r($aOperatorInfo); }//LEFT:[$leftOperand] oper:[$operator] RIGHT:[$rightOperand]";
- $operator = $aOperatorInfo['operator'];
- /////////////////////////////////////////////
- // Recursively process the operator
- // Check the kind of operator.
- switch ($operator) {
- case ' or ':
- case ' and ':
- $operatorType = 'Boolean';
- break;
- case '+':
- case '-':
- case '*':
- case ' div ':
- case ' mod ':
- $operatorType = 'Integer';
- break;
- case ' | ':
- $operatorType = 'NodeSet';
- break;
- case '<=':
- case '<':
- case '>=':
- case '>':
- case '=':
- case '!=':
- $operatorType = 'Multi';
- break;
- default:
- $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
- }
- if ($bDebugThisFunction) echo "\nOperator is a [$operator]($operatorType operator)";
- /////////////////////////////////////////////
- // Evaluate the operands
- // Evaluate the left part.
- if ($bDebugThisFunction) echo "\nEvaluating LEFT:[{$aOperatorInfo['left operand']}]\n";
- $left = $this->_evaluateExpr($aOperatorInfo['left operand'], $context);
- if ($bDebugThisFunction) {echo "{$aOperatorInfo['left operand']} evals as:\n"; print_r($left); }
-
- // If it is a boolean operator, it's possible we don't need to evaluate the right part.
- // Only evaluate the right part if we need to.
- $right = '';
- if ($operatorType == 'Boolean') {
- // Is the left part false?
- $left = $this->_handleFunction_boolean($left, $context);
- if (!$left and ($operator == ' and ')) {
- $result = FALSE;
- break;
- } else if ($left and ($operator == ' or ')) {
- $result = TRUE;
- break;
- }
- }
- // Evaluate the right part
- if ($bDebugThisFunction) echo "\nEvaluating RIGHT:[{$aOperatorInfo['right operand']}]\n";
- $right = $this->_evaluateExpr($aOperatorInfo['right operand'], $context);
- if ($bDebugThisFunction) {echo "{$aOperatorInfo['right operand']} evals as:\n"; print_r($right); echo "\n";}
- /////////////////////////////////////////////
- // Combine the operands
- // If necessary, work out how to treat the multi operators
- if ($operatorType != 'Multi') {
- $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
- } else {
- // http://www.w3.org/TR/xpath#booleans
- // If both objects to be compared are node-sets, then the comparison will be true if and
- // only if there is a node in the first node-set and a node in the second node-set such
- // that the result of performing the comparison on the string-values of the two nodes is
- // true.
- //
- // If one object to be compared is a node-set and the other is a number, then the
- // comparison will be true if and only if there is a node in the node-set such that the
- // result of performing the comparison on the number to be compared and on the result of
- // converting the string-value of that node to a number using the number function is true.
- //
- // If one object to be compared is a node-set and the other is a string, then the comparison
- // will be true if and only if there is a node in the node-set such that the result of performing
- // the comparison on the string-value of the node and the other string is true.
- //
- // If one object to be compared is a node-set and the other is a boolean, then the comparison
- // will be true if and only if the result of performing the comparison on the boolean and on
- // the result of converting the node-set to a boolean using the boolean function is true.
- if (is_array($left) || is_array($right)) {
- if ($bDebugThisFunction) echo "As one of the operands is an array, we will need to loop\n";
- if (is_array($left) && is_array($right)) {
- $operatorType = 'String';
- } elseif (is_numeric($left) || is_numeric($right)) {
- $operatorType = 'Integer';
- } elseif (is_bool($left)) {
- $operatorType = 'Boolean';
- $right = $this->_handleFunction_boolean($right, $context);
- } elseif (is_bool($right)) {
- $operatorType = 'Boolean';
- $left = $this->_handleFunction_boolean($left, $context);
- } else {
- $operatorType = 'String';
- }
- if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
- // Turn both operands into arrays to simplify logic
- $aLeft = $left;
- $aRight = $right;
- if (!is_array($aLeft)) $aLeft = array($aLeft);
- if (!is_array($aRight)) $aRight = array($aRight);
- $result = FALSE;
- if (!empty($aLeft)) {
- foreach ($aLeft as $leftItem) {
- if (empty($aRight)) break;
- // If the item is from a node set, we should evaluate it's string-value
- if (is_array($left)) {
- if ($bDebugThisFunction) echo "\tObtaining string-value of LHS:$leftItem as it's from a nodeset\n";
- $leftItem = $this->_stringValue($leftItem);
- }
- foreach ($aRight as $rightItem) {
- // If the item is from a node set, we should evaluate it's string-value
- if (is_array($right)) {
- if ($bDebugThisFunction) echo "\tObtaining string-value of RHS:$rightItem as it's from a nodeset\n";
- $rightItem = $this->_stringValue($rightItem);
- }
- if ($bDebugThisFunction) echo "\tEvaluating $leftItem $operator $rightItem\n";
- $result = $this->_evaluateOperator($leftItem, $operator, $rightItem, $operatorType, $context);
- if ($result === TRUE) break;
- }
- if ($result === TRUE) break;
- }
- }
- }
- // When neither object to be compared is a node-set and the operator is = or !=, then the
- // objects are compared by converting them to a common type as follows and then comparing
- // them.
- //
- // If at least one object to be compared is a boolean, then each object to be compared
- // is converted to a boolean as if by applying the boolean function.
- //
- // Otherwise, if at least one object to be compared is a number, then each object to be
- // compared is converted to a number as if by applying the number function.
- //
- // Otherwise, both objects to be compared are converted to strings as if by applying
- // the string function.
- //
- // The = comparison will be true if and only if the objects are equal; the != comparison
- // will be true if and only if the objects are not equal. Numbers are compared for equality
- // according to IEEE 754 [IEEE 754]. Two booleans are equal if either both are true or
- // both are false. Two strings are equal if and only if they consist of the same sequence
- // of UCS characters.
- else {
- if (is_bool($left) || is_bool($right)) {
- $operatorType = 'Boolean';
- } elseif (is_numeric($left) || is_numeric($right)) {
- $operatorType = 'Integer';
- } else {
- $operatorType = 'String';
- }
- if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
- $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
- }
- }
- } while (FALSE);
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
- /**
- * Evaluate the result of an operator whose operands have been evaluated
- *
- * If the operator type is not "NodeSet", then neither the left or right operators
- * will be node sets, as the processing when one or other is an array is complex,
- * and should be handled by the caller.
- *
- * @param $left (mixed) The left operand
- * @param $right (mixed) The right operand
- * @param $operator (string) The operator to use to combine the operands
- * @param $operatorType (string) The type of the operator. Either 'Boolean',
- * 'Integer', 'String', or 'NodeSet'
- * @param $context (array) The context from which to evaluate
- * @return (mixed) The result of the XPath expression. Either:
- * node-set (an ordered collection of nodes without duplicates)
- * boolean (true or false)
- * number (a floating-point number)
- * string (a sequence of UCS characters)
- */
- function _evaluateOperator($left, $operator, $right, $operatorType, $context) {
- $ThisFunctionName = '_evaluateOperator';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "left: $left\n";
- echo "right: $right\n";
- echo "operator: $operator\n";
- echo "operator type: $operatorType\n";
- }
- // Do while false loop
- do {
- // Handle the operator depending on the operator type.
- switch ($operatorType) {
- case 'Boolean':
- {
- // Boolify the arguments. (The left arg is already a bool)
- $right = $this->_handleFunction_boolean($right, $context);
- switch ($operator) {
- case '=': // Compare the two results.
- $result = (bool)($left == $right);
- break;
- case ' or ': // Return the two results connected by an 'or'.
- $result = (bool)( $left or $right );
- break;
- case ' and ': // Return the two results connected by an 'and'.
- $result = (bool)( $left and $right );
- break;
- case '!=': // Check whether the two results are not equal.
- $result = (bool)( $left != $right );
- break;
- default:
- $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
- }
- }
- break;
- case 'Integer':
- {
- // Convert both left and right operands into numbers.
- if (empty($left) && ($operator == '-')) {
- // There may be no left operator if the op is '-'
- $left = 0;
- } else {
- $left = $this->_handleFunction_number($left, $context);
- }
- $right = $this->_handleFunction_number($right, $context);
- if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
- switch ($operator) {
- case '=': // Compare the two results.
- $result = (bool)($left == $right);
- break;
- case '!=': // Compare the two results.
- $result = (bool)($left != $right);
- break;
- case '+': // Return the result by adding one result to the other.
- $result = $left + $right;
- break;
- case '-': // Return the result by decrease one result by the other.
- $result = $left - $right;
- break;
- case '*': // Return a multiplication of the two results.
- $result = $left * $right;
- break;
- case ' div ': // Return a division of the two results.
- $result = $left / $right;
- break;
- case ' mod ': // Return a modulo division of the two results.
- $result = $left % $right;
- break;
- case '<=': // Compare the two results.
- $result = (bool)( $left <= $right );
- break;
- case '<': // Compare the two results.
- $result = (bool)( $left < $right );
- break;
- case '>=': // Compare the two results.
- $result = (bool)( $left >= $right );
- break;
- case '>': // Compare the two results.
- $result = (bool)( $left > $right );
- break;
- default:
- $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
- }
- }
- break;
- case 'NodeSet':
- // Add the nodes to the result set
- $result = array_merge($left, $right);
- // Remove duplicated nodes.
- $result = array_unique($result);
- // Preserve doc order if there was more than one query.
- if (count($result) > 1) {
- $result = $this->_sortByDocOrder($result);
- }
- break;
- case 'String':
- $left = $this->_handleFunction_string($left, $context);
- $right = $this->_handleFunction_string($right, $context);
- if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
- switch ($operator) {
- case '=': // Compare the two results.
- $result = (bool)($left == $right);
- break;
- case '!=': // Compare the two results.
- $result = (bool)($left != $right);
- break;
- default:
- $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
- }
- break;
- default:
- $this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
- }
- } while (FALSE);
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
-
- /**
- * Evaluates an XPath PathExpr
- *
- * It handles the following syntax:
- *
- * http://www.w3.org/TR/xpath#node-sets
- * http://www.w3.org/TR/xpath#NT-LocationPath
- * http://www.w3.org/TR/xpath#path-abbrev
- * http://www.w3.org/TR/xpath#NT-Step
- *
- * [19] PathExpr ::= LocationPath
- * | FilterExpr
- * | FilterExpr '/' RelativeLocationPath
- * | FilterExpr '//' RelativeLocationPath
- * [20] FilterExpr ::= PrimaryExpr
- * | FilterExpr Predicate
- * [1] LocationPath ::= RelativeLocationPath
- * | AbsoluteLocationPath
- * [2] AbsoluteLocationPath ::= '/' RelativeLocationPath?
- * | AbbreviatedAbsoluteLocationPath
- * [3] RelativeLocationPath ::= Step
- * | RelativeLocationPath '/' Step
- * | AbbreviatedRelativeLocationPath
- * [4] Step ::= AxisSpecifier NodeTest Predicate*
- * | AbbreviatedStep
- * [5] AxisSpecifier ::= AxisName '::'
- * | AbbreviatedAxisSpecifier
- * [10] AbbreviatedAbsoluteLocationPath
- * ::= '//' RelativeLocationPath
- * [11] AbbreviatedRelativeLocationPath
- * ::= RelativeLocationPath '//' Step
- * [12] AbbreviatedStep ::= '.'
- * | '..'
- * [13] AbbreviatedAxisSpecifier
- * ::= '@'?
- *
- * If you expand all the abbreviated versions, then the grammer simplifies to:
- *
- * [19] PathExpr ::= RelativeLocationPath
- * | '/' RelativeLocationPath?
- * | FilterExpr
- * | FilterExpr '/' RelativeLocationPath
- * [20] FilterExpr ::= PrimaryExpr
- * | FilterExpr Predicate
- * [3] RelativeLocationPath ::= Step
- * | RelativeLocationPath '/' Step
- * [4] Step ::= AxisName '::' NodeTest Predicate*
- *
- * Conceptually you can say that we should split by '/' and try to treat the parts
- * as steps, and if that fails then try to treat it as a PrimaryExpr.
- *
- * @param $PathExpr (string) PathExpr syntactical element
- * @param $context (array) The context from which to evaluate
- * @return (mixed) The result of the XPath expression. Either:
- * node-set (an ordered collection of nodes without duplicates)
- * boolean (true or false)
- * number (a floating-point number)
- * string (a sequence of UCS characters)
- * @see evaluate()
- */
- function _evaluatePathExpr($PathExpr, $context) {
- $ThisFunctionName = '_evaluatePathExpr';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "PathExpr: $PathExpr\n";
- echo "Context:";
- $this->_printContext($context);
- echo "\n";
- }
-
- // Numpty check
- if (empty($PathExpr)) {
- $this->_displayError("The \$PathExpr argument must have a value.", __LINE__, __FILE__);
- return FALSE;
- }
- //////////////////////////////////////////////
- // Parsing the expression into steps is a cachable operation as it doesn't depend on the context
- static $aResultsCache = array();
- if (isset($aResultsCache[$PathExpr])) {
- $steps = $aResultsCache[$PathExpr];
- } else {
- // Note that we have used $this->slashes2descendant to simplify this logic, so the
- // "Abbreviated" paths basically never exist as '//' never exists.
- // mini syntax check
- if (!$this->_bracketsCheck($PathExpr)) {
- $this->_displayError('While parsing an XPath query, in the PathExpr "' .
- $PathExpr.
- '", there was an invalid number of brackets or a bracket mismatch.', __LINE__, __FILE__);
- }
- // Save the current path.
- $this->currentXpathQuery = $PathExpr;
- // Split the path at every slash *outside* a bracket.
- $steps = $this->_bracketExplode('/', $PathExpr);
- if ($bDebugThisFunction) { echo "<hr>Split the path '$PathExpr' at every slash *outside* a bracket.\n "; print_r($steps); }
- // Check whether the first element is empty.
- if (empty($steps[0])) {
- // Remove the first and empty element. It's a starting '//'.
- array_shift($steps);
- }
- $aResultsCache[$PathExpr] = $steps;
- }
- // Start to evaluate the steps.
- // ### Consider implementing an evaluateSteps() function that removes recursion from
- // evaluateStep()
- $result = $this->_evaluateStep($steps, $context);
- // Preserve doc order if there was more than one result
- if (count($result) > 1) {
- $result = $this->_sortByDocOrder($result);
- }
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
- /**
- * Sort an xPathSet by doc order.
- *
- * @param $xPathSet (array) Array of full paths to nodes that need to be sorted
- * @return (array) Array containing the same contents as $xPathSet, but
- * with the contents in doc order
- */
- function _sortByDocOrder($xPathSet) {
- $ThisFunctionName = '_sortByDocOrder';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "_sortByDocOrder(xPathSet:[".count($xPathSet)."])";
- echo "xPathSet:\n";
- print_r($xPathSet);
- echo "<hr>\n";
- }
- //////////////////////////////////////////////
- $aResult = array();
- // Spot some common shortcuts.
- if (count($xPathSet) < 1) {
- $aResult = $xPathSet;
- } else {
- // Build an array of doc-pos indexes.
- $aDocPos = array();
- $nodeCount = count($this->nodeIndex);
- $aPaths = array_keys($this->nodeIndex);
- if ($bDebugThisFunction) {
- echo "searching for path indices in array_keys(this->nodeIndex)...\n";
- //print_r($aPaths);
- }
- // The last index we found. In general the elements will be in groups
- // that are themselves in order.
- $iLastIndex = 0;
- foreach ($xPathSet as $path) {
- // Cycle round the nodes, starting at the last index, looking for the path.
- $foundNode = FALSE;
- for ($iIndex = $iLastIndex; $iIndex < $nodeCount + $iLastIndex; $iIndex++) {
- $iThisIndex = $iIndex % $nodeCount;
- if (!strcmp($aPaths[$iThisIndex],$path)) {
- // we have found the doc-position index of the path
- $aDocPos[] = $iThisIndex;
- $iLastIndex = $iThisIndex;
- $foundNode = TRUE;
- break;
- }
- }
- if ($bDebugThisFunction) {
- if (!$foundNode)
- echo "Error: $path not found in \$this->nodeIndex\n";
- else
- echo "Found node after ".($iIndex - $iLastIndex)." iterations\n";
- }
- }
- // Now count the number of doc pos we have and the number of results and
- // confirm that we have the same number of each.
- $iDocPosCount = count($aDocPos);
- $iResultCount = count($xPathSet);
- if ($iDocPosCount != $iResultCount) {
- if ($bDebugThisFunction) {
- echo "count(\$aDocPos)=$iDocPosCount; count(\$result)=$iResultCount\n";
- print_r(array_keys($this->nodeIndex));
- }
- $this->_displayError('Results from _InternalEvaluate() are corrupt. '.
- 'Do you need to call reindexNodeTree()?', __LINE__, __FILE__);
- }
- // Now sort the indexes.
- sort($aDocPos);
- // And now convert back to paths.
- $iPathCount = count($aDocPos);
- for ($iIndex = 0; $iIndex < $iPathCount; $iIndex++) {
- $aResult[] = $aPaths[$aDocPos[$iIndex]];
- }
- }
- // Our result from the function is this array.
- $result = $aResult;
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
- /**
- * Evaluate a step from a XPathQuery expression at a specific contextPath.
- *
- * Steps are the arguments of a XPathQuery when divided by a '/'. A contextPath is a
- * absolute XPath (or vector of XPaths) to a starting node(s) from which the step should
- * be evaluated.
- *
- * @param $steps (array) Vector containing the remaining steps of the current
- * XPathQuery expression.
- * @param $context (array) The context from which to evaluate
- * @return (array) Vector of absolute XPath's as a result of the step
- * evaluation. The results will not necessarily be in doc order
- * @see _evaluatePathExpr()
- */
- function _evaluateStep($steps, $context) {
- $ThisFunctionName = '_evaluateStep';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Context:";
- $this->_printContext($context);
- echo "\n";
- echo "Steps: ";
- print_r($steps);
- echo "<hr>\n";
- }
- //////////////////////////////////////////////
- $result = array(); // Create an empty array for saving the abs. XPath's found.
- $contextPaths = array(); // Create an array to save the new contexts.
- $step = trim(array_shift($steps)); // Get this step.
- if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n";
-
- $axis = $this->_getAxis($step); // Get the axis of the current step.
- // If there was no axis, then it must be a PrimaryExpr
- if ($axis == FALSE) {
- if ($bDebugThisFunction) echo __LINE__.":Step is not an axis but a PrimaryExpr\n";
- // ### This isn't correct, as the result of this function might not be a node set.
- $error = $this->_evaluatePrimaryExpr($step, $context, $contextPaths);
- if (!empty($error)) {
- $this->_displayError("Expression failed to parse as PrimaryExpr because: $error"
- , __LINE__, __FILE__, FALSE);
- }
- } else {
- if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; print_r($axis); echo "\n";}
- $method = '_handleAxis_' . $axis['axis']; // Create the name of the method.
-
- // Check whether the axis handler is defined. If not display an error message.
- if (!method_exists($this, $method)) {
- $this->_displayError('While parsing an XPath query, the axis ' .
- $axis['axis'] . ' could not be handled, because this version does not support this axis.', __LINE__, __FILE__);
- }
- if ($bDebugThisFunction) echo __LINE__.":Calling user method $method\n";
-
- // Perform an axis action.
- $contextPaths = $this->$method($axis, $context['nodePath']);
- if ($bDebugThisFunction) { echo __LINE__.":We found these contexts from this step:\n"; print_r( $contextPaths ); echo "\n";}
- }
- // Check whether there are predicates.
- if (count($contextPaths) > 0 && count($axis['predicate']) > 0) {
- if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by predicate...\n";
-
- // Check whether each node fits the predicates.
- $contextPaths = $this->_checkPredicates($contextPaths, $axis['predicate']);
- }
- // Check whether there are more steps left.
- if (count($steps) > 0) {
- if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given the context of the first step...\n";
-
- // Continue the evaluation of the next steps.
- // Run through the array.
- $size = sizeOf($contextPaths);
- for ($pos=0; $pos<$size; $pos++) {
- // Build new context
- $newContext = array('nodePath' => $contextPaths[$pos], 'size' => $size, 'pos' => $pos + 1);
- if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the {$contextPaths[$pos]} context...\n";
- // Call this method for this single path.
- $xPathSetNew = $this->_evaluateStep($steps, $newContext);
- if ($bDebugThisFunction) {echo "New results for this context:\n"; print_r($xPathSetNew);}
- $result = array_merge($result, $xPathSetNew);
- }
- // Remove duplicated nodes.
- $result = array_unique($result);
- } else {
- $result = $contextPaths; // Save the found contexts.
- }
-
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
-
- /**
- * Checks whether a node matches predicates.
- *
- * This method checks whether a list of nodes passed to this method match
- * a given list of predicates.
- *
- * @param $xPathSet (array) Array of full paths of all nodes to be tested.
- * @param $predicates (array) Array of predicates to use.
- * @return (array) Vector of absolute XPath's that match the given predicates.
- * @see _evaluateStep()
- */
- function _checkPredicates($xPathSet, $predicates) {
- $ThisFunctionName = '_checkPredicates';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "XPathSet:";
- print_r($xPathSet);
- echo "Predicates:";
- print_r($predicates);
- echo "<hr>";
- }
- //////////////////////////////////////////////
- // Create an empty set of nodes.
- $result = array();
- // Run through all predicates.
- $pSize = sizeOf($predicates);
- for ($j=0; $j<$pSize; $j++) {
- $predicate = $predicates[$j];
- if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
- // This will contain all the nodes that match this predicate
- $aNewSet = array();
-
- // Run through all nodes.
- $contextSize = count($xPathSet);
- for ($contextPos=0; $contextPos<$contextSize; $contextPos++) {
- $xPath = $xPathSet[$contextPos];
- // Build the context for this predicate
- $context = array('nodePath' => $xPath, 'size' => $contextSize, 'pos' => $contextPos + 1);
-
- // Check whether the predicate is just an number.
- if (preg_match('/^\d+$/', $predicate)) {
- if ($bDebugThisFunction) echo "Taking short cut and calling _handleFunction_position() directly.\n";
- // Take a short cut. If it is just a position, then call
- // _handleFunction_position() directly. 70% of the
- // time this will be the case. ## N.S
- // $check = (bool) ($predicate == $context['pos']);
- $check = (bool) ($predicate == $this->_handleFunction_position('', $context));
- } else {
- // Else do the predicate check the long and through way.
- $check = $this->_evaluateExpr($predicate, $context);
- }
- if ($bDebugThisFunction) {
- echo "Evaluating the predicate returned ";
- var_dump($check);
- echo "\n";
- }
- if (is_int($check)) { // Check whether it's an integer.
- // Check whether it's the current position.
- $check = (bool) ($check == $this->_handleFunction_position('', $context));
- } else {
- $check = (bool) ($this->_handleFunction_boolean($check, $context));
- // if ($bDebugThisFunction) {echo $this->_handleFunction_string($check, $context);}
- }
- if ($bDebugThisFunction) echo "Node $xPath matches predicate $predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
- // Do we add it?
- if ($check) $aNewSet[] = $xPath;
- }
-
- // Use the newly filtered list.
- $xPathSet = $aNewSet;
- if ($bDebugThisFunction) {echo "Node set now contains : "; print_r($xPathSet); }
- }
- $result = $xPathSet;
- //////////////////////////////////////////////
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the array of nodes.
- return $result;
- }
-
- /**
- * Evaluates an XPath function
- *
- * This method evaluates a given XPath function with its arguments on a
- * specific node of the document.
- *
- * @param $function (string) Name of the function to be evaluated.
- * @param $arguments (string) String containing the arguments being
- * passed to the function.
- * @param $context (array) The context from which to evaluate
- * @return (mixed) This method returns the result of the evaluation of
- * the function. Depending on the function the type of the
- * return value can be different.
- * @see evaluate()
- */
- function _evaluateFunction($function, $arguments, $context) {
- $ThisFunctionName = '_evaluateFunction';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- if (is_array($arguments)) {
- echo "Arguments:\n";
- print_r($arguments);
- } else {
- echo "Arguments: $arguments\n";
- }
- echo "Context:";
- $this->_printContext($context);
- echo "\n";
- echo "<hr>\n";
- }
- /////////////////////////////////////
- // Remove whitespaces.
- $function = trim($function);
- $arguments = trim($arguments);
- // Create the name of the function handling function.
- $method = '_handleFunction_'. $function;
-
- // Check whether the function handling function is available.
- if (!method_exists($this, $method)) {
- // Display an error message.
- $this->_displayError("While parsing an XPath query, ".
- "the function \"$function\" could not be handled, because this ".
- "version does not support this function.", __LINE__, __FILE__);
- }
- if ($bDebugThisFunction) echo "Calling function $method($arguments)\n";
-
- // Return the result of the function.
- $result = $this->$method($arguments, $context);
-
- //////////////////////////////////////////////
- // Return the nodes found.
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- // Return the result.
- return $result;
- }
-
- /**
- * Checks whether a node matches a node-test.
- *
- * This method checks whether a node in the document matches a given node-test.
- * A node test is something like text(), node(), or an element name.
- *
- * @param $contextPath (string) Full xpath of the node, which should be tested for
- * matching the node-test.
- * @param $nodeTest (string) String containing the node-test for the node.
- * @return (boolean) This method returns TRUE if the node matches the
- * node-test, otherwise FALSE.
- * @see evaluate()
- */
- function _checkNodeTest($contextPath, $nodeTest) {
- // Empty node test means that it must match
- if (empty($nodeTest)) return TRUE;
- if ($nodeTest == '*') {
- // * matches all element nodes.
- return (!preg_match(':/[^/]+\(\)\[\d+\]$:U', $contextPath));
- }
- elseif (preg_match('/^[\w-:\.]+$/', $nodeTest)) {
- // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
- // The real spec for what constitutes whitespace is quite elaborate, and
- // we currently just hope that "\w" catches them all. In reality it should
- // start with a letter too, not a number, but we've just left it simple.
- // It's just a node name test. It should end with "/$nodeTest[x]"
- return (preg_match('"/'.$nodeTest.'\[\d+\]$"', $contextPath));
- }
- elseif (preg_match('/\(/U', $nodeTest)) { // Check whether it's a function.
- // Get the type of function to use.
- $function = $this->_prestr($nodeTest, '(');
- // Check whether the node fits the method.
- switch ($function) {
- case 'node': // Add this node to the list of nodes.
- return TRUE;
- case 'text': // Check whether the node has some text.
- $tmp = implode('', $this->nodeIndex[$contextPath]['textParts']);
- if (!empty($tmp)) {
- return TRUE; // Add this node to the list of nodes.
- }
- break;
- /******** NOT supported (yet?)
- case 'comment': // Check whether the node has some comment.
- if (!empty($this->nodeIndex[$contextPath]['comment'])) {
- return TRUE; // Add this node to the list of nodes.
- }
- break;
- case 'processing-instruction':
- $literal = $this->_afterstr($axis['node-test'], '('); // Get the literal argument.
- $literal = substr($literal, 0, strlen($literal) - 1); // Cut the literal.
-
- // Check whether a literal was given.
- if (!empty($literal)) {
- // Check whether the node's processing instructions are matching the literals given.
- if ($this->nodeIndex[$context]['processing-instructions'] == $literal) {
- return TRUE; // Add this node to the node-set.
- }
- } else {
- // Check whether the node has processing instructions.
- if (!empty($this->nodeIndex[$contextPath]['processing-instructions'])) {
- return TRUE; // Add this node to the node-set.
- }
- }
- break;
- ***********/
- default: // Display an error message.
- $this->_displayError('While parsing an XPath query there was an undefined function called "' .
- str_replace($function, '<b>'.$function.'</b>', $this->currentXpathQuery) .'"', __LINE__, __FILE__);
- }
- }
- else { // Display an error message.
- $this->_displayError("While parsing the XPath query \"{$this->currentXpathQuery}\" ".
- "an empty and therefore invalid node-test has been found.", __LINE__, __FILE__, FALSE);
- }
-
- return FALSE; // Don't add this context.
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ XPath AXIS Handlers ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Retrieves axis information from an XPath query step.
- *
- * This method tries to extract the name of the axis and its node-test
- * from a given step of an XPath query at a given node. If it can't parse
- * the step, then we treat it as a PrimaryExpr.
- *
- * [4] Step ::= AxisSpecifier NodeTest Predicate*
- * | AbbreviatedStep
- * [5] AxisSpecifier ::= AxisName '::'
- * | AbbreviatedAxisSpecifier
- * [12] AbbreviatedStep ::= '.'
- * | '..'
- * [13] AbbreviatedAxisSpecifier
- * ::= '@'?
- *
- * [7] NodeTest ::= NameTest
- * | NodeType '(' ')'
- * | 'processing-instruction' '(' Literal ')'
- * [37] NameTest ::= '*'
- * | NCName ':' '*'
- * | QName
- * [38] NodeType ::= 'comment'
- * | 'text'
- * | 'processing-instruction'
- * | 'node'
- *
- * @param $step (string) String containing a step of an XPath query.
- * @return (array) Contains information about the axis found in the step, or FALSE
- * if the string isn't a valid step.
- * @see _evaluateStep()
- */
- function _getAxis($step) {
- // The results of this function are very cachable, as it is completely independant of context.
- static $aResultsCache = array();
- // Create an array to save the axis information.
- $axis = array(
- 'axis' => '',
- 'node-test' => '',
- 'predicate' => array()
- );
- $cacheKey = $step;
- do { // parse block
- $parseBlock = 1;
- if (isset($aResultsCache[$cacheKey])) {
- return $aResultsCache[$cacheKey];
- } else {
- // We have some danger of causing recursion here if we refuse to parse a step as having an
- // axis, and demand it be treated as a PrimaryExpr. So if we are going to fail, make sure
- // we record what we tried, so that we can catch to see if it comes straight back.
- $guess = array(
- 'axis' => 'child',
- 'node-test' => $step,
- 'predicate' => array());
- $aResultsCache[$cacheKey] = $guess;
- }
- ///////////////////////////////////////////////////
- // Spot the steps that won't come with an axis
- // Check whether the step is empty or only self.
- if (empty($step) OR ($step == '.') OR ($step == 'current()')) {
- // Set it to the default value.
- $step = '.';
- $axis['axis'] = 'self';
- $axis['node-test'] = '*';
- break $parseBlock;
- }
- if ($step == '..') {
- // Select the parent axis.
- $axis['axis'] = 'parent';
- $axis['node-test'] = '*';
- break $parseBlock;
- }
- ///////////////////////////////////////////////////
- // Pull off the predicates
- // Check whether there are predicates and add the predicate to the list
- // of predicates without []. Get contents of every [] found.
- $groups = $this->_getEndGroups($step);
- //print_r($groups);
- $groupCount = count($groups);
- while (($groupCount > 0) && ($groups[$groupCount - 1][0] == '[')) {
- // Remove the [] and add the predicate to the top of the list
- $predicate = substr($groups[$groupCount - 1], 1, -1);
- array_unshift($axis['predicate'], $predicate);
- // Pop a group off the end of the list
- array_pop($groups);
- $groupCount--;
- }
- // Finally stick the rest back together and this is the rest of our step
- if ($groupCount > 0) {
- $step = implode('', $groups);
- }
- ///////////////////////////////////////////////////
- // Pull off the axis
- // Check for abbreviated syntax
- if ($step[0] == '@') {
- // Use the attribute axis and select the attribute.
- $axis['axis'] = 'attribute';
- $step = substr($step, 1);
- } else {
- // Check whether the axis is given in plain text.
- if (preg_match("/^([^:]*)::(.*)$/", $step, $match)) {
- // Split the step to extract axis and node-test.
- $axis['axis'] = $match[1];
- $step = $match[2];
- } else {
- // The default axis is child
- $axis['axis'] = 'child';
- }
- }
- ///////////////////////////////////////////////////
- // Process the rest which will either a node test, or else this isn't a step.
- // Check whether is an abbreviated syntax.
- if ($step == '*') {
- // Use the child axis and select all children.
- $axis['node-test'] = '*';
- break $parseBlock;
- }
- // ### I'm pretty sure our current handling of cdata is a fudge, and we should
- // really do this better, but leave this as is for now.
- if ($step == "text()") {
- // Handle the text node
- $axis["node-test"] = "cdata";
- break $parseBlock;
- }
- // There are a few node tests that we match verbatim.
- if ($step == "node()"
- || $step == "comment()"
- || $step == "text()"
- || $step == "processing-instruction") {
- $axis["node-test"] = $step;
- break $parseBlock;
- }
- // processing-instruction() is allowed to take an argument, but if it does, the argument
- // is a literal, which we will have parsed out to $[number].
- if (preg_match(":processing-instruction\(\$\d*\):", $step)) {
- $axis["node-test"] = $step;
- break $parseBlock;
- }
- // The only remaining way this can be a step, is if the remaining string is a simple name
- // or else a :* name.
- // http://www.w3.org/TR/xpath#NT-NameTest
- // NameTest ::= '*'
- // | NCName ':' '*'
- // | QName
- // QName ::= (Prefix ':')? LocalPart
- // Prefix ::= NCName
- // LocalPart ::= NCName
- //
- // ie
- // NameTest ::= '*'
- // | NCName ':' '*'
- // | (NCName ':')? NCName
- // NCName ::= (Letter | '_') (NCNameChar)*
- $NCName = "[a-zA-Z_][\w\.\-_]*";
- if (preg_match("/^$NCName:$NCName$/", $step)
- || preg_match("/^$NCName:*$/", $step)) {
- $axis['node-test'] = $step;
- if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
- // Case in-sensitive
- $axis['node-test'] = strtoupper($axis['node-test']);
- }
- // Not currently recursing
- $LastFailedStep = '';
- $LastFailedContext = '';
- break $parseBlock;
- }
- // It's not a node then, we must treat it as a PrimaryExpr
- // Check for recursion
- if ($LastFailedStep == $step) {
- $this->_displayError('Recursion detected while parsing an XPath query, in the step ' .
- str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery)
- , __LINE__, __FILE__, FALSE);
- $axis['node-test'] = $step;
- } else {
- $LastFailedStep = $step;
- $axis = FALSE;
- }
-
- } while(FALSE); // end parse block
-
- // Check whether it's a valid axis.
- if ($axis !== FALSE) {
- if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) {
- // Display an error message.
- $this->_displayError('While parsing an XPath query, in the step ' .
- str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) .
- ' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__, __FILE__, FALSE);
- }
- }
- // Cache the real axis information
- $aResultsCache[$cacheKey] = $axis;
- // Return the axis information.
- return $axis;
- }
-
- /**
- * Handles the XPath child axis.
- *
- * This method handles the XPath child axis. It essentially filters out the
- * children to match the name specified after the '/'.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should
- * be processed.
- * @return (array) A vector containing all nodes that were found, during
- * the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_child($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set to hold the results of the child matches
- if ($axis["node-test"] == "cdata") {
- if (!isSet($this->nodeIndex[$contextPath]['textParts']) ) return '';
- $tSize = sizeOf($this->nodeIndex[$contextPath]['textParts']);
- for ($i=1; $i<=$tSize; $i++) {
- $xPathSet[] = $contextPath . '/text()['.$i.']';
- }
- }
- else {
- // Get a list of all children.
- $allChildren = $this->nodeIndex[$contextPath]['childNodes'];
-
- // Run through all children in the order they where set.
- $cSize = sizeOf($allChildren);
- for ($i=0; $i<$cSize; $i++) {
- $childPath = $contextPath .'/'. $allChildren[$i]['name'] .'['. $allChildren[$i]['contextPos'] .']';
- $textChildPath = $contextPath.'/text()['.($i + 1).']';
- // Check the text node
- if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
- $xPathSet[] = $textChildPath; // Add the child to the node-set.
- }
- // Check the actual node
- if ($this->_checkNodeTest($childPath, $axis['node-test'])) { // node test check
- $xPathSet[] = $childPath; // Add the child to the node-set.
- }
- }
- // Finally there will be one more text node to try
- $textChildPath = $contextPath.'/text()['.($cSize + 1).']';
- // Check the text node
- if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
- $xPathSet[] = $textChildPath; // Add the child to the node-set.
- }
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath parent axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the
- * evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_parent($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Check whether the parent matches the node-test.
- $parentPath = $this->getParentXPath($contextPath);
- if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
- $xPathSet[] = $parentPath; // Add this node to the list of nodes.
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath attribute axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_attribute($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Check whether all nodes should be selected.
- $nodeAttr = $this->nodeIndex[$contextPath]['attributes'];
- if ($axis['node-test'] == '*'
- || $axis['node-test'] == 'node()') {
- foreach($nodeAttr as $key=>$dummy) { // Run through the attributes.
- $xPathSet[] = $contextPath.'/attribute::'.$key; // Add this node to the node-set.
- }
- }
- elseif (isset($nodeAttr[$axis['node-test']])) {
- $xPathSet[] = $contextPath . '/attribute::'. $axis['node-test']; // Add this node to the node-set.
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath self axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_self($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Check whether the context match the node-test.
- if ($this->_checkNodeTest($contextPath, $axis['node-test'])) {
- $xPathSet[] = $contextPath; // Add this node to the node-set.
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath descendant axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_descendant($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Get a list of all children.
- $allChildren = $this->nodeIndex[$contextPath]['childNodes'];
-
- // Run through all children in the order they where set.
- $cSize = sizeOf($allChildren);
- for ($i=0; $i<$cSize; $i++) {
- $childPath = $allChildren[$i]['xpath'];
- // Check whether the child matches the node-test.
- if ($this->_checkNodeTest($childPath, $axis['node-test'])) {
- $xPathSet[] = $childPath; // Add the child to the list of nodes.
- }
- // Recurse to the next level.
- $xPathSet = array_merge($xPathSet, $this->_handleAxis_descendant($axis, $childPath));
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath ancestor axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_ancestor($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- $parentPath = $this->getParentXPath($contextPath); // Get the parent of the current node.
-
- // Check whether the parent isn't super-root.
- if (!empty($parentPath)) {
- // Check whether the parent matches the node-test.
- if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
- $xPathSet[] = $parentPath; // Add the parent to the list of nodes.
- }
- // Handle all other ancestors.
- $xPathSet = array_merge($this->_handleAxis_ancestor($axis, $parentPath), $xPathSet);
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath namespace axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_namespace($axis, $contextPath) {
- $this->_displayError("The axis 'namespace is not suported'", __LINE__, __FILE__, FALSE);
- }
-
- /**
- * Handles the XPath following axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_following($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- do { // try-block
- $node = $this->nodeIndex[$contextPath]; // Get the current node
- $position = $node['pos']; // Get the current tree position.
- $parent = $node['parentNode'];
- // Check if there is a following sibling at all; if not end.
- if ($position >= sizeOf($parent['childNodes'])) break; // try-block
- // Build the starting abs. XPath
- $startXPath = $parent['childNodes'][$position+1]['xpath'];
- // Run through all nodes of the document.
- $nodeKeys = array_keys($this->nodeIndex);
- $nodeSize = sizeOf($nodeKeys);
- for ($k=0; $k<$nodeSize; $k++) {
- if ($nodeKeys[$k] == $startXPath) break; // Check whether this is the starting abs. XPath
- }
- for (; $k<$nodeSize; $k++) {
- // Check whether the node fits the node-test.
- if ($this->_checkNodeTest($nodeKeys[$k], $axis['node-test'])) {
- $xPathSet[] = $nodeKeys[$k]; // Add the node to the list of nodes.
- }
- }
- } while(FALSE);
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath preceding axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_preceding($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Run through all nodes of the document.
- foreach ($this->nodeIndex as $xPath=>$dummy) {
- if (empty($xPath)) continue; // skip super-Root
-
- // Check whether this is the context node.
- if ($xPath == $contextPath) {
- break; // After this we won't look for more nodes.
- }
- if (!strncmp($xPath, $contextPath, strLen($xPath))) {
- continue;
- }
- // Check whether the node fits the node-test.
- if ($this->_checkNodeTest($xPath, $axis['node-test'])) {
- $xPathSet[] = $xPath; // Add the node to the list of nodes.
- }
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath following-sibling axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_following_sibling($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Get all children from the parent.
- $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
- // Create a flag whether the context node was already found.
- $found = FALSE;
-
- // Run through all siblings.
- $size = sizeOf($siblings);
- for ($i=0; $i<$size; $i++) {
- $sibling = $siblings[$i];
-
- // Check whether the context node was already found.
- if ($found) {
- // Check whether the sibling matches the node-test.
- if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
- $xPathSet[] = $sibling; // Add the sibling to the list of nodes.
- }
- }
- // Check if we reached *this* context node.
- if ($sibling == $contextPath) {
- $found = TRUE; // Continue looking for other siblings.
- }
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath preceding-sibling axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_preceding_sibling($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Get all children from the parent.
- $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
-
- // Run through all siblings.
- $size = sizeOf($siblings);
- for ($i=0; $i<$size; $i++) {
- $sibling = $siblings[$i];
- // Check whether this is the context node.
- if ($sibling == $contextPath) {
- break; // Don't continue looking for other siblings.
- }
- // Check whether the sibling matches the node-test.
- if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
- $xPathSet[] = $sibling; // Add the sibling to the list of nodes.
- }
- }
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath descendant-or-self axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_descendant_or_self($axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Read the nodes.
- $xPathSet = array_merge(
- $this->_handleAxis_self($axis, $contextPath),
- $this->_handleAxis_descendant($axis, $contextPath)
- );
- return $xPathSet; // Return the nodeset.
- }
-
- /**
- * Handles the XPath ancestor-or-self axis.
- *
- * This method handles the XPath ancestor-or-self axis.
- *
- * @param $axis (array) Array containing information about the axis.
- * @param $contextPath (string) xpath to starting node from which the axis should be processed.
- * @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
- * @see evaluate()
- */
- function _handleAxis_ancestor_or_self ( $axis, $contextPath) {
- $xPathSet = array(); // Create an empty node-set.
-
- // Read the nodes.
- $xPathSet = array_merge(
- $this->_handleAxis_ancestor($axis, $contextPath),
- $this->_handleAxis_self($axis, $contextPath)
- );
- return $xPathSet; // Return the nodeset.
- }
-
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ XPath FUNCTION Handlers ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Handles the XPath function last.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_last($arguments, $context) {
- return $context['size'];
- }
-
- /**
- * Handles the XPath function position.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_position($arguments, $context) {
- return $context['pos'];
- }
-
- /**
- * Handles the XPath function count.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_count($arguments, $context) {
- // Evaluate the argument of the method as an XPath and return the number of results.
- return count($this->_evaluateExpr($arguments, $context));
- }
-
- /**
- * Handles the XPath function id.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_id($arguments, $context) {
- $arguments = trim($arguments); // Trim the arguments.
- $arguments = explode(' ', $arguments); // Now split the arguments into an array.
- // Create a list of nodes.
- $resultXPaths = array();
- // Run through all nodes of the document.
- $keys = array_keys($this->nodeIndex);
- $kSize = $sizeOf($keys);
- for ($i=0; $i<$kSize; $i++) {
- if (empty($keys[$i])) continue; // skip super-Root
- if (in_array($this->nodeIndex[$keys[$i]]['attributes']['id'], $arguments)) {
- $resultXPaths[] = $context['nodePath']; // Add this node to the list of nodes.
- }
- }
- return $resultXPaths; // Return the list of nodes.
- }
-
- /**
- * Handles the XPath function name.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_name($arguments, $context) {
- // If the argument it omitted, it defaults to a node-set with the context node as its only member.
- if (empty($arguments)) {
- return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['name']);
- }
- // Evaluate the argument to get a node set.
- $nodeSet = $this->_evaluateExpr($arguments, $context);
- if (!is_array($nodeSet)) return '';
- if (count($nodeSet) < 1) return '';
- if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
- // Return a reference to the name of the node.
- return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['name']);
- }
-
- /**
- * Handles the XPath function string.
- *
- * http://www.w3.org/TR/xpath#section-String-Functions
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_string($arguments, $context) {
- // Check what type of parameter is given
- if (is_array($arguments)) {
- // Get the value of the first result (which means we want to concat all the text...unless
- // a specific text() node has been given, and it will switch off to substringData
- if (!count($arguments)) $result = '';
- else {
- $result = $this->_stringValue($arguments[0]);
- if (($literal = $this->_asLiteral($result)) !== FALSE) {
- $result = $literal;
- }
- }
- }
- // Is it a number string?
- elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR preg_match('/^\.[0-9]+$/', $arguments)) {
- // ### Note no support for NaN and Infinity.
- $number = doubleval($arguments); // Convert the digits to a number.
- $result = strval($number); // Return the number.
- }
- elseif (is_bool($arguments)) { // Check whether it's TRUE or FALSE and return as string.
- // ### Note that we used to return TRUE and FALSE which was incorrect according to the standard.
- if ($arguments === TRUE) {
- $result = 'true';
- } else {
- $result = 'false';
- }
- }
- elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
- return $literal;
- }
- elseif (!empty($arguments)) {
- // Spec says:
- // "An object of a type other than the four basic types is converted to a string in a way that
- // is dependent on that type."
- // Use the argument as an XPath.
- $result = $this->_evaluateExpr($arguments, $context);
- if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
- $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_string($result)", __LINE__, __FILE__, FALSE);
- return '';
- } else {
- $result = $this->_handleFunction_string($result, $context);
- }
- }
- else {
- $result = ''; // Return an empty string.
- }
- return $result;
- }
-
- /**
- * Handles the XPath function concat.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_concat($arguments, $context) {
- // Split the arguments.
- $arguments = explode(',', $arguments);
- // Run through each argument and evaluate it.
- $size = sizeof($arguments);
- for ($i=0; $i<$size; $i++) {
- $arguments[$i] = trim($arguments[$i]); // Trim each argument.
- // Evaluate it.
- $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
- }
- $arguments = implode('', $arguments); // Put the string together and return it.
- return $this->_addLiteral($arguments);
- }
-
- /**
- * Handles the XPath function starts-with.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_starts_with($arguments, $context) {
- // Get the arguments.
- $first = trim($this->_prestr($arguments, ','));
- $second = trim($this->_afterstr($arguments, ','));
- // Evaluate each argument.
- $first = $this->_handleFunction_string($first, $context);
- $second = $this->_handleFunction_string($second, $context);
- // Check whether the first string starts with the second one.
- return (bool) ereg('^'.$second, $first);
- }
-
- /**
- * Handles the XPath function contains.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_contains($arguments, $context) {
- // Get the arguments.
- $first = trim($this->_prestr($arguments, ','));
- $second = trim($this->_afterstr($arguments, ','));
- //echo "Predicate: $arguments First: ".$first." Second: ".$second."\n";
- // Evaluate each argument.
- $first = $this->_handleFunction_string($first, $context);
- $second = $this->_handleFunction_string($second, $context);
- //echo $second.": ".$first."\n";
- // If the search string is null, then the provided there is a value it will contain it as
- // it is considered that all strings contain the empty string. ## N.S.
- if ($second==='') return TRUE;
- // Check whether the first string starts with the second one.
- if (strpos($first, $second) === FALSE) {
- return FALSE;
- } else {
- return TRUE;
- }
- }
-
- /**
- * Handles the XPath function substring-before.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_substring_before($arguments, $context) {
- // Get the arguments.
- $first = trim($this->_prestr($arguments, ','));
- $second = trim($this->_afterstr($arguments, ','));
- // Evaluate each argument.
- $first = $this->_handleFunction_string($first, $context);
- $second = $this->_handleFunction_string($second, $context);
- // Return the substring.
- return $this->_addLiteral($this->_prestr(strval($first), strval($second)));
- }
-
- /**
- * Handles the XPath function substring-after.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_substring_after($arguments, $context) {
- // Get the arguments.
- $first = trim($this->_prestr($arguments, ','));
- $second = trim($this->_afterstr($arguments, ','));
- // Evaluate each argument.
- $first = $this->_handleFunction_string($first, $context);
- $second = $this->_handleFunction_string($second, $context);
- // Return the substring.
- return $this->_addLiteral($this->_afterstr(strval($first), strval($second)));
- }
-
- /**
- * Handles the XPath function substring.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_substring($arguments, $context) {
- // Split the arguments.
- $arguments = explode(",", $arguments);
- $size = sizeOf($arguments);
- for ($i=0; $i<$size; $i++) { // Run through all arguments.
- $arguments[$i] = trim($arguments[$i]); // Trim the string.
- // Evaluate each argument.
- $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
- }
- // Check whether a third argument was given and return the substring..
- if (!empty($arguments[2])) {
- return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1, $arguments[2]));
- } else {
- return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1));
- }
- }
-
- /**
- * Handles the XPath function string-length.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_string_length($arguments, $context) {
- $arguments = trim($arguments); // Trim the argument.
- // Evaluate the argument.
- $arguments = $this->_handleFunction_string($arguments, $context);
- return strlen(strval($arguments)); // Return the length of the string.
- }
- /**
- * Handles the XPath function normalize-space.
- *
- * The normalize-space function returns the argument string with whitespace
- * normalized by stripping leading and trailing whitespace and replacing sequences
- * of whitespace characters by a single space.
- * If the argument is omitted, it defaults to the context node converted to a string,
- * in other words the string-value of the context node
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (stri)g trimed string
- * @see evaluate()
- */
- function _handleFunction_normalize_space($arguments, $context) {
- if (empty($arguments)) {
- $arguments = $this->getParentXPath($context['nodePath']).'/'.$this->nodeIndex[$context['nodePath']]['name'].'['.$this->nodeIndex[$context['nodePath']]['contextPos'].']';
- } else {
- $arguments = $this->_handleFunction_string($arguments, $context);
- }
- $arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments));
- return $this->_addLiteral($arguments);
- }
- /**
- * Handles the XPath function translate.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_translate($arguments, $context) {
- $arguments = explode(',', $arguments); // Split the arguments.
- $size = sizeOf($arguments);
- for ($i=0; $i<$size; $i++) { // Run through all arguments.
- $arguments[$i] = trim($arguments[$i]); // Trim the argument.
- // Evaluate the argument.
- $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
- }
- // Return the translated string.
- return $this->_addLiteral(strtr($arguments[0], $arguments[1], $arguments[2]));
- }
- /**
- * Handles the XPath function boolean.
- *
- * http://www.w3.org/TR/xpath#section-Boolean-Functions
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_boolean($arguments, $context) {
- if (empty($arguments)) {
- return FALSE; // Sorry, there were no arguments.
- }
- // a bool is dead obvious
- elseif (is_bool($arguments)) {
- return $arguments;
- }
- // a node-set is true if and only if it is non-empty
- elseif (is_array($arguments)) {
- return (count($arguments) > 0);
- }
- // a number is true if and only if it is neither positive or negative zero nor NaN
- // (Straight out of the XPath spec.. makes no sense?????)
- elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || preg_match('/^\.[0-9]+$/', $arguments)) {
- $number = doubleval($arguments); // Convert the digits to a number.
- // If number zero return FALSE else TRUE.
- if ($number == 0) return FALSE; else return TRUE;
- }
- // a string is true if and only if its length is non-zero
- elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
- return (strlen($literal) != 0);
- }
- // an object of a type other than the four basic types is converted to a boolean in a
- // way that is dependent on that type
- else {
- // Spec says:
- // "An object of a type other than the four basic types is converted to a number in a way
- // that is dependent on that type"
- // Try to evaluate the argument as an XPath.
- $result = $this->_evaluateExpr($arguments, $context);
- if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
- $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_boolean($result)", __LINE__, __FILE__, FALSE);
- return FALSE;
- } else {
- return $this->_handleFunction_boolean($result, $context);
- }
- }
- }
-
- /**
- * Handles the XPath function not.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_not($arguments, $context) {
- // Return the negative value of the content of the brackets.
- $bArgResult = $this->_handleFunction_boolean($arguments, $context);
- //echo "Before inversion: ".($bArgResult?"TRUE":"FALSE")."\n";
- return !$bArgResult;
- }
-
- /**
- * Handles the XPath function TRUE.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_true($arguments, $context) {
- return TRUE; // Return TRUE.
- }
-
- /**
- * Handles the XPath function FALSE.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_false($arguments, $context) {
- return FALSE; // Return FALSE.
- }
-
- /**
- * Handles the XPath function lang.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_lang($arguments, $context) {
- $arguments = trim($arguments); // Trim the arguments.
- $currentNode = $this->nodeIndex[$context['nodePath']];
- while (!empty($currentNode['name'])) { // Run through the ancestors.
- // Check whether the node has an language attribute.
- if (isSet($currentNode['attributes']['xml:lang'])) {
- // Check whether it's the language, the user asks for; if so return TRUE else FALSE
- return eregi('^'.$arguments, $currentNode['attributes']['xml:lang']);
- }
- $currentNode = $currentNode['parentNode']; // Move up to parent
- } // End while
- return FALSE;
- }
-
- /**
- * Handles the XPath function number.
- *
- * http://www.w3.org/TR/xpath#section-Number-Functions
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_number($arguments, $context) {
- // Check the type of argument.
- // A string that is a number
- if (is_numeric($arguments)) {
- return doubleval($arguments); // Return the argument as a number.
- }
- // A bool
- elseif (is_bool($arguments)) { // Return TRUE/FALSE as a number.
- if ($arguments === TRUE) return 1; else return 0;
- }
- // A node set
- elseif (is_array($arguments)) {
- // Is converted to a string then handled like a string
- $string = $this->_handleFunction_string($arguments, $context);
- if (is_numeric($string))
- return doubleval($string);
- }
- elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
- if (is_numeric($literal)) {
- return doubleval($literal);
- } else {
- // If we are to stick strictly to the spec, we should return NaN, but lets just
- // leave PHP to see if can do some dynamic conversion.
- return $literal;
- }
- }
- else {
- // Spec says:
- // "An object of a type other than the four basic types is converted to a number in a way
- // that is dependent on that type"
- // Try to evaluate the argument as an XPath.
- $result = $this->_evaluateExpr($arguments, $context);
- if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
- $this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_number($result)", __LINE__, __FILE__, FALSE);
- return FALSE;
- } else {
- return $this->_handleFunction_number($result, $context);
- }
- }
- }
- /**
- * Handles the XPath function sum.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_sum($arguments, $context) {
- $arguments = trim($arguments); // Trim the arguments.
- // Evaluate the arguments as an XPath query.
- $result = $this->_evaluateExpr($arguments, $context);
- $sum = 0; // Create a variable to save the sum.
- // The sum function expects a node set as an argument.
- if (is_array($result)) {
- // Run through all results.
- $size = sizeOf($result);
- for ($i=0; $i<$size; $i++) {
- $value = $this->_stringValue($result[$i], $context);
- if (($literal = $this->_asLiteral($value)) !== FALSE) {
- $value = $literal;
- }
- $sum += doubleval($value); // Add it to the sum.
- }
- }
- return $sum; // Return the sum.
- }
- /**
- * Handles the XPath function floor.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_floor($arguments, $context) {
- if (!is_numeric($arguments)) {
- $arguments = $this->_handleFunction_number($arguments, $context);
- }
- $arguments = doubleval($arguments); // Convert the arguments to a number.
- return floor($arguments); // Return the result
- }
-
- /**
- * Handles the XPath function ceiling.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_ceiling($arguments, $context) {
- if (!is_numeric($arguments)) {
- $arguments = $this->_handleFunction_number($arguments, $context);
- }
- $arguments = doubleval($arguments); // Convert the arguments to a number.
- return ceil($arguments); // Return the result
- }
-
- /**
- * Handles the XPath function round.
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_round($arguments, $context) {
- if (!is_numeric($arguments)) {
- $arguments = $this->_handleFunction_number($arguments, $context);
- }
- $arguments = doubleval($arguments); // Convert the arguments to a number.
- return round($arguments); // Return the result
- }
- //-----------------------------------------------------------------------------------------
- // XPath ------ XPath Extension FUNCTION Handlers ------
- //-----------------------------------------------------------------------------------------
- /**
- * Handles the XPath function x-lower.
- *
- * lower case a string.
- * string x-lower(string)
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_x_lower($arguments, $context) {
- // Evaluate the argument.
- $string = $this->_handleFunction_string($arguments, $context);
- // Return a reference to the lowercased string
- return $this->_addLiteral(strtolower(strval($string)));
- }
- /**
- * Handles the XPath function x-upper.
- *
- * upper case a string.
- * string x-upper(string)
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @see evaluate()
- */
- function _handleFunction_x_upper($arguments, $context) {
- // Evaluate the argument.
- $string = $this->_handleFunction_string($arguments, $context);
- // Return a reference to the lowercased string
- return $this->_addLiteral(strtoupper(strval($string)));
- }
- /**
- * Handles the XPath function generate-id.
- *
- * Produce a unique id for the first node of the node set.
- *
- * Example usage, produces an index of all the nodes in an .xml document, where the content of each
- * "section" is the exported node as XML.
- *
- * $aFunctions = $xPath->match('//');
- *
- * foreach ($aFunctions as $Function) {
- * $id = $xPath->match("generate-id($Function)");
- * echo "<a href='#$id'>$Function</a><br>";
- * }
- *
- * foreach ($aFunctions as $Function) {
- * $id = $xPath->match("generate-id($Function)");
- * echo "<h2 id='$id'>$Function</h2>";
- * echo htmlspecialchars($xPath->exportAsXml($Function));
- * }
- *
- * @param $arguments (string) String containing the arguments that were passed to the function.
- * @param $context (array) The context from which to evaluate the function
- * @return (mixed) Depending on the type of function being processed
- * @author Ricardo Garcia
- * @see evaluate()
- */
- function _handleFunction_generate_id($arguments, $context) {
- // If the argument is omitted, it defaults to a node-set with the context node as its only member.
- if (is_string($arguments) && empty($arguments)) {
- // We need ids then
- $this->_generate_ids();
- return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['generated_id']);
- }
- // Evaluate the argument to get a node set.
- $nodeSet = $this->_evaluateExpr($arguments, $context);
- if (!is_array($nodeSet)) return '';
- if (count($nodeSet) < 1) return '';
- if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
- // Return a reference to the name of the node.
- // We need ids then
- $this->_generate_ids();
- return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['generated_id']);
- }
- //-----------------------------------------------------------------------------------------
- // XPathEngine ------ Help Stuff ------
- //-----------------------------------------------------------------------------------------
- /**
- * Decodes the character set entities in the given string.
- *
- * This function is given for convenience, as all text strings or attributes
- * are going to come back to you with their entities still encoded. You can
- * use this function to remove these entites.
- *
- * It makes use of the get_html_translation_table(HTML_ENTITIES) php library
- * call, so is limited in the same ways. At the time of writing this seemed
- * be restricted to iso-8859-1
- *
- * ### Provide an option that will do this by default.
- *
- * @param $encodedData (mixed) The string or array that has entities you would like to remove
- * @param $reverse (bool) If TRUE entities will be encoded rather than decoded, ie
- * < to < rather than < to <.
- * @return (mixed) The string or array returned with entities decoded.
- */
- function decodeEntities($encodedData, $reverse=FALSE) {
- static $aEncodeTbl;
- static $aDecodeTbl;
- // Get the translation entities, but we'll cache the result to enhance performance.
- if (empty($aDecodeTbl)) {
- // Get the translation entities.
- $aEncodeTbl = get_html_translation_table(HTML_ENTITIES);
- $aDecodeTbl = array_flip($aEncodeTbl);
- }
- // If it's just a single string.
- if (!is_array($encodedData)) {
- if ($reverse) {
- return strtr($encodedData, $aEncodeTbl);
- } else {
- return strtr($encodedData, $aDecodeTbl);
- }
- }
- $result = array();
- foreach($encodedData as $string) {
- if ($reverse) {
- $result[] = strtr($string, $aEncodeTbl);
- } else {
- $result[] = strtr($string, $aDecodeTbl);
- }
- }
- return $result;
- }
-
- /**
- * Compare two nodes to see if they are equal (point to the same node in the doc)
- *
- * 2 nodes are considered equal if the absolute XPath is equal.
- *
- * @param $node1 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
- * @param $node2 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
- * @return (bool) TRUE if equal (see text above), FALSE if not (and on error).
- */
- function equalNodes($node1, $node2) {
- $xPath_1 = is_string($node1) ? $node1 : $this->getNodePath($node1);
- $xPath_2 = is_string($node2) ? $node2 : $this->getNodePath($node2);
- return (strncasecmp ($xPath_1, $xPath_2, strLen($xPath_1)) == 0);
- }
-
- /**
- * Get the absolute XPath of a node that is in a document tree.
- *
- * @param $node (array) A real tree-node (hash-array)
- * @return (string) The string path to the node or FALSE on error.
- */
- function getNodePath($node) {
- if (!empty($node['xpath'])) return $node['xpath'];
- $pathInfo = array();
- do {
- if (empty($node['name']) OR empty($node['parentNode'])) break; // End criteria
- $pathInfo[] = array('name' => $node['name'], 'contextPos' => $node['contextPos']);
- $node = $node['parentNode'];
- } while (TRUE);
-
- $xPath = '';
- for ($i=sizeOf($pathInfo)-1; $i>=0; $i--) {
- $xPath .= '/' . $pathInfo[$i]['name'] . '[' . $pathInfo[$i]['contextPos'] . ']';
- }
- if (empty($xPath)) return FALSE;
- return $xPath;
- }
-
- /**
- * Retrieves the absolute parent XPath query.
- *
- * The parents stored in the tree are only relative parents...but all the parent
- * information is stored in the XPath query itself...so instead we use a function
- * to extract the parent from the absolute Xpath query
- *
- * @param $childPath (string) String containing an absolute XPath query
- * @return (string) returns the absolute XPath of the parent
- */
- function getParentXPath($absoluteXPath) {
- $lastSlashPos = strrpos($absoluteXPath, '/');
- if ($lastSlashPos == 0) { // it's already the root path
- return ''; // 'super-root'
- } else {
- return (substr($absoluteXPath, 0, $lastSlashPos));
- }
- }
-
- /**
- * Returns TRUE if the given node has child nodes below it
- *
- * @param $absoluteXPath (string) full path of the potential parent node
- * @return (bool) TRUE if this node exists and has a child, FALSE otherwise
- */
- function hasChildNodes($absoluteXPath) {
- if ($this->_indexIsDirty) $this->reindexNodeTree();
- return (bool) (isSet($this->nodeIndex[$absoluteXPath])
- AND sizeOf($this->nodeIndex[$absoluteXPath]['childNodes']));
- }
-
- /**
- * Translate all ampersands to it's literal entities '&' and back.
- *
- * I wasn't aware of this problem at first but it's important to understand why we do this.
- * At first you must know:
- * a) PHP's XML parser *translates* all entities to the equivalent char E.g. < is returned as '<'
- * b) PHP's XML parser (in V 4.1.0) has problems with most *literal* entities! The only one's that are
- * recognized are &, < > and ". *ALL* others (like © a.s.o.) cause an
- * XML_ERROR_UNDEFINED_ENTITY error. I reported this as bug at http://bugs.php.net/bug.php?id=15092
- * (It turned out not to be a 'real' bug, but one of those nice W3C-spec things).
- *
- * Forget position b) now. It's just for info. Because the way we will solve a) will also solve b) too.
- *
- * THE PROBLEM
- * To understand the problem, here a sample:
- * Given is the following XML: "<AAA> < > </AAA>"
- * Try to parse it and PHP's XML parser will fail with a XML_ERROR_UNDEFINED_ENTITY becaus of
- * the unknown litteral-entity ' '. (The numeric equivalent ' ' would work though).
- * Next try is to use the numeric equivalent 160 for ' ', thus "<AAA> <   > </AAA>"
- * The data we receive in the tag <AAA> is " < > ". So we get the *translated entities* and
- * NOT the 3 entities <   >. Thus, we will not even notice that there were entities at all!
- * In *most* cases we're not able to tell if the data was given as entity or as 'normal' char.
- * E.g. When receiving a quote or a single space were not able to tell if it was given as 'normal' char
- * or as or ". Thus we loose the entity-information of the XML-data!
- *
- * THE SOLUTION
- * The better solution is to keep the data 'as is' by replacing the '&' before parsing begins.
- * E.g. Taking the original input from above, this would result in "<AAA> &lt; &nbsp; &gt; </AAA>"
- * The data we receive now for the tag <AAA> is " < > ". and that's what we want.
- *
- * The bad thing is, that a global replace will also replace data in section that are NOT translated by the
- * PHP XML-parser. That is comments (<!-- -->), IP-sections (stuff between <? ? >) and CDATA-block too.
- * So all data comming from those sections must be reversed. This is done during the XML parse phase.
- * So:
- * a) Replacement of all '&' in the XML-source.
- * b) All data that is not char-data or in CDATA-block have to be reversed during the XML-parse phase.
- *
- * @param $xmlSource (string) The XML string
- * @return (string) The XML string with translated ampersands.
- */
- function _translateAmpersand($xmlSource, $reverse=FALSE) {
- $PHP5 = (substr(phpversion(), 0, 1) == '5');
- if ($PHP5) {
- //otherwise we receive &nbsp; instead of
- return $xmlSource;
- } else {
- return ($reverse ? str_replace('&', '&', $xmlSource) : str_replace('&', '&', $xmlSource));
- }
- }
- } // END OF CLASS XPathEngine
- /************************************************************************************************
- * ===============================================================================================
- * X P a t h - Class
- * ===============================================================================================
- ************************************************************************************************/
- define('XPATH_QUERYHIT_ALL' , 1);
- define('XPATH_QUERYHIT_FIRST' , 2);
- define('XPATH_QUERYHIT_UNIQUE', 3);
- class XPath extends XPathEngine {
-
- /**
- * Constructor of the class
- *
- * Optionally you may call this constructor with the XML-filename to parse and the
- * XML option vector. A option vector sample:
- * $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, XML_OPTION_SKIP_WHITE => TRUE);
- *
- * @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, <optionID>=><value>, ...)
- * @param $fileName (string) (optional) Filename of XML file to load from.
- * It is recommended that you call importFromFile()
- * instead as you will get an error code. If the
- * import fails, the object will be set to FALSE.
- * @see parent::XPathEngine()
- */
- function XPath($fileName='', $userXmlOptions=array()) {
- parent::XPathEngine($userXmlOptions);
- $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
- if ($fileName) {
- if (!$this->importFromFile($fileName)) {
- // Re-run the base constructor to "reset" the object. If the user has any sense, then
- // they will have created the object, and then explicitly called importFromFile(), giving
- // them the chance to catch and handle the error properly.
- parent::XPathEngine($userXmlOptions);
- }
- }
- }
-
- /**
- * Resets the object so it's able to take a new xml sting/file
- *
- * Constructing objects is slow. If you can, reuse ones that you have used already
- * by using this reset() function.
- */
- function reset() {
- parent::reset();
- $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ Get / Set Stuff ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Resolves and xPathQuery array depending on the property['modMatch']
- *
- * Most of the modification functions of XPath will also accept a xPathQuery (instead
- * of an absolute Xpath). The only problem is that the query could match more the one
- * node. The question is, if the none, the fist or all nodes are to be modified.
- * The behaver can be set with setModMatch()
- *
- * @param $modMatch (int) One of the following:
- * - XPATH_QUERYHIT_ALL (default)
- * - XPATH_QUERYHIT_FIRST
- * - XPATH_QUERYHIT_UNIQUE // If the query matches more then one node.
- * @see _resolveXPathQuery()
- */
- function setModMatch($modMatch = XPATH_QUERYHIT_ALL) {
- switch($modMatch) {
- case XPATH_QUERYHIT_UNIQUE : $this->properties['modMatch'] = XPATH_QUERYHIT_UNIQUE; break;
- case XPATH_QUERYHIT_FIRST: $this->properties['modMatch'] = XPATH_QUERYHIT_FIRST; break;
- default: $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
- }
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ DOM Like Modification ------
- //-----------------------------------------------------------------------------------------
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ Child (Node) Set/Get ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Retrieves the name(s) of a node or a group of document nodes.
- *
- * This method retrieves the names of a group of document nodes
- * specified in the argument. So if the argument was '/A[1]/B[2]' then it
- * would return 'B' if the node did exist in the tree.
- *
- * @param $xPathQuery (mixed) Array or single full document path(s) of the node(s),
- * from which the names should be retrieved.
- * @return (mixed) Array or single string of the names of the specified
- * nodes, or just the individual name. If the node did
- * not exist, then returns FALSE.
- */
- function nodeName($xPathQuery) {
- if (is_array($xPathQuery)) {
- $xPathSet = $xPathQuery;
- } else {
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'nodeName');
- }
- if (count($xPathSet) == 0) return FALSE;
- // For each node, get it's name
- $result = array();
- foreach($xPathSet as $xPath) {
- $node = &$this->getNode($xPath);
- if (!$node) {
- // ### Fatal internal error??
- continue;
- }
- $result[] = $node['name'];
- }
- // If just a single string, return string
- if (count($xPathSet) == 1) $result = $result[0];
- // Return result.
- return $result;
- }
-
- /**
- * Removes a node from the XML document.
- *
- * This method removes a node from the tree of nodes of the XML document. If the node
- * is a document node, all children of the node and its character data will be removed.
- * If the node is an attribute node, only this attribute will be removed, the node to which
- * the attribute belongs as well as its children will remain unmodified.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (bool) TRUE on success, FALSE on error;
- * @see setModMatch(), reindexNodeTree()
- */
- function removeChild($xPathQuery, $autoReindex=TRUE) {
- $ThisFunctionName = 'removeChild';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Node: $xPathQuery\n";
- echo '<hr>';
- }
- $NULL = NULL;
- $status = FALSE;
- do { // try-block
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'removeChild');
- if (sizeOf($xPathSet) === 0) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $mustReindex = FALSE;
- // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
- for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- $absoluteXPath = $xPathSet[$i];
- if (preg_match(';/attribute::;', $absoluteXPath)) { // Handle the case of an attribute node
- $xPath = $this->_prestr($absoluteXPath, '/attribute::'); // Get the path to the attribute node's parent.
- $attribute = $this->_afterstr($absoluteXPath, '/attribute::'); // Get the name of the attribute.
- unSet($this->nodeIndex[$xPath]['attributes'][$attribute]); // Unset the attribute
- if ($bDebugThisFunction) echo "We removed the attribute '$attribute' of node '$xPath'.\n";
- continue;
- }
- // Otherwise remove the node by setting it to NULL. It will be removed on the next reindexNodeTree() call.
- $mustReindex = $autoReindex;
- // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
- $this->_indexIsDirty = TRUE;
-
- $theNode = $this->nodeIndex[$absoluteXPath];
- $theNode['parentNode']['childNodes'][$theNode['pos']] =& $NULL;
- if ($bDebugThisFunction) echo "We removed the node '$absoluteXPath'.\n";
- }
- // Reindex the node tree again
- if ($mustReindex) $this->reindexNodeTree();
- $status = TRUE;
- } while(FALSE);
-
- $this->_closeDebugFunction($ThisFunctionName, $status, $bDebugThisFunction);
- return $status;
- }
-
- /**
- * Replace a node with any data string. The $data is taken 1:1.
- *
- * This function will delete the node you define by $absoluteXPath (plus it's sub-nodes) and
- * substitute it by the string $text. Often used to push in not well formed HTML.
- * WARNING:
- * The $data is taken 1:1.
- * You are in charge that the data you enter is valid XML if you intend
- * to export and import the content again.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $data (string) String containing the content to be set. *READONLY*
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (bool) TRUE on success, FALSE on error;
- * @see setModMatch(), replaceChild(), reindexNodeTree()
- */
- function replaceChildByData($xPathQuery, $data, $autoReindex=TRUE) {
- $ThisFunctionName = 'replaceChildByData';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Node: $xPathQuery\n";
- }
- $NULL = NULL;
- $status = FALSE;
- do { // try-block
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChildByData');
- if (sizeOf($xPathSet) === 0) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $mustReindex = FALSE;
- // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
- for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- $mustReindex = $autoReindex;
- // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
- $this->_indexIsDirty = TRUE;
-
- $absoluteXPath = $xPathSet[$i];
- $theNode = $this->nodeIndex[$absoluteXPath];
- $pos = $theNode['pos'];
- $theNode['parentNode']['textParts'][$pos] .= $data;
- $theNode['parentNode']['childNodes'][$pos] =& $NULL;
- if ($bDebugThisFunction) echo "We replaced the node '$absoluteXPath' with data.\n";
- }
- // Reindex the node tree again
- if ($mustReindex) $this->reindexNodeTree();
- $status = TRUE;
- } while(FALSE);
-
- $this->_closeDebugFunction($ThisFunctionName, ($status) ? 'Success' : '!!! FAILD !!!', $bDebugThisFunction);
- return $status;
- }
-
- /**
- * Replace the node(s) that matches the xQuery with the passed node (or passed node-tree)
- *
- * If the passed node is a string it's assumed to be XML and replaceChildByXml()
- * will be called.
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) Xpath to the node being replaced.
- * @param $node (mixed) String or Array (Usually a String)
- * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
- * If array: A Node (can be a whole sub-tree) (See comment in header)
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (array) The last replaced $node (can be a whole sub-tree)
- * @see reindexNodeTree()
- */
- function &replaceChild($xPathQuery, $node, $autoReindex=TRUE) {
- $NULL = NULL;
- if (is_string($node)) {
- if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
- return array();
- } else {
- if (!($node = $this->_xml2Document($node))) return FALSE;
- }
- }
-
- // Special case if it's 'super root'. We then have to take the child node == top node
- if (empty($node['parentNode'])) $node = $node['childNodes'][0];
-
- $status = FALSE;
- do { // try-block
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChild');
- if (sizeOf($xPathSet) === 0) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- break; // try-block
- }
- $mustReindex = FALSE;
-
- // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
- for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- $mustReindex = $autoReindex;
- // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
- $this->_indexIsDirty = TRUE;
-
- $absoluteXPath = $xPathSet[$i];
- $childNode =& $this->nodeIndex[$absoluteXPath];
- $parentNode =& $childNode['parentNode'];
- $childNode['parentNode'] =& $NULL;
- $childPos = $childNode['pos'];
- $parentNode['childNodes'][$childPos] =& $this->cloneNode($node);
- }
- if ($mustReindex) $this->reindexNodeTree();
- $status = TRUE;
- } while(FALSE);
-
- if (!$status) return FALSE;
- return $childNode;
- }
-
- /**
- * Insert passed node (or passed node-tree) at the node(s) that matches the xQuery.
- *
- * With parameters you can define if the 'hit'-node is shifted to the right or left
- * and if it's placed before of after the text-part.
- * Per derfault the 'hit'-node is shifted to the right and the node takes the place
- * the of the 'hit'-node.
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * E.g. Following is given: AAA[1]
- * / \
- * ..BBB[1]..BBB[2] ..
- *
- * a) insertChild('/AAA[1]/BBB[2]', <node CCC>)
- * b) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE)
- * c) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE, $afterText=FALSE)
- *
- * a) b) c)
- * AAA[1] AAA[1] AAA[1]
- * / | \ / | \ / | \
- * ..BBB[1]..CCC[1]BBB[2].. ..BBB[1]..BBB[2]..CCC[1] ..BBB[1]..BBB[2]CCC[1]..
- *
- * #### Do a complete review of the "(optional)" tag after several arguments.
- *
- * @param $xPathQuery (string) Xpath to the node to append.
- * @param $node (mixed) String or Array (Usually a String)
- * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
- * If array: A Node (can be a whole sub-tree) (See comment in header)
- * @param $shiftRight (bool) (optional, default=TRUE) Shift the target node to the right.
- * @param $afterText (bool) (optional, default=TRUE) Insert after the text.
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
- * appended nodes. That is: Array of paths if more then 1 node was added or
- * a single path string if only one node was added.
- * NOTE: If autoReindex is FALSE, then we can't return the *complete* path
- * as the exact doc-pos isn't available without reindexing. In that case we leave
- * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
- * @see appendChildByXml(), reindexNodeTree()
- */
- function insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText=TRUE, $autoReindex=TRUE) {
- if (is_string($node)) {
- if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
- return FALSE;
- } else {
- if (!($node = $this->_xml2Document($node))) return FALSE;
- }
- }
- // Special case if it's 'super root'. We then have to take the child node == top node
- if (empty($node['parentNode'])) $node = $node['childNodes'][0];
-
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'insertChild');
- if (sizeOf($xPathSet) === 0) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- $mustReindex = FALSE;
- $newNodes = array();
- $result = array();
- // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
- for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- $absoluteXPath = $xPathSet[$i];
- $childNode =& $this->nodeIndex[$absoluteXPath];
- $parentNode =& $childNode['parentNode'];
- // We can't insert at the super root or at the root.
- if (empty($absoluteXPath) || (!$parentNode['parentNode'])) {
- $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- $mustReindex = $autoReindex;
- // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
- $this->_indexIsDirty = TRUE;
-
- //Special case: It not possible to add siblings to the top node.
- if (empty($parentNode['name'])) continue;
- $newNode =& $this->cloneNode($node);
- $pos = $shiftRight ? $childNode['pos'] : $childNode['pos']+1;
- $parentNode['childNodes'] = array_merge(
- array_slice($parentNode['childNodes'], 0, $pos),
- array(&$newNode),
- array_slice($parentNode['childNodes'], $pos)
- );
- $pos += $afterText ? 1 : 0;
- $parentNode['textParts'] = array_merge(
- array_slice($parentNode['textParts'], 0, $pos),
- array(''),
- array_slice($parentNode['textParts'], $pos)
- );
-
- // We are going from bottom to top, but the user will want results from top to bottom.
- if ($mustReindex) {
- // We'll have to wait till after the reindex to get the full path to this new node.
- $newNodes[] = &$newNode;
- } else {
- // If we are reindexing the tree later, then we can't return the user any
- // useful results, so we just return them the count.
- $newNodePath = $parentNode['xpath'].'/'.$newNode['name'];
- array_unshift($result, $newNodePath);
- }
- }
- if ($mustReindex) {
- $this->reindexNodeTree();
- // Now we must fill in the result array. Because until now we did not
- // know what contextpos our newly added entries had, just their pos within
- // the siblings.
- foreach ($newNodes as $newNode) {
- array_unshift($result, $newNode['xpath']);
- }
- }
- if (count($result) == 1) $result = $result[0];
- return $result;
- }
-
- /**
- * Appends a child to anothers children.
- *
- * If you intend to do a lot of appending, you should leave autoIndex as FALSE
- * and then call reindexNodeTree() when you are finished all the appending.
- *
- * @param $xPathQuery (string) Xpath to the node to append to.
- * @param $node (mixed) String or Array (Usually a String)
- * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
- * If array: A Node (can be a whole sub-tree) (See comment in header)
- * @param $afterText (bool) (optional, default=FALSE) Insert after the text.
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
- * appended nodes. That is: Array of paths if more then 1 node was added or
- * a single path string if only one node was added.
- * NOTE: If autoReindex is FALSE, then we can't return the *complete* path
- * as the exact doc-pos isn't available without reindexing. In that case we leave
- * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
- * @see insertChild(), reindexNodeTree()
- */
- function appendChild($xPathQuery, $node, $afterText=FALSE, $autoReindex=TRUE) {
- if (is_string($node)) {
- if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
- return FALSE;
- } else {
- if (!($node = $this->_xml2Document($node))) return FALSE;
- }
- }
-
- // Special case if it's 'super root'. We then have to take the child node == top node
- if (empty($node['parentNode'])) $node = $node['childNodes'][0];
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQueryForNodeMod($xPathQuery, 'appendChild');
- if (sizeOf($xPathSet) === 0) return FALSE;
- $mustReindex = FALSE;
- $newNodes = array();
- $result = array();
- // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
- for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
- $mustReindex = $autoReindex;
- // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
- $this->_indexIsDirty = TRUE;
-
- $absoluteXPath = $xPathSet[$i];
- $parentNode =& $this->nodeIndex[$absoluteXPath];
- $newNode =& $this->cloneNode($node);
- $parentNode['childNodes'][] =& $newNode;
- $pos = count($parentNode['textParts']);
- $pos -= $afterText ? 0 : 1;
- $parentNode['textParts'] = array_merge(
- array_slice($parentNode['textParts'], 0, $pos),
- array(''),
- array_slice($parentNode['textParts'], $pos)
- );
- // We are going from bottom to top, but the user will want results from top to bottom.
- if ($mustReindex) {
- // We'll have to wait till after the reindex to get the full path to this new node.
- $newNodes[] = &$newNode;
- } else {
- // If we are reindexing the tree later, then we can't return the user any
- // useful results, so we just return them the count.
- array_unshift($result, "$absoluteXPath/{$newNode['name']}");
- }
- }
- if ($mustReindex) {
- $this->reindexNodeTree();
- // Now we must fill in the result array. Because until now we did not
- // know what contextpos our newly added entries had, just their pos within
- // the siblings.
- foreach ($newNodes as $newNode) {
- array_unshift($result, $newNode['xpath']);
- }
- }
- if (count($result) == 1) $result = $result[0];
- return $result;
- }
-
- /**
- * Inserts a node before the reference node with the same parent.
- *
- * If you intend to do a lot of appending, you should leave autoIndex as FALSE
- * and then call reindexNodeTree() when you are finished all the appending.
- *
- * @param $xPathQuery (string) Xpath to the node to insert new node before
- * @param $node (mixed) String or Array (Usually a String)
- * If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
- * If array: A Node (can be a whole sub-tree) (See comment in header)
- * @param $afterText (bool) (optional, default=FLASE) Insert after the text.
- * @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
- * the changes. A performance helper. See reindexNodeTree()
- * @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
- * appended nodes. That is: Array of paths if more then 1 node was added or
- * a single path string if only one node was added.
- * NOTE: If autoReindex is FALSE, then we can't return the *complete* path
- * as the exact doc-pos isn't available without reindexing. In that case we leave
- * out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
- * @see reindexNodeTree()
- */
- function insertBefore($xPathQuery, $node, $afterText=TRUE, $autoReindex=TRUE) {
- return $this->insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText, $autoReindex);
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ Attribute Set/Get ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Retrieves a dedecated attribute value or a hash-array of all attributes of a node.
- *
- * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results
- * to *one* xpath. If the second param $attrName is not set, a hash-array of all attributes
- * of that node is returned.
- *
- * Optionally you may pass an attrubute name in $attrName and the function will return the
- * string value of that attribute.
- *
- * @param $absoluteXPath (string) Full xpath OR a xpath-query that results to *one* xpath.
- * @param $attrName (string) (Optional) The name of the attribute. See above.
- * @return (mixed) hash-array or a string of attributes depending if the
- * parameter $attrName was set (see above). FALSE if the
- * node or attribute couldn't be found.
- * @see setAttribute(), removeAttribute()
- */
- function getAttributes($absoluteXPath, $attrName=NULL) {
- // Numpty check
- if (!isSet($this->nodeIndex[$absoluteXPath])) {
- $xPathSet = $this->_resolveXPathQuery($absoluteXPath,'getAttributes');
- if (empty($xPathSet)) return FALSE;
- // only use the first entry
- $absoluteXPath = $xPathSet[0];
- }
- if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
- // Case in-sensitive
- $attrName = strtoupper($attrName);
- }
-
- // Return the complete list or just the desired element
- if (is_null($attrName)) {
- return $this->nodeIndex[$absoluteXPath]['attributes'];
- } elseif (isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attrName])) {
- return $this->nodeIndex[$absoluteXPath]['attributes'][$attrName];
- }
- return FALSE;
- }
-
- /**
- * Set attributes of a node(s).
- *
- * This method sets a number single attributes. An existing attribute is overwritten (default)
- * with the new value, but setting the last param to FALSE will prevent overwritten.
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $name (string) Attribute name.
- * @param $value (string) Attribute value.
- * @param $overwrite (bool) If the attribute is already set we overwrite it (see text above)
- * @return (bool) TRUE on success, FALSE on failure.
- * @see getAttributes(), removeAttribute()
- */
- function setAttribute($xPathQuery, $name, $value, $overwrite=TRUE) {
- return $this->setAttributes($xPathQuery, array($name => $value), $overwrite);
- }
-
- /**
- * Version of setAttribute() that sets multiple attributes to node(s).
- *
- * This method sets a number of attributes. Existing attributes are overwritten (default)
- * with the new values, but setting the last param to FALSE will prevent overwritten.
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $attributes (array) associative array of attributes to set.
- * @param $overwrite (bool) If the attributes are already set we overwrite them (see text above)
- * @return (bool) TRUE on success, FALSE otherwise
- * @see setAttribute(), getAttributes(), removeAttribute()
- */
- function setAttributes($xPathQuery, $attributes, $overwrite=TRUE) {
- $status = FALSE;
- do { // try-block
- // The attributes parameter should be an associative array.
- if (!is_array($attributes)) break; // try-block
-
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'setAttributes');
- foreach($xPathSet as $absoluteXPath) {
- // Add the attributes to the node.
- $theNode =& $this->nodeIndex[$absoluteXPath];
- if (empty($theNode['attributes'])) {
- $this->nodeIndex[$absoluteXPath]['attributes'] = $attributes;
- } else {
- $theNode['attributes'] = $overwrite ? array_merge($theNode['attributes'],$attributes) : array_merge($attributes, $theNode['attributes']);
- }
- }
- $status = TRUE;
- } while(FALSE); // END try-block
-
- return $status;
- }
-
- /**
- * Removes an attribute of a node(s).
- *
- * This method removes *ALL* attributres per default unless the second parameter $attrList is set.
- * $attrList can be either a single attr-name as string OR a vector of attr-names as array.
- * E.g.
- * removeAttribute(<xPath>); # will remove *ALL* attributes.
- * removeAttribute(<xPath>, 'A'); # will only remove attributes called 'A'.
- * removeAttribute(<xPath>, array('A_1','A_2')); # will remove attribute 'A_1' and 'A_2'.
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $attrList (mixed) (optional) if not set will delete *all* (see text above)
- * @return (bool) TRUE on success, FALSE if the node couldn't be found
- * @see getAttributes(), setAttribute()
- */
- function removeAttribute($xPathQuery, $attrList=NULL) {
- // Check for a valid xPathQuery
- $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'removeAttribute');
-
- if (!empty($attrList) AND is_string($attrList)) $attrList = array($attrList);
- if (!is_array($attrList)) return FALSE;
-
- foreach($xPathSet as $absoluteXPath) {
- // If the attribute parameter wasn't set then remove all the attributes
- if ($attrList[0] === NULL) {
- $this->nodeIndex[$absoluteXPath]['attributes'] = array();
- continue;
- }
- // Remove all the elements in the array then.
- foreach($attrList as $name) {
- unset($this->nodeIndex[$absoluteXPath]['attributes'][$name]);
- }
- }
- return TRUE;
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ Text Set/Get ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Retrieve all the text from a node as a single string.
- *
- * Sample
- * Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA>
- * Return of getData('/AA[1]') would be: " This is sometext "
- * The first param $xPathQuery must be a valid xpath OR a xpath-query that
- * results to *one* xpath.
- *
- * @param $xPathQuery (string) xpath to the node - resolves to *one* xpath.
- * @return (mixed) The returned string (see above), FALSE if the node
- * couldn't be found or is not unique.
- * @see getDataParts()
- */
- function getData($xPathQuery) {
- $aDataParts = $this->getDataParts($xPathQuery);
- if ($aDataParts === FALSE) return FALSE;
- return implode('', $aDataParts);
- }
-
- /**
- * Retrieve all the text from a node as a vector of strings
- *
- * Where each element of the array was interrupted by a non-text child element.
- *
- * Sample
- * Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA>
- * Return of getDataParts('/AA[1]') would be: array([0]=>' This ', [1]=>'is ', [2]=>' some', [3]=>'text ');
- * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results
- * to *one* xpath.
- *
- * @param $xPathQuery (string) xpath to the node - resolves to *one* xpath.
- * @return (mixed) The returned array (see above), or FALSE if node is not
- * found or is not unique.
- * @see getData()
- */
- function getDataParts($xPathQuery) {
- // Resolve xPath argument
- $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'getDataParts');
- if (1 !== ($setSize=count($xPathSet))) {
- $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery) . "Not unique xpath-query, matched {$setSize}-times.", __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- $absoluteXPath = $xPathSet[0];
- // Is it an attribute node?
- if (preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches)) {
- $absoluteXPath = $matches[1];
- $attribute = $matches[2];
- if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
- $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
- continue;
- }
- return array($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]);
- } else if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
- $absoluteXPath = $matches[1];
- $textPartNr = $matches[2];
- return array($this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr]);
- } else {
- return $this->nodeIndex[$absoluteXPath]['textParts'];
- }
- }
-
- /**
- * Retrieves a sub string of a text-part OR attribute-value.
- *
- * This method retrieves the sub string of a specific text-part OR (if the
- * $absoluteXPath references an attribute) the the sub string of the attribute value.
- * If no 'direct referencing' is used (Xpath ends with text()[<part-number>]), then
- * the first text-part of the node ist returned (if exsiting).
- *
- * @param $absoluteXPath (string) Xpath to the node (See note above).
- * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr())
- * @param $count (number) (optional, default is ALL) Character count (Just like PHP's substr())
- * @return (mixed) The sub string, FALSE if not found or on error
- * @see XPathEngine::wholeText(), PHP's substr()
- */
- function substringData($absoluteXPath, $offset = 0, $count = NULL) {
- if (!($text = $this->wholeText($absoluteXPath))) return FALSE;
- if (is_null($count)) {
- return substr($text, $offset);
- } else {
- return substr($text, $offset, $count);
- }
- }
-
- /**
- * Replace a sub string of a text-part OR attribute-value.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $replacement (string) The string to replace with.
- * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace ())
- * @param $count (number) (optional, default is 0=ALL) Character count (Just like PHP's substr_replace())
- * @param $textPartNr (int) (optional) (see _getTextSet() )
- * @return (bool) The new string value on success, FALSE if not found or on error
- * @see substringData()
- */
- function replaceData($xPathQuery, $replacement, $offset = 0, $count = 0, $textPartNr=1) {
- if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
- $tSize=sizeOf($textSet);
- for ($i=0; $i<$tSize; $i++) {
- if ($count) {
- $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset, $count);
- } else {
- $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset);
- }
- }
- return TRUE;
- }
-
- /**
- * Insert a sub string in a text-part OR attribute-value.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $data (string) The string to replace with.
- * @param $offset (int) (optional, default is 0) Offset at which to insert the data.
- * @return (bool) The new string on success, FALSE if not found or on error
- * @see replaceData()
- */
- function insertData($xPathQuery, $data, $offset=0) {
- return $this->replaceData($xPathQuery, $data, $offset, 0);
- }
-
- /**
- * Append text data to the end of the text for an attribute OR node text-part.
- *
- * This method adds content to a node. If it's an attribute node, then
- * the value of the attribute will be set, otherwise the passed data will append to
- * character data of the node text-part. Per default the first text-part is taken.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) to the node(s) (See note above).
- * @param $data (string) String containing the content to be added.
- * @param $textPartNr (int) (optional, default is 1) (see _getTextSet())
- * @return (bool) TRUE on success, otherwise FALSE
- * @see _getTextSet()
- */
- function appendData($xPathQuery, $data, $textPartNr=1) {
- if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
- $tSize=sizeOf($textSet);
- for ($i=0; $i<$tSize; $i++) {
- $textSet[$i] .= $data;
- }
- return TRUE;
- }
-
- /**
- * Delete the data of a node.
- *
- * This method deletes content of a node. If it's an attribute node, then
- * the value of the attribute will be removed, otherwise the node text-part.
- * will be deleted. Per default the first text-part is deleted.
- *
- * NOTE: When passing a xpath-query instead of an abs. Xpath.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) to the node(s) (See note above).
- * @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace())
- * @param $count (number) (optional, default is 0=ALL) Character count. (Just like PHP's substr_replace())
- * @param $textPartNr (int) (optional, default is 0) the text part to delete (see _getTextSet())
- * @return (bool) TRUE on success, otherwise FALSE
- * @see _getTextSet()
- */
- function deleteData($xPathQuery, $offset=0, $count=0, $textPartNr=1) {
- if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
- $tSize=sizeOf($textSet);
- for ($i=0; $i<$tSize; $i++) {
- if (!$count)
- $textSet[$i] = "";
- else
- $textSet[$i] = substr_replace($textSet[$i],'', $offset, $count);
- }
- return TRUE;
- }
-
- //-----------------------------------------------------------------------------------------
- // XPath ------ Help Stuff ------
- //-----------------------------------------------------------------------------------------
-
- /**
- * Parse the XML to a node-tree. A so called 'document'
- *
- * @param $xmlString (string) The string to turn into a document node.
- * @return (&array) a node-tree
- */
- function &_xml2Document($xmlString) {
- $xmlOptions = array(
- XML_OPTION_CASE_FOLDING => $this->getProperties('caseFolding'),
- XML_OPTION_SKIP_WHITE => $this->getProperties('skipWhiteSpaces')
- );
- $xmlParser =& new XPathEngine($xmlOptions);
- $xmlParser->setVerbose($this->properties['verboseLevel']);
- // Parse the XML string
- if (!$xmlParser->importFromString($xmlString)) {
- $this->_displayError($xmlParser->getLastError(), __LINE__, __FILE__, FALSE);
- return FALSE;
- }
- return $xmlParser->getNode('/');
- }
-
- /**
- * Get a reference-list to node text part(s) or node attribute(s).
- *
- * If the Xquery references an attribute(s) (Xquery ends with attribute::),
- * then the text value of the node-attribute(s) is/are returned.
- * Otherwise the Xquery is referencing to text part(s) of node(s). This can be either a
- * direct reference to text part(s) (Xquery ends with text()[<nr>]) or indirect reference
- * (a simple Xquery to node(s)).
- * 1) Direct Reference (Xquery ends with text()[<part-number>]):
- * If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
- * Negative numbers are allowed, where -1 is the last text-part a.s.o.
- * 2) Indirect Reference (a simple Xquery to node(s)):
- * Default is to return the first text part(s). Optionally you may pass a parameter
- * $textPartNr to define the text-part you want; starting by 1.
- * Negative numbers are allowed, where -1 is the last text-part a.s.o.
- *
- * NOTE I : The returned vector is a set of references to the text parts / attributes.
- * This is handy, if you wish to modify the contents.
- * NOTE II: text-part numbers out of range will not be in the list
- * NOTE III:Instead of an absolute xpath you may also pass a xpath-query.
- * Depending on setModMatch() one, none or multiple nodes are affected.
- *
- * @param $xPathQuery (string) xpath to the node (See note above).
- * @param $textPartNr (int) String containing the content to be set.
- * @return (mixed) A vector of *references* to the text that match, or
- * FALSE on error
- * @see XPathEngine::wholeText()
- */
- function _getTextSet($xPathQuery, $textPartNr=1) {
- $ThisFunctionName = '_getTextSet';
- $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
- $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
- if ($bDebugThisFunction) {
- echo "Node: $xPathQuery\n";
- echo "Text Part Number: $textPartNr\n";
- echo "<hr>";
- }
-
- $status = FALSE;
- $funcName = '_getTextSet';
- $textSet = array();
-
- do { // try-block
- // Check if it's a Xpath reference to an attribut(s). Xpath ends with attribute::)
- if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, $matches)) {
- $xPathQuery = $matches[1];
- $attribute = $matches[3];
- // Quick out
- if (isSet($this->nodeIndex[$xPathQuery])) {
- $xPathSet[] = $xPathQuery;
- } else {
- // Try to evaluate the absoluteXPath (since it seems to be an Xquery and not an abs. Xpath)
- $xPathSet = $this->_resolveXPathQuery("$xPathQuery/attribute::$attribute", $funcName);
- }
- foreach($xPathSet as $absoluteXPath) {
- preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches);
- $absoluteXPath = $matches[1];
- $attribute = $matches[2];
- if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
- $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
- continue;
- }
- $textSet[] =& $this->nodes[$absoluteXPath]['attributes'][$attribute];
- }
- $status = TRUE;
- break; // try-block
- }
-
- // Check if it's a Xpath reference direct to a text-part(s). (xpath ends with text()[<part-number>])
- if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
- $xPathQuery = $matches[1];
- // default to the first text node if a text node was not specified
- $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
- // Quick check
- if (isSet($this->nodeIndex[$xPathQuery])) {
- $xPathSet[] = $xPathQuery;
- } else {
- // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
- $xPathSet = $this->_resolveXPathQuery("$xPathQuery/text()[$textPartNr]", $funcName);
- }
- }
- else {
- // At this point we have been given an xpath with neither a 'text()' or 'attribute::' axis at the end
- // So this means to get the text-part of the node. If parameter $textPartNr was not set, use the last
- // text-part.
- if (isSet($this->nodeIndex[$xPathQuery])) {
- $xPathSet[] = $xPathQuery;
- } else {
- // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
- $xPathSet = $this->_resolveXPathQuery($xPathQuery, $funcName);
- }
- }
- if ($bDebugThisFunction) {
- echo "Looking up paths for:\n";
- print_r($xPathSet);
- }
- // Now fetch all text-parts that match. (May be 0,1 or many)
- foreach($xPathSet as $absoluteXPath) {
- unset($text);
- if ($text =& $this->wholeText($absoluteXPath, $textPartNr)) {
- $textSet[] =& $text;
- } else {
- // The node does not yet have any text, so we have to add a '' string so that
- // if we insert or replace to it, then we'll actually have something to op on.
- $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1] = '';
- $textSet[] =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1];
- }
- }
- $status = TRUE;
- } while (FALSE); // END try-block
-
- if (!$status) $result = FALSE;
- else $result = $textSet;
- $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
- return $result;
- }
-
- /**
- * Resolves an xPathQuery vector for a node op for modification
- *
- * It is possible to create a brand new object, and try to append and insert nodes
- * into it, so this is a version of _resolveXPathQuery() that will autocreate the
- * super root if it detects that it is not present and the $xPathQuery is empty.
- *
- * Also it demands that there be at least one node returned, and displays a suitable
- * error message if the returned xPathSet does not contain any nodes.
- *
- * @param $xPathQuery (string) An xpath query targeting a single node. If empty()
- * returns the root node and auto creates the root node
- * if it doesn't exist.
- * @param $function (string) The function in which this check was called
- * @return (array) Vector of $absoluteXPath's (May be empty)
- * @see _resolveXPathQuery()
- */
- function _resolveXPathQueryForNodeMod($xPathQuery, $functionName) {
- $xPathSet = array();
- if (empty($xPathQuery)) {
- // You can append even if the root node doesn't exist.
- if (!isset($this->nodeIndex[$xPathQuery])) $this->_createSuperRoot();
- $xPathSet[] = '';
- // However, you can only append to the super root, if there isn't already a root entry.
- $rootNodes = $this->_resolveXPathQuery('/*','appendChild');
- if (count($rootNodes) !== 0) {
- $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
- return array();
- }
- } else {
- $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
- if (sizeOf($xPathSet) === 0) {
- $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
- return array();
- }
- }
- return $xPathSet;
- }
- /**
- * Resolves an xPathQuery vector depending on the property['modMatch']
- *
- * To:
- * - all matches,
- * - the first
- * - none (If the query matches more then one node.)
- * see setModMatch() for details
- *
- * @param $xPathQuery (string) An xpath query targeting a single node. If empty()
- * returns the root node (if it exists).
- * @param $function (string) The function in which this check was called
- * @return (array) Vector of $absoluteXPath's (May be empty)
- * @see setModMatch()
- */
- function _resolveXPathQuery($xPathQuery, $function) {
- $xPathSet = array();
- do { // try-block
- if (isSet($this->nodeIndex[$xPathQuery])) {
- $xPathSet[] = $xPathQuery;
- break; // try-block
- }
- if (empty($xPathQuery)) break; // try-block
- if (substr($xPathQuery, -1) === '/') break; // If the xPathQuery ends with '/' then it cannot be a good query.
- // If this xPathQuery is not absolute then attempt to evaluate it
- $xPathSet = $this->match($xPathQuery);
-
- $resultSize = sizeOf($xPathSet);
- switch($this->properties['modMatch']) {
- case XPATH_QUERYHIT_UNIQUE :
- if ($resultSize >1) {
- $xPathSet = array();
- 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);
- }
- break;
- case XPATH_QUERYHIT_FIRST :
- if ($resultSize >1) {
- $xPathSet = array($xPathSet[0]);
- 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);
- }
- break;
- default: ; // DO NOTHING
- }
- } while (FALSE);
-
- if ($this->properties['verboseLevel'] >= 2) $this->_displayMessage("'{$xPathQuery}' parameter from '{$function}' returned the following nodes: ".(count($xPathSet)?implode('<br>', $xPathSet):'[none]'), __LINE__, __FILE__);
- return $xPathSet;
- }
- } // END OF CLASS XPath
- // -----------------------------------------------------------------------------------------
- // -----------------------------------------------------------------------------------------
- // -----------------------------------------------------------------------------------------
- // -----------------------------------------------------------------------------------------
- /**************************************************************************************************
- // Usage Sample:
- // -------------
- // Following code will give you an idea how to work with PHP.XPath. It's a working sample
- // to help you get started. :o)
- // Take the comment tags away and run this file.
- **************************************************************************************************/
- /**
- * Produces a short title line.
- */
- function _title($title) {
- echo "<br><hr><b>" . htmlspecialchars($title) . "</b><hr>\n";
- }
- $self = isSet($_SERVER) ? $_SERVER['PHP_SELF'] : $PHP_SELF;
- if (basename($self) == 'XPath.class.php') {
- // The sampe source:
- $q = '?';
- $xmlSource = <<< EOD
- <{$q}Process_Instruction test="© All right reserved" {$q}>
- <AAA foo="bar"> ,,1,,
- ..1.. <![CDATA[ bla bla
- newLine blo blo ]]>
- <BBB foo="bar">
- ..2..
- </BBB>..3..<CC/> ..4..</AAA>
- EOD;
-
- // The sample code:
- $xmlOptions = array(XML_OPTION_CASE_FOLDING => TRUE, XML_OPTION_SKIP_WHITE => TRUE);
- $xPath =& new XPath(FALSE, $xmlOptions);
- //$xPath->bDebugXmlParse = TRUE;
- if (!$xPath->importFromString($xmlSource)) { echo $xPath->getLastError(); exit; }
-
- _title("Following was imported:");
- echo $xPath->exportAsHtml();
-
- _title("Get some content");
- echo "Last text part in <AAA>: '" . $xPath->wholeText('/AAA[1]', -1) ."'<br>\n";
- echo "All the text in <AAA>: '" . $xPath->wholeText('/AAA[1]') ."'<br>\n";
- echo "The attibute value in <BBB> using getAttributes('/AAA[1]/BBB[1]', 'FOO'): '" . $xPath->getAttributes('/AAA[1]', 'FOO') ."'<br>\n";
- echo "The attibute value in <BBB> using getData('/AAA[1]/@FOO'): '" . $xPath->getData('/AAA[1]/@FOO') ."'<br>\n";
-
- _title("Append some additional XML below /AAA/BBB:");
- $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node </CCC>', $afterText=FALSE);
- $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node </CCC>', $afterText=TRUE);
- $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node </CCC>', $afterText=TRUE);
- echo $xPath->exportAsHtml();
-
- _title("Insert some additional XML below <AAA>:");
- $xPath->reindexNodeTree();
- $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 1. Insert new node </BB>', $shiftRight=TRUE, $afterText=TRUE);
- $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 2. Insert new node </BB>', $shiftRight=FALSE, $afterText=TRUE);
- $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 3. Insert new node </BB>', $shiftRight=FALSE, $afterText=FALSE);
- echo $xPath->exportAsHtml();
- _title("Replace the last <BB> node with new XML data '<DDD> Replaced last BB </DDD>':");
- $xPath->reindexNodeTree();
- $xPath->replaceChild('/AAA[1]/BB[last()]', '<DDD> Replaced last BB </DDD>', $afterText=FALSE);
- echo $xPath->exportAsHtml();
-
- _title("Replace second <BB> node with normal text");
- $xPath->reindexNodeTree();
- $xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"');
- echo $xPath->exportAsHtml();
- }
- ?>