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