The EM now stores the list of extensions from TER in the database, see http://bugs...
[Packages/TYPO3.CMS.git] / typo3 / mod / tools / em / class.em_xmlhandler.php
1 <?php
2 /* **************************************************************
3 * Copyright notice
4 *
5 * (c) 2006 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 * Holds the parsed XML from extensions.xml.gz
41 * @see parseExtensionsXML()
42 *
43 * @var array
44 */
45 var $emObj;
46 var $extXMLResult = array();
47 var $extensionsXML = array();
48 var $reviewStates = null;
49 var $useUnchecked = false;
50 var $useObsolete = false;
51
52 /**
53 * Reduces the entries in $this->extensionsXML to the latest version per extension and removes entries not matching the search parameter
54 *
55 * @param string $search The list of extensions is reduced to entries matching this. If empty, the full list is returned.
56 * @param boolean $latest If true, only the latest version is kept in the list
57 * @return [type] ...
58 */
59 function searchExtensionsXML($search, $owner='', $order = '', $allExt = false, $allVer = false, $offset = 0, $limit = 500) {
60 $where = '1=1';
61 if ($search) {
62 $where .= ' AND extkey LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($GLOBALS['TYPO3_DB']->escapeStrForLike($search, 'cache_extensions'), 'cache_extensions').'%\'';
63 }
64 if ($owner) {
65 $where .= ' AND ownerusername='.$GLOBALS['TYPO3_DB']->fullQuoteStr($owner, 'cache_extensions');
66 }
67 if(!(strlen($owner) || $this->useUnchecked || $allExt)) {
68 $where .= ' AND reviewstate>0';
69 }
70 if(!($this->useObsolete || $allExt)) {
71 $where .= ' AND state!=5'; // 5 == obsolete
72 }
73 switch ($order) {
74 case 'author_company':
75 $forder = 'authorname, authorcompany';
76 break;
77 case 'state':
78 $forder = 'state';
79 break;
80 case 'cat':
81 default:
82 $forder = 'category';
83 break;
84 }
85 $order = $forder.', title';
86 if (!$allVer) {
87 if ($this->useUnchecked) {
88 $where .= ' AND lastversion>0';
89 } else {
90 $where .= ' AND lastreviewedversion>0';
91 }
92 }
93 $this->catArr = array();
94 $idx = 0;
95 foreach ($this->emObj->defaultCategories['cat'] as $catKey => $tmp) {
96 $this->catArr[$idx] = $catKey;
97 $idx++;
98 }
99 $this->stateArr = array();
100 $idx = 0;
101 foreach ($this->emObj->states as $state => $tmp) {
102 $this->stateArr[$idx] = $state;
103 $idx++;
104 }
105
106 // Fetch count
107 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('count(*) as cnt', 'cache_extensions', $where, '', $order);
108 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
109 $this->matchingCount = $row['cnt'];
110 $GLOBALS['TYPO3_DB']->sql_free_result($res);
111
112 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('count(*) as cnt', 'cache_extensions', $where, '', $order);
113
114 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'cache_extensions', $where, '', $order, $offset.','.$limit);
115 $this->extensionsXML = array();
116 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
117 $row['category'] = $this->catArr[$row['category']];
118 $row['state'] = $this->stateArr[$row['state']];
119
120 if (!is_array($this->extensionsXML[$row['extkey']])) {
121 $this->extensionsXML[$row['extkey']] = array();
122 $this->extensionsXML[$row['extkey']]['downloadcounter'] = $row['alldownloadcounter'];
123 }
124 if (!is_array($this->extensionsXML[$row['extkey']]['versions'])) {
125 $this->extensionsXML[$row['extkey']]['versions'] = array();
126 }
127 $row['dependencies'] = unserialize($row['dependencies']);
128 $this->extensionsXML[$row['extkey']]['versions'][$row['version']] = $row;
129 }
130 $GLOBALS['TYPO3_DB']->sql_free_result($res);
131 }
132
133 function countExtensions() {
134 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('extkey', 'cache_extensions', '1=1', 'extkey');
135 $cnt = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
136 $GLOBALS['TYPO3_DB']->sql_free_result($res);
137 return $cnt;
138 }
139
140 /**
141 * Loads the pre-parsed extension list
142 *
143 * @return boolean true on success, false on error
144 */
145 function loadExtensionsXML() {
146 $this->searchExtensionsXML('', '', '', true);
147 }
148
149 /**
150 * Frees the pre-parsed extension list
151 *
152 * @return void
153 */
154 function freeExtensionsXML() {
155 unset($this->extensionsXML);
156 $this->extensionsXML = array();
157 }
158
159 /**
160 * Removes all extension with a certain state from the list
161 *
162 * @param array &$extensions The "versions" subpart of the extension list
163 * @return void
164 */
165 function removeObsolete(&$extensions) {
166 if($this->useObsolete) return;
167
168 reset($extensions);
169 while (list($version, $data) = each($extensions)) {
170 if($data['state']=='obsolete')
171 unset($extensions[$version]);
172 }
173 }
174
175 /**
176 + * Returns the reviewstate of a specific extension-key/version
177 *
178 * @param string $extKey
179 * @param string $version: ...
180 * @return integer Review state, if none is set 0 is returned as default.
181 */
182 function getReviewState($extKey, $version) {
183 $where = 'extkey='.$GLOBALS['TYPO3_DB']->fullQuoteStr($extKey, 'cache_extensions').' AND version='.$GLOBALS['TYPO3_DB']->fullQuoteStr($version, 'cache_extensions');
184 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('reviewstate', 'cache_extensions', $where);
185 if ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
186 return $row['reviewstate'];
187 }
188 $GLOBALS['TYPO3_DB']->sql_free_result($res);
189 return 0;
190 }
191
192 /**
193 * Removes all extension versions from $extensions that have a reviewstate<1, unless explicitly allowed
194 *
195 * @param array &$extensions The "versions" subpart of the extension list
196 * @return void
197 */
198 function checkReviewState(&$extensions) {
199 if ($this->useUnchecked) return;
200
201 reset($extensions);
202 while (list($version, $data) = each($extensions)) {
203 if($data['reviewstate']<1)
204 unset($extensions[$version]);
205 }
206 }
207
208 /**
209 * Removes all extension versions from the list of available extensions that have a reviewstate<1, unless explicitly allowed
210 *
211 * @return void
212 */
213 function checkReviewStateGlobal() {
214 if($this->useUnchecked) return;
215
216 reset($this->extensionsXML);
217 while (list($extkey, $data) = each($this->extensionsXML)) {
218 while (list($version, $vdata) = each($data['versions'])) {
219 if($vdata['reviewstate']<1) unset($this->extensionsXML[$extkey]['versions'][$version]);
220 }
221 if(!count($this->extensionsXML[$extkey]['versions'])) unset($this->extensionsXML[$extkey]);
222 }
223 }
224
225
226 /**
227 * ***************PARSING METHODS***********************
228 */
229 /**
230 * Enter description here...
231 *
232 * @param unknown_type $parser
233 * @param unknown_type $name
234 * @param unknown_type $attrs
235 * @return [type] ...
236 */
237 function startElement($parser, $name, $attrs) {
238 switch($name) {
239 case 'extensions':
240 break;
241 case 'extension':
242 $this->currentExt = $attrs['extensionkey'];
243 break;
244 case 'version':
245 $this->currentVersion = $attrs['version'];
246 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion] = array();
247 break;
248 default:
249 $this->currentTag = $name;
250 }
251 }
252
253 /**
254 * Enter description here...
255 *
256 * @param unknown_type $parser
257 * @param unknown_type $name
258 * @return [type] ...
259 */
260 function endElement($parser, $name) {
261 switch($name) {
262 case 'extension':
263 unset($this->currentExt);
264 break;
265 case 'version':
266 unset($this->currentVersion);
267 break;
268 default:
269 unset($this->currentTag);
270 }
271 }
272
273 /**
274 * Enter description here...
275 *
276 * @param unknown_type $parser
277 * @param unknown_type $data
278 * @return [type] ...
279 */
280 function characterData($parser, $data) {
281 if(isset($this->currentTag)) {
282 if(!isset($this->currentVersion) && $this->currentTag == 'downloadcounter') {
283 $this->extXMLResult[$this->currentExt]['downloadcounter'] = trim($data);
284 } elseif($this->currentTag == 'dependencies') {
285 $data = @unserialize($data);
286 if(is_array($data)) {
287 $dep = array();
288 foreach($data as $v) {
289 $dep[$v['kind']][$v['extensionKey']] = $v['versionRange'];
290 }
291 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion]['dependencies'] = $dep;
292 }
293 } elseif($this->currentTag == 'reviewstate') {
294 $this->reviewStates[$this->currentExt][$this->currentVersion] = (int)trim($data);
295 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion]['reviewstate'] = (int)trim($data);
296 } else {
297 $this->extXMLResult[$this->currentExt]['versions'][$this->currentVersion][$this->currentTag] .= trim($data);
298 }
299 }
300 }
301
302 /**
303 * Parses content of mirrors.xml into a suitable array
304 *
305 * @param string XML data file to parse
306 * @return string HTLML output informing about result
307 */
308 function parseExtensionsXML($filename) {
309
310 $parser = xml_parser_create();
311 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
312 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
313 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
314 xml_set_element_handler($parser, array(&$this,'startElement'), array(&$this,'endElement'));
315 xml_set_character_data_handler($parser, array(&$this,'characterData'));
316
317 $fp = gzopen($filename, 'rb');
318 if (!$fp) {
319 $content.= 'Error opening XML extension file "'.$filename.'"';
320 return $content;
321 }
322 $string = gzread($fp, 0xffff); // Read 64KB
323
324 $this->revCatArr = array();
325 $idx = 0;
326 foreach ($this->emObj->defaultCategories['cat'] as $catKey => $tmp) {
327 $this->revCatArr[$catKey] = $idx++;
328 }
329
330 $this->revStateArr = array();
331 $idx = 0;
332 foreach ($this->emObj->states as $state => $tmp) {
333 $this->revStateArr[$state] = $idx++;
334 }
335
336 $GLOBALS['TYPO3_DB']->exec_DELETEquery('cache_extensions', '1=1');
337
338 $extcount = 0;
339 @ini_set('pcre.backtrack_limit', 500000);
340 do {
341 if (preg_match('/.*(<extension\s+extensionkey="[^"]+">.*<\/extension>)/suU', $string, $match)) {
342 // Parse content:
343 if (!xml_parse($parser, $match[0], 0)) {
344 $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));
345 $error = true;
346 break;
347 }
348 $this->storeXMLResult();
349 $this->extXMLResult = array();
350 $extcount++;
351 $string = substr($string, strlen($match[0]));
352 } elseif(function_exists('preg_last_error') && preg_last_error()) {
353 $errorcodes = array(
354 0 => 'PREG_NO_ERROR',
355 1 => 'PREG_INTERNAL_ERROR',
356 2 => 'PREG_BACKTRACK_LIMIT_ERROR',
357 3 => 'PREG_RECURSION_LIMIT_ERROR',
358 4 => 'PREG_BAD_UTF8_ERROR'
359 );
360 $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>';
361 $error = true;
362 break;
363 } else {
364 if(gzeof($fp)) break; // Nothing more can be read
365 $string .= gzread($fp, 0xffff); // Read another 64KB
366 }
367 } while (true);
368
369 xml_parser_free($parser);
370 gzclose($fp);
371
372 if(!$error) {
373 $content.= '<p>The extensions list has been updated and now contains '.$extcount.' extension entries.</p>';
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 if ((double)phpversion()>=5) {
480 $preg_result = array();
481 preg_match('/^[[:space:]]*<\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/',substr($string,0,200),$preg_result);
482 $theCharset = $preg_result[1] ? $preg_result[1] : ($TYPO3_CONF_VARS['BE']['forceCharset'] ? $TYPO3_CONF_VARS['BE']['forceCharset'] : 'iso-8859-1');
483 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset); // us-ascii / utf-8 / iso-8859-1
484 }
485
486 // Parse content:
487 xml_parse_into_struct($parser, $string, $vals, $index);
488
489 // If error, return error message:
490 if (xml_get_error_code($parser)) {
491 $line = xml_get_current_line_number($parser);
492 $error = xml_error_string(xml_get_error_code($parser));
493 xml_parser_free($parser);
494 return 'Error in XML parser while decoding mirrors XML file. Line '.$line.': '.$error;
495 } else {
496 // Init vars:
497 $stack = array(array());
498 $stacktop = 0;
499 $mirrornumber = 0;
500 $current=array();
501 $tagName = '';
502 $documentTag = '';
503
504 // Traverse the parsed XML structure:
505 foreach($vals as $val) {
506
507 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
508 $tagName = ($val['tag']=='mirror' && $val['type']=='open') ? '__plh' : $val['tag'];
509 if (!$documentTag) $documentTag = $tagName;
510
511 // Setting tag-values, manage stack:
512 switch($val['type']) {
513 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
514 $current[$tagName] = array(); // Setting blank place holder
515 $stack[$stacktop++] = $current;
516 $current = array();
517 break;
518 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
519 $oldCurrent = $current;
520 $current = $stack[--$stacktop];
521 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
522 if($tagName=='mirror') {
523 unset($current['__plh']);
524 $current[$oldCurrent['host']] = $oldCurrent;
525 } else {
526 $current[key($current)] = $oldCurrent;
527 }
528 unset($oldCurrent);
529 break;
530 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
531 $current[$tagName] = (string)$val['value']; // Had to cast it as a string - otherwise it would be evaluate false if tested with isset()!!
532 break;
533 }
534 }
535 return $current[$tagName];
536 }
537 }
538
539 /**
540 * Parses content of *-l10n.xml into a suitable array
541 *
542 * @param string $string: XML data to parse
543 * @return array Array representation of XML data
544 */
545 function parseL10nXML($string) {
546 // Create parser:
547 $parser = xml_parser_create();
548 $vals = array();
549 $index = array();
550
551 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
552 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
553
554 // Parse content:
555 xml_parse_into_struct($parser, $string, $vals, $index);
556
557 // If error, return error message:
558 if (xml_get_error_code($parser)) {
559 $line = xml_get_current_line_number($parser);
560 $error = xml_error_string(xml_get_error_code($parser));
561 debug($error);
562 xml_parser_free($parser);
563 return 'Error in XML parser while decoding l10n XML file. Line '.$line.': '.$error;
564 } else {
565 // Init vars:
566 $stack = array(array());
567 $stacktop = 0;
568 $mirrornumber = 0;
569 $current=array();
570 $tagName = '';
571 $documentTag = '';
572
573 // Traverse the parsed XML structure:
574 foreach($vals as $val) {
575
576 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
577 $tagName = ($val['tag']=='languagepack' && $val['type']=='open') ? $val['attributes']['language'] : $val['tag'];
578 if (!$documentTag) $documentTag = $tagName;
579
580 // Setting tag-values, manage stack:
581 switch($val['type']) {
582 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
583 $current[$tagName] = array(); // Setting blank place holder
584 $stack[$stacktop++] = $current;
585 $current = array();
586 break;
587 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
588 $oldCurrent = $current;
589 $current = $stack[--$stacktop];
590 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
591 $current[key($current)] = $oldCurrent;
592 unset($oldCurrent);
593 break;
594 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
595 $current[$tagName] = (string)$val['value']; // Had to cast it as a string - otherwise it would be evaluate false if tested with isset()!!
596 break;
597 }
598 }
599 return $current[$tagName];
600 }
601 }
602 }
603 ?>