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