Cleanup: Updated copyright comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / em / classes / tools / class.tx_em_tools_xmlhandler.php
1 <?php
2 /* **************************************************************
3 * Copyright notice
4 *
5 * (c) 2006-2011 Karsten Dambekalns <karsten@typo3.org>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 * A copy is found in the textfile GPL.txt and important notices to the license
17 * from the author is found in LICENSE.txt distributed with these scripts.
18 *
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27
28 /**
29 * XML handling class for the TYPO3 Extension Manager.
30 *
31 * It contains methods for handling the XML files involved with the EM,
32 * such as the list of extension mirrors and the list of available extensions.
33 *
34 * @author Karsten Dambekalns <karsten@typo3.org>
35 * @package TYPO3
36 * @subpackage EM
37 */
38 class tx_em_Tools_XmlHandler {
39
40
41 /**
42 * Holds the parsed XML from extensions.xml.gz
43 * @see parseExtensionsXML()
44 *
45 * @var array
46 */
47 protected $extXMLResult = array();
48
49 /**
50 * @var array
51 */
52 public $extensionsXML = array();
53
54 /**
55 * @var NULL
56 */
57 protected $reviewStates = NULL;
58
59 /**
60 * @var bool
61 */
62 public $useObsolete = FALSE;
63
64 /**
65 * @var array
66 */
67 protected $catArr = array();
68
69 /**
70 * @var array
71 */
72 protected $stateArr = array();
73
74 /**
75 * @var int
76 */
77 public $matchingCount = 0;
78
79 /**
80 * @var array
81 */
82 protected $revCatArr = array();
83
84 /**
85 * @var array
86 */
87 protected $revStateArr = array();
88
89 /**
90 * @var string
91 */
92 protected $currentExt = '';
93
94 /**
95 * @var string
96 */
97 protected $currentVersion = '';
98
99 /**
100 * @var string
101 */
102 protected $currentTag = '';
103
104 /**
105 * Reduces the entries in $this->extensionsXML to the latest version per extension and removes entries not matching the search parameter
106 *
107 * @param string $search The list of extensions is reduced to entries matching this. If empty, the full list is returned.
108 * @param string $owner If set only extensions of that user are fetched
109 * @param string $order A field to order the result by
110 * @param boolean $allExt If set also unreviewed and obsolete extensions are shown
111 * @param boolean $allVer If set returns all version of an extension, otherwise only the last
112 * @param integer $offset Offset to return result from (goes into LIMIT clause)
113 * @param integer $limit Maximum number of entries to return (goes into LIMIT clause)
114 * @param boolean $exactMatch If set search is done for exact matches of extension keys only
115 * @return void
116 */
117 function searchExtensionsXML($search, $owner = '', $order = '', $allExt = FALSE, $allVer = FALSE, $offset = 0, $limit = 500, $exactMatch = FALSE) {
118 $where = '1=1';
119 if ($search && $exactMatch) {
120 $where .= ' AND extkey=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($search, 'cache_extensions');
121 } elseif ($search) {
122 $quotedSearch = $GLOBALS['TYPO3_DB']->escapeStrForLike(
123 $GLOBALS['TYPO3_DB']->quoteStr($search, 'cache_extensions'),
124 'cache_extensions'
125 );
126 $where .= ' AND (extkey LIKE \'%' . $quotedSearch . '%\' OR title LIKE \'%' . $quotedSearch . '%\')';
127
128 }
129 if ($owner) {
130 $where .= ' AND ownerusername=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($owner, 'cache_extensions');
131 }
132
133 // Show extensions without a review or that have passed a review, but not insecure extensions
134 $where .= ' AND reviewstate >= 0';
135
136 if (!$this->useObsolete) {
137 // 5 == obsolete
138 $where .= ' AND state != 5';
139 }
140
141 switch ($order) {
142 case 'author_company':
143 $forder = 'authorname, authorcompany';
144 break;
145 case 'state':
146 $forder = 'state';
147 break;
148 case 'cat':
149 default:
150 $forder = 'category';
151 break;
152 }
153 $order = $forder . ', title';
154 if (!$allVer) {
155 $where .= ' AND lastversion > 0';
156 }
157
158 $idx = 0;
159 $defaultCategories = tx_em_Tools::getDefaultCategory();
160 foreach ($defaultCategories as $catKey => $tmp) {
161 $this->catArr[$idx] = $catKey;
162 $idx++;
163 }
164
165 $idx = 0;
166 $states = tx_em_Tools::getStates();
167 foreach ($states as $state => $tmp) {
168 $this->stateArr[$idx] = $state;
169 $idx++;
170 }
171
172 // Fetch count
173 $count = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('*', 'cache_extensions', $where);
174 $this->matchingCount = $count;
175
176 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'cache_extensions', $where, '', $order, $offset . ',' . $limit);
177 $this->extensionsXML = array();
178 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
179 $row['category'] = $this->catArr[$row['category']];
180 $row['state'] = $this->stateArr[$row['state']];
181
182 if (!is_array($this->extensionsXML[$row['extkey']])) {
183 $this->extensionsXML[$row['extkey']] = array();
184 $this->extensionsXML[$row['extkey']]['downloadcounter'] = $row['alldownloadcounter'];
185 }
186 if (!is_array($this->extensionsXML[$row['extkey']]['versions'])) {
187 $this->extensionsXML[$row['extkey']]['versions'] = array();
188 }
189 $row['dependencies'] = unserialize($row['dependencies']);
190 $this->extensionsXML[$row['extkey']]['versions'][$row['version']] = $row;
191 }
192 $GLOBALS['TYPO3_DB']->sql_free_result($res);
193 }
194
195 /**
196 * Reduces the entries in $this->extensionsXML to the latest version per extension and removes entries not matching the search parameter
197 * The extension key has to be a valid one as search is done for exact matches only.
198 *
199 * @param string $search The list of extensions is reduced to entries with exactely this extension key. If empty, the full list is returned.
200 * @param string $owner If set only extensions of that user are fetched
201 * @param string $order A field to order the result by
202 * @param boolean $allExt If set also unreviewed and obsolete extensions are shown
203 * @param boolean $allVer If set returns all version of an extension, otherwise only the last
204 * @param integer $offset Offset to return result from (goes into LIMIT clause)
205 * @param integer $limit Maximum number of entries to return (goes into LIMIT clause)
206 * @return void
207 */
208 function searchExtensionsXMLExact($search, $owner = '', $order = '', $allExt = FALSE, $allVer = FALSE, $offset = 0, $limit = 500) {
209 $this->searchExtensionsXML($search, $owner, $order, $allExt, $allVer, $offset, $limit, TRUE);
210 }
211
212 function countExtensions() {
213 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('extkey', 'cache_extensions', '1=1', 'extkey');
214 $cnt = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
215 $GLOBALS['TYPO3_DB']->sql_free_result($res);
216 return $cnt;
217 }
218
219 /**
220 * Loads the pre-parsed extension list
221 *
222 * @return boolean TRUE on success, FALSE on error
223 */
224 function loadExtensionsXML() {
225 $this->searchExtensionsXML('', '', '', TRUE);
226 }
227
228 /**
229 * Frees the pre-parsed extension list
230 *
231 * @return void
232 */
233 function freeExtensionsXML() {
234 unset($this->extensionsXML);
235 $this->extensionsXML = array();
236 }
237
238 /**
239 * Removes all extension with a certain state from the list
240 *
241 * @param array &$extensions The "versions" subpart of the extension list
242 * @return void
243 */
244 function removeObsolete(&$extensions) {
245 if ($this->useObsolete) {
246 return;
247 }
248
249 foreach ($extensions as $version => $data) {
250 if ($data['state'] == 'obsolete') {
251 unset($extensions[$version]);
252 }
253 }
254 }
255
256 /**
257 * Returns the reviewstate of a specific extension-key/version
258 *
259 * @param string $extKey
260 * @param string $version: ...
261 * @return integer Review state, if none is set 0 is returned as default.
262 */
263 function getReviewState($extKey, $version) {
264 $where = 'extkey=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($extKey, 'cache_extensions') . ' AND version=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($version, 'cache_extensions');
265 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('reviewstate', 'cache_extensions', $where);
266 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
267 return $row['reviewstate'];
268 }
269 $GLOBALS['TYPO3_DB']->sql_free_result($res);
270 return 0;
271 }
272
273
274 /**
275 * ***************PARSING METHODS***********************
276 */
277 /**
278 * Enter description here...
279 *
280 * @param unknown_type $parser
281 * @param unknown_type $name
282 * @param unknown_type $attrs
283 * @return [type] ...
284 */
285 function startElement($parser, $name, $attrs) {
286 switch ($name) {
287 case 'extensions':
288 break;
289 case 'extension':
290 $this->currentExt = $attrs['extensionkey'];
291 break;
292 case 'version':
293 $this->currentVersion = $attrs['version'];
294 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion] = array();
295 break;
296 default:
297 $this->currentTag = $name;
298 }
299 }
300
301 /**
302 * Enter description here...
303 *
304 * @param unknown_type $parser
305 * @param unknown_type $name
306 * @return [type] ...
307 */
308 function endElement($parser, $name) {
309 switch ($name) {
310 case 'extension':
311 unset($this->currentExt);
312 break;
313 case 'version':
314 unset($this->currentVersion);
315 break;
316 default:
317 unset($this->currentTag);
318 }
319 }
320
321 /**
322 * Enter description here...
323 *
324 * @param unknown_type $parser
325 * @param unknown_type $data
326 * @return [type] ...
327 */
328 function characterData($parser, $data) {
329 if (isset($this->currentTag)) {
330 if (!isset($this->currentVersion) && $this->currentTag == 'downloadcounter') {
331 $this->extXMLResult[$this->currentExt]['downloadcounter'] = trim($data);
332 } elseif ($this->currentTag == 'dependencies') {
333 $data = @unserialize($data);
334 if (is_array($data)) {
335 $dep = array();
336 foreach ($data as $v) {
337 $dep[$v['kind']][$v['extensionKey']] = $v['versionRange'];
338 }
339 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion]['dependencies'] = $dep;
340 }
341 } elseif ($this->currentTag == 'reviewstate') {
342 $this->reviewStates[$this->currentExt][$this->currentVersion] = (int) trim($data);
343 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion]['reviewstate'] = (int) trim($data);
344 } else {
345 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion][$this->currentTag] .= trim($data);
346 }
347 }
348 }
349
350 /**
351 * Parses content of mirrors.xml into a suitable array
352 *
353 * @param string XML data file to parse
354 * @return string HTLML output informing about result
355 */
356 function parseExtensionsXML($filename) {
357
358 $parser = xml_parser_create();
359 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
360 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
361 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
362 xml_set_element_handler($parser, array(&$this, 'startElement'), array(&$this, 'endElement'));
363 xml_set_character_data_handler($parser, array(&$this, 'characterData'));
364
365 $fp = gzopen($filename, 'rb');
366 if (!$fp) {
367 $content .= 'Error opening XML extension file "' . $filename . '"';
368 return $content;
369 }
370 $string = gzread($fp, 0xffff); // Read 64KB
371
372
373 $idx = 0;
374 $defaultCategories = tx_em_Tools::getDefaultCategory();
375 foreach ($defaultCategories as $catKey => $tmp) {
376 $this->revCatArr[$catKey] = $idx++;
377 }
378
379
380 $idx = 0;
381 $states = tx_em_Tools::getStates();
382 foreach ($states as $state => $tmp) {
383 $this->revStateArr[$state] = $idx++;
384 }
385
386 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery('cache_extensions');
387
388 $extcount = 0;
389 @ini_set('pcre.backtrack_limit', 500000);
390 do {
391 if (preg_match('/.*(<extension\s+extensionkey="[^"]+">.*<\/extension>)/suU', $string, $match)) {
392 // Parse content:
393 if (!xml_parse($parser, $match[0], 0)) {
394 $content .= 'Error in XML parser while decoding extensions XML file. Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
395 $error = TRUE;
396 break;
397 }
398 $this->storeXMLResult();
399 $this->extXMLResult = array();
400 $extcount++;
401 $string = substr($string, strlen($match[0]));
402 } elseif (function_exists('preg_last_error') && preg_last_error()) {
403 $errorcodes = array(
404 0 => 'PREG_NO_ERROR',
405 1 => 'PREG_INTERNAL_ERROR',
406 2 => 'PREG_BACKTRACK_LIMIT_ERROR',
407 3 => 'PREG_RECURSION_LIMIT_ERROR',
408 4 => 'PREG_BAD_UTF8_ERROR'
409 );
410 $content .= 'Error in regular expression matching, code: ' . $errorcodes[preg_last_error()] . '<br />See <a href="http://www.php.net/manual/en/function.preg-last-error.php" target="_blank">http://www.php.net/manual/en/function.preg-last-error.php</a>';
411 $error = TRUE;
412 break;
413 } else {
414 if (gzeof($fp)) {
415 break;
416 } // Nothing more can be read
417 $string .= gzread($fp, 0xffff); // Read another 64KB
418 }
419 } while (TRUE);
420
421 xml_parser_free($parser);
422 gzclose($fp);
423
424 if (!$error) {
425 /** @var $flashMessage t3lib_FlashMessage */
426 $flashMessage = t3lib_div::makeInstance(
427 't3lib_FlashMessage',
428 sprintf($GLOBALS['LANG']->getLL('ext_import_extlist_updated'), $extcount),
429 $GLOBALS['LANG']->getLL('ext_import_extlist_updated_header')
430 );
431 $content .= $flashMessage->render();
432 }
433
434 return $content;
435 }
436
437 function storeXMLResult() {
438 foreach ($this->extXMLResult as $extkey => $extArr) {
439 $max = -1;
440 $maxrev = -1;
441 $last = '';
442 $lastrev = '';
443 $usecat = '';
444 $usetitle = '';
445 $usestate = '';
446 $useauthorcompany = '';
447 $useauthorname = '';
448 $verArr = array();
449 foreach ($extArr['versions'] as $version => $vArr) {
450 $iv = tx_em_Tools::makeVersion($version, 'int');
451 if ($vArr['title'] && !$usetitle) {
452 $usetitle = $vArr['title'];
453 }
454 if ($vArr['state'] && !$usestate) {
455 $usestate = $vArr['state'];
456 }
457 if ($vArr['authorcompany'] && !$useauthorcompany) {
458 $useauthorcompany = $vArr['authorcompany'];
459 }
460 if ($vArr['authorname'] && !$useauthorname) {
461 $useauthorname = $vArr['authorname'];
462 }
463 $verArr[$version] = $iv;
464 if ($iv > $max) {
465 $max = $iv;
466 $last = $version;
467 if ($vArr['title']) {
468 $usetitle = $vArr['title'];
469 }
470 if ($vArr['state']) {
471 $usestate = $vArr['state'];
472 }
473 if ($vArr['authorcompany']) {
474 $useauthorcompany = $vArr['authorcompany'];
475 }
476 if ($vArr['authorname']) {
477 $useauthorname = $vArr['authorname'];
478 }
479 $usecat = $vArr['category'];
480 }
481 if ($vArr['reviewstate'] && ($iv > $maxrev)) {
482 $maxrev = $iv;
483 $lastrev = $version;
484 }
485 }
486 if (!strlen($usecat)) {
487 $usecat = 4; // Extensions without a category end up in "misc"
488 } else {
489 if (isset($this->revCatArr[$usecat])) {
490 $usecat = $this->revCatArr[$usecat];
491 } else {
492 $usecat = 4; // Extensions without a category end up in "misc"
493 }
494 }
495 if (isset($this->revStateArr[$usestate])) {
496 $usestate = $this->revCatArr[$usestate];
497 } else {
498 $usestate = 999; // Extensions without a category end up in "misc"
499 }
500 foreach ($extArr['versions'] as $version => $vArr) {
501 $vArr['version'] = $version;
502 $vArr['intversion'] = $verArr[$version];
503 $vArr['extkey'] = $extkey;
504 $vArr['alldownloadcounter'] = $extArr['downloadcounter'];
505 $vArr['dependencies'] = serialize($vArr['dependencies']);
506 $vArr['category'] = $usecat;
507 $vArr['title'] = $usetitle;
508 if ($version == $last) {
509 $vArr['lastversion'] = 1;
510 }
511 if ($version == $lastrev) {
512 $vArr['lastreviewedversion'] = 1;
513 }
514 $vArr['state'] = isset($this->revStateArr[$vArr['state']]) ? $this->revStateArr[$vArr['state']] : $usestate; // 999 = not set category
515 $GLOBALS['TYPO3_DB']->exec_INSERTquery('cache_extensions', $vArr);
516 }
517 }
518 }
519
520 /**
521 * Parses content of mirrors.xml into a suitable array
522 *
523 * @param string $string: XML data to parse
524 * @return string HTLML output informing about result
525 */
526 function parseMirrorsXML($string) {
527 global $TYPO3_CONF_VARS;
528
529 // Create parser:
530 $parser = xml_parser_create();
531 $vals = array();
532 $index = array();
533
534 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
535 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
536
537 $preg_result = array();
538 preg_match('/^[[:space:]]*<\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $preg_result);
539 $theCharset = $preg_result[1] ? $preg_result[1] : ($TYPO3_CONF_VARS['BE']['forceCharset'] ? $TYPO3_CONF_VARS['BE']['forceCharset'] : 'iso-8859-1');
540 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset); // us-ascii / utf-8 / iso-8859-1
541
542 // Parse content:
543 xml_parse_into_struct($parser, $string, $vals, $index);
544
545 // If error, return error message:
546 if (xml_get_error_code($parser)) {
547 $line = xml_get_current_line_number($parser);
548 $error = xml_error_string(xml_get_error_code($parser));
549 xml_parser_free($parser);
550 return 'Error in XML parser while decoding mirrors XML file. Line ' . $line . ': ' . $error;
551 } else {
552 // Init vars:
553 $stack = array(array());
554 $stacktop = 0;
555 $mirrornumber = 0;
556 $current = array();
557 $tagName = '';
558 $documentTag = '';
559
560 // Traverse the parsed XML structure:
561 foreach ($vals as $val) {
562
563 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
564 $tagName = ($val['tag'] == 'mirror' && $val['type'] == 'open') ? '__plh' : $val['tag'];
565 if (!$documentTag) {
566 $documentTag = $tagName;
567 }
568
569 // Setting tag-values, manage stack:
570 switch ($val['type']) {
571 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
572 $current[$tagName] = array(); // Setting blank place holder
573 $stack[$stacktop++] = $current;
574 $current = array();
575 break;
576 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
577 $oldCurrent = $current;
578 $current = $stack[--$stacktop];
579 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
580 if ($tagName == 'mirror') {
581 unset($current['__plh']);
582 $current[$oldCurrent['host']] = $oldCurrent;
583 } else {
584 $current[key($current)] = $oldCurrent;
585 }
586 unset($oldCurrent);
587 break;
588 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
589 $current[$tagName] = (string) $val['value']; // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
590 break;
591 }
592 }
593 return $current[$tagName];
594 }
595 }
596
597 /**
598 * Parses content of *-l10n.xml into a suitable array
599 *
600 * @param string $string: XML data to parse
601 * @return array Array representation of XML data
602 */
603 function parseL10nXML($string) {
604 // Create parser:
605 $parser = xml_parser_create();
606 $vals = array();
607 $index = array();
608
609 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
610 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
611
612 // Parse content:
613 xml_parse_into_struct($parser, $string, $vals, $index);
614
615 // If error, return error message:
616 if (xml_get_error_code($parser)) {
617 $line = xml_get_current_line_number($parser);
618 $error = xml_error_string(xml_get_error_code($parser));
619 xml_parser_free($parser);
620 return 'Error in XML parser while decoding l10n XML file. Line ' . $line . ': ' . $error;
621 } else {
622 // Init vars:
623 $stack = array(array());
624 $stacktop = 0;
625 $mirrornumber = 0;
626 $current = array();
627 $tagName = '';
628 $documentTag = '';
629
630 // Traverse the parsed XML structure:
631 foreach ($vals as $val) {
632
633 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
634 $tagName = ($val['tag'] == 'languagepack' && $val['type'] == 'open') ? $val['attributes']['language'] : $val['tag'];
635 if (!$documentTag) {
636 $documentTag = $tagName;
637 }
638
639 // Setting tag-values, manage stack:
640 switch ($val['type']) {
641 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
642 $current[$tagName] = array(); // Setting blank place holder
643 $stack[$stacktop++] = $current;
644 $current = array();
645 break;
646 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
647 $oldCurrent = $current;
648 $current = $stack[--$stacktop];
649 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
650 $current[key($current)] = $oldCurrent;
651 unset($oldCurrent);
652 break;
653 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
654 $current[$tagName] = (string) $val['value']; // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
655 break;
656 }
657 }
658 return $current[$tagName];
659 }
660 }
661 }
662
663 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['typo3/sysext/em/classes/tools/class.tx_em_tools_smlhandler.php'])) {
664 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['typo3/sysext/em/classes/tools/class.tx_em_tools_xmlhandler.php']);
665 }
666
667 ?>