|
|
|
<?php /* <!-- xmd.lib.php -->
|
|
|
|
<!-- XML MiniDom, 2006/12/13 -->
|
|
|
|
|
|
|
|
<!-- Copyright (C) 2005 rene.haentjens@UGent.be - see note at end of text -->
|
|
|
|
<!-- Released under the GNU GPL V2, see http://www.gnu.org/licenses/gpl.html -->
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
==============================================================================
|
|
|
|
* This is the XML Dom library for Dokeos.
|
|
|
|
* Include/require it in your code to use its functionality.
|
|
|
|
*
|
|
|
|
* @author Rene Haentjens
|
|
|
|
* @package dokeos.library
|
|
|
|
==============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
class xmddoc
|
|
|
|
{
|
|
|
|
/* This MiniDom for XML essentially implements an array of elements, each
|
|
|
|
with a parent, a name, a namespace-URI, attributes & namespace definitions,
|
|
|
|
and children. Child nodes are a mix of texts and subelements. Parent
|
|
|
|
and subelements are stored as elementnumbers, the root is element 0.
|
|
|
|
|
|
|
|
Parsing is built on James Clark's expat, by default enabled in PHP.
|
|
|
|
|
|
|
|
The MiniDom is an alternative to the experimental DOM XML functions.
|
|
|
|
It is open source PHP and requires no extra libraries.
|
|
|
|
|
|
|
|
Restrictions of the MiniDom:
|
|
|
|
- no two attributes with same name (different namespaces) on one element;
|
|
|
|
- only 'ISO-8859-1' right now; author will investigate 'UTF-8' later;
|
|
|
|
- processing instructions & external entities are ignored;
|
|
|
|
- no distinction between text and cdata child nodes;
|
|
|
|
- xmd_xml(nonrootelement) may not generate all needed namespace definitions;
|
|
|
|
- xmd_value, xmd_html_value, xmd_select_xxx, xmd_update, xmd_update_many:
|
|
|
|
path parameter uses names without namespaces
|
|
|
|
and supports only a small subset of XPath, with some extensions;
|
|
|
|
- maximum 11 auto-generated namespace prefixes (can be changed in xmddoc)
|
|
|
|
|
|
|
|
Namespace definitions are stored as attributes, with name = 'xmlns...'
|
|
|
|
e.g. xmlns:xml='http://www.w3.org/XML/1998/namespace'
|
|
|
|
e.g. xmlns='http://www.imsglobal.org/xsd/imscp_v1p1' (default namespace)
|
|
|
|
|
|
|
|
Exposed methods:
|
|
|
|
|
|
|
|
new xmddoc(array_of_strings, charset = 'ISO-8859-1'): parse strings
|
|
|
|
new xmddoc(names, numbers, textstring): restore from cached arrays & string
|
|
|
|
|
|
|
|
xmd_add_element(complete_name, parent, attributes_with_complete_names)
|
|
|
|
complete name = [ URI + ':' + ] name
|
|
|
|
xmd_set_attribute(element, complete_attribute_name, value) (id. as above)
|
|
|
|
xmd_add_text(text, element)
|
|
|
|
xmd_add_text_element(complete_name, text, parent, attributes) =
|
|
|
|
xmd_add_text(text, xmd_add_element(complete_name, parent, attributes))
|
|
|
|
|
|
|
|
xmd_get_element(element) => children, attributes, '?name', '?parent'
|
|
|
|
xmd_get_ns_uri(element [, attribute_name_without_uri] )
|
|
|
|
|
|
|
|
xmd_text(element): combines element and subelements' text nodes
|
|
|
|
xmd_xml(): generate XML-formatted string (reverse parsing)
|
|
|
|
xmd_xml(indent_increase, initial_indent, lbr): e.g. ' ', '', "\n"
|
|
|
|
|
|
|
|
xmd_value(path): follow path from root, return attribute value or text
|
|
|
|
e.g. 'manifest/organizations/@default' 'body/p[1]' (1=first, -1=last)
|
|
|
|
xmd_value(path, parent, fix, function): find value(s) with path from parent,
|
|
|
|
apply function and decorate with fix = ('pre'=>..., 'in'=>..., 'post')
|
|
|
|
e.g. 'general/title/*' array('in' => ', ')
|
|
|
|
extensions to XPath:
|
|
|
|
- and + for previous and next sibling, e.g. general/title/+/string
|
|
|
|
-name and +name for sibling with specific name, e.g. item[2]/+item
|
|
|
|
.. for parent, e.g. general/title/../../technical/format (stops at root)
|
|
|
|
@* for location (element number within siblings, starts at 1)
|
|
|
|
@*name for location in siblings with specific name
|
|
|
|
@. for element name, e.g. organization/*[1]/@.
|
|
|
|
namespaces are not supported in paths: they use names without URI
|
|
|
|
|
|
|
|
xmd_html_value(pathFix, parent, fun): 'path' 'path infix' 'prefix -% path'
|
|
|
|
'path infix %- postfix': fun = 'htmlspecialchars' by default
|
|
|
|
|
|
|
|
xmd_select_elements(path, parent): find element nodes with path (see above)
|
|
|
|
xmd_select_single_element (id.) returns -1 or elementnumber
|
|
|
|
xmd_select_elements_where(path, subpath, value, parent): e.g. '@id', '12'
|
|
|
|
is like XPath with path[@id='12']; subpath = '.' means text
|
|
|
|
xmd_select_elements_where_notempty(path, subpath, parent): e.g. '@id'
|
|
|
|
xmd_select_xxx methods only select elements, not attributes
|
|
|
|
|
|
|
|
xmd_remove_element(childelement_number)
|
|
|
|
xmd_remove_nodes(childelement_numbers_and_strings, parent)
|
|
|
|
|
|
|
|
xmd_update(path, text, parent): select single element, then:
|
|
|
|
text element: replace text by new text
|
|
|
|
attribute: give attribute new value = text
|
|
|
|
somepath/!newtag: create new child element containing text
|
|
|
|
somepath/~: delete single (first or only) element
|
|
|
|
xmd_update_many(paths, subpath, ...): paths can be path1,path2,...:
|
|
|
|
for all elements selected by all paths, update with subpath
|
|
|
|
|
|
|
|
xmd_copy_foreign_child(fdoc, child, parent):
|
|
|
|
copies fdoc's child as a new child of parent;
|
|
|
|
note this method hasn't been tested for all cases (namespaces...)
|
|
|
|
|
|
|
|
xmd_cache(): dump xmddoc into names+numbers+textstring for serialization
|
|
|
|
|
|
|
|
Order of parameters (if present) for xmd_xxx methods:
|
|
|
|
name, text, children, path, subPath, value,
|
|
|
|
parent, fix, fun, attributes (name value)
|
|
|
|
|
|
|
|
|
|
|
|
Properties: (G)lobal to xmddoc or array (one for each xmddoc (E)lement)
|
|
|
|
|
|
|
|
e.g. $this->name[0] is the name of the document root element
|
|
|
|
e.g. $this->names[$this->ns[0]] is its namespace URI
|
|
|
|
e.g. $this->attributes[0]['title'] is the value of its attribute 'title'
|
|
|
|
e.g. $this->attributes[0]['xmlns:a'] is the URI for prefix 'a:'
|
|
|
|
*/
|
|
|
|
|
|
|
|
var $names; //G array: n => namespace URI (0 => '')
|
|
|
|
var $numbers; //G array: numeric dump of xmddoc for caching
|
|
|
|
var $textstring; //G string: string dump of xmddoc for caching
|
|
|
|
var $error; //G string: empty or parsing error message
|
|
|
|
var $_nesting; //G array: nested elements while parsing (internal)
|
|
|
|
var $_ns; //G array: namespace defs for upcoming element (id.)
|
|
|
|
var $_concat; //G bool: concatenate cData with previous (id.)
|
|
|
|
var $_nsp; //G array: namespace prefixes in use somewhere (id.)
|
|
|
|
var $_last; //G int: last used elementnumber (id.)
|
|
|
|
var $_strings; //G int: number of string child nodes cached (id.)
|
|
|
|
|
|
|
|
var $parent; //E int: elementnumber: 0 is root, -1 is parent of root
|
|
|
|
var $name; //E string: element name, without namespace
|
|
|
|
var $ns; //E int: index into $names to find namespace URI
|
|
|
|
var $attributes; //E array: attribute name(without namespace) => value
|
|
|
|
var $atns; //E array: attribute name(id.) => index into $names
|
|
|
|
var $children; //E array: elementnumbers and strings (text children)
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_get_element($parent = 0) // for convenience, readonly copy
|
|
|
|
{
|
|
|
|
// returns mixed array: children + texts have numeric key,
|
|
|
|
// other elements are attributes, '?name' and '?parent'
|
|
|
|
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return array();
|
|
|
|
|
|
|
|
return array_merge($this->children[$parent], $this->attributes[$parent],
|
|
|
|
array('?name' => $this->name[$parent],
|
|
|
|
'?parent' => $this->parent[$parent]));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_get_ns_uri($parent = 0, $attName = '')
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return '';
|
|
|
|
|
|
|
|
return $attName ? $this->names[$this->atns[$parent][$attName]] :
|
|
|
|
$this->names[$this->ns[$parent]];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_remove_element($child) // success = TRUE
|
|
|
|
{
|
|
|
|
if ($child <= 0 || $child > $this->_last) return FALSE;
|
|
|
|
|
|
|
|
$parent = $this->parent[$child];
|
|
|
|
|
|
|
|
foreach ($this->children[$parent] as $key => $value)
|
|
|
|
if ($value === $child)
|
|
|
|
{
|
|
|
|
unset($this->children[$parent][$key]); return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_remove_nodes($children, $parent = 0) // success = TRUE
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return FALSE;
|
|
|
|
|
|
|
|
if (!is_array($children)) $children = array($children);
|
|
|
|
|
|
|
|
foreach ($children as $child)
|
|
|
|
{
|
|
|
|
$childFound = FALSE;
|
|
|
|
foreach ($this->children[$parent] as $key => $value)
|
|
|
|
if ($value === $child)
|
|
|
|
{
|
|
|
|
unset($this->children[$parent][$key]);
|
|
|
|
$childFound = TRUE; break;
|
|
|
|
}
|
|
|
|
if (!$childFound) return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_update($xmPath, $text = '', $parent = 0) // success = TRUE
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last ||
|
|
|
|
!is_string($text) || !is_string($xmPath)) return FALSE;
|
|
|
|
|
|
|
|
$m = array();
|
|
|
|
if (ereg('^(.*)([~!@])(.*)$', $xmPath, $m)) // split on ~ or ! or @
|
|
|
|
{
|
|
|
|
$xmPath = $m[1]; $op = $m[2]; $name = $m[3];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (($elem = $this->xmd_select_single_element($xmPath, $parent)) == -1)
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
if (isset($op))
|
|
|
|
{
|
|
|
|
if ($op == '!' && $name)
|
|
|
|
{
|
|
|
|
$this->xmd_add_text_element($name, $text, $elem); return TRUE;
|
|
|
|
}
|
|
|
|
elseif ($op == '@' && $name)
|
|
|
|
{
|
|
|
|
$this->attributes[$elem][$name] = $text; return TRUE;
|
|
|
|
}
|
|
|
|
elseif ($op == '~' && !$name)
|
|
|
|
return $this->xmd_remove_element($elem);
|
|
|
|
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (($nch = count($this->children[$elem])) > 1) return FALSE;
|
|
|
|
|
|
|
|
$this->children[$elem][0] = $text; return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_update_many($xmPaths, $subPath = '', $text = '', $parent = 0)
|
|
|
|
{
|
|
|
|
$result = TRUE;
|
|
|
|
|
|
|
|
foreach (explode(',', $xmPaths) as $xmPath)
|
|
|
|
foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
|
|
|
|
$result &= $this->xmd_update($subPath, $text, $elem);
|
|
|
|
// '&=' always evaluates rhs, '&&=' skips it if $result is FALSE
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_copy_foreign_child($fdoc, $fchild = 0, $parent = 0)
|
|
|
|
{
|
|
|
|
$my_queue = array($fchild, $parent); // optimization, see below
|
|
|
|
|
|
|
|
while (!is_null($fchild = array_shift($my_queue)))
|
|
|
|
{
|
|
|
|
$parent = array_shift($my_queue);
|
|
|
|
|
|
|
|
if (is_string($fchild))
|
|
|
|
$this->xmd_add_text($fchild, $parent);
|
|
|
|
|
|
|
|
elseif (isset($fdoc->name[$fchild]))
|
|
|
|
{
|
|
|
|
$fullname = $fdoc->name[$fchild];
|
|
|
|
$attribs = array(); $nsdefs = array();
|
|
|
|
|
|
|
|
if (($nsn = $fdoc->ns[$fchild]))
|
|
|
|
$fullname = $fdoc->names[$nsn] . ':' . $fullname;
|
|
|
|
|
|
|
|
foreach ($fdoc->attributes[$fchild] as $name => $value)
|
|
|
|
{
|
|
|
|
if (($p = strrpos($name, ':')) !== FALSE) // 'xmlns:...'
|
|
|
|
$nsdefs[$name] = $value;
|
|
|
|
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (($nsn = $fdoc->atns[$fchild][$name]))
|
|
|
|
$name = $fdoc->names[$nsn] . ':' . $name;
|
|
|
|
$attribs[$name] = $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$child = $this->xmd_add_element($fullname, $parent,
|
|
|
|
array_merge($attribs, $nsdefs));
|
|
|
|
|
|
|
|
foreach ($fdoc->children[$fchild] as $ch)
|
|
|
|
array_push($my_queue, $ch, $child);
|
|
|
|
// recursive call was 10 times slower...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_add_element($name, $parent = 0, $attribs = array())
|
|
|
|
{
|
|
|
|
if (!is_string($name) || $name == '') return -1;
|
|
|
|
|
|
|
|
if (($p = strrpos($name, ':')) !== FALSE) // URI + ':' + name
|
|
|
|
if ($p == 0 || $p == strlen($name) - 1) return -1;
|
|
|
|
|
|
|
|
$child = ($this->_last += 1); $uris = array(); $uri = '';
|
|
|
|
|
|
|
|
if ($p)
|
|
|
|
{
|
|
|
|
$uri = substr($name, 0, $p); $name = substr($name, $p + 1);
|
|
|
|
$uris[] = $uri; // check uris after defining all attributes
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->parent[$child] = $parent; $this->name[$child] = $name;
|
|
|
|
$this->ns[$child] = $uri ? $this->_lookup($uri) : 0;
|
|
|
|
$this->children[$child] = array();
|
|
|
|
|
|
|
|
$this->attributes[$child] = array(); $this->atns[$child] = array();
|
|
|
|
|
|
|
|
foreach ($attribs as $name => $value)
|
|
|
|
if (($uri = $this->xmd_set_attribute($child, $name, $value, FALSE)))
|
|
|
|
$uris[] = $uri; // check at end, not immediately
|
|
|
|
|
|
|
|
if ($parent >= 0 && $parent <= $this->_last)
|
|
|
|
$this->children[$parent][] = $child; // link to parent
|
|
|
|
|
|
|
|
foreach ($uris as $uri) $this->_nsPfx($child, $uri);
|
|
|
|
// find prefix (child and upwards) or create new prefix at root
|
|
|
|
|
|
|
|
return $child;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_set_attribute($parent, $name, $value, $checkurihaspfx = TRUE)
|
|
|
|
{
|
|
|
|
if (!is_string($name) || $name == '') return '';
|
|
|
|
|
|
|
|
if (($p = strrpos($name, ':')) !== FALSE) // URI + ':' + name
|
|
|
|
if ($p == 0 || $p == strlen($name) - 1) return '';
|
|
|
|
|
|
|
|
$uri = ''; // beware of 'xmlns...', which is a namespace def!
|
|
|
|
|
|
|
|
if ($p) if (substr($name, 0, 6) != 'xmlns:')
|
|
|
|
{
|
|
|
|
$uri = substr($name, 0, $p); $name = substr($name, $p + 1);
|
|
|
|
}
|
|
|
|
$this->attributes[$parent][$name] = $value;
|
|
|
|
$this->atns[$parent][$name] = $uri ? $this->_lookup($uri) : 0;
|
|
|
|
if ($checkurihaspfx) if ($uri) $this->_nsPfx($parent, $uri);
|
|
|
|
|
|
|
|
if (substr($name, 0, 6) == 'xmlns:') // namespace def with prefix
|
|
|
|
$this->_nsp[substr($name, 6)] = $value; // prefix is in use
|
|
|
|
|
|
|
|
return $uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_add_text($text, $parent = 0) // success = TRUE
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last || !is_string($text))
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
if ($text) $this->children[$parent][] = $text; return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_add_text_element($name, $text, $parent = 0, $attribs = array())
|
|
|
|
{
|
|
|
|
$this->xmd_add_text($text,
|
|
|
|
$child = $this->xmd_add_element($name, $parent, $attribs));
|
|
|
|
|
|
|
|
return $child;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_text($parent = 0)
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return '';
|
|
|
|
|
|
|
|
$text = ''; // assemble text subnodes and text in child elements
|
|
|
|
|
|
|
|
foreach ($this->children[$parent] as $child)
|
|
|
|
$text .= is_string($child) ? $child : $this->xmd_text($child);
|
|
|
|
|
|
|
|
return $text;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_xml($increase = ' ', $indent = '', $lbr = "\n", $parent = 0)
|
|
|
|
{
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return '';
|
|
|
|
|
|
|
|
$uri = $this->names[$this->ns[$parent]];
|
|
|
|
$pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
|
|
|
|
|
|
|
|
$dbg = ''; // ($uri == '') ? '' : (' <!-- ' . $uri . ' -->');
|
|
|
|
|
|
|
|
$result = $indent . '<' . ($element = $pfxc . $this->name[$parent]);
|
|
|
|
|
|
|
|
$atnsp = $this->atns[$parent];
|
|
|
|
|
|
|
|
foreach ($this->attributes[$parent] as $name => $value)
|
|
|
|
{
|
|
|
|
if (isset($atnsp[$name]))
|
|
|
|
$atnsn = $atnsp[$name];
|
|
|
|
elseif (isset($atnsn))
|
|
|
|
unset($atnsn);
|
|
|
|
$uri = isset($atnsn) && isset($this->names[$atnsn]) ?
|
|
|
|
$this->names[$atnsn] : '';
|
|
|
|
$pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
|
|
|
|
$result .= ' ' . $pfxc . $name
|
|
|
|
. '="' . htmlspecialchars($value) . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (count($this->children[$parent]) == 0)
|
|
|
|
return $result . ' />' . $dbg;
|
|
|
|
|
|
|
|
$result .= '>';
|
|
|
|
|
|
|
|
foreach ($this->children[$parent] as $child)
|
|
|
|
$result .= is_string($child) ? htmlspecialchars($child) : ($lbr .
|
|
|
|
$this->xmd_xml($increase, $indent.$increase, $lbr, $child));
|
|
|
|
|
|
|
|
if (!is_string($child)) $result .= $lbr . $indent; // last $child
|
|
|
|
|
|
|
|
return $result . '</' . $element . '>' . $dbg;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_value($xmPath, $parent = 0, $fix = array(), $fun = '')
|
|
|
|
{
|
|
|
|
// extensions: @*[name] for element position (starts at 1)
|
|
|
|
// @. for element (tag)name
|
|
|
|
|
|
|
|
if ($parent < 0 || $parent > $this->_last || !is_string($xmPath))
|
|
|
|
return '';
|
|
|
|
|
|
|
|
if (($p = strrpos($xmPath, '@')) !== FALSE)
|
|
|
|
{
|
|
|
|
$attName = substr($xmPath, $p+1); $xmPath = substr($xmPath, 0, $p);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!($elems = $this->xmd_select_elements($xmPath, $parent))) return '';
|
|
|
|
|
|
|
|
$result = ''; $fixin = isset($fix['in']) ? $fix['in'] : '';
|
|
|
|
|
|
|
|
foreach ($elems as $elem)
|
|
|
|
{
|
|
|
|
$value = isset($attName) && strlen($attName) >= 1 ?
|
|
|
|
($attName == '.' ? $this->name[$elem] :
|
|
|
|
($attName{0} == '*' ?
|
|
|
|
$this->_sibnum($elem, substr($attName, 1)) :
|
|
|
|
$this->attributes[$elem][$attName])) :
|
|
|
|
$this->xmd_text($elem);
|
|
|
|
$result .= $fixin . ($fun ? $fun($value) : $value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (isset($fix['pre']) ? $fix['pre'] : '') .
|
|
|
|
substr($result, strlen($fixin)) .
|
|
|
|
(isset($fix['post']) ? $fix['post'] : '');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_html_value($xmPath, $parent = 0, $fun = 'htmlspecialchars')
|
|
|
|
{
|
|
|
|
if (!is_string($xmPath)) return '';
|
|
|
|
|
|
|
|
$fix = array();
|
|
|
|
|
|
|
|
if (($p = strpos($xmPath, ' -% ')) !== FALSE)
|
|
|
|
{
|
|
|
|
$fix['pre'] = substr($xmPath, 0, $p);
|
|
|
|
$xmPath = substr($xmPath, $p+4);
|
|
|
|
}
|
|
|
|
if (($p = strpos($xmPath, ' %- ')) !== FALSE)
|
|
|
|
{
|
|
|
|
$fix['post'] = substr($xmPath, $p+4);
|
|
|
|
$xmPath = substr($xmPath, 0, $p);
|
|
|
|
}
|
|
|
|
if (($p = strpos($xmPath, ' ')) !== FALSE)
|
|
|
|
{
|
|
|
|
$fix['in'] = substr($xmPath, $p+1);
|
|
|
|
$xmPath = substr($xmPath, 0, $p);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->xmd_value($xmPath, $parent, $fix, $fun);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_select_single_element($xmPath, $parent = 0) // for convenience
|
|
|
|
{
|
|
|
|
$elements = $this->xmd_select_elements($xmPath, $parent);
|
|
|
|
if (count($elements) == 0) return -1;
|
|
|
|
return $elements[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_select_elements_where($xmPath,
|
|
|
|
$subPath = '.', $value = '', $parent = 0)
|
|
|
|
{
|
|
|
|
if (!is_string($subPath)) return array();
|
|
|
|
|
|
|
|
$elems = array(); if ($subPath == '.') $subPath = '';
|
|
|
|
|
|
|
|
foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
|
|
|
|
if ($this->xmd_value($subPath, $elem) == $value) $elems[] = $elem;
|
|
|
|
|
|
|
|
return $elems;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_select_elements_where_notempty($xmPath,
|
|
|
|
$subPath = '.', $parent = 0)
|
|
|
|
{
|
|
|
|
if (!is_string($subPath)) return array();
|
|
|
|
|
|
|
|
$elems = array(); if ($subPath == '.') $subPath = '';
|
|
|
|
|
|
|
|
foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
|
|
|
|
if ($this->xmd_value($subPath, $elem)) $elems[] = $elem;
|
|
|
|
|
|
|
|
return $elems;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmd_select_elements($xmPath, $parent = 0)
|
|
|
|
{
|
|
|
|
// XPath subset: e1/e2/.../en, also * and e[n] and *[n] (at 1 or -1)
|
|
|
|
// /*/... starts from root, regardless of $parent
|
|
|
|
// extensions: e= - or + (previous & next sibling)
|
|
|
|
// e= -name or +name (sibling of specific name)
|
|
|
|
// e= .. (stops at root, so too many doesn't matter)
|
|
|
|
|
|
|
|
if (substr($xmPath, 0, 3) == '/*/')
|
|
|
|
{
|
|
|
|
$xmPath = substr($xmPath, 3); $parent = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($parent < 0 || $parent > $this->_last) return array();
|
|
|
|
|
|
|
|
while (substr($xmPath, 0, 1) == '/') $xmPath = substr($xmPath, 1);
|
|
|
|
while (substr($xmPath, -1) == '/') $xmPath = substr($xmPath, 0, -1);
|
|
|
|
|
|
|
|
if ($xmPath == '' || $xmPath == '.') return array($parent);
|
|
|
|
|
|
|
|
if ($xmPath == '..')
|
|
|
|
{
|
|
|
|
if ($parent > 0) return array($this->parent[$parent]);
|
|
|
|
return array($parent);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($xmPath{0} == '-' || $xmPath{0} == '+')
|
|
|
|
{
|
|
|
|
$sib = $this->_sibnum($parent, substr($xmPath, 1), $xmPath{0});
|
|
|
|
if ($sib == -1) return array(); return array($sib);
|
|
|
|
}
|
|
|
|
|
|
|
|
$m = array();
|
|
|
|
if (ereg('^(.+)/([^/]+)$', $xmPath, $m)) // split on last /
|
|
|
|
{
|
|
|
|
if (!($set = $this->xmd_select_elements($m[1], $parent)))
|
|
|
|
return $set; // which is empty array
|
|
|
|
if (count($set) == 1)
|
|
|
|
return $this->xmd_select_elements($m[2], $set[0]);
|
|
|
|
|
|
|
|
$bigset = array(); $m2 = $m[2];
|
|
|
|
foreach ($set as $e)
|
|
|
|
$bigset = array_merge($bigset,
|
|
|
|
$this->xmd_select_elements($m2, $e));
|
|
|
|
return $bigset;
|
|
|
|
}
|
|
|
|
|
|
|
|
$xmName = $xmPath; $xmNum = 0; $elems = array();
|
|
|
|
|
|
|
|
if (ereg('^(.+)\[(-?[0-9]+)\]$', $xmPath, $m))
|
|
|
|
{
|
|
|
|
$xmName = $m[1]; $xmNum = (int) $m[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this->children[$parent] as $child) if (!is_string($child))
|
|
|
|
if ($xmName == '*' || ($this->name[$child]) == $xmName)
|
|
|
|
$elems[] = $child;
|
|
|
|
|
|
|
|
if ($xmNum == 0) return $elems;
|
|
|
|
|
|
|
|
$xmNum = ($xmNum > 0) ? $xmNum - 1 : count($elems) + $xmNum;
|
|
|
|
|
|
|
|
return ($xmNum < count($elems)) ? array($elems[$xmNum]) : array();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Notes on parsing and caching:
|
|
|
|
// - parsing 388 KB -> 0.94 sec
|
|
|
|
// - caching 298 KB <- 1.63 sec: 11387 elements, 5137 string nodes
|
|
|
|
// - uncache 298 KB -> 0.42 sec
|
|
|
|
// - $this->children[$n][] in a loop is quicker than a temporary array
|
|
|
|
// $children[] and copying $this->children[$n] = $children after the loop
|
|
|
|
// - incremental operator ++$numptr is not quicker than ($numptr += 1)
|
|
|
|
// - numbers & textstring: more compact with base64_encode(gzcompress())
|
|
|
|
|
|
|
|
function xmd_cache() // store all data in numbers+names+textstring
|
|
|
|
{
|
|
|
|
$this->numbers = array(); $this->textstring = ''; $this->_strings = 0;
|
|
|
|
// add all element- and attributenames to names - see below
|
|
|
|
|
|
|
|
for ($n = 0; $n <= $this->_last; $n++)
|
|
|
|
{
|
|
|
|
$this->numbers[] = count($this->children[$n]);
|
|
|
|
|
|
|
|
foreach ($this->children[$n] as $ch)
|
|
|
|
{
|
|
|
|
if (is_string($ch))
|
|
|
|
{
|
|
|
|
$this->numbers[] = 0; $this->_strings += 1;
|
|
|
|
$this->numbers[] = strlen($ch); $this->textstring .= $ch;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$this->numbers[] = ($ch-$n); // more compact than $ch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->numbers[] = count($this->attributes[$n]);
|
|
|
|
|
|
|
|
foreach ($this->attributes[$n] as $name => $value)
|
|
|
|
{
|
|
|
|
$this->numbers[] = $this->_lookup($name);
|
|
|
|
$this->numbers[] = $this->atns[$n][$name];
|
|
|
|
$this->numbers[] = strlen($value); $this->textstring .= $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->numbers[] = $this->_lookup($this->name[$n]);
|
|
|
|
$this->numbers[] = $this->ns[$n];
|
|
|
|
$this->numbers[] = $n - $this->parent[$n]; // more compact
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmddoc($strings, $charset = 'ISO-8859-1', $textstring = '')
|
|
|
|
{
|
|
|
|
$this->parent = array(); $this->name = array();
|
|
|
|
$this->ns = array(); $this->attributes = array();
|
|
|
|
$this->atns = array(); $this->children = array();
|
|
|
|
$this->error = ''; $this->_nesting = array();
|
|
|
|
$this->_ns = array(); $this->_last = -1;
|
|
|
|
|
|
|
|
$this->_nsp = array();
|
|
|
|
foreach (explode(',', 'eb,tn,eg,ut,as,ne,jt,ne,ah,en,er') as $pfx)
|
|
|
|
$this->_nsp[$pfx] = '';
|
|
|
|
|
|
|
|
if (is_array($charset)) // new xmddoc($names, $numbers, $textstring)
|
|
|
|
{
|
|
|
|
$this->names = $strings; $this->numbers = $charset;
|
|
|
|
$this->textstring = $textstring; $this->_uncache(); return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->names = array(); $this->_lookup(''); // empty ns is number 0
|
|
|
|
|
|
|
|
$xml_parser = xml_parser_create_ns($charset, ':');
|
|
|
|
xml_set_object($xml_parser,$this); // instead of ...,&$this
|
|
|
|
// See PHP manual: Passing by Reference vs. xml_set_object
|
|
|
|
xml_set_element_handler($xml_parser, '_startElement', '_endElement');
|
|
|
|
xml_set_character_data_handler($xml_parser, '_cData');
|
|
|
|
xml_set_start_namespace_decl_handler($xml_parser, '_startNs');
|
|
|
|
// xml_set_end_namespace_decl_handler($xml_parser, '_endNs');
|
|
|
|
// xml_set_default_handler ($xml_parser, '');
|
|
|
|
xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, FALSE);
|
|
|
|
|
|
|
|
if (!is_array($strings)) $strings = array($strings);
|
|
|
|
|
|
|
|
if (count($strings) && (substr($strings[0], 0, 5) != '<?xml') &&
|
|
|
|
!xml_parse($xml_parser,
|
|
|
|
'<?xml version="1.0" encoding="' . $charset . '"?>', FALSE))
|
|
|
|
{
|
|
|
|
$this->error = 'Encoding ' . $charset . ': ' .
|
|
|
|
xml_error_string(xml_get_error_code($xml_parser));
|
|
|
|
$strings = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($strings as $s)
|
|
|
|
{
|
|
|
|
if (substr($s, -1) != "\n") $s .= "\n";
|
|
|
|
|
|
|
|
if (!xml_parse($xml_parser, $s, FALSE))
|
|
|
|
{
|
|
|
|
$errCode = xml_get_error_code($xml_parser);
|
|
|
|
$this->error = 'Line '. xml_get_current_line_number($xml_parser) .
|
|
|
|
' (c' . xml_get_current_column_number($xml_parser) .
|
|
|
|
// ', b' . xml_get_current_byte_index($xml_parser) .
|
|
|
|
'): error ' . $errCode . '= ' . xml_error_string($errCode);
|
|
|
|
break; // the error string is English...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
xml_parse($xml_parser, '', TRUE); xml_parser_free($xml_parser);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// internal methods
|
|
|
|
|
|
|
|
function _sibnum($parent, $name = '', $pmn = 'N') // sibling or number
|
|
|
|
{
|
|
|
|
if ($parent <= 0) return -1;
|
|
|
|
|
|
|
|
$found = FALSE; $prev = -1; $next = -1; $num = 0;
|
|
|
|
|
|
|
|
foreach ($this->children[$this->parent[$parent]] as $child)
|
|
|
|
{
|
|
|
|
if (is_string($child)) continue;
|
|
|
|
|
|
|
|
$name_ok = $name ? ($this->name[$child] == $name) : TRUE;
|
|
|
|
|
|
|
|
if ($found && $name_ok)
|
|
|
|
{
|
|
|
|
$next = $child; break;
|
|
|
|
}
|
|
|
|
elseif ($parent === $child)
|
|
|
|
{
|
|
|
|
$num ++; $found = TRUE;
|
|
|
|
}
|
|
|
|
elseif ($name_ok)
|
|
|
|
{
|
|
|
|
$num ++; $prev = $child;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ($pmn == '-') ? $prev : (($pmn == '+') ? $next : $num);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _uncache() // restore all data from numbers+names+textstring
|
|
|
|
{
|
|
|
|
$n = -1; $numptr = -1; $txtptr = 0; $count = count($this->numbers);
|
|
|
|
$A0 = array(); // believe it or not, this makes the loops quicker!
|
|
|
|
|
|
|
|
while (++$numptr < $count)
|
|
|
|
{
|
|
|
|
$n++;
|
|
|
|
|
|
|
|
if (($countdown = $this->numbers[$numptr]) == 0)
|
|
|
|
{
|
|
|
|
$this->children[$n] = $A0;
|
|
|
|
}
|
|
|
|
else while (--$countdown >= 0)
|
|
|
|
{
|
|
|
|
if (($chc = $this->numbers[++$numptr]) == 0)
|
|
|
|
{
|
|
|
|
$this->children[$n][] = substr($this->textstring,
|
|
|
|
$txtptr, ($len = $this->numbers[++$numptr]));
|
|
|
|
$txtptr += $len;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$this->children[$n][] = $n + $chc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (($countdown = $this->numbers[++$numptr]) == 0)
|
|
|
|
{
|
|
|
|
$this->attributes[$n] = $this->atns[$n] = $A0;
|
|
|
|
}
|
|
|
|
else while (--$countdown >= 0)
|
|
|
|
{
|
|
|
|
$name = $this->names[$this->numbers[++$numptr]];
|
|
|
|
$this->atns[$n][$name] = $this->numbers[++$numptr];
|
|
|
|
$this->attributes[$n][$name] = substr($this->textstring,
|
|
|
|
$txtptr, ($len = $this->numbers[++$numptr]));
|
|
|
|
$txtptr += $len;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->name[$n] = $this->names[$this->numbers[++$numptr]];
|
|
|
|
$this->ns[$n] = $this->numbers[++$numptr];
|
|
|
|
|
|
|
|
$this->parent[$n] = $n - $this->numbers[++$numptr];
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->_last = $n;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _startElement($parser, $name, $attribs)
|
|
|
|
{
|
|
|
|
$level = count($this->_nesting);
|
|
|
|
$parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
|
|
|
|
|
|
|
|
$child = $this->xmd_add_element($name, $parent,
|
|
|
|
array_merge($attribs, $this->_ns));
|
|
|
|
|
|
|
|
$this->_nesting[] = $child; $this->_ns = array();
|
|
|
|
|
|
|
|
$this->_concat = FALSE; // see _cData
|
|
|
|
}
|
|
|
|
|
|
|
|
function _endElement($parser, $name)
|
|
|
|
{
|
|
|
|
array_pop($this->_nesting); $this->_concat = FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _cData($parser, $data)
|
|
|
|
{
|
|
|
|
if (!ltrim($data)) return; // empty line, or whitespace preceding <tag>
|
|
|
|
|
|
|
|
$level = count($this->_nesting);
|
|
|
|
$parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
|
|
|
|
|
|
|
|
if ($parent >= 0)
|
|
|
|
{
|
|
|
|
$nc = count($this->children[$parent]);
|
|
|
|
$pcs = ($nc > 0 && is_string($this->children[$parent][$nc - 1]));
|
|
|
|
|
|
|
|
if ($pcs && strlen($data) == 1) $this->_concat = TRUE;
|
|
|
|
// expat parser puts &xx; in a separate cData, try to re-assemble
|
|
|
|
|
|
|
|
if ($pcs && $data{0} > '~') $this->_concat = TRUE;
|
|
|
|
// PHP5 expat breaks before 8-bit characters
|
|
|
|
|
|
|
|
if ($this->_concat)
|
|
|
|
{
|
|
|
|
$this->children[$parent][$nc - 1] .= $data;
|
|
|
|
$this->_concat = (strlen($data) == 1);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
$this->children[$parent][] = $pcs ? "\n" . $data : $data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _startNs($parser, $pfx, $uri) // called before _startElement
|
|
|
|
{
|
|
|
|
$this->_ns['xmlns' . ($pfx ? ':'.$pfx : '')] = $uri;
|
|
|
|
$this->_nsp[$pfx] = $uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _nsPfx($ppar, $uri) // find namespace prefix
|
|
|
|
{
|
|
|
|
while ($ppar >= 0)
|
|
|
|
{
|
|
|
|
foreach ($this->attributes[$ppar] as $name => $value)
|
|
|
|
if (substr($name, 0, 5) == 'xmlns' && $value == $uri)
|
|
|
|
{
|
|
|
|
$pfxc = substr($name, 6) . substr($name, 5, 1); break 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ppar = $this->parent[$ppar];
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ppar >= 0) return $pfxc; if ($uri == '') return '';
|
|
|
|
|
|
|
|
if ($uri == 'http://www.w3.org/XML/1998/namespace') return 'xml:';
|
|
|
|
|
|
|
|
foreach($this->_nsp as $pfx => $used) if (!$used) break;
|
|
|
|
|
|
|
|
$this->_nsp[$pfx] = $uri; $xmlnspfx = 'xmlns:' . $pfx;
|
|
|
|
$this->attributes[0][$xmlnspfx] = $uri; $this->atns[0][$xmlnspfx] = 0;
|
|
|
|
|
|
|
|
return $pfx . ':';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function _lookup($name) // for namespaces + see cache
|
|
|
|
{
|
|
|
|
$where = array_search($name, $this->names);
|
|
|
|
|
|
|
|
if ($where === FALSE || $where === NULL)
|
|
|
|
{
|
|
|
|
$where = count($this->names); $this->names[] = $name;
|
|
|
|
}
|
|
|
|
return $where;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
<!--
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
|
|
modify it under the terms of the GNU General Public License
|
|
|
|
as published by the Free Software Foundation; either version 2
|
|
|
|
of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
-->
|
|
|
|
*/
|
|
|
|
?>
|