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