Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageUpdateDispatcher.class.php
1 <?php
2 namespace wcf\system\package;
3 use wcf\data\package\update\server\PackageUpdateServer;
4 use wcf\data\package\update\server\PackageUpdateServerEditor;
5 use wcf\data\package\update\version\PackageUpdateVersionEditor;
6 use wcf\data\package\update\version\PackageUpdateVersionList;
7 use wcf\data\package\update\PackageUpdateEditor;
8 use wcf\data\package\update\PackageUpdateList;
9 use wcf\data\package\Package;
10 use wcf\system\cache\builder\PackageUpdateCacheBuilder;
11 use wcf\system\database\util\PreparedStatementConditionBuilder;
12 use wcf\system\exception\HTTPUnauthorizedException;
13 use wcf\system\exception\SystemException;
14 use wcf\system\package\PackageUpdateUnauthorizedException;
15 use wcf\system\Regex;
16 use wcf\system\SingletonFactory;
17 use wcf\system\WCF;
18 use wcf\util\HTTPRequest;
19 use wcf\util\XML;
20
21 /**
22 * Provides functions to manage package updates.
23 *
24 * @author Alexander Ebert
25 * @copyright 2001-2014 WoltLab GmbH
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package com.woltlab.wcf
28 * @subpackage system.package
29 * @category Community Framework
30 */
31 class PackageUpdateDispatcher extends SingletonFactory {
32 /**
33 * Refreshes the package database.
34 *
35 * @param array<integer> $packageUpdateServerIDs
36 * @param boolean $ignoreCache
37 */
38 public function refreshPackageDatabase(array $packageUpdateServerIDs = array(), $ignoreCache = false) {
39 // get update server data
40 $updateServers = PackageUpdateServer::getActiveUpdateServers($packageUpdateServerIDs);
41
42 // loop servers
43 $refreshedPackageLists = false;
44 foreach ($updateServers as $updateServer) {
45 if ($ignoreCache || $updateServer->lastUpdateTime < TIME_NOW - 600) {
46 $errorMessage = '';
47
48 try {
49 $this->getPackageUpdateXML($updateServer);
50 $refreshedPackageLists = true;
51 }
52 catch (SystemException $e) {
53 $errorMessage = $e->getMessage();
54 }
55 catch (PackageUpdateUnauthorizedException $e) {
56 $reply = $e->getRequest()->getReply();
57 $errorMessage = reset($reply['httpHeaders']);
58 }
59
60 if ($errorMessage) {
61 // save error status
62 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
63 $updateServerEditor->update(array(
64 'status' => 'offline',
65 'errorMessage' => $errorMessage
66 ));
67 }
68 }
69 }
70
71 if ($refreshedPackageLists) {
72 PackageUpdateCacheBuilder::getInstance()->reset();
73 }
74 }
75
76 /**
77 * Gets the package_update.xml from an update server.
78 *
79 * @param \wcf\data\package\update\server\PackageUpdateServer $updateServer
80 */
81 protected function getPackageUpdateXML(PackageUpdateServer $updateServer) {
82 $authData = $updateServer->getAuthData();
83 $settings = array();
84 if ($authData) $settings['auth'] = $authData;
85
86 $postData = array(
87 'lastUpdateTime' => $updateServer->lastUpdateTime
88 );
89
90 // append auth code if set and update server resolves to woltlab.com
91 if (PACKAGE_SERVER_AUTH_CODE && Regex::compile('^https?://[a-z]+.woltlab.com/')->match($updateServer->serverURL)) {
92 $postData['authCode'] = PACKAGE_SERVER_AUTH_CODE;
93 }
94
95 $request = new HTTPRequest($updateServer->serverURL, $settings, $postData);
96
97 try {
98 $request->execute();
99 $reply = $request->getReply();
100 }
101 catch (HTTPUnauthorizedException $e) {
102 throw new PackageUpdateUnauthorizedException($request, $updateServer);
103 }
104 catch (SystemException $e) {
105 $reply = $request->getReply();
106
107 $statusCode = (is_array($reply['statusCode'])) ? reset($reply['statusCode']) : $reply['statusCode'];
108 throw new SystemException(WCF::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' ('.$statusCode.')');
109 }
110
111 // parse given package update xml
112 $allNewPackages = $this->parsePackageUpdateXML($reply['body']);
113 unset($request, $reply);
114
115 // save packages
116 if (!empty($allNewPackages)) {
117 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID);
118 }
119 unset($allNewPackages);
120
121 // update server status
122 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
123 $updateServerEditor->update(array(
124 'lastUpdateTime' => TIME_NOW,
125 'status' => 'online',
126 'errorMessage' => ''
127 ));
128 }
129
130 /**
131 * Parses a stream containing info from a packages_update.xml.
132 *
133 * @param string $content
134 * @return array $allNewPackages
135 */
136 protected function parsePackageUpdateXML($content) {
137 // load xml document
138 $xml = new XML();
139 $xml->loadXML('packageUpdateServer.xml', $content);
140 $xpath = $xml->xpath();
141
142 // loop through <package> tags inside the <section> tag.
143 $allNewPackages = array();
144 $packages = $xpath->query('/ns:section/ns:package');
145 foreach ($packages as $package) {
146 if (!Package::isValidPackageName($package->getAttribute('name'))) {
147 throw new SystemException("'".$package->getAttribute('name')."' is not a valid package name.");
148 }
149
150 $allNewPackages[$package->getAttribute('name')] = $this->parsePackageUpdateXMLBlock($xpath, $package);
151 }
152
153 return $allNewPackages;
154 }
155
156 /**
157 * Parses the xml stucture from a packages_update.xml.
158 *
159 * @param \DOMXPath $xpath
160 * @param \DOMNode $package
161 */
162 protected function parsePackageUpdateXMLBlock(\DOMXPath $xpath, \DOMNode $package) {
163 // define default values
164 $packageInfo = array(
165 'author' => '',
166 'authorURL' => '',
167 'isApplication' => 0,
168 'packageDescription' => '',
169 'versions' => array()
170 );
171
172 // parse package information
173 $elements = $xpath->query('./ns:packageinformation/*', $package);
174 foreach ($elements as $element) {
175 switch ($element->tagName) {
176 case 'packagename':
177 $packageInfo['packageName'] = $element->nodeValue;
178 break;
179
180 case 'packagedescription':
181 $packageInfo['packageDescription'] = $element->nodeValue;
182 break;
183
184 case 'isapplication':
185 $packageInfo['isApplication'] = intval($element->nodeValue);
186 break;
187 }
188 }
189
190 // parse author information
191 $elements = $xpath->query('./ns:authorinformation/*', $package);
192 foreach ($elements as $element) {
193 switch ($element->tagName) {
194 case 'author':
195 $packageInfo['author'] = $element->nodeValue;
196 break;
197
198 case 'authorurl':
199 $packageInfo['authorURL'] = $element->nodeValue;
200 break;
201 }
202 }
203
204 // parse versions
205 $elements = $xpath->query('./ns:versions/ns:version', $package);
206 foreach ($elements as $element) {
207 $versionNo = $element->getAttribute('name');
208 $packageInfo['versions'][$versionNo] = array(
209 'isAccessible' => ($element->getAttribute('accessible') == 'true' ? true : false),
210 'isCritical' => ($element->getAttribute('critical') == 'true' ? true : false)
211 );
212
213 $children = $xpath->query('child::*', $element);
214 foreach ($children as $child) {
215 switch ($child->tagName) {
216 case 'fromversions':
217 $fromversions = $xpath->query('child::*', $child);
218 foreach ($fromversions as $fromversion) {
219 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue;
220 }
221 break;
222
223 case 'timestamp':
224 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue;
225 break;
226
227 case 'file':
228 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue;
229 break;
230
231 case 'requiredpackages':
232 $requiredPackages = $xpath->query('child::*', $child);
233 foreach ($requiredPackages as $requiredPackage) {
234 $minVersion = $requiredPackage->getAttribute('minversion');
235 $required = $requiredPackage->nodeValue;
236
237 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = array();
238 if (!empty($minVersion)) {
239 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
240 }
241 }
242 break;
243
244 case 'optionalpackages':
245 $packageInfo['versions'][$versionNo]['optionalPackages'] = array();
246
247 $optionalPackages = $xpath->query('child::*', $child);
248 foreach ($optionalPackages as $optionalPackage) {
249 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue;
250 }
251 break;
252
253 case 'excludedpackages':
254 $excludedpackages = $xpath->query('child::*', $child);
255 foreach ($excludedpackages as $excludedPackage) {
256 $exclusion = $excludedPackage->nodeValue;
257 $version = $excludedPackage->getAttribute('version');
258
259 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = array();
260 if (!empty($version)) {
261 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion]['version'] = $version;
262 }
263 }
264 break;
265
266 case 'license':
267 $packageInfo['versions'][$versionNo]['license'] = array(
268 'license' => $child->nodeValue,
269 'licenseURL' => ($child->hasAttribute('url') ? $child->getAttribute('url') : '')
270 );
271 break;
272 }
273 }
274 }
275
276 return $packageInfo;
277 }
278
279 /**
280 * Updates information parsed from a packages_update.xml into the database.
281 *
282 * @param array $allNewPackages
283 * @param integer $packageUpdateServerID
284 */
285 protected function savePackageUpdates(array &$allNewPackages, $packageUpdateServerID) {
286 // find existing packages and delete them
287 // get existing packages
288 $existingPackages = array();
289 $packageUpdateList = new PackageUpdateList();
290 $packageUpdateList->getConditionBuilder()->add("package_update.packageUpdateServerID = ? AND package_update.package IN (?)", array($packageUpdateServerID, array_keys($allNewPackages)));
291 $packageUpdateList->readObjects();
292 $tmp = $packageUpdateList->getObjects();
293
294 foreach ($tmp as $packageUpdate) {
295 $existingPackages[$packageUpdate->package] = $packageUpdate;
296 }
297
298 // get existing versions
299 $existingPackageVersions = array();
300 if (!empty($existingPackages)) {
301 // get package update ids
302 $packageUpdateIDs = array();
303 foreach ($existingPackages as $packageUpdate) {
304 $packageUpdateIDs[] = $packageUpdate->packageUpdateID;
305 }
306
307 // get version list
308 $versionList = new PackageUpdateVersionList();
309 $versionList->getConditionBuilder()->add("package_update_version.packageUpdateID IN (?)", array($packageUpdateIDs));
310 $versionList->readObjects();
311 $tmp = $versionList->getObjects();
312
313 foreach ($tmp as $version) {
314 if (!isset($existingPackageVersions[$version->packageUpdateID])) $existingPackageVersions[$version->packageUpdateID] = array();
315 $existingPackageVersions[$version->packageUpdateID][$version->packageVersion] = $version;
316 }
317 }
318
319 // insert updates
320 $excludedPackagesParameters = $fromversionParameters = $insertParameters = $optionalInserts = $requirementInserts = array();
321 foreach ($allNewPackages as $identifier => $packageData) {
322 if (isset($existingPackages[$identifier])) {
323 $packageUpdateID = $existingPackages[$identifier]->packageUpdateID;
324
325 // update database entry
326 $packageUpdateEditor = new PackageUpdateEditor($existingPackages[$identifier]);
327 $packageUpdateEditor->update(array(
328 'packageName' => $packageData['packageName'],
329 'packageDescription' => $packageData['packageDescription'],
330 'author' => $packageData['author'],
331 'authorURL' => $packageData['authorURL'],
332 'isApplication' => $packageData['isApplication']
333 ));
334 }
335 else {
336 // create new database entry
337 $packageUpdate = PackageUpdateEditor::create(array(
338 'packageUpdateServerID' => $packageUpdateServerID,
339 'package' => $identifier,
340 'packageName' => $packageData['packageName'],
341 'packageDescription' => $packageData['packageDescription'],
342 'author' => $packageData['author'],
343 'authorURL' => $packageData['authorURL'],
344 'isApplication' => $packageData['isApplication']
345 ));
346
347 $packageUpdateID = $packageUpdate->packageUpdateID;
348 }
349
350 // register version(s) of this update package.
351 if (isset($packageData['versions'])) {
352 foreach ($packageData['versions'] as $packageVersion => $versionData) {
353 if (isset($versionData['file'])) $packageFile = $versionData['file'];
354 else $packageFile = '';
355
356 if (isset($existingPackageVersions[$packageUpdateID]) && isset($existingPackageVersions[$packageUpdateID][$packageVersion])) {
357 $packageUpdateVersionID = $existingPackageVersions[$packageUpdateID][$packageVersion]->packageUpdateVersionID;
358
359 // update database entry
360 $versionEditor = new PackageUpdateVersionEditor($existingPackageVersions[$packageUpdateID][$packageVersion]);
361 $versionEditor->update(array(
362 'filename' => $packageFile,
363 'isAccessible' => ($versionData['isAccessible'] ? 1 : 0),
364 'isCritical' => ($versionData['isCritical'] ? 1 : 0),
365 'license' => (isset($versionData['license']['license']) ? $versionData['license']['license'] : ''),
366 'licenseURL' => (isset($versionData['license']['license']) ? $versionData['license']['licenseURL'] : ''),
367 'packageDate' => $versionData['packageDate']
368 ));
369 }
370 else {
371 // create new database entry
372 $version = PackageUpdateVersionEditor::create(array(
373 'filename' => $packageFile,
374 'license' => (isset($versionData['license']['license']) ? $versionData['license']['license'] : ''),
375 'licenseURL' => (isset($versionData['license']['license']) ? $versionData['license']['licenseURL'] : ''),
376 'isAccessible' => ($versionData['isAccessible'] ? 1 : 0),
377 'isCritical' => ($versionData['isCritical'] ? 1 : 0),
378 'packageDate' => $versionData['packageDate'],
379 'packageUpdateID' => $packageUpdateID,
380 'packageVersion' => $packageVersion
381 ));
382
383 $packageUpdateVersionID = $version->packageUpdateVersionID;
384 }
385
386 // register requirement(s) of this update package version.
387 if (isset($versionData['requiredPackages'])) {
388 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
389 $requirementInserts[] = array(
390 'packageUpdateVersionID' => $packageUpdateVersionID,
391 'package' => $requiredIdentifier,
392 'minversion' => (isset($required['minversion']) ? $required['minversion'] : '')
393 );
394 }
395 }
396
397 // register optional packages of this update package version
398 if (isset($versionData['optionalPackages'])) {
399 foreach ($versionData['optionalPackages'] as $optionalPackage) {
400 $optionalInserts[] = array(
401 'packageUpdateVersionID' => $packageUpdateVersionID,
402 'package' => $optionalPackage
403 );
404 }
405 }
406
407 // register excluded packages of this update package version.
408 if (isset($versionData['excludedPackages'])) {
409 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
410 $excludedPackagesParameters[] = array(
411 'packageUpdateVersionID' => $packageUpdateVersionID,
412 'excludedPackage' => $excludedIdentifier,
413 'excludedPackageVersion' => (isset($exclusion['version']) ? $exclusion['version'] : '')
414 );
415 }
416 }
417
418 // register fromversions of this update package version.
419 if (isset($versionData['fromversions'])) {
420 foreach ($versionData['fromversions'] as $fromversion) {
421 $fromversionInserts[] = array(
422 'packageUpdateVersionID' => $packageUpdateVersionID,
423 'fromversion' => $fromversion
424 );
425 }
426 }
427 }
428 }
429 }
430
431 // save requirements, excluded packages and fromversions
432 // use multiple inserts to save some queries
433 if (!empty($requirementInserts)) {
434 // clear records
435 $sql = "DELETE pur FROM wcf".WCF_N."_package_update_requirement pur
436 LEFT JOIN wcf".WCF_N."_package_update_version puv
437 ON (puv.packageUpdateVersionID = pur.packageUpdateVersionID)
438 LEFT JOIN wcf".WCF_N."_package_update pu
439 ON (pu.packageUpdateID = puv.packageUpdateID)
440 WHERE pu.packageUpdateServerID = ?";
441 $statement = WCF::getDB()->prepareStatement($sql);
442 $statement->execute(array($packageUpdateServerID));
443
444 // insert requirements
445 $sql = "INSERT INTO wcf".WCF_N."_package_update_requirement
446 (packageUpdateVersionID, package, minversion)
447 VALUES (?, ?, ?)";
448 $statement = WCF::getDB()->prepareStatement($sql);
449 WCF::getDB()->beginTransaction();
450 foreach ($requirementInserts as $requirement) {
451 $statement->execute(array(
452 $requirement['packageUpdateVersionID'],
453 $requirement['package'],
454 $requirement['minversion']
455 ));
456 }
457 WCF::getDB()->commitTransaction();
458 }
459
460 if (!empty($optionalInserts)) {
461 // clear records
462 $sql = "DELETE puo FROM wcf".WCF_N."_package_update_optional puo
463 LEFT JOIN wcf".WCF_N."_package_update_version puv
464 ON (puv.packageUpdateVersionID = puo.packageUpdateVersionID)
465 LEFT JOIN wcf".WCF_N."_package_update pu
466 ON (pu.packageUpdateID = puv.packageUpdateID)
467 WHERE pu.packageUpdateServerID = ?";
468 $statement = WCF::getDB()->prepareStatement($sql);
469 $statement->execute(array($packageUpdateServerID));
470
471 // insert requirements
472 $sql = "INSERT INTO wcf".WCF_N."_package_update_optional
473 (packageUpdateVersionID, package)
474 VALUES (?, ?)";
475 $statement = WCF::getDB()->prepareStatement($sql);
476 WCF::getDB()->beginTransaction();
477 foreach ($optionalInserts as $requirement) {
478 $statement->execute(array(
479 $requirement['packageUpdateVersionID'],
480 $requirement['package']
481 ));
482 }
483 WCF::getDB()->commitTransaction();
484 }
485
486 if (!empty($excludedPackagesParameters)) {
487 // clear records
488 $sql = "DELETE pue FROM wcf".WCF_N."_package_update_exclusion pue
489 LEFT JOIN wcf".WCF_N."_package_update_version puv
490 ON (puv.packageUpdateVersionID = pue.packageUpdateVersionID)
491 LEFT JOIN wcf".WCF_N."_package_update pu
492 ON (pu.packageUpdateID = puv.packageUpdateID)
493 WHERE pu.packageUpdateServerID = ?";
494 $statement = WCF::getDB()->prepareStatement($sql);
495 $statement->execute(array($packageUpdateServerID));
496
497 // insert excludes
498 $sql = "INSERT INTO wcf".WCF_N."_package_update_exclusion
499 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
500 VALUES (?, ?, ?)";
501 $statement = WCF::getDB()->prepareStatement($sql);
502 WCF::getDB()->beginTransaction();
503 foreach ($excludedPackagesParameters as $excludedPackage) {
504 $statement->execute(array(
505 $excludedPackage['packageUpdateVersionID'],
506 $excludedPackage['excludedPackage'],
507 $excludedPackage['excludedPackageVersion']
508 ));
509 }
510 WCF::getDB()->commitTransaction();
511 }
512
513 if (!empty($fromversionInserts)) {
514 // clear records
515 $sql = "DELETE puf FROM wcf".WCF_N."_package_update_fromversion puf
516 LEFT JOIN wcf".WCF_N."_package_update_version puv
517 ON (puv.packageUpdateVersionID = puf.packageUpdateVersionID)
518 LEFT JOIN wcf".WCF_N."_package_update pu
519 ON (pu.packageUpdateID = puv.packageUpdateID)
520 WHERE pu.packageUpdateServerID = ?";
521 $statement = WCF::getDB()->prepareStatement($sql);
522 $statement->execute(array($packageUpdateServerID));
523
524 // insert excludes
525 $sql = "INSERT INTO wcf".WCF_N."_package_update_fromversion
526 (packageUpdateVersionID, fromversion)
527 VALUES (?, ?)";
528 $statement = WCF::getDB()->prepareStatement($sql);
529 WCF::getDB()->beginTransaction();
530 foreach ($fromversionInserts as $fromversion) {
531 $statement->execute(array(
532 $fromversion['packageUpdateVersionID'],
533 $fromversion['fromversion']
534 ));
535 }
536 WCF::getDB()->commitTransaction();
537 }
538 }
539
540 /**
541 * Returns a list of available updates for installed packages.
542 *
543 * @param boolean $removeRequirements
544 * @param boolean $removeOlderMinorReleases
545 * @return array
546 */
547 public function getAvailableUpdates($removeRequirements = true, $removeOlderMinorReleases = false) {
548 $updates = array();
549
550 // get update server data
551 $updateServers = PackageUpdateServer::getActiveUpdateServers();
552 $packageUpdateServerIDs = array_keys($updateServers);
553 if (empty($packageUpdateServerIDs)) return $updates;
554
555 // get existing packages and their versions
556 $existingPackages = array();
557 $sql = "SELECT packageID, package, packageDescription, packageName,
558 packageVersion, packageDate, author, authorURL, isApplication
559 FROM wcf".WCF_N."_package";
560 $statement = WCF::getDB()->prepareStatement($sql);
561 $statement->execute();
562 while ($row = $statement->fetchArray()) {
563 $existingPackages[$row['package']][] = $row;
564 }
565 if (empty($existingPackages)) return $updates;
566
567 // get all update versions
568 $conditions = new PreparedStatementConditionBuilder();
569 $conditions->add("pu.packageUpdateServerID IN (?)", array($packageUpdateServerIDs));
570 $conditions->add("package IN (SELECT DISTINCT package FROM wcf".WCF_N."_package)");
571
572 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
573 puv.packageUpdateVersionID, puv.isCritical, puv.packageDate, puv.filename, puv.packageVersion
574 FROM wcf".WCF_N."_package_update pu
575 LEFT JOIN wcf".WCF_N."_package_update_version puv
576 ON (puv.packageUpdateID = pu.packageUpdateID AND puv.isAccessible = 1)
577 ".$conditions;
578 $statement = WCF::getDB()->prepareStatement($sql);
579 $statement->execute($conditions->getParameters());
580 while ($row = $statement->fetchArray()) {
581 // test version
582 foreach ($existingPackages[$row['package']] as $existingVersion) {
583 if (Package::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
584 // package data
585 if (!isset($updates[$existingVersion['packageID']])) {
586 $existingVersion['versions'] = array();
587 $updates[$existingVersion['packageID']] = $existingVersion;
588 }
589
590 // version data
591 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
592 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = array(
593 'isCritical' => $row['isCritical'],
594 'packageDate' => $row['packageDate'],
595 'packageVersion' => $row['packageVersion'],
596 'servers' => array()
597 );
598 }
599
600 // server data
601 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = array(
602 'packageUpdateID' => $row['packageUpdateID'],
603 'packageUpdateServerID' => $row['packageUpdateServerID'],
604 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
605 'filename' => $row['filename']
606 );
607 }
608 }
609 }
610
611 // sort package versions
612 // and remove old versions
613 foreach ($updates as $packageID => $data) {
614 uksort($updates[$packageID]['versions'], array('wcf\data\package\Package', 'compareVersion'));
615 $updates[$packageID]['version'] = end($updates[$packageID]['versions']);
616 }
617
618 // remove requirements of application packages
619 if ($removeRequirements) {
620 foreach ($existingPackages as $identifier => $instances) {
621 foreach ($instances as $instance) {
622 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
623 $updates = $this->removeUpdateRequirements($updates, $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
624 }
625 }
626 }
627 }
628
629 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
630 if ($removeOlderMinorReleases) {
631 foreach ($updates as &$updateData) {
632 $highestVersions = array();
633 foreach ($updateData['versions'] as $versionNumber => $dummy) {
634 if (preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
635 $major = $matches[1];
636 if (isset($highestVersions[$major])) {
637 if (version_compare($highestVersions[$major], $versionNumber, '<')) {
638 // version is newer, discard current version
639 unset($updateData['versions'][$highestVersions[$major]]);
640 $highestVersions[$major] = $versionNumber;
641 }
642 else {
643 // version is lower, discard
644 unset($updateData['versions'][$versionNumber]);
645 }
646 }
647 else {
648 $highestVersions[$major] = $versionNumber;
649 }
650 }
651 }
652 }
653 unset($updateData);
654 }
655
656 return $updates;
657 }
658
659 /**
660 * Removes unnecessary updates of requirements from the list of available updates.
661 *
662 * @param array $updates
663 * @param integer $packageUpdateVersionID
664 * @return array $updates
665 */
666 protected function removeUpdateRequirements(array $updates, $packageUpdateVersionID) {
667 $sql = "SELECT pur.package, pur.minversion, p.packageID
668 FROM wcf".WCF_N."_package_update_requirement pur
669 LEFT JOIN wcf".WCF_N."_package p
670 ON (p.package = pur.package)
671 WHERE pur.packageUpdateVersionID = ?";
672 $statement = WCF::getDB()->prepareStatement($sql);
673 $statement->execute(array($packageUpdateVersionID));
674 while ($row = $statement->fetchArray()) {
675 if (isset($updates[$row['packageID']])) {
676 $updates = $this->removeUpdateRequirements($updates, $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
677 if (Package::compareVersion($row['minversion'], $updates[$row['packageID']]['version']['packageVersion'], '>=')) {
678 unset($updates[$row['packageID']]);
679 }
680 }
681 }
682
683 return $updates;
684 }
685
686 /**
687 * Creates a new package installation scheduler.
688 *
689 * @param array $selectedPackages
690 * @return \wcf\system\package\PackageInstallationScheduler
691 */
692 public function prepareInstallation(array $selectedPackages) {
693 return new PackageInstallationScheduler($selectedPackages);
694 }
695
696 /**
697 * Gets package update versions of a package.
698 *
699 * @param string $package package identifier
700 * @param string $version package version
701 * @return array package update versions
702 */
703 public function getPackageUpdateVersions($package, $version = '') {
704 // get newest package version
705 if (empty($version)) {
706 $version = $this->getNewestPackageVersion($package);
707 }
708
709 // get versions
710 $versions = array();
711 $sql = "SELECT puv.*, pu.*, pus.loginUsername, pus.loginPassword
712 FROM wcf".WCF_N."_package_update_version puv
713 LEFT JOIN wcf".WCF_N."_package_update pu
714 ON (pu.packageUpdateID = puv.packageUpdateID)
715 LEFT JOIN wcf".WCF_N."_package_update_server pus
716 ON (pus.packageUpdateServerID = pu.packageUpdateServerID)
717 WHERE pu.package = ?
718 AND puv.packageVersion = ?";
719 $statement = WCF::getDB()->prepareStatement($sql);
720 $statement->execute(array(
721 $package,
722 $version
723 ));
724 while ($row = $statement->fetchArray()) {
725 $versions[] = $row;
726 }
727
728 if (empty($versions)) {
729 throw new SystemException("Can not find package '".$package."' in version '".$version."'");
730 }
731
732 return $versions;
733 }
734
735 /**
736 * Returns the newest available version of a package.
737 *
738 * @param string $package package identifier
739 * @return string newest package version
740 */
741 public function getNewestPackageVersion($package) {
742 // get all versions
743 $versions = array();
744 $sql = "SELECT packageVersion
745 FROM wcf".WCF_N."_package_update_version
746 WHERE packageUpdateID IN (
747 SELECT packageUpdateID
748 FROM wcf".WCF_N."_package_update
749 WHERE package = ?
750 )";
751 $statement = WCF::getDB()->prepareStatement($sql);
752 $statement->execute(array($package));
753 while ($row = $statement->fetchArray()) {
754 $versions[$row['packageVersion']] = $row['packageVersion'];
755 }
756
757 // sort by version number
758 usort($versions, array('wcf\data\package\Package', 'compareVersion'));
759
760 // take newest (last)
761 return array_pop($versions);
762 }
763
764 /**
765 * Stores the filename of a download in session.
766 *
767 * @param string $package package identifier
768 * @param string $version package version
769 * @param string $filename
770 */
771 public function cacheDownload($package, $version, $filename) {
772 $cachedDownloads = WCF::getSession()->getVar('cachedPackageUpdateDownloads');
773 if (!is_array($cachedDownloads)) {
774 $cachedDownloads = array();
775 }
776
777 // store in session
778 $cachedDownloads[$package.'@'.$version] = $filename;
779 WCF::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);
780 }
781 }