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