Subversion Repositories ALCASAR

Rev

Rev 325 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log

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