xmd.lib.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  1. <?php /* <!-- xmd.lib.php -->
  2. <!-- XML MiniDom, 2006/12/13 -->
  3. <!-- Copyright (C) 2005 rene.haentjens@UGent.be - see note at end of text -->
  4. <!-- Released under the GNU GPL V2, see http://www.gnu.org/licenses/gpl.html -->
  5. */
  6. /**
  7. ==============================================================================
  8. * This is the XML Dom library for Dokeos.
  9. * Include/require it in your code to use its functionality.
  10. *
  11. * @author Rene Haentjens
  12. * @package dokeos.library
  13. ==============================================================================
  14. */
  15. class xmddoc
  16. {
  17. /* This MiniDom for XML essentially implements an array of elements, each
  18. with a parent, a name, a namespace-URI, attributes & namespace definitions,
  19. and children. Child nodes are a mix of texts and subelements. Parent
  20. and subelements are stored as elementnumbers, the root is element 0.
  21. Parsing is built on James Clark's expat, by default enabled in PHP.
  22. The MiniDom is an alternative to the experimental DOM XML functions.
  23. It is open source PHP and requires no extra libraries.
  24. Restrictions of the MiniDom:
  25. - no two attributes with same name (different namespaces) on one element;
  26. - only 'ISO-8859-1' right now; author will investigate 'UTF-8' later;
  27. - processing instructions & external entities are ignored;
  28. - no distinction between text and cdata child nodes;
  29. - xmd_xml(nonrootelement) may not generate all needed namespace definitions;
  30. - xmd_value, xmd_html_value, xmd_select_xxx, xmd_update, xmd_update_many:
  31. path parameter uses names without namespaces
  32. and supports only a small subset of XPath, with some extensions;
  33. - maximum 11 auto-generated namespace prefixes (can be changed in xmddoc)
  34. Namespace definitions are stored as attributes, with name = 'xmlns...'
  35. e.g. xmlns:xml='http://www.w3.org/XML/1998/namespace'
  36. e.g. xmlns='http://www.imsglobal.org/xsd/imscp_v1p1' (default namespace)
  37. Exposed methods:
  38. new xmddoc(array_of_strings, charset = 'ISO-8859-1'): parse strings
  39. new xmddoc(names, numbers, textstring): restore from cached arrays & string
  40. xmd_add_element(complete_name, parent, attributes_with_complete_names)
  41. complete name = [ URI + ':' + ] name
  42. xmd_set_attribute(element, complete_attribute_name, value) (id. as above)
  43. xmd_add_text(text, element)
  44. xmd_add_text_element(complete_name, text, parent, attributes) =
  45. xmd_add_text(text, xmd_add_element(complete_name, parent, attributes))
  46. xmd_get_element(element) => children, attributes, '?name', '?parent'
  47. xmd_get_ns_uri(element [, attribute_name_without_uri] )
  48. xmd_text(element): combines element and subelements' text nodes
  49. xmd_xml(): generate XML-formatted string (reverse parsing)
  50. xmd_xml(indent_increase, initial_indent, lbr): e.g. ' ', '', "\n"
  51. xmd_value(path): follow path from root, return attribute value or text
  52. e.g. 'manifest/organizations/@default' 'body/p[1]' (1=first, -1=last)
  53. xmd_value(path, parent, fix, function): find value(s) with path from parent,
  54. apply function and decorate with fix = ('pre'=>..., 'in'=>..., 'post')
  55. e.g. 'general/title/*' array('in' => ', ')
  56. extensions to XPath:
  57. - and + for previous and next sibling, e.g. general/title/+/string
  58. -name and +name for sibling with specific name, e.g. item[2]/+item
  59. .. for parent, e.g. general/title/../../technical/format (stops at root)
  60. @* for location (element number within siblings, starts at 1)
  61. @*name for location in siblings with specific name
  62. @. for element name, e.g. organization/*[1]/@.
  63. namespaces are not supported in paths: they use names without URI
  64. xmd_html_value(pathFix, parent, fun): 'path' 'path infix' 'prefix -% path'
  65. 'path infix %- postfix': fun = 'htmlspecialchars' by default
  66. xmd_select_elements(path, parent): find element nodes with path (see above)
  67. xmd_select_single_element (id.) returns -1 or elementnumber
  68. xmd_select_elements_where(path, subpath, value, parent): e.g. '@id', '12'
  69. is like XPath with path[@id='12']; subpath = '.' means text
  70. xmd_select_elements_where_notempty(path, subpath, parent): e.g. '@id'
  71. xmd_select_xxx methods only select elements, not attributes
  72. xmd_remove_element(childelement_number)
  73. xmd_remove_nodes(childelement_numbers_and_strings, parent)
  74. xmd_update(path, text, parent): select single element, then:
  75. text element: replace text by new text
  76. attribute: give attribute new value = text
  77. somepath/!newtag: create new child element containing text
  78. somepath/~: delete single (first or only) element
  79. xmd_update_many(paths, subpath, ...): paths can be path1,path2,...:
  80. for all elements selected by all paths, update with subpath
  81. xmd_copy_foreign_child(fdoc, child, parent):
  82. copies fdoc's child as a new child of parent;
  83. note this method hasn't been tested for all cases (namespaces...)
  84. xmd_cache(): dump xmddoc into names+numbers+textstring for serialization
  85. Order of parameters (if present) for xmd_xxx methods:
  86. name, text, children, path, subPath, value,
  87. parent, fix, fun, attributes (name value)
  88. Properties: (G)lobal to xmddoc or array (one for each xmddoc (E)lement)
  89. e.g. $this->name[0] is the name of the document root element
  90. e.g. $this->names[$this->ns[0]] is its namespace URI
  91. e.g. $this->attributes[0]['title'] is the value of its attribute 'title'
  92. e.g. $this->attributes[0]['xmlns:a'] is the URI for prefix 'a:'
  93. */
  94. var $names; //G array: n => namespace URI (0 => '')
  95. var $numbers; //G array: numeric dump of xmddoc for caching
  96. var $textstring; //G string: string dump of xmddoc for caching
  97. var $error; //G string: empty or parsing error message
  98. var $_nesting; //G array: nested elements while parsing (internal)
  99. var $_ns; //G array: namespace defs for upcoming element (id.)
  100. var $_concat; //G bool: concatenate cData with previous (id.)
  101. var $_nsp; //G array: namespace prefixes in use somewhere (id.)
  102. var $_last; //G int: last used elementnumber (id.)
  103. var $_strings; //G int: number of string child nodes cached (id.)
  104. var $parent; //E int: elementnumber: 0 is root, -1 is parent of root
  105. var $name; //E string: element name, without namespace
  106. var $ns; //E int: index into $names to find namespace URI
  107. var $attributes; //E array: attribute name(without namespace) => value
  108. var $atns; //E array: attribute name(id.) => index into $names
  109. var $children; //E array: elementnumbers and strings (text children)
  110. function xmd_get_element($parent = 0) // for convenience, readonly copy
  111. {
  112. // returns mixed array: children + texts have numeric key,
  113. // other elements are attributes, '?name' and '?parent'
  114. if ($parent < 0 || $parent > $this->_last) return array();
  115. return array_merge($this->children[$parent], $this->attributes[$parent],
  116. array('?name' => $this->name[$parent],
  117. '?parent' => $this->parent[$parent]));
  118. }
  119. function xmd_get_ns_uri($parent = 0, $attName = '')
  120. {
  121. if ($parent < 0 || $parent > $this->_last) return '';
  122. return $attName ? $this->names[$this->atns[$parent][$attName]] :
  123. $this->names[$this->ns[$parent]];
  124. }
  125. function xmd_remove_element($child) // success = TRUE
  126. {
  127. if ($child <= 0 || $child > $this->_last) return FALSE;
  128. $parent = $this->parent[$child];
  129. foreach ($this->children[$parent] as $key => $value)
  130. if ($value === $child)
  131. {
  132. unset($this->children[$parent][$key]); return TRUE;
  133. }
  134. return FALSE;
  135. }
  136. function xmd_remove_nodes($children, $parent = 0) // success = TRUE
  137. {
  138. if ($parent < 0 || $parent > $this->_last) return FALSE;
  139. if (!is_array($children)) $children = array($children);
  140. foreach ($children as $child)
  141. {
  142. $childFound = FALSE;
  143. foreach ($this->children[$parent] as $key => $value)
  144. if ($value === $child)
  145. {
  146. unset($this->children[$parent][$key]);
  147. $childFound = TRUE; break;
  148. }
  149. if (!$childFound) return FALSE;
  150. }
  151. return TRUE;
  152. }
  153. function xmd_update($xmPath, $text = '', $parent = 0) // success = TRUE
  154. {
  155. if ($parent < 0 || $parent > $this->_last ||
  156. !is_string($text) || !is_string($xmPath)) return FALSE;
  157. $m = array();
  158. if (ereg('^(.*)([~!@])(.*)$', $xmPath, $m)) // split on ~ or ! or @
  159. {
  160. $xmPath = $m[1]; $op = $m[2]; $name = $m[3];
  161. }
  162. if (($elem = $this->xmd_select_single_element($xmPath, $parent)) == -1)
  163. return FALSE;
  164. if (isset($op))
  165. {
  166. if ($op == '!' && $name)
  167. {
  168. $this->xmd_add_text_element($name, $text, $elem); return TRUE;
  169. }
  170. elseif ($op == '@' && $name)
  171. {
  172. $this->attributes[$elem][$name] = $text; return TRUE;
  173. }
  174. elseif ($op == '~' && !$name)
  175. return $this->xmd_remove_element($elem);
  176. return FALSE;
  177. }
  178. if (($nch = count($this->children[$elem])) > 1) return FALSE;
  179. $this->children[$elem][0] = $text; return TRUE;
  180. }
  181. function xmd_update_many($xmPaths, $subPath = '', $text = '', $parent = 0)
  182. {
  183. $result = TRUE;
  184. foreach (explode(',', $xmPaths) as $xmPath)
  185. foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
  186. $result &= $this->xmd_update($subPath, $text, $elem);
  187. // '&=' always evaluates rhs, '&&=' skips it if $result is FALSE
  188. return $result;
  189. }
  190. function xmd_copy_foreign_child($fdoc, $fchild = 0, $parent = 0)
  191. {
  192. $my_queue = array($fchild, $parent); // optimization, see below
  193. while (!is_null($fchild = array_shift($my_queue)))
  194. {
  195. $parent = array_shift($my_queue);
  196. if (is_string($fchild))
  197. $this->xmd_add_text($fchild, $parent);
  198. elseif (isset($fdoc->name[$fchild]))
  199. {
  200. $fullname = $fdoc->name[$fchild];
  201. $attribs = array(); $nsdefs = array();
  202. if (($nsn = $fdoc->ns[$fchild]))
  203. $fullname = $fdoc->names[$nsn] . ':' . $fullname;
  204. foreach ($fdoc->attributes[$fchild] as $name => $value)
  205. {
  206. if (($p = strrpos($name, ':')) !== FALSE) // 'xmlns:...'
  207. $nsdefs[$name] = $value;
  208. else
  209. {
  210. if (($nsn = $fdoc->atns[$fchild][$name]))
  211. $name = $fdoc->names[$nsn] . ':' . $name;
  212. $attribs[$name] = $value;
  213. }
  214. }
  215. $child = $this->xmd_add_element($fullname, $parent,
  216. array_merge($attribs, $nsdefs));
  217. foreach ($fdoc->children[$fchild] as $ch)
  218. array_push($my_queue, $ch, $child);
  219. // recursive call was 10 times slower...
  220. }
  221. }
  222. }
  223. function xmd_add_element($name, $parent = 0, $attribs = array())
  224. {
  225. if (!is_string($name) || $name == '') return -1;
  226. if (($p = strrpos($name, ':')) !== FALSE) // URI + ':' + name
  227. if ($p == 0 || $p == strlen($name) - 1) return -1;
  228. $child = ($this->_last += 1); $uris = array(); $uri = '';
  229. if ($p)
  230. {
  231. $uri = substr($name, 0, $p); $name = substr($name, $p + 1);
  232. $uris[] = $uri; // check uris after defining all attributes
  233. }
  234. $this->parent[$child] = $parent; $this->name[$child] = $name;
  235. $this->ns[$child] = $uri ? $this->_lookup($uri) : 0;
  236. $this->children[$child] = array();
  237. $this->attributes[$child] = array(); $this->atns[$child] = array();
  238. foreach ($attribs as $name => $value)
  239. if (($uri = $this->xmd_set_attribute($child, $name, $value, FALSE)))
  240. $uris[] = $uri; // check at end, not immediately
  241. if ($parent >= 0 && $parent <= $this->_last)
  242. $this->children[$parent][] = $child; // link to parent
  243. foreach ($uris as $uri) $this->_nsPfx($child, $uri);
  244. // find prefix (child and upwards) or create new prefix at root
  245. return $child;
  246. }
  247. function xmd_set_attribute($parent, $name, $value, $checkurihaspfx = TRUE)
  248. {
  249. if (!is_string($name) || $name == '') return '';
  250. if (($p = strrpos($name, ':')) !== FALSE) // URI + ':' + name
  251. if ($p == 0 || $p == strlen($name) - 1) return '';
  252. $uri = ''; // beware of 'xmlns...', which is a namespace def!
  253. if ($p) if (substr($name, 0, 6) != 'xmlns:')
  254. {
  255. $uri = substr($name, 0, $p); $name = substr($name, $p + 1);
  256. }
  257. $this->attributes[$parent][$name] = $value;
  258. $this->atns[$parent][$name] = $uri ? $this->_lookup($uri) : 0;
  259. if ($checkurihaspfx) if ($uri) $this->_nsPfx($parent, $uri);
  260. if (substr($name, 0, 6) == 'xmlns:') // namespace def with prefix
  261. $this->_nsp[substr($name, 6)] = $value; // prefix is in use
  262. return $uri;
  263. }
  264. function xmd_add_text($text, $parent = 0) // success = TRUE
  265. {
  266. if ($parent < 0 || $parent > $this->_last || !is_string($text))
  267. return FALSE;
  268. if ($text) $this->children[$parent][] = $text; return TRUE;
  269. }
  270. function xmd_add_text_element($name, $text, $parent = 0, $attribs = array())
  271. {
  272. $this->xmd_add_text($text,
  273. $child = $this->xmd_add_element($name, $parent, $attribs));
  274. return $child;
  275. }
  276. function xmd_text($parent = 0)
  277. {
  278. if ($parent < 0 || $parent > $this->_last) return '';
  279. $text = ''; // assemble text subnodes and text in child elements
  280. foreach ($this->children[$parent] as $child)
  281. $text .= is_string($child) ? $child : $this->xmd_text($child);
  282. return $text;
  283. }
  284. function xmd_xml($increase = ' ', $indent = '', $lbr = "\n", $parent = 0)
  285. {
  286. if ($parent < 0 || $parent > $this->_last) return '';
  287. $uri = $this->names[$this->ns[$parent]];
  288. $pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
  289. $dbg = ''; // ($uri == '') ? '' : (' <!-- ' . $uri . ' -->');
  290. $result = $indent . '<' . ($element = $pfxc . $this->name[$parent]);
  291. $atnsp = $this->atns[$parent];
  292. foreach ($this->attributes[$parent] as $name => $value)
  293. {
  294. if (isset($atnsp[$name]))
  295. $atnsn = $atnsp[$name];
  296. elseif (isset($atnsn))
  297. unset($atnsn);
  298. $uri = isset($atnsn) && isset($this->names[$atnsn]) ?
  299. $this->names[$atnsn] : '';
  300. $pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
  301. $result .= ' ' . $pfxc . $name
  302. . '="' . htmlspecialchars($value) . '"';
  303. }
  304. if (count($this->children[$parent]) == 0)
  305. return $result . ' />' . $dbg;
  306. $result .= '>';
  307. foreach ($this->children[$parent] as $child)
  308. $result .= is_string($child) ? htmlspecialchars($child) : ($lbr .
  309. $this->xmd_xml($increase, $indent.$increase, $lbr, $child));
  310. if (!is_string($child)) $result .= $lbr . $indent; // last $child
  311. return $result . '</' . $element . '>' . $dbg;
  312. }
  313. function xmd_value($xmPath, $parent = 0, $fix = array(), $fun = '')
  314. {
  315. // extensions: @*[name] for element position (starts at 1)
  316. // @. for element (tag)name
  317. if ($parent < 0 || $parent > $this->_last || !is_string($xmPath))
  318. return '';
  319. if (($p = strrpos($xmPath, '@')) !== FALSE)
  320. {
  321. $attName = substr($xmPath, $p+1); $xmPath = substr($xmPath, 0, $p);
  322. }
  323. if (!($elems = $this->xmd_select_elements($xmPath, $parent))) return '';
  324. $result = ''; $fixin = isset($fix['in']) ? $fix['in'] : '';
  325. foreach ($elems as $elem)
  326. {
  327. $value = isset($attName) && strlen($attName) >= 1 ?
  328. ($attName == '.' ? $this->name[$elem] :
  329. ($attName{0} == '*' ?
  330. $this->_sibnum($elem, substr($attName, 1)) :
  331. $this->attributes[$elem][$attName])) :
  332. $this->xmd_text($elem);
  333. $result .= $fixin . ($fun ? $fun($value) : $value);
  334. }
  335. return (isset($fix['pre']) ? $fix['pre'] : '') .
  336. substr($result, strlen($fixin)) .
  337. (isset($fix['post']) ? $fix['post'] : '');
  338. }
  339. function xmd_html_value($xmPath, $parent = 0, $fun = 'htmlspecialchars')
  340. {
  341. if (!is_string($xmPath)) return '';
  342. $fix = array();
  343. if (($p = strpos($xmPath, ' -% ')) !== FALSE)
  344. {
  345. $fix['pre'] = substr($xmPath, 0, $p);
  346. $xmPath = substr($xmPath, $p+4);
  347. }
  348. if (($p = strpos($xmPath, ' %- ')) !== FALSE)
  349. {
  350. $fix['post'] = substr($xmPath, $p+4);
  351. $xmPath = substr($xmPath, 0, $p);
  352. }
  353. if (($p = strpos($xmPath, ' ')) !== FALSE)
  354. {
  355. $fix['in'] = substr($xmPath, $p+1);
  356. $xmPath = substr($xmPath, 0, $p);
  357. }
  358. return $this->xmd_value($xmPath, $parent, $fix, $fun);
  359. }
  360. function xmd_select_single_element($xmPath, $parent = 0) // for convenience
  361. {
  362. $elements = $this->xmd_select_elements($xmPath, $parent);
  363. if (count($elements) == 0) return -1;
  364. return $elements[0];
  365. }
  366. function xmd_select_elements_where($xmPath,
  367. $subPath = '.', $value = '', $parent = 0)
  368. {
  369. if (!is_string($subPath)) return array();
  370. $elems = array(); if ($subPath == '.') $subPath = '';
  371. foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
  372. if ($this->xmd_value($subPath, $elem) == $value) $elems[] = $elem;
  373. return $elems;
  374. }
  375. function xmd_select_elements_where_notempty($xmPath,
  376. $subPath = '.', $parent = 0)
  377. {
  378. if (!is_string($subPath)) return array();
  379. $elems = array(); if ($subPath == '.') $subPath = '';
  380. foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
  381. if ($this->xmd_value($subPath, $elem)) $elems[] = $elem;
  382. return $elems;
  383. }
  384. function xmd_select_elements($xmPath, $parent = 0)
  385. {
  386. // XPath subset: e1/e2/.../en, also * and e[n] and *[n] (at 1 or -1)
  387. // /*/... starts from root, regardless of $parent
  388. // extensions: e= - or + (previous & next sibling)
  389. // e= -name or +name (sibling of specific name)
  390. // e= .. (stops at root, so too many doesn't matter)
  391. if (substr($xmPath, 0, 3) == '/*/')
  392. {
  393. $xmPath = substr($xmPath, 3); $parent = 0;
  394. }
  395. if ($parent < 0 || $parent > $this->_last) return array();
  396. while (substr($xmPath, 0, 1) == '/') $xmPath = substr($xmPath, 1);
  397. while (substr($xmPath, -1) == '/') $xmPath = substr($xmPath, 0, -1);
  398. if ($xmPath == '' || $xmPath == '.') return array($parent);
  399. if ($xmPath == '..')
  400. {
  401. if ($parent > 0) return array($this->parent[$parent]);
  402. return array($parent);
  403. }
  404. if ($xmPath{0} == '-' || $xmPath{0} == '+')
  405. {
  406. $sib = $this->_sibnum($parent, substr($xmPath, 1), $xmPath{0});
  407. if ($sib == -1) return array(); return array($sib);
  408. }
  409. $m = array();
  410. if (ereg('^(.+)/([^/]+)$', $xmPath, $m)) // split on last /
  411. {
  412. if (!($set = $this->xmd_select_elements($m[1], $parent)))
  413. return $set; // which is empty array
  414. if (count($set) == 1)
  415. return $this->xmd_select_elements($m[2], $set[0]);
  416. $bigset = array(); $m2 = $m[2];
  417. foreach ($set as $e)
  418. $bigset = array_merge($bigset,
  419. $this->xmd_select_elements($m2, $e));
  420. return $bigset;
  421. }
  422. $xmName = $xmPath; $xmNum = 0; $elems = array();
  423. if (ereg('^(.+)\[(-?[0-9]+)\]$', $xmPath, $m))
  424. {
  425. $xmName = $m[1]; $xmNum = (int) $m[2];
  426. }
  427. foreach ($this->children[$parent] as $child) if (!is_string($child))
  428. if ($xmName == '*' || ($this->name[$child]) == $xmName)
  429. $elems[] = $child;
  430. if ($xmNum == 0) return $elems;
  431. $xmNum = ($xmNum > 0) ? $xmNum - 1 : count($elems) + $xmNum;
  432. return ($xmNum < count($elems)) ? array($elems[$xmNum]) : array();
  433. }
  434. // Notes on parsing and caching:
  435. // - parsing 388 KB -> 0.94 sec
  436. // - caching 298 KB <- 1.63 sec: 11387 elements, 5137 string nodes
  437. // - uncache 298 KB -> 0.42 sec
  438. // - $this->children[$n][] in a loop is quicker than a temporary array
  439. // $children[] and copying $this->children[$n] = $children after the loop
  440. // - incremental operator ++$numptr is not quicker than ($numptr += 1)
  441. // - numbers & textstring: more compact with base64_encode(gzcompress())
  442. function xmd_cache() // store all data in numbers+names+textstring
  443. {
  444. $this->numbers = array(); $this->textstring = ''; $this->_strings = 0;
  445. // add all element- and attributenames to names - see below
  446. for ($n = 0; $n <= $this->_last; $n++)
  447. {
  448. $this->numbers[] = count($this->children[$n]);
  449. foreach ($this->children[$n] as $ch)
  450. {
  451. if (is_string($ch))
  452. {
  453. $this->numbers[] = 0; $this->_strings += 1;
  454. $this->numbers[] = strlen($ch); $this->textstring .= $ch;
  455. }
  456. else
  457. {
  458. $this->numbers[] = ($ch-$n); // more compact than $ch
  459. }
  460. }
  461. $this->numbers[] = count($this->attributes[$n]);
  462. foreach ($this->attributes[$n] as $name => $value)
  463. {
  464. $this->numbers[] = $this->_lookup($name);
  465. $this->numbers[] = $this->atns[$n][$name];
  466. $this->numbers[] = strlen($value); $this->textstring .= $value;
  467. }
  468. $this->numbers[] = $this->_lookup($this->name[$n]);
  469. $this->numbers[] = $this->ns[$n];
  470. $this->numbers[] = $n - $this->parent[$n]; // more compact
  471. }
  472. }
  473. function xmddoc($strings, $charset = 'ISO-8859-1', $textstring = '')
  474. {
  475. $this->parent = array(); $this->name = array();
  476. $this->ns = array(); $this->attributes = array();
  477. $this->atns = array(); $this->children = array();
  478. $this->error = ''; $this->_nesting = array();
  479. $this->_ns = array(); $this->_last = -1;
  480. $this->_nsp = array();
  481. foreach (explode(',', 'eb,tn,eg,ut,as,ne,jt,ne,ah,en,er') as $pfx)
  482. $this->_nsp[$pfx] = '';
  483. if (is_array($charset)) // new xmddoc($names, $numbers, $textstring)
  484. {
  485. $this->names = $strings; $this->numbers = $charset;
  486. $this->textstring = $textstring; $this->_uncache(); return;
  487. }
  488. $this->names = array(); $this->_lookup(''); // empty ns is number 0
  489. $xml_parser = xml_parser_create_ns($charset, ':');
  490. xml_set_object($xml_parser,$this); // instead of ...,&$this
  491. // See PHP manual: Passing by Reference vs. xml_set_object
  492. xml_set_element_handler($xml_parser, '_startElement', '_endElement');
  493. xml_set_character_data_handler($xml_parser, '_cData');
  494. xml_set_start_namespace_decl_handler($xml_parser, '_startNs');
  495. // xml_set_end_namespace_decl_handler($xml_parser, '_endNs');
  496. // xml_set_default_handler ($xml_parser, '');
  497. xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, FALSE);
  498. if (!is_array($strings)) $strings = array($strings);
  499. if (count($strings) && (substr($strings[0], 0, 5) != '<?xml') &&
  500. !xml_parse($xml_parser,
  501. '<?xml version="1.0" encoding="' . $charset . '"?>', FALSE))
  502. {
  503. $this->error = 'Encoding ' . $charset . ': ' .
  504. xml_error_string(xml_get_error_code($xml_parser));
  505. $strings = array();
  506. }
  507. foreach ($strings as $s)
  508. {
  509. if (substr($s, -1) != "\n") $s .= "\n";
  510. if (!xml_parse($xml_parser, $s, FALSE))
  511. {
  512. $errCode = xml_get_error_code($xml_parser);
  513. $this->error = 'Line '. xml_get_current_line_number($xml_parser) .
  514. ' (c' . xml_get_current_column_number($xml_parser) .
  515. // ', b' . xml_get_current_byte_index($xml_parser) .
  516. '): error ' . $errCode . '= ' . xml_error_string($errCode);
  517. break; // the error string is English...
  518. }
  519. }
  520. xml_parse($xml_parser, '', TRUE); xml_parser_free($xml_parser);
  521. }
  522. // internal methods
  523. function _sibnum($parent, $name = '', $pmn = 'N') // sibling or number
  524. {
  525. if ($parent <= 0) return -1;
  526. $found = FALSE; $prev = -1; $next = -1; $num = 0;
  527. foreach ($this->children[$this->parent[$parent]] as $child)
  528. {
  529. if (is_string($child)) continue;
  530. $name_ok = $name ? ($this->name[$child] == $name) : TRUE;
  531. if ($found && $name_ok)
  532. {
  533. $next = $child; break;
  534. }
  535. elseif ($parent === $child)
  536. {
  537. $num ++; $found = TRUE;
  538. }
  539. elseif ($name_ok)
  540. {
  541. $num ++; $prev = $child;
  542. }
  543. }
  544. return ($pmn == '-') ? $prev : (($pmn == '+') ? $next : $num);
  545. }
  546. function _uncache() // restore all data from numbers+names+textstring
  547. {
  548. $n = -1; $numptr = -1; $txtptr = 0; $count = count($this->numbers);
  549. $A0 = array(); // believe it or not, this makes the loops quicker!
  550. while (++$numptr < $count)
  551. {
  552. $n++;
  553. if (($countdown = $this->numbers[$numptr]) == 0)
  554. {
  555. $this->children[$n] = $A0;
  556. }
  557. else while (--$countdown >= 0)
  558. {
  559. if (($chc = $this->numbers[++$numptr]) == 0)
  560. {
  561. $this->children[$n][] = substr($this->textstring,
  562. $txtptr, ($len = $this->numbers[++$numptr]));
  563. $txtptr += $len;
  564. }
  565. else
  566. {
  567. $this->children[$n][] = $n + $chc;
  568. }
  569. }
  570. if (($countdown = $this->numbers[++$numptr]) == 0)
  571. {
  572. $this->attributes[$n] = $this->atns[$n] = $A0;
  573. }
  574. else while (--$countdown >= 0)
  575. {
  576. $name = $this->names[$this->numbers[++$numptr]];
  577. $this->atns[$n][$name] = $this->numbers[++$numptr];
  578. $this->attributes[$n][$name] = substr($this->textstring,
  579. $txtptr, ($len = $this->numbers[++$numptr]));
  580. $txtptr += $len;
  581. }
  582. $this->name[$n] = $this->names[$this->numbers[++$numptr]];
  583. $this->ns[$n] = $this->numbers[++$numptr];
  584. $this->parent[$n] = $n - $this->numbers[++$numptr];
  585. }
  586. $this->_last = $n;
  587. }
  588. function _startElement($parser, $name, $attribs)
  589. {
  590. $level = count($this->_nesting);
  591. $parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
  592. $child = $this->xmd_add_element($name, $parent,
  593. array_merge($attribs, $this->_ns));
  594. $this->_nesting[] = $child; $this->_ns = array();
  595. $this->_concat = FALSE; // see _cData
  596. }
  597. function _endElement($parser, $name)
  598. {
  599. array_pop($this->_nesting); $this->_concat = FALSE;
  600. }
  601. function _cData($parser, $data)
  602. {
  603. if (!ltrim($data)) return; // empty line, or whitespace preceding <tag>
  604. $level = count($this->_nesting);
  605. $parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
  606. if ($parent >= 0)
  607. {
  608. $nc = count($this->children[$parent]);
  609. $pcs = ($nc > 0 && is_string($this->children[$parent][$nc - 1]));
  610. if ($pcs && strlen($data) == 1) $this->_concat = TRUE;
  611. // expat parser puts &xx; in a separate cData, try to re-assemble
  612. if ($pcs && $data{0} > '~') $this->_concat = TRUE;
  613. // PHP5 expat breaks before 8-bit characters
  614. if ($this->_concat)
  615. {
  616. $this->children[$parent][$nc - 1] .= $data;
  617. $this->_concat = (strlen($data) == 1);
  618. }
  619. else
  620. $this->children[$parent][] = $pcs ? "\n" . $data : $data;
  621. }
  622. }
  623. function _startNs($parser, $pfx, $uri) // called before _startElement
  624. {
  625. $this->_ns['xmlns' . ($pfx ? ':'.$pfx : '')] = $uri;
  626. $this->_nsp[$pfx] = $uri;
  627. }
  628. function _nsPfx($ppar, $uri) // find namespace prefix
  629. {
  630. while ($ppar >= 0)
  631. {
  632. foreach ($this->attributes[$ppar] as $name => $value)
  633. if (substr($name, 0, 5) == 'xmlns' && $value == $uri)
  634. {
  635. $pfxc = substr($name, 6) . substr($name, 5, 1); break 2;
  636. }
  637. $ppar = $this->parent[$ppar];
  638. }
  639. if ($ppar >= 0) return $pfxc; if ($uri == '') return '';
  640. if ($uri == 'http://www.w3.org/XML/1998/namespace') return 'xml:';
  641. foreach($this->_nsp as $pfx => $used) if (!$used) break;
  642. $this->_nsp[$pfx] = $uri; $xmlnspfx = 'xmlns:' . $pfx;
  643. $this->attributes[0][$xmlnspfx] = $uri; $this->atns[0][$xmlnspfx] = 0;
  644. return $pfx . ':';
  645. }
  646. function _lookup($name) // for namespaces + see cache
  647. {
  648. $where = array_search($name, $this->names);
  649. if ($where === FALSE || $where === NULL)
  650. {
  651. $where = count($this->names); $this->names[] = $name;
  652. }
  653. return $where;
  654. }
  655. }
  656. /*
  657. <!--
  658. This program is free software; you can redistribute it and/or
  659. modify it under the terms of the GNU General Public License
  660. as published by the Free Software Foundation; either version 2
  661. of the License, or (at your option) any later version.
  662. This program is distributed in the hope that it will be useful,
  663. but WITHOUT ANY WARRANTY; without even the implied warranty of
  664. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  665. GNU General Public License for more details.
  666. -->
  667. */
  668. ?>