xmd.lib.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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 (api_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 == api_strlen($name) - 1) return -1;
  228. $child = ($this->_last += 1); $uris = array(); $uri = '';
  229. if ($p)
  230. {
  231. $uri = api_substr($name, 0, $p); $name = api_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 == api_strlen($name) - 1) return '';
  252. $uri = ''; // beware of 'xmlns...', which is a namespace def!
  253. if ($p) if (api_substr($name, 0, 6) != 'xmlns:')
  254. {
  255. $uri = api_substr($name, 0, $p); $name = api_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 (api_substr($name, 0, 6) == 'xmlns:') // namespace def with prefix
  261. $this->_nsp[api_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. global $charset;
  287. if ($parent < 0 || $parent > $this->_last) return '';
  288. $uri = $this->names[$this->ns[$parent]];
  289. $pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
  290. $dbg = ''; // ($uri == '') ? '' : (' <!-- ' . $uri . ' -->');
  291. $result = $indent . '<' . ($element = $pfxc . $this->name[$parent]);
  292. $atnsp = $this->atns[$parent];
  293. foreach ($this->attributes[$parent] as $name => $value)
  294. {
  295. if (isset($atnsp[$name]))
  296. $atnsn = $atnsp[$name];
  297. elseif (isset($atnsn))
  298. unset($atnsn);
  299. $uri = isset($atnsn) && isset($this->names[$atnsn]) ?
  300. $this->names[$atnsn] : '';
  301. $pfxc = ($uri == '') ? '' : $this->_nsPfx($parent, $uri);
  302. $result .= ' ' . $pfxc . $name
  303. . '="' . htmlspecialchars($value, ENT_QUOTES, $charset) . '"';
  304. }
  305. if (count($this->children[$parent]) == 0)
  306. return $result . ' />' . $dbg;
  307. $result .= '>';
  308. foreach ($this->children[$parent] as $child)
  309. $result .= is_string($child) ? htmlspecialchars($child, ENT_QUOTES, $charset) : ($lbr .
  310. $this->xmd_xml($increase, $indent.$increase, $lbr, $child));
  311. if (!is_string($child)) $result .= $lbr . $indent; // last $child
  312. return $result . '</' . $element . '>' . $dbg;
  313. }
  314. function xmd_value($xmPath, $parent = 0, $fix = array(), $fun = '')
  315. {
  316. // extensions: @*[name] for element position (starts at 1)
  317. // @. for element (tag)name
  318. if ($parent < 0 || $parent > $this->_last || !is_string($xmPath))
  319. return '';
  320. if (($p = strrpos($xmPath, '@')) !== FALSE)
  321. {
  322. $attName = api_substr($xmPath, $p+1); $xmPath = api_substr($xmPath, 0, $p);
  323. }
  324. if (!($elems = $this->xmd_select_elements($xmPath, $parent))) return '';
  325. $result = ''; $fixin = isset($fix['in']) ? $fix['in'] : '';
  326. foreach ($elems as $elem)
  327. {
  328. $value = isset($attName) && api_strlen($attName) >= 1 ?
  329. ($attName == '.' ? $this->name[$elem] :
  330. ($attName{0} == '*' ?
  331. $this->_sibnum($elem, api_substr($attName, 1)) :
  332. $this->attributes[$elem][$attName])) :
  333. $this->xmd_text($elem);
  334. $result .= $fixin . ($fun ? $fun($value) : $value);
  335. }
  336. return (isset($fix['pre']) ? $fix['pre'] : '') .
  337. api_substr($result, api_strlen($fixin)) .
  338. (isset($fix['post']) ? $fix['post'] : '');
  339. }
  340. function xmd_html_value($xmPath, $parent = 0, $fun = 'htmlspecialchars')
  341. {
  342. if (!is_string($xmPath)) return '';
  343. $fix = array();
  344. if (($p = api_strpos($xmPath, ' -% ')) !== FALSE)
  345. {
  346. $fix['pre'] = api_substr($xmPath, 0, $p);
  347. $xmPath = api_substr($xmPath, $p+4);
  348. }
  349. if (($p = api_strpos($xmPath, ' %- ')) !== FALSE)
  350. {
  351. $fix['post'] = api_substr($xmPath, $p+4);
  352. $xmPath = api_substr($xmPath, 0, $p);
  353. }
  354. if (($p = api_strpos($xmPath, ' ')) !== FALSE)
  355. {
  356. $fix['in'] = api_substr($xmPath, $p+1);
  357. $xmPath = api_substr($xmPath, 0, $p);
  358. }
  359. return $this->xmd_value($xmPath, $parent, $fix, $fun);
  360. }
  361. function xmd_select_single_element($xmPath, $parent = 0) // for convenience
  362. {
  363. $elements = $this->xmd_select_elements($xmPath, $parent);
  364. if (count($elements) == 0) return -1;
  365. return $elements[0];
  366. }
  367. function xmd_select_elements_where($xmPath,
  368. $subPath = '.', $value = '', $parent = 0)
  369. {
  370. if (!is_string($subPath)) return array();
  371. $elems = array(); if ($subPath == '.') $subPath = '';
  372. foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
  373. if ($this->xmd_value($subPath, $elem) == $value) $elems[] = $elem;
  374. return $elems;
  375. }
  376. function xmd_select_elements_where_notempty($xmPath,
  377. $subPath = '.', $parent = 0)
  378. {
  379. if (!is_string($subPath)) return array();
  380. $elems = array(); if ($subPath == '.') $subPath = '';
  381. foreach ($this->xmd_select_elements($xmPath, $parent) as $elem)
  382. if ($this->xmd_value($subPath, $elem)) $elems[] = $elem;
  383. return $elems;
  384. }
  385. function xmd_select_elements($xmPath, $parent = 0)
  386. {
  387. // XPath subset: e1/e2/.../en, also * and e[n] and *[n] (at 1 or -1)
  388. // /*/... starts from root, regardless of $parent
  389. // extensions: e= - or + (previous & next sibling)
  390. // e= -name or +name (sibling of specific name)
  391. // e= .. (stops at root, so too many doesn't matter)
  392. if (api_substr($xmPath, 0, 3) == '/*/')
  393. {
  394. $xmPath = api_substr($xmPath, 3); $parent = 0;
  395. }
  396. if ($parent < 0 || $parent > $this->_last) return array();
  397. while (api_substr($xmPath, 0, 1) == '/') $xmPath = api_substr($xmPath, 1);
  398. while (api_substr($xmPath, -1) == '/') $xmPath = api_substr($xmPath, 0, -1);
  399. if ($xmPath == '' || $xmPath == '.') return array($parent);
  400. if ($xmPath == '..')
  401. {
  402. if ($parent > 0) return array($this->parent[$parent]);
  403. return array($parent);
  404. }
  405. if ($xmPath{0} == '-' || $xmPath{0} == '+')
  406. {
  407. $sib = $this->_sibnum($parent, api_substr($xmPath, 1), $xmPath{0});
  408. if ($sib == -1) return array(); return array($sib);
  409. }
  410. $m = array();
  411. if (api_ereg('^(.+)/([^/]+)$', $xmPath, $m)) // split on last /
  412. {
  413. if (!($set = $this->xmd_select_elements($m[1], $parent)))
  414. return $set; // which is empty array
  415. if (count($set) == 1)
  416. return $this->xmd_select_elements($m[2], $set[0]);
  417. $bigset = array(); $m2 = $m[2];
  418. foreach ($set as $e)
  419. $bigset = array_merge($bigset,
  420. $this->xmd_select_elements($m2, $e));
  421. return $bigset;
  422. }
  423. $xmName = $xmPath; $xmNum = 0; $elems = array();
  424. if (api_ereg('^(.+)\[(-?[0-9]+)\]$', $xmPath, $m))
  425. {
  426. $xmName = $m[1]; $xmNum = (int) $m[2];
  427. }
  428. foreach ($this->children[$parent] as $child) if (!is_string($child))
  429. if ($xmName == '*' || ($this->name[$child]) == $xmName)
  430. $elems[] = $child;
  431. if ($xmNum == 0) return $elems;
  432. $xmNum = ($xmNum > 0) ? $xmNum - 1 : count($elems) + $xmNum;
  433. return ($xmNum < count($elems)) ? array($elems[$xmNum]) : array();
  434. }
  435. // Notes on parsing and caching:
  436. // - parsing 388 KB -> 0.94 sec
  437. // - caching 298 KB <- 1.63 sec: 11387 elements, 5137 string nodes
  438. // - uncache 298 KB -> 0.42 sec
  439. // - $this->children[$n][] in a loop is quicker than a temporary array
  440. // $children[] and copying $this->children[$n] = $children after the loop
  441. // - incremental operator ++$numptr is not quicker than ($numptr += 1)
  442. // - numbers & textstring: more compact with base64_encode(gzcompress())
  443. function xmd_cache() // store all data in numbers+names+textstring
  444. {
  445. $this->numbers = array(); $this->textstring = ''; $this->_strings = 0;
  446. // add all element- and attributenames to names - see below
  447. for ($n = 0; $n <= $this->_last; $n++)
  448. {
  449. $this->numbers[] = count($this->children[$n]);
  450. foreach ($this->children[$n] as $ch)
  451. {
  452. if (is_string($ch))
  453. {
  454. $this->numbers[] = 0; $this->_strings += 1;
  455. $this->numbers[] = strlen($ch); $this->textstring .= $ch; //!!! Here strlen() has not been changed to api_strlen(). To be investigated. Ivan Tcholakov, 29-AUG-2008.
  456. }
  457. else
  458. {
  459. $this->numbers[] = ($ch-$n); // more compact than $ch
  460. }
  461. }
  462. $this->numbers[] = count($this->attributes[$n]);
  463. foreach ($this->attributes[$n] as $name => $value)
  464. {
  465. $this->numbers[] = $this->_lookup($name);
  466. $this->numbers[] = $this->atns[$n][$name];
  467. $this->numbers[] = strlen($value); $this->textstring .= $value; //!!! Here strlen() has not been changed to api_strlen(). To be investigated. Ivan Tcholakov, 29-AUG-2008.
  468. }
  469. $this->numbers[] = $this->_lookup($this->name[$n]);
  470. $this->numbers[] = $this->ns[$n];
  471. $this->numbers[] = $n - $this->parent[$n]; // more compact
  472. }
  473. }
  474. function xmddoc($strings, $charset = null, $textstring = '')
  475. {
  476. if (empty($charset))
  477. {
  478. $charset = api_get_system_encoding();
  479. }
  480. $this->parent = array(); $this->name = array();
  481. $this->ns = array(); $this->attributes = array();
  482. $this->atns = array(); $this->children = array();
  483. $this->error = ''; $this->_nesting = array();
  484. $this->_ns = array(); $this->_last = -1;
  485. $this->_nsp = array();
  486. foreach (explode(',', 'eb,tn,eg,ut,as,ne,jt,ne,ah,en,er') as $pfx)
  487. $this->_nsp[$pfx] = '';
  488. if (is_array($charset)) // new xmddoc($names, $numbers, $textstring)
  489. {
  490. $this->names = $strings; $this->numbers = $charset;
  491. $this->textstring = $textstring; $this->_uncache(); return;
  492. }
  493. $this->names = array(); $this->_lookup(''); // empty ns is number 0
  494. // This is a quick workaround.
  495. // The xml-parser supports only ISO-8859-1, UTF-8 and US-ASCII.
  496. // See http://php.net/manual/en/function.xml-parser-create-ns.php
  497. //$xml_parser = xml_parser_create_ns($charset, ':');
  498. $xml_parser = xml_parser_create_ns(api_is_utf8($charset) ? 'UTF-8' : 'ISO-8859-1', ':');
  499. xml_set_object($xml_parser,$this); // instead of ...,&$this
  500. // See PHP manual: Passing by Reference vs. xml_set_object
  501. xml_set_element_handler($xml_parser, '_startElement', '_endElement');
  502. xml_set_character_data_handler($xml_parser, '_cData');
  503. xml_set_start_namespace_decl_handler($xml_parser, '_startNs');
  504. // xml_set_end_namespace_decl_handler($xml_parser, '_endNs');
  505. // xml_set_default_handler ($xml_parser, '');
  506. xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, FALSE);
  507. if (!is_array($strings)) $strings = array($strings);
  508. if (count($strings) && (api_substr($strings[0], 0, 5) != '<?xml') &&
  509. !xml_parse($xml_parser,
  510. '<?xml version="1.0" encoding="' . $charset . '"?>', FALSE))
  511. {
  512. $this->error = 'Encoding ' . $charset . ': ' .
  513. xml_error_string(xml_get_error_code($xml_parser));
  514. $strings = array();
  515. }
  516. foreach ($strings as $s)
  517. {
  518. if (api_substr($s, -1) != "\n") $s .= "\n";
  519. if (!xml_parse($xml_parser, $s, FALSE))
  520. {
  521. $errCode = xml_get_error_code($xml_parser);
  522. $this->error = 'Line '. xml_get_current_line_number($xml_parser) .
  523. ' (c' . xml_get_current_column_number($xml_parser) .
  524. // ', b' . xml_get_current_byte_index($xml_parser) .
  525. '): error ' . $errCode . '= ' . xml_error_string($errCode);
  526. break; // the error string is English...
  527. }
  528. }
  529. xml_parse($xml_parser, '', TRUE); xml_parser_free($xml_parser);
  530. }
  531. // internal methods
  532. function _sibnum($parent, $name = '', $pmn = 'N') // sibling or number
  533. {
  534. if ($parent <= 0) return -1;
  535. $found = FALSE; $prev = -1; $next = -1; $num = 0;
  536. foreach ($this->children[$this->parent[$parent]] as $child)
  537. {
  538. if (is_string($child)) continue;
  539. $name_ok = $name ? ($this->name[$child] == $name) : TRUE;
  540. if ($found && $name_ok)
  541. {
  542. $next = $child; break;
  543. }
  544. elseif ($parent === $child)
  545. {
  546. $num ++; $found = TRUE;
  547. }
  548. elseif ($name_ok)
  549. {
  550. $num ++; $prev = $child;
  551. }
  552. }
  553. return ($pmn == '-') ? $prev : (($pmn == '+') ? $next : $num);
  554. }
  555. function _uncache() // restore all data from numbers+names+textstring
  556. {
  557. $n = -1; $numptr = -1; $txtptr = 0; $count = count($this->numbers);
  558. $A0 = array(); // believe it or not, this makes the loops quicker!
  559. while (++$numptr < $count)
  560. {
  561. $n++;
  562. if (($countdown = $this->numbers[$numptr]) == 0)
  563. {
  564. $this->children[$n] = $A0;
  565. }
  566. else while (--$countdown >= 0)
  567. {
  568. if (($chc = $this->numbers[++$numptr]) == 0)
  569. {
  570. $this->children[$n][] = api_substr($this->textstring,
  571. $txtptr, ($len = $this->numbers[++$numptr]));
  572. $txtptr += $len;
  573. }
  574. else
  575. {
  576. $this->children[$n][] = $n + $chc;
  577. }
  578. }
  579. if (($countdown = $this->numbers[++$numptr]) == 0)
  580. {
  581. $this->attributes[$n] = $this->atns[$n] = $A0;
  582. }
  583. else while (--$countdown >= 0)
  584. {
  585. $name = $this->names[$this->numbers[++$numptr]];
  586. $this->atns[$n][$name] = $this->numbers[++$numptr];
  587. $this->attributes[$n][$name] = api_substr($this->textstring,
  588. $txtptr, ($len = $this->numbers[++$numptr]));
  589. $txtptr += $len;
  590. }
  591. $this->name[$n] = $this->names[$this->numbers[++$numptr]];
  592. $this->ns[$n] = $this->numbers[++$numptr];
  593. $this->parent[$n] = $n - $this->numbers[++$numptr];
  594. }
  595. $this->_last = $n;
  596. }
  597. function _startElement($parser, $name, $attribs)
  598. {
  599. $level = count($this->_nesting);
  600. $parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
  601. $child = $this->xmd_add_element($name, $parent,
  602. array_merge($attribs, $this->_ns));
  603. $this->_nesting[] = $child; $this->_ns = array();
  604. $this->_concat = FALSE; // see _cData
  605. }
  606. function _endElement($parser, $name)
  607. {
  608. array_pop($this->_nesting); $this->_concat = FALSE;
  609. }
  610. function _cData($parser, $data)
  611. {
  612. if (!ltrim($data)) return; // empty line, or whitespace preceding <tag>
  613. $level = count($this->_nesting);
  614. $parent = ($level == 0) ? -1 : $this->_nesting[$level-1];
  615. if ($parent >= 0)
  616. {
  617. $nc = count($this->children[$parent]);
  618. $pcs = ($nc > 0 && is_string($this->children[$parent][$nc - 1]));
  619. if ($pcs && api_strlen($data) == 1) $this->_concat = TRUE;
  620. // expat parser puts &xx; in a separate cData, try to re-assemble
  621. if ($pcs && $data{0} > '~') $this->_concat = TRUE;
  622. // PHP5 expat breaks before 8-bit characters
  623. if ($this->_concat)
  624. {
  625. $this->children[$parent][$nc - 1] .= $data;
  626. $this->_concat = (api_strlen($data) == 1);
  627. }
  628. else
  629. $this->children[$parent][] = $pcs ? "\n" . $data : $data;
  630. }
  631. }
  632. function _startNs($parser, $pfx, $uri) // called before _startElement
  633. {
  634. $this->_ns['xmlns' . ($pfx ? ':'.$pfx : '')] = $uri;
  635. $this->_nsp[$pfx] = $uri;
  636. }
  637. function _nsPfx($ppar, $uri) // find namespace prefix
  638. {
  639. while ($ppar >= 0)
  640. {
  641. foreach ($this->attributes[$ppar] as $name => $value)
  642. if (api_substr($name, 0, 5) == 'xmlns' && $value == $uri)
  643. {
  644. $pfxc = api_substr($name, 6) . api_substr($name, 5, 1); break 2;
  645. }
  646. $ppar = $this->parent[$ppar];
  647. }
  648. if ($ppar >= 0) return $pfxc; if ($uri == '') return '';
  649. if ($uri == 'http://www.w3.org/XML/1998/namespace') return 'xml:';
  650. foreach($this->_nsp as $pfx => $used) if (!$used) break;
  651. $this->_nsp[$pfx] = $uri; $xmlnspfx = 'xmlns:' . $pfx;
  652. $this->attributes[0][$xmlnspfx] = $uri; $this->atns[0][$xmlnspfx] = 0;
  653. return $pfx . ':';
  654. }
  655. function _lookup($name) // for namespaces + see cache
  656. {
  657. $where = array_search($name, $this->names);
  658. if ($where === FALSE || $where === NULL)
  659. {
  660. $where = count($this->names); $this->names[] = $name;
  661. }
  662. return $where;
  663. }
  664. }
  665. /*
  666. <!--
  667. This program is free software; you can redistribute it and/or
  668. modify it under the terms of the GNU General Public License
  669. as published by the Free Software Foundation; either version 2
  670. of the License, or (at your option) any later version.
  671. This program is distributed in the hope that it will be useful,
  672. but WITHOUT ANY WARRANTY; without even the implied warranty of
  673. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  674. GNU General Public License for more details.
  675. -->
  676. */
  677. ?>