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