2 /* **************************************************************
5 * (c) 2006-2009 Karsten Dambekalns <karsten@typo3.org>
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.
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.
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.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
29 * XML handling class for the TYPO3 Extension Manager.
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.
34 * @author Karsten Dambekalns <karsten@typo3.org>
38 class SC_mod_tools_em_xmlhandler
{
41 * Enxtension Manager module
43 * @var SC_mod_tools_em_index
48 * Holds the parsed XML from extensions.xml.gz
49 * @see parseExtensionsXML()
53 var $extXMLResult = array();
54 var $extensionsXML = array();
55 var $reviewStates = null;
56 var $useUnchecked = false;
57 var $useObsolete = false;
60 * Reduces the entries in $this->extensionsXML to the latest version per extension and removes entries not matching the search parameter
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
72 function searchExtensionsXML($search, $owner='', $order='', $allExt=false, $allVer=false, $offset=0, $limit=500, $exactMatch=false) {
74 if ($search && $exactMatch) {
75 $where.= ' AND extkey=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($search, 'cache_extensions');
77 $where.= ' AND extkey LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($GLOBALS['TYPO3_DB']->escapeStrForLike($search, 'cache_extensions'), 'cache_extensions').'%\'';
80 $where.= ' AND ownerusername='.$GLOBALS['TYPO3_DB']->fullQuoteStr($owner, 'cache_extensions');
82 if (strlen($owner) ||
$this->useUnchecked ||
$allExt) {
83 // show extensions without review or that have passed review
84 $where.= ' AND reviewstate >= 0';
86 // only display extensions that have passed review
87 $where.= ' AND reviewstate > 0';
89 if (!$this->useObsolete
&& !$allExt) {
90 $where.= ' AND state!=5'; // 5 == obsolete
93 case 'author_company':
94 $forder = 'authorname, authorcompany';
101 $forder = 'category';
104 $order = $forder.', title';
106 if ($this->useUnchecked
) {
107 $where .= ' AND lastversion>0';
109 $where .= ' AND lastreviewedversion>0';
112 $this->catArr
= array();
114 foreach ($this->emObj
->defaultCategories
['cat'] as $catKey => $tmp) {
115 $this->catArr
[$idx] = $catKey;
118 $this->stateArr
= array();
120 foreach ($this->emObj
->states
as $state => $tmp) {
121 $this->stateArr
[$idx] = $state;
125 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'cache_extensions', $where, '', $order, $offset.','.$limit);
126 $this->matchingCount
= $GLOBALS['TYPO3_DB']->sql_num_rows($res);
127 $this->extensionsXML
= array();
128 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
129 $row['category'] = $this->catArr
[$row['category']];
130 $row['state'] = $this->stateArr
[$row['state']];
132 if (!is_array($this->extensionsXML
[$row['extkey']])) {
133 $this->extensionsXML
[$row['extkey']] = array();
134 $this->extensionsXML
[$row['extkey']]['downloadcounter'] = $row['alldownloadcounter'];
136 if (!is_array($this->extensionsXML
[$row['extkey']]['versions'])) {
137 $this->extensionsXML
[$row['extkey']]['versions'] = array();
139 $row['dependencies'] = unserialize($row['dependencies']);
140 $this->extensionsXML
[$row['extkey']]['versions'][$row['version']] = $row;
142 $GLOBALS['TYPO3_DB']->sql_free_result($res);
146 * Reduces the entries in $this->extensionsXML to the latest version per extension and removes entries not matching the search parameter
147 * The extension key has to be a valid one as search is done for exact matches only.
149 * @param string $search The list of extensions is reduced to entries with exactely this extension key. If empty, the full list is returned.
150 * @param string $owner If set only extensions of that user are fetched
151 * @param string $order A field to order the result by
152 * @param boolean $allExt If set also unreviewed and obsolete extensions are shown
153 * @param boolean $allVer If set returns all version of an extension, otherwise only the last
154 * @param integer $offset Offset to return result from (goes into LIMIT clause)
155 * @param integer $limit Maximum number of entries to return (goes into LIMIT clause)
158 function searchExtensionsXMLExact($search, $owner='', $order='', $allExt=false, $allVer=false, $offset=0, $limit=500) {
159 $this->searchExtensionsXML($search, $owner, $order, $allExt, $allVer, $offset, $limit, true);
162 function countExtensions() {
163 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('extkey', 'cache_extensions', '1=1', 'extkey');
164 $cnt = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
165 $GLOBALS['TYPO3_DB']->sql_free_result($res);
170 * Loads the pre-parsed extension list
172 * @return boolean true on success, false on error
174 function loadExtensionsXML() {
175 $this->searchExtensionsXML('', '', '', true);
179 * Frees the pre-parsed extension list
183 function freeExtensionsXML() {
184 unset($this->extensionsXML
);
185 $this->extensionsXML
= array();
189 * Removes all extension with a certain state from the list
191 * @param array &$extensions The "versions" subpart of the extension list
194 function removeObsolete(&$extensions) {
195 if($this->useObsolete
) return;
198 while (list($version, $data) = each($extensions)) {
199 if($data['state']=='obsolete')
200 unset($extensions[$version]);
205 * Returns the reviewstate of a specific extension-key/version
207 * @param string $extKey
208 * @param string $version: ...
209 * @return integer Review state, if none is set 0 is returned as default.
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'];
217 $GLOBALS['TYPO3_DB']->sql_free_result($res);
222 * Removes all extension versions from $extensions that have a reviewstate<1, unless explicitly allowed
224 * @param array &$extensions The "versions" subpart of the extension list
227 function checkReviewState(&$extensions) {
228 if ($this->useUnchecked
) return;
231 while (list($version, $data) = each($extensions)) {
232 if($data['reviewstate']<1)
233 unset($extensions[$version]);
238 * Removes all extension versions from the list of available extensions that have a reviewstate<1, unless explicitly allowed
242 function checkReviewStateGlobal() {
243 if($this->useUnchecked
) return;
245 reset($this->extensionsXML
);
246 while (list($extkey, $data) = each($this->extensionsXML
)) {
247 while (list($version, $vdata) = each($data['versions'])) {
248 if($vdata['reviewstate']<1) unset($this->extensionsXML
[$extkey]['versions'][$version]);
250 if(!count($this->extensionsXML
[$extkey]['versions'])) unset($this->extensionsXML
[$extkey]);
256 * ***************PARSING METHODS***********************
259 * Enter description here...
261 * @param unknown_type $parser
262 * @param unknown_type $name
263 * @param unknown_type $attrs
266 function startElement($parser, $name, $attrs) {
271 $this->currentExt
= $attrs['extensionkey'];
274 $this->currentVersion
= $attrs['version'];
275 $this->extXMLResult
[$this->currentExt
]['versions'][$this->currentVersion
] = array();
278 $this->currentTag
= $name;
283 * Enter description here...
285 * @param unknown_type $parser
286 * @param unknown_type $name
289 function endElement($parser, $name) {
292 unset($this->currentExt
);
295 unset($this->currentVersion
);
298 unset($this->currentTag
);
303 * Enter description here...
305 * @param unknown_type $parser
306 * @param unknown_type $data
309 function characterData($parser, $data) {
310 if(isset($this->currentTag
)) {
311 if(!isset($this->currentVersion
) && $this->currentTag
== 'downloadcounter') {
312 $this->extXMLResult
[$this->currentExt
]['downloadcounter'] = trim($data);
313 } elseif($this->currentTag
== 'dependencies') {
314 $data = @unserialize
($data);
315 if(is_array($data)) {
317 foreach($data as $v) {
318 $dep[$v['kind']][$v['extensionKey']] = $v['versionRange'];
320 $this->extXMLResult
[$this->currentExt
]['versions'][$this->currentVersion
]['dependencies'] = $dep;
322 } elseif($this->currentTag
== 'reviewstate') {
323 $this->reviewStates
[$this->currentExt
][$this->currentVersion
] = (int)trim($data);
324 $this->extXMLResult
[$this->currentExt
]['versions'][$this->currentVersion
]['reviewstate'] = (int)trim($data);
326 $this->extXMLResult
[$this->currentExt
]['versions'][$this->currentVersion
][$this->currentTag
] .= trim($data);
332 * Parses content of mirrors.xml into a suitable array
334 * @param string XML data file to parse
335 * @return string HTLML output informing about result
337 function parseExtensionsXML($filename) {
339 $parser = xml_parser_create();
340 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING
, 0);
341 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE
, 0);
342 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING
, 'utf-8');
343 xml_set_element_handler($parser, array(&$this,'startElement'), array(&$this,'endElement'));
344 xml_set_character_data_handler($parser, array(&$this,'characterData'));
346 $fp = gzopen($filename, 'rb');
348 $content.= 'Error opening XML extension file "'.$filename.'"';
351 $string = gzread($fp, 0xffff); // Read 64KB
353 $this->revCatArr
= array();
355 foreach ($this->emObj
->defaultCategories
['cat'] as $catKey => $tmp) {
356 $this->revCatArr
[$catKey] = $idx++
;
359 $this->revStateArr
= array();
361 foreach ($this->emObj
->states
as $state => $tmp) {
362 $this->revStateArr
[$state] = $idx++
;
365 $GLOBALS['TYPO3_DB']->exec_DELETEquery('cache_extensions', '1=1');
368 @ini_set
('pcre.backtrack_limit', 500000);
370 if (preg_match('/.*(<extension\s+extensionkey="[^"]+">.*<\/extension>)/suU', $string, $match)) {
372 if (!xml_parse($parser, $match[0], 0)) {
373 $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));
377 $this->storeXMLResult();
378 $this->extXMLResult
= array();
380 $string = substr($string, strlen($match[0]));
381 } elseif(function_exists('preg_last_error') && preg_last_error()) {
383 0 => 'PREG_NO_ERROR',
384 1 => 'PREG_INTERNAL_ERROR',
385 2 => 'PREG_BACKTRACK_LIMIT_ERROR',
386 3 => 'PREG_RECURSION_LIMIT_ERROR',
387 4 => 'PREG_BAD_UTF8_ERROR'
389 $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>';
393 if(gzeof($fp)) break; // Nothing more can be read
394 $string .= gzread($fp, 0xffff); // Read another 64KB
398 xml_parser_free($parser);
402 $content.= '<p>The extensions list has been updated and now contains '.$extcount.' extension entries.</p>';
408 function storeXMLResult() {
409 foreach ($this->extXMLResult
as $extkey => $extArr) {
417 $useauthorcompany = '';
420 foreach ($extArr['versions'] as $version => $vArr) {
421 $iv = $this->emObj
->makeVersion($version, 'int');
422 if ($vArr['title']&&!$usetitle) {
423 $usetitle = $vArr['title'];
425 if ($vArr['state']&&!$usestate) {
426 $usestate = $vArr['state'];
428 if ($vArr['authorcompany']&&!$useauthorcompany) {
429 $useauthorcompany = $vArr['authorcompany'];
431 if ($vArr['authorname']&&!$useauthorname) {
432 $useauthorname = $vArr['authorname'];
434 $verArr[$version] = $iv;
438 if ($vArr['title']) {
439 $usetitle = $vArr['title'];
441 if ($vArr['state']) {
442 $usestate = $vArr['state'];
444 if ($vArr['authorcompany']) {
445 $useauthorcompany = $vArr['authorcompany'];
447 if ($vArr['authorname']) {
448 $useauthorname = $vArr['authorname'];
450 $usecat = $vArr['category'];
452 if ($vArr['reviewstate'] && ($iv>$maxrev)) {
457 if (!strlen($usecat)) {
458 $usecat = 4; // Extensions without a category end up in "misc"
460 if (isset($this->revCatArr
[$usecat])) {
461 $usecat = $this->revCatArr
[$usecat];
463 $usecat = 4; // Extensions without a category end up in "misc"
466 if (isset($this->revStateArr
[$usestate])) {
467 $usestate = $this->revCatArr
[$usestate];
469 $usestate = 999; // Extensions without a category end up in "misc"
471 foreach ($extArr['versions'] as $version => $vArr) {
472 $vArr['version'] = $version;
473 $vArr['intversion'] = $verArr[$version];
474 $vArr['extkey'] = $extkey;
475 $vArr['alldownloadcounter'] = $extArr['downloadcounter'];
476 $vArr['dependencies'] = serialize($vArr['dependencies']);
477 $vArr['category'] = $usecat;
478 $vArr['title'] = $usetitle;
479 if ($version==$last) {
480 $vArr['lastversion'] = 1;
482 if ($version==$lastrev) {
483 $vArr['lastreviewedversion'] = 1;
485 $vArr['state'] = isset($this->revStateArr
[$vArr['state']])?
$this->revStateArr
[$vArr['state']]:$usestate; // 999 = not set category
486 $GLOBALS['TYPO3_DB']->exec_INSERTquery('cache_extensions', $vArr);
492 * Parses content of mirrors.xml into a suitable array
494 * @param string $string: XML data to parse
495 * @return string HTLML output informing about result
497 function parseMirrorsXML($string) {
498 global $TYPO3_CONF_VARS;
501 $parser = xml_parser_create();
505 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING
, 0);
506 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE
, 0);
508 $preg_result = array();
509 preg_match('/^[[:space:]]*<\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/',substr($string,0,200),$preg_result);
510 $theCharset = $preg_result[1] ?
$preg_result[1] : ($TYPO3_CONF_VARS['BE']['forceCharset'] ?
$TYPO3_CONF_VARS['BE']['forceCharset'] : 'iso-8859-1');
511 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING
, $theCharset); // us-ascii / utf-8 / iso-8859-1
514 xml_parse_into_struct($parser, $string, $vals, $index);
516 // If error, return error message:
517 if (xml_get_error_code($parser)) {
518 $line = xml_get_current_line_number($parser);
519 $error = xml_error_string(xml_get_error_code($parser));
520 xml_parser_free($parser);
521 return 'Error in XML parser while decoding mirrors XML file. Line '.$line.': '.$error;
524 $stack = array(array());
531 // Traverse the parsed XML structure:
532 foreach($vals as $val) {
534 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
535 $tagName = ($val['tag']=='mirror' && $val['type']=='open') ?
'__plh' : $val['tag'];
536 if (!$documentTag) $documentTag = $tagName;
538 // Setting tag-values, manage stack:
539 switch($val['type']) {
540 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
541 $current[$tagName] = array(); // Setting blank place holder
542 $stack[$stacktop++
] = $current;
545 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
546 $oldCurrent = $current;
547 $current = $stack[--$stacktop];
548 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
549 if($tagName=='mirror') {
550 unset($current['__plh']);
551 $current[$oldCurrent['host']] = $oldCurrent;
553 $current[key($current)] = $oldCurrent;
557 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
558 $current[$tagName] = (string)$val['value']; // Had to cast it as a string - otherwise it would be evaluate false if tested with isset()!!
562 return $current[$tagName];
567 * Parses content of *-l10n.xml into a suitable array
569 * @param string $string: XML data to parse
570 * @return array Array representation of XML data
572 function parseL10nXML($string) {
574 $parser = xml_parser_create();
578 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING
, 0);
579 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE
, 0);
582 xml_parse_into_struct($parser, $string, $vals, $index);
584 // If error, return error message:
585 if (xml_get_error_code($parser)) {
586 $line = xml_get_current_line_number($parser);
587 $error = xml_error_string(xml_get_error_code($parser));
589 xml_parser_free($parser);
590 return 'Error in XML parser while decoding l10n XML file. Line '.$line.': '.$error;
593 $stack = array(array());
600 // Traverse the parsed XML structure:
601 foreach($vals as $val) {
603 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
604 $tagName = ($val['tag']=='languagepack' && $val['type']=='open') ?
$val['attributes']['language'] : $val['tag'];
605 if (!$documentTag) $documentTag = $tagName;
607 // Setting tag-values, manage stack:
608 switch($val['type']) {
609 case 'open': // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
610 $current[$tagName] = array(); // Setting blank place holder
611 $stack[$stacktop++
] = $current;
614 case 'close': // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
615 $oldCurrent = $current;
616 $current = $stack[--$stacktop];
617 end($current); // Going to the end of array to get placeholder key, key($current), and fill in array next:
618 $current[key($current)] = $oldCurrent;
621 case 'complete': // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
622 $current[$tagName] = (string)$val['value']; // Had to cast it as a string - otherwise it would be evaluate false if tested with isset()!!
626 return $current[$tagName];