2 declare(strict_types
=1);
3 namespace wcf\system\package
;
4 use wcf\data\package\update\server\PackageUpdateServer
;
5 use wcf\data\package\update\server\PackageUpdateServerEditor
;
6 use wcf\data\package\update\version\PackageUpdateVersionEditor
;
7 use wcf\data\package\update\PackageUpdateEditor
;
8 use wcf\data\package\Package
;
9 use wcf\system\cache\builder\PackageUpdateCacheBuilder
;
10 use wcf\system\database\util\PreparedStatementConditionBuilder
;
11 use wcf\system\exception\HTTPUnauthorizedException
;
12 use wcf\system\exception\SystemException
;
13 use wcf\system\io\RemoteFile
;
14 use wcf\system\package\validation\PackageValidationException
;
15 use wcf\system\SingletonFactory
;
17 use wcf\util\HTTPRequest
;
22 * Provides functions to manage package updates.
24 * @author Alexander Ebert
25 * @copyright 2001-2018 WoltLab GmbH
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package WoltLabSuite\Core\System\Package
29 class PackageUpdateDispatcher
extends SingletonFactory
{
30 protected $hasAuthCode = false;
31 protected $purchasedVersions = [
37 * Refreshes the package database.
39 * @param integer[] $packageUpdateServerIDs
40 * @param boolean $ignoreCache
42 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], $ignoreCache = false) {
43 // get update server data
44 $tmp = PackageUpdateServer
::getActiveUpdateServers($packageUpdateServerIDs);
48 $foundWoltLabServer = false;
49 $requirePurchasedVersions = false;
50 foreach ($tmp as $updateServer) {
51 if ($ignoreCache ||
$updateServer->lastUpdateTime
< TIME_NOW
- 600) {
52 if (preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)) {
53 $requirePurchasedVersions = true;
55 // move a woltlab.com update server to the front of the queue to probe for SSL support
56 if (!$foundWoltLabServer) {
57 array_unshift($updateServers, $updateServer);
58 $foundWoltLabServer = true;
64 $updateServers[] = $updateServer;
68 if ($requirePurchasedVersions && PACKAGE_SERVER_AUTH_CODE
) {
69 $this->getPurchasedVersions();
73 $refreshedPackageLists = false;
74 foreach ($updateServers as $updateServer) {
78 $this->getPackageUpdateXML($updateServer);
79 $refreshedPackageLists = true;
81 catch (SystemException
$e) {
82 $errorMessage = $e->getMessage();
84 catch (PackageUpdateUnauthorizedException
$e) {
85 $reply = $e->getRequest()->getReply();
86 list($errorMessage) = reset($reply['httpHeaders']);
91 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
92 $updateServerEditor->update([
93 'status' => 'offline',
94 'errorMessage' => $errorMessage
99 if ($refreshedPackageLists) {
100 PackageUpdateCacheBuilder
::getInstance()->reset();
104 protected function getPurchasedVersions() {
105 if (!RemoteFile
::supportsSSL()) {
109 $request = new HTTPRequest(
110 'https://api.woltlab.com/1.0/customer/license/list.json',
112 ['authCode' => PACKAGE_SERVER_AUTH_CODE
]
117 $reply = JSON
::decode($request->getReply()['body']);
118 if ($reply['status'] == 200) {
119 $this->hasAuthCode
= true;
120 $this->purchasedVersions
= [
121 'woltlab' => (isset($reply['woltlab']) ?
$reply['woltlab'] : []),
122 'pluginstore' => (isset($reply['pluginstore']) ?
$reply['pluginstore'] : [])
126 catch (SystemException
$e) {
132 * Fetches the package_update.xml from an update server.
134 * @param PackageUpdateServer $updateServer
135 * @param boolean $forceHTTP
136 * @throws PackageUpdateUnauthorizedException
137 * @throws SystemException
139 protected function getPackageUpdateXML(PackageUpdateServer
$updateServer, $forceHTTP = false) {
141 $authData = $updateServer->getAuthData();
142 if ($authData) $settings['auth'] = $authData;
144 $secureConnection = $updateServer->attemptSecureConnection();
145 if ($secureConnection && !$forceHTTP) $settings['timeout'] = 5;
147 $request = new HTTPRequest($updateServer->getListURL($forceHTTP), $settings);
149 $apiVersion = $updateServer->apiVersion
;
150 if (in_array($apiVersion, ['2.1', '3.1'])) {
151 // skip etag check for WoltLab servers when an auth code is provided
152 if (!preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
) ||
!PACKAGE_SERVER_AUTH_CODE
) {
153 $metaData = $updateServer->getMetaData();
154 if (isset($metaData['list']['etag'])) $request->addHeader('if-none-match', $metaData['list']['etag']);
155 if (isset($metaData['list']['lastModified'])) $request->addHeader('if-modified-since', $metaData['list']['lastModified']);
161 $reply = $request->getReply();
163 catch (HTTPUnauthorizedException
$e) {
164 throw new PackageUpdateUnauthorizedException($request, $updateServer);
166 catch (SystemException
$e) {
167 $reply = $request->getReply();
169 $statusCode = is_array($reply['statusCode']) ?
reset($reply['statusCode']) : $reply['statusCode'];
170 // status code 0 is a connection timeout
171 if (!$statusCode && $secureConnection) {
172 if (preg_match('~https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)) {
173 // woltlab.com servers are most likely to be available, thus we assume that SSL connections are dropped
174 RemoteFile
::disableSSL();
178 $this->getPackageUpdateXML($updateServer, true);
182 throw new SystemException(WCF
::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' ('.$statusCode.')');
186 'lastUpdateTime' => TIME_NOW
,
187 'status' => 'online',
191 // check if server indicates support for a newer API
192 if ($updateServer->apiVersion
!== '3.1' && !empty($reply['httpHeaders']['wcf-update-server-api'])) {
193 $apiVersions = explode(' ', reset($reply['httpHeaders']['wcf-update-server-api']));
194 if (in_array('3.1', $apiVersions)) {
195 $apiVersion = $data['apiVersion'] = '3.1';
197 else if (in_array('2.1', $apiVersions)) {
198 $apiVersion = $data['apiVersion'] = '2.1';
202 // parse given package update xml
203 $allNewPackages = false;
204 if ($apiVersion === '2.0' ||
$reply['statusCode'] != 304) {
205 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $reply['body'], $apiVersion);
209 if (in_array($apiVersion, ['2.1', '3.1'])) {
210 if (empty($reply['httpHeaders']['etag']) && empty($reply['httpHeaders']['last-modified'])) {
211 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
213 else if (empty($reply['httpHeaders']['wcf-update-server-ssl'])) {
214 throw new SystemException("Missing required HTTP header 'wcf-update-server-ssl'.");
217 $metaData['list'] = [];
218 if (!empty($reply['httpHeaders']['etag'])) $metaData['list']['etag'] = reset($reply['httpHeaders']['etag']);
219 if (!empty($reply['httpHeaders']['last-modified'])) $metaData['list']['lastModified'] = reset($reply['httpHeaders']['last-modified']);
221 $metaData['ssl'] = (reset($reply['httpHeaders']['wcf-update-server-ssl']) == 'true') ?
true : false;
223 $data['metaData'] = serialize($metaData);
225 unset($request, $reply);
227 if ($allNewPackages !== false) {
228 // purge package list
229 $sql = "DELETE FROM wcf".WCF_N
."_package_update
230 WHERE packageUpdateServerID = ?";
231 $statement = WCF
::getDB()->prepareStatement($sql);
232 $statement->execute([$updateServer->packageUpdateServerID
]);
235 if (!empty($allNewPackages)) {
236 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID
);
238 unset($allNewPackages);
241 // update server status
242 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
243 $updateServerEditor->update($data);
247 * Parses a stream containing info from a packages_update.xml.
249 * @param PackageUpdateServer $updateServer
250 * @param string $content
251 * @param string $apiVersion
253 * @throws SystemException
255 protected function parsePackageUpdateXML(PackageUpdateServer
$updateServer, $content, $apiVersion) {
256 $isTrustedServer = $updateServer->isTrustedServer();
260 $xml->loadXML('packageUpdateServer.xml', $content);
261 $xpath = $xml->xpath();
263 $allNewPackages = [];
264 $packages = $xpath->query('/ns:section/ns:package');
265 /** @var \DOMElement $package */
266 foreach ($packages as $package) {
267 if (!Package
::isValidPackageName($package->getAttribute('name'))) {
268 throw new SystemException("'".$package->getAttribute('name')."' is not a valid package name.");
271 $name = $package->getAttribute('name');
272 if (strpos($name, 'com.woltlab.') === 0 && !$isTrustedServer) {
273 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
274 throw new SystemException("The server '".$updateServer->serverURL
."' attempted to provide an official WoltLab package, but is not authorized.");
277 // silently ignore this package to avoid unexpected errors for regular users
281 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
284 return $allNewPackages;
288 * Parses the xml structure from a packages_update.xml.
290 * @param PackageUpdateServer $updateServer
291 * @param \DOMXPath $xpath
292 * @param \DOMElement $package
293 * @param string $apiVersion
295 * @throws PackageValidationException
297 protected function parsePackageUpdateXMLBlock(PackageUpdateServer
$updateServer, \DOMXPath
$xpath, \DOMElement
$package, $apiVersion) {
298 // define default values
302 'isApplication' => 0,
303 'packageDescription' => '',
305 'pluginStoreFileID' => 0
308 // parse package information
309 $elements = $xpath->query('./ns:packageinformation/*', $package);
310 foreach ($elements as $element) {
311 switch ($element->tagName
) {
313 $packageInfo['packageName'] = $element->nodeValue
;
316 case 'packagedescription':
317 $packageInfo['packageDescription'] = $element->nodeValue
;
320 case 'isapplication':
321 $packageInfo['isApplication'] = intval($element->nodeValue
);
324 case 'pluginStoreFileID':
325 if ($updateServer->isWoltLabStoreServer()) {
326 $packageInfo['pluginStoreFileID'] = intval($element->nodeValue
);
332 // parse author information
333 $elements = $xpath->query('./ns:authorinformation/*', $package);
334 foreach ($elements as $element) {
335 switch ($element->tagName
) {
337 $packageInfo['author'] = $element->nodeValue
;
341 $packageInfo['authorURL'] = $element->nodeValue
;
347 if ($this->hasAuthCode
) {
348 if ($updateServer->isWoltLabUpdateServer()) $key = 'woltlab';
349 else if ($updateServer->isWoltLabStoreServer()) $key = 'pluginstore';
353 $elements = $xpath->query('./ns:versions/ns:version', $package);
354 /** @var \DOMElement $element */
355 foreach ($elements as $element) {
356 $versionNo = $element->getAttribute('name');
358 $isAccessible = ($element->getAttribute('accessible') == 'true') ?
1 : 0;
359 if ($key && $element->getAttribute('requireAuth') == 'true') {
360 $packageName = $package->getAttribute('name');
361 if (isset($this->purchasedVersions
[$key][$packageName])) {
362 if ($this->purchasedVersions
[$key][$packageName] == '*') {
366 $isAccessible = (Package
::compareVersion($versionNo, $this->purchasedVersions
[$key][$packageName] . '99', '<=') ?
1 : 0);
374 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
376 $children = $xpath->query('child::*', $element);
377 /** @var \DOMElement $child */
378 foreach ($children as $child) {
379 switch ($child->tagName
) {
381 $fromversions = $xpath->query('child::*', $child);
382 foreach ($fromversions as $fromversion) {
383 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue
;
388 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue
;
392 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue
;
395 case 'requiredpackages':
396 $requiredPackages = $xpath->query('child::*', $child);
398 /** @var \DOMElement $requiredPackage */
399 foreach ($requiredPackages as $requiredPackage) {
400 $minVersion = $requiredPackage->getAttribute('minversion');
401 $required = $requiredPackage->nodeValue
;
403 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
404 if (!empty($minVersion)) {
405 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
410 case 'optionalpackages':
411 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
413 $optionalPackages = $xpath->query('child::*', $child);
414 foreach ($optionalPackages as $optionalPackage) {
415 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue
;
419 case 'excludedpackages':
420 $excludedpackages = $xpath->query('child::*', $child);
421 /** @var \DOMElement $excludedPackage */
422 foreach ($excludedpackages as $excludedPackage) {
423 $exclusion = $excludedPackage->nodeValue
;
424 $version = $excludedPackage->getAttribute('version');
426 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [];
427 if (!empty($version)) {
428 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion]['version'] = $version;
434 $packageInfo['versions'][$versionNo]['license'] = [
435 'license' => $child->nodeValue
,
436 'licenseURL' => $child->hasAttribute('url') ?
$child->getAttribute('url') : ''
440 case 'compatibility':
441 if ($apiVersion !== '3.1') continue;
443 $packageInfo['versions'][$versionNo]['compatibility'] = [];
445 /** @var \DOMElement $compatibleVersion */
446 foreach ($xpath->query('child::*', $child) as $compatibleVersion) {
447 if ($compatibleVersion->nodeName
=== 'api' && $compatibleVersion->hasAttribute('version')) {
448 $versionNumber = $compatibleVersion->getAttribute('version');
449 if (!preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $versionNumber)) {
450 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
451 throw new PackageValidationException(PackageValidationException
::INVALID_API_VERSION
, ['version' => $versionNumber]);
458 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
470 * Updates information parsed from a packages_update.xml into the database.
472 * @param array $allNewPackages
473 * @param integer $packageUpdateServerID
475 protected function savePackageUpdates(array &$allNewPackages, $packageUpdateServerID) {
477 $excludedPackagesParameters = $fromversionParameters = $insertParameters = $optionalInserts = $requirementInserts = $compatibilityInserts = [];
478 WCF
::getDB()->beginTransaction();
479 foreach ($allNewPackages as $identifier => $packageData) {
480 // create new database entry
481 $packageUpdate = PackageUpdateEditor
::create([
482 'packageUpdateServerID' => $packageUpdateServerID,
483 'package' => $identifier,
484 'packageName' => $packageData['packageName'],
485 'packageDescription' => $packageData['packageDescription'],
486 'author' => $packageData['author'],
487 'authorURL' => $packageData['authorURL'],
488 'isApplication' => $packageData['isApplication'],
489 'pluginStoreFileID' => $packageData['pluginStoreFileID']
492 $packageUpdateID = $packageUpdate->packageUpdateID
;
494 // register version(s) of this update package.
495 if (isset($packageData['versions'])) {
496 foreach ($packageData['versions'] as $packageVersion => $versionData) {
497 if (isset($versionData['file'])) $packageFile = $versionData['file'];
498 else $packageFile = '';
500 // create new database entry
501 $version = PackageUpdateVersionEditor
::create([
502 'filename' => $packageFile,
503 'license' => isset($versionData['license']['license']) ?
$versionData['license']['license'] : '',
504 'licenseURL' => isset($versionData['license']['license']) ?
$versionData['license']['licenseURL'] : '',
505 'isAccessible' => $versionData['isAccessible'] ?
1 : 0,
506 'packageDate' => $versionData['packageDate'],
507 'packageUpdateID' => $packageUpdateID,
508 'packageVersion' => $packageVersion
511 $packageUpdateVersionID = $version->packageUpdateVersionID
;
513 // register requirement(s) of this update package version.
514 if (isset($versionData['requiredPackages'])) {
515 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
516 $requirementInserts[] = [
517 'packageUpdateVersionID' => $packageUpdateVersionID,
518 'package' => $requiredIdentifier,
519 'minversion' => isset($required['minversion']) ?
$required['minversion'] : ''
524 // register optional packages of this update package version
525 if (isset($versionData['optionalPackages'])) {
526 foreach ($versionData['optionalPackages'] as $optionalPackage) {
527 $optionalInserts[] = [
528 'packageUpdateVersionID' => $packageUpdateVersionID,
529 'package' => $optionalPackage
534 // register excluded packages of this update package version.
535 if (isset($versionData['excludedPackages'])) {
536 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
537 $excludedPackagesParameters[] = [
538 'packageUpdateVersionID' => $packageUpdateVersionID,
539 'excludedPackage' => $excludedIdentifier,
540 'excludedPackageVersion' => isset($exclusion['version']) ?
$exclusion['version'] : ''
545 // register fromversions of this update package version.
546 if (isset($versionData['fromversions'])) {
547 foreach ($versionData['fromversions'] as $fromversion) {
548 $fromversionInserts[] = [
549 'packageUpdateVersionID' => $packageUpdateVersionID,
550 'fromversion' => $fromversion
555 // register compatibility versions of this update package version.
556 if (isset($versionData['compatibility'])) {
557 foreach ($versionData['compatibility'] as $version) {
558 $compatibilityInserts[] = [
559 'packageUpdateVersionID' => $packageUpdateVersionID,
560 'version' => $version
567 WCF
::getDB()->commitTransaction();
569 // save requirements, excluded packages and fromversions
570 // insert requirements
571 if (!empty($requirementInserts)) {
572 $sql = "INSERT INTO wcf".WCF_N
."_package_update_requirement
573 (packageUpdateVersionID, package, minversion)
575 $statement = WCF
::getDB()->prepareStatement($sql);
576 WCF
::getDB()->beginTransaction();
577 foreach ($requirementInserts as $requirement) {
578 $statement->execute([
579 $requirement['packageUpdateVersionID'],
580 $requirement['package'],
581 $requirement['minversion']
584 WCF
::getDB()->commitTransaction();
588 if (!empty($optionalInserts)) {
589 $sql = "INSERT INTO wcf".WCF_N
."_package_update_optional
590 (packageUpdateVersionID, package)
592 $statement = WCF
::getDB()->prepareStatement($sql);
593 WCF
::getDB()->beginTransaction();
594 foreach ($optionalInserts as $requirement) {
595 $statement->execute([
596 $requirement['packageUpdateVersionID'],
597 $requirement['package']
600 WCF
::getDB()->commitTransaction();
604 if (!empty($excludedPackagesParameters)) {
605 $sql = "INSERT INTO wcf".WCF_N
."_package_update_exclusion
606 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
608 $statement = WCF
::getDB()->prepareStatement($sql);
609 WCF
::getDB()->beginTransaction();
610 foreach ($excludedPackagesParameters as $excludedPackage) {
611 $statement->execute([
612 $excludedPackage['packageUpdateVersionID'],
613 $excludedPackage['excludedPackage'],
614 $excludedPackage['excludedPackageVersion']
617 WCF
::getDB()->commitTransaction();
620 // insert fromversions
621 if (!empty($fromversionInserts)) {
622 $sql = "INSERT INTO wcf".WCF_N
."_package_update_fromversion
623 (packageUpdateVersionID, fromversion)
625 $statement = WCF
::getDB()->prepareStatement($sql);
626 WCF
::getDB()->beginTransaction();
627 foreach ($fromversionInserts as $fromversion) {
628 $statement->execute([
629 $fromversion['packageUpdateVersionID'],
630 $fromversion['fromversion']
633 WCF
::getDB()->commitTransaction();
636 // insert compatibility versions
637 if (!empty($compatibilityInserts)) {
638 $sql = "INSERT INTO wcf".WCF_N
."_package_update_compatibility
639 (packageUpdateVersionID, version)
641 $statement = WCF
::getDB()->prepareStatement($sql);
642 WCF
::getDB()->beginTransaction();
643 foreach ($compatibilityInserts as $versionData) {
644 $statement->execute([
645 $versionData['packageUpdateVersionID'],
646 $versionData['version']
649 WCF
::getDB()->commitTransaction();
654 * Returns a list of available updates for installed packages.
656 * @param boolean $removeRequirements
657 * @param boolean $removeOlderMinorReleases
659 * @throws SystemException
661 public function getAvailableUpdates($removeRequirements = true, $removeOlderMinorReleases = false) {
664 // get update server data
665 $updateServers = PackageUpdateServer
::getActiveUpdateServers();
666 $packageUpdateServerIDs = array_keys($updateServers);
667 if (empty($packageUpdateServerIDs)) return $updates;
669 // get existing packages and their versions
670 $existingPackages = [];
671 $sql = "SELECT packageID, package, packageDescription, packageName,
672 packageVersion, packageDate, author, authorURL, isApplication
673 FROM wcf".WCF_N
."_package";
674 $statement = WCF
::getDB()->prepareStatement($sql);
675 $statement->execute();
676 while ($row = $statement->fetchArray()) {
677 $existingPackages[$row['package']][] = $row;
679 if (empty($existingPackages)) return $updates;
681 // get all update versions
682 $conditions = new PreparedStatementConditionBuilder();
683 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
684 $conditions->add("package IN (SELECT DISTINCT package FROM wcf".WCF_N
."_package)");
686 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
687 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
688 FROM wcf".WCF_N
."_package_update pu
689 LEFT JOIN wcf".WCF_N
."_package_update_version puv
690 ON (puv.packageUpdateID = pu.packageUpdateID AND puv.isAccessible = 1)
692 $statement = WCF
::getDB()->prepareStatement($sql);
693 $statement->execute($conditions->getParameters());
694 while ($row = $statement->fetchArray()) {
695 if (!isset($existingPackages[$row['package']])) {
696 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
697 throw new SystemException("Invalid package update data, identifier '" . $row['package'] . "' does not match any installed package (case-mismatch).");
700 // case-mismatch, skip the update
705 foreach ($existingPackages[$row['package']] as $existingVersion) {
706 if (Package
::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
708 if (!isset($updates[$existingVersion['packageID']])) {
709 $existingVersion['versions'] = [];
710 $updates[$existingVersion['packageID']] = $existingVersion;
714 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
715 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
716 'packageDate' => $row['packageDate'],
717 'packageVersion' => $row['packageVersion'],
723 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
724 'packageUpdateID' => $row['packageUpdateID'],
725 'packageUpdateServerID' => $row['packageUpdateServerID'],
726 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
727 'filename' => $row['filename']
733 // sort package versions
734 // and remove old versions
735 foreach ($updates as $packageID => $data) {
736 uksort($updates[$packageID]['versions'], ['wcf\data\package\Package', 'compareVersion']);
737 $updates[$packageID]['version'] = end($updates[$packageID]['versions']);
740 // remove requirements of application packages
741 if ($removeRequirements) {
742 foreach ($existingPackages as $identifier => $instances) {
743 foreach ($instances as $instance) {
744 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
745 $updates = $this->removeUpdateRequirements($updates, $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
751 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
752 if ($removeOlderMinorReleases) {
753 foreach ($updates as &$updateData) {
754 $highestVersions = [];
755 foreach ($updateData['versions'] as $versionNumber => $dummy) {
756 if (preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
757 $major = $matches[1];
758 if (isset($highestVersions[$major])) {
759 if (Package
::compareVersion($highestVersions[$major], $versionNumber, '<')) {
760 // version is newer, discard current version
761 unset($updateData['versions'][$highestVersions[$major]]);
762 $highestVersions[$major] = $versionNumber;
765 // version is lower, discard
766 unset($updateData['versions'][$versionNumber]);
770 $highestVersions[$major] = $versionNumber;
782 * Removes unnecessary updates of requirements from the list of available updates.
784 * @param array $updates
785 * @param integer $packageUpdateVersionID
786 * @return array $updates
788 protected function removeUpdateRequirements(array $updates, $packageUpdateVersionID) {
789 $sql = "SELECT pur.package, pur.minversion, p.packageID
790 FROM wcf".WCF_N
."_package_update_requirement pur
791 LEFT JOIN wcf".WCF_N
."_package p
792 ON (p.package = pur.package)
793 WHERE pur.packageUpdateVersionID = ?";
794 $statement = WCF
::getDB()->prepareStatement($sql);
795 $statement->execute([$packageUpdateVersionID]);
796 while ($row = $statement->fetchArray()) {
797 if (isset($updates[$row['packageID']])) {
798 $updates = $this->removeUpdateRequirements($updates, $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
799 if (Package
::compareVersion($row['minversion'], $updates[$row['packageID']]['version']['packageVersion'], '>=')) {
800 unset($updates[$row['packageID']]);
809 * Creates a new package installation scheduler.
811 * @param array $selectedPackages
812 * @return PackageInstallationScheduler
814 public function prepareInstallation(array $selectedPackages) {
815 return new PackageInstallationScheduler($selectedPackages);
819 * Returns package update versions of the specified package.
821 * @param string $package package identifier
822 * @param string $version package version
823 * @return array package update versions
824 * @throws SystemException
826 public function getPackageUpdateVersions($package, $version = '') {
827 // get newest package version
828 if (empty($version)) {
829 $version = $this->getNewestPackageVersion($package);
833 $sql = "SELECT puv.*, pu.*, pus.loginUsername, pus.loginPassword
834 FROM wcf".WCF_N
."_package_update_version puv
835 LEFT JOIN wcf".WCF_N
."_package_update pu
836 ON (pu.packageUpdateID = puv.packageUpdateID)
837 LEFT JOIN wcf".WCF_N
."_package_update_server pus
838 ON (pus.packageUpdateServerID = pu.packageUpdateServerID)
840 AND puv.packageVersion = ?
841 AND puv.isAccessible = ?
842 AND pus.isDisabled = ?";
843 $statement = WCF
::getDB()->prepareStatement($sql);
844 $statement->execute([
850 $versions = $statement->fetchAll(\PDO
::FETCH_ASSOC
);
852 if (empty($versions)) {
853 throw new SystemException("Cannot find package '".$package."' in version '".$version."'");
860 * Returns the newest available version of a package.
862 * @param string $package package identifier
863 * @return string newest package version
865 public function getNewestPackageVersion($package) {
868 $sql = "SELECT packageVersion
869 FROM wcf".WCF_N
."_package_update_version
870 WHERE packageUpdateID IN (
871 SELECT packageUpdateID
872 FROM wcf".WCF_N
."_package_update
875 $statement = WCF
::getDB()->prepareStatement($sql);
876 $statement->execute([$package]);
877 while ($row = $statement->fetchArray()) {
878 $versions[$row['packageVersion']] = $row['packageVersion'];
881 // sort by version number
882 usort($versions, [Package
::class, 'compareVersion']);
884 // take newest (last)
885 return array_pop($versions);
889 * Stores the filename of a download in session.
891 * @param string $package package identifier
892 * @param string $version package version
893 * @param string $filename
895 public function cacheDownload($package, $version, $filename) {
896 $cachedDownloads = WCF
::getSession()->getVar('cachedPackageUpdateDownloads');
897 if (!is_array($cachedDownloads)) {
898 $cachedDownloads = [];
902 $cachedDownloads[$package.'@'.$version] = $filename;
903 WCF
::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);