3 namespace wcf\system\package
;
5 use GuzzleHttp\Psr7\Request
;
6 use Psr\Http\Client\ClientExceptionInterface
;
7 use wcf\data\package\Package
;
8 use wcf\data\package\update\server\PackageUpdateServer
;
9 use wcf\data\package\update\server\PackageUpdateServerEditor
;
10 use wcf\event\package\PackageUpdateListChanged
;
11 use wcf\system\cache\builder\PackageUpdateCacheBuilder
;
12 use wcf\system\database\util\PreparedStatementConditionBuilder
;
13 use wcf\system\event\EventHandler
;
14 use wcf\system\exception\HTTPUnauthorizedException
;
15 use wcf\system\exception\SystemException
;
16 use wcf\system\io\HttpFactory
;
17 use wcf\system\package\validation\PackageValidationException
;
18 use wcf\system\SingletonFactory
;
20 use wcf\util\HTTPRequest
;
22 use wcf\util\StringUtil
;
26 * Provides functions to manage package updates.
28 * @author Alexander Ebert
29 * @copyright 2001-2019 WoltLab GmbH
30 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
32 final class PackageUpdateDispatcher
extends SingletonFactory
34 private bool $hasAuthCode = false;
36 private array $purchasedVersions = [
42 * Refreshes the package database.
44 * @param int[] $packageUpdateServerIDs
46 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], bool $ignoreCache = false)
48 // get update server data
49 $tmp = PackageUpdateServer
::getActiveUpdateServers($packageUpdateServerIDs);
53 $requirePurchasedVersions = false;
54 foreach ($tmp as $updateServer) {
55 if ($ignoreCache ||
$updateServer->lastUpdateTime
< TIME_NOW
- 600) {
56 if (\
preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)) {
57 $requirePurchasedVersions = true;
60 $updateServers[] = $updateServer;
64 if ($requirePurchasedVersions && PACKAGE_SERVER_AUTH_CODE
) {
65 $this->getPurchasedVersions();
69 $refreshedPackageLists = false;
70 foreach ($updateServers as $updateServer) {
74 $this->getPackageUpdateXML($updateServer);
75 $refreshedPackageLists = true;
76 } catch (SystemException
$e) {
77 $errorMessage = $e->getMessage();
78 } catch (PackageUpdateUnauthorizedException
$e) {
79 $body = $e->getRequest()->getReply()['body'];
81 // Try to find the page <title>.
82 if (\
preg_match('~<title>(?<title>.*?)</title>~', $body, $matches)) {
83 $errorMessage = $matches['title'];
85 $errorMessage = $body;
88 $errorMessage = \
mb_substr(StringUtil
::trim(\
strip_tags($errorMessage)), 0, 65000);
93 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
94 $updateServerEditor->update([
95 'status' => 'offline',
96 'errorMessage' => $errorMessage,
101 if ($refreshedPackageLists) {
102 PackageUpdateCacheBuilder
::getInstance()->reset();
103 foreach ($updateServers as $updateServer) {
104 EventHandler
::getInstance()->fire(
105 new PackageUpdateListChanged($updateServer)
111 private function getPurchasedVersions()
113 $client = HttpFactory
::makeClientWithTimeout(5);
114 $request = new Request(
116 'https://api.woltlab.com/1.0/customer/license/list.json',
118 'content-type' => 'application/x-www-form-urlencoded',
121 'authCode' => PACKAGE_SERVER_AUTH_CODE
,
122 ], '', '&', \PHP_QUERY_RFC1738
)
126 $response = $client->send($request);
128 $reply = JSON
::decode((string)$response->getBody());
130 if ($reply['status'] == 200) {
131 $this->hasAuthCode
= true;
132 $this->purchasedVersions
= [
133 'woltlab' => ($reply['woltlab'] ??
[]),
134 'pluginstore' => ($reply['pluginstore'] ??
[]),
137 } catch (ClientExceptionInterface | SystemException
) {
143 * Fetches the package_update.xml from an update server.
145 * @throws PackageUpdateUnauthorizedException
146 * @throws SystemException
148 private function getPackageUpdateXML(PackageUpdateServer
$updateServer)
151 $authData = $updateServer->getAuthData();
153 $settings['auth'] = $authData;
156 $request = new HTTPRequest($updateServer->getListURL(), $settings);
158 $requestedVersion = \wcf\
getMinorVersion();
159 if (PackageUpdateServer
::isUpgradeOverrideEnabled()) {
160 $requestedVersion = WCF
::AVAILABLE_UPGRADE_VERSION
;
164 'requested-woltlab-suite-version',
168 $apiVersion = $updateServer->apiVersion
;
169 if (\
in_array($apiVersion, ['2.1', '3.1'])) {
170 // skip etag check for WoltLab servers when an auth code is provided
172 !\
preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)
173 ||
!PACKAGE_SERVER_AUTH_CODE
175 $metaData = $updateServer->getMetaData();
176 if (isset($metaData['list']['etag'])) {
177 $request->addHeader('if-none-match', $metaData['list']['etag']);
179 if (isset($metaData['list']['lastModified'])) {
180 $request->addHeader('if-modified-since', $metaData['list']['lastModified']);
187 $reply = $request->getReply();
188 } catch (HTTPUnauthorizedException
$e) {
189 throw new PackageUpdateUnauthorizedException($request, $updateServer);
190 } catch (SystemException
$e) {
191 $reply = $request->getReply();
193 $statusCode = \
is_array($reply['statusCode']) ? \reset
($reply['statusCode']) : $reply['statusCode'];
195 throw new SystemException(
196 WCF
::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' (' . $statusCode . ')'
201 'lastUpdateTime' => TIME_NOW
,
202 'status' => 'online',
203 'errorMessage' => '',
206 // check if server indicates support for a newer API
207 if ($updateServer->apiVersion
!== '3.1' && !empty($reply['httpHeaders']['wcf-update-server-api'])) {
208 $apiVersions = \
explode(' ', \reset
($reply['httpHeaders']['wcf-update-server-api']));
209 if (\
in_array('3.1', $apiVersions)) {
210 $apiVersion = $data['apiVersion'] = '3.1';
211 } elseif (\
in_array('2.1', $apiVersions)) {
212 $apiVersion = $data['apiVersion'] = '2.1';
216 // parse given package update xml
217 $allNewPackages = false;
218 if ($apiVersion === '2.0' ||
$reply['statusCode'] != 304) {
219 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $reply['body'], $apiVersion);
223 if (\
in_array($apiVersion, ['2.1', '3.1'])) {
224 if (empty($reply['httpHeaders']['etag']) && empty($reply['httpHeaders']['last-modified'])) {
225 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
228 $metaData['list'] = [];
229 if (!empty($reply['httpHeaders']['etag'])) {
230 $metaData['list']['etag'] = \reset
($reply['httpHeaders']['etag']);
232 if (!empty($reply['httpHeaders']['last-modified'])) {
233 $metaData['list']['lastModified'] = \reset
($reply['httpHeaders']['last-modified']);
236 $data['metaData'] = \
serialize($metaData);
238 unset($request, $reply);
240 if ($allNewPackages !== false) {
241 // purge package list
242 $sql = "DELETE FROM wcf1_package_update
243 WHERE packageUpdateServerID = ?";
244 $statement = WCF
::getDB()->prepare($sql);
245 $statement->execute([$updateServer->packageUpdateServerID
]);
248 if (!empty($allNewPackages)) {
249 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID
);
251 unset($allNewPackages);
254 // update server status
255 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
256 $updateServerEditor->update($data);
260 * Parses a stream containing info from a packages_update.xml.
262 * @throws SystemException
264 private function parsePackageUpdateXML(PackageUpdateServer
$updateServer, string $content, string $apiVersion): array
266 $isTrustedServer = $updateServer->isTrustedServer();
270 $xml->loadXML('packageUpdateServer.xml', $content);
271 $xpath = $xml->xpath();
273 $allNewPackages = [];
274 $packages = $xpath->query('/ns:section/ns:package');
275 /** @var \DOMElement $package */
276 foreach ($packages as $package) {
277 if (!Package
::isValidPackageName($package->getAttribute('name'))) {
278 throw new SystemException("'" . $package->getAttribute('name') . "' is not a valid package name.");
281 $name = $package->getAttribute('name');
282 if (\
strpos($name, 'com.woltlab.') === 0 && !$isTrustedServer) {
283 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
284 throw new SystemException("The server '" . $updateServer->serverURL
. "' attempted to provide an official WoltLab package, but is not authorized.");
287 // silently ignore this package to avoid unexpected errors for regular users
291 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
294 return $allNewPackages;
298 * Parses the xml structure from a packages_update.xml.
300 * @throws PackageValidationException
302 private function parsePackageUpdateXMLBlock(
303 PackageUpdateServer
$updateServer,
305 \DOMElement
$package,
308 // define default values
312 'isApplication' => 0,
313 'packageDescription' => '',
315 'pluginStoreFileID' => 0,
318 // parse package information
319 $elements = $xpath->query('./ns:packageinformation/*', $package);
320 foreach ($elements as $element) {
321 switch ($element->tagName
) {
323 $packageInfo['packageName'] = $element->nodeValue
;
326 case 'packagedescription':
327 $packageInfo['packageDescription'] = $element->nodeValue
;
330 case 'isapplication':
331 $packageInfo['isApplication'] = \
intval($element->nodeValue
);
334 case 'pluginStoreFileID':
335 if ($updateServer->isWoltLabStoreServer()) {
336 $packageInfo['pluginStoreFileID'] = \
intval($element->nodeValue
);
342 // parse author information
343 $elements = $xpath->query('./ns:authorinformation/*', $package);
344 foreach ($elements as $element) {
345 switch ($element->tagName
) {
347 $packageInfo['author'] = $element->nodeValue
;
351 $packageInfo['authorURL'] = $element->nodeValue
;
357 if ($this->hasAuthCode
) {
358 if ($updateServer->isWoltLabUpdateServer()) {
360 } elseif ($updateServer->isWoltLabStoreServer()) {
361 $key = 'pluginstore';
366 $elements = $xpath->query('./ns:versions/ns:version', $package);
367 /** @var \DOMElement $element */
368 foreach ($elements as $element) {
369 $versionNo = $element->getAttribute('name');
371 $isAccessible = ($element->getAttribute('accessible') == 'true') ?
1 : 0;
372 if ($key && $element->getAttribute('requireAuth') == 'true') {
373 $packageName = $package->getAttribute('name');
374 if (isset($this->purchasedVersions
[$key][$packageName])) {
375 if ($this->purchasedVersions
[$key][$packageName] == '*') {
378 $isAccessible = (Package
::compareVersion(
380 $this->purchasedVersions
[$key][$packageName] . '.99',
389 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
391 $children = $xpath->query('child::*', $element);
392 /** @var \DOMElement $child */
393 foreach ($children as $child) {
394 switch ($child->tagName
) {
396 $fromversions = $xpath->query('child::*', $child);
397 foreach ($fromversions as $fromversion) {
398 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue
;
403 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue
;
407 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue
;
410 case 'requiredpackages':
411 $requiredPackages = $xpath->query('child::*', $child);
413 /** @var \DOMElement $requiredPackage */
414 foreach ($requiredPackages as $requiredPackage) {
415 $minVersion = $requiredPackage->getAttribute('minversion');
416 $required = $requiredPackage->nodeValue
;
418 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
419 if (!empty($minVersion)) {
420 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
425 case 'optionalpackages':
426 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
428 $optionalPackages = $xpath->query('child::*', $child);
429 foreach ($optionalPackages as $optionalPackage) {
430 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue
;
434 case 'excludedpackages':
435 $excludedpackages = $xpath->query('child::*', $child);
436 /** @var \DOMElement $excludedPackage */
437 foreach ($excludedpackages as $excludedPackage) {
438 $exclusion = $excludedPackage->nodeValue
;
439 $version = $excludedPackage->getAttribute('version');
441 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [
442 'version' => $version,
448 $packageInfo['versions'][$versionNo]['license'] = [
449 'license' => $child->nodeValue
,
450 'licenseURL' => $child->hasAttribute('url') ?
$child->getAttribute('url') : '',
454 case 'compatibility':
455 if ($apiVersion !== '3.1') {
459 $packageInfo['versions'][$versionNo]['compatibility'] = [];
461 /** @var \DOMElement $compatibleVersion */
462 foreach ($xpath->query('child::*', $child) as $compatibleVersion) {
463 if ($compatibleVersion->nodeName
=== 'api' && $compatibleVersion->hasAttribute('version')) {
464 $versionNumber = $compatibleVersion->getAttribute('version');
465 if (!\
preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $versionNumber)) {
466 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
467 throw new PackageValidationException(
468 PackageValidationException
::INVALID_API_VERSION
,
469 ['version' => $versionNumber]
476 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
488 * Updates information parsed from a packages_update.xml into the database.
490 private function savePackageUpdates(array $allNewPackages, int $packageUpdateServerID): void
492 $excludedPackagesParameters = $requirementInserts = $fromversionInserts = [];
493 $sql = "INSERT INTO wcf1_package_update
494 (packageUpdateServerID, package, packageName, packageDescription, author, authorURL, isApplication, pluginStoreFileID)
495 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
496 $statement = WCF
::getDB()->prepare($sql);
497 WCF
::getDB()->beginTransaction();
498 foreach ($allNewPackages as $identifier => $packageData) {
499 $statement->execute([
500 $packageUpdateServerID,
502 $packageData['packageName'],
503 $packageData['packageDescription'],
504 $packageData['author'],
505 $packageData['authorURL'],
506 $packageData['isApplication'],
507 $packageData['pluginStoreFileID'],
510 WCF
::getDB()->commitTransaction();
512 $sql = "SELECT packageUpdateID, package
513 FROM wcf1_package_update
514 WHERE packageUpdateServerID = ?";
515 $statement = WCF
::getDB()->prepare($sql);
516 $statement->execute([$packageUpdateServerID]);
517 $packageUpdateIDs = $statement->fetchMap('package', 'packageUpdateID');
519 $sql = "INSERT INTO wcf1_package_update_version
520 (filename, license, licenseURL, isAccessible, packageDate, packageUpdateID, packageVersion)
521 VALUES (?, ?, ?, ?, ?, ?, ?)";
522 $statement = WCF
::getDB()->prepare($sql);
523 WCF
::getDB()->beginTransaction();
524 foreach ($allNewPackages as $package => $packageData) {
525 foreach ($packageData['versions'] as $packageVersion => $versionData) {
526 $statement->execute([
527 $versionData['file'] ??
'',
528 $versionData['license']['license'] ??
'',
529 $versionData['license']['licenseURL'] ??
'',
530 $versionData['isAccessible'] ?
1 : 0,
531 $versionData['packageDate'],
532 $packageUpdateIDs[$package],
537 WCF
::getDB()->commitTransaction();
539 $conditions = new PreparedStatementConditionBuilder();
540 $conditions->add('packageUpdateID IN (?)', [\array_values
($packageUpdateIDs)]);
541 $sql = "SELECT packageUpdateVersionID, packageUpdateID, packageVersion
542 FROM wcf1_package_update_version
544 $statement = WCF
::getDB()->prepare($sql);
545 $statement->execute($conditions->getParameters());
546 $packageUpdateVersions = [];
547 while ($row = $statement->fetchArray()) {
548 if (!isset($packageUpdateVersions[$row['packageUpdateID']])) {
549 $packageUpdateVersions[$row['packageUpdateID']] = [];
552 $packageUpdateVersions[$row['packageUpdateID']][$row['packageVersion']] = $row['packageUpdateVersionID'];
555 foreach ($allNewPackages as $package => $packageData) {
556 foreach ($packageData['versions'] as $packageVersion => $versionData) {
557 $packageUpdateID = $packageUpdateIDs[$package];
558 $packageUpdateVersionID = $packageUpdateVersions[$packageUpdateID][$packageVersion];
560 if (isset($versionData['requiredPackages'])) {
561 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
562 $requirementInserts[] = [
563 'packageUpdateVersionID' => $packageUpdateVersionID,
564 'package' => $requiredIdentifier,
565 'minversion' => $required['minversion'] ??
'',
570 if (isset($versionData['excludedPackages'])) {
571 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
572 $excludedPackagesParameters[] = [
573 'packageUpdateVersionID' => $packageUpdateVersionID,
574 'excludedPackage' => $excludedIdentifier,
575 'excludedPackageVersion' => $exclusion['version'],
580 if (isset($versionData['fromversions'])) {
581 foreach ($versionData['fromversions'] as $fromversion) {
582 $fromversionInserts[] = [
583 'packageUpdateVersionID' => $packageUpdateVersionID,
584 'fromversion' => $fromversion,
589 // The API compatibility versions are deprecated, any package that exposes them must
590 // exclude at most `com.woltlab.wcf` in version `6.0.0 Alpha 1`.
591 if (!empty($versionData['compatibility'])) {
592 if (!isset($versionData['excludedPackages'])) {
593 $versionData['excludedPackages'] = [];
595 $excludeCore60 = '6.0.0 Alpha 1';
598 $versionData['excludedPackages'] = \array_filter
(
599 $versionData['excludedPackages'],
600 static function ($excludedPackage, $excludedVersion) use (&$coreExclude) {
601 if ($excludedPackage === 'com.woltlab.wcf') {
602 $coreExclude = $excludedVersion;
609 \ARRAY_FILTER_USE_BOTH
612 if ($coreExclude === null || Package
::compareVersion($coreExclude, $excludeCore60, '>')) {
613 $versionData['excludedPackages'][] = [
614 'packageUpdateVersionID' => $packageUpdateVersionID,
615 'excludedPackage' => 'com.woltlab.wcf',
616 'excludedPackageVersion' => $excludeCore60,
623 $sql = "INSERT INTO wcf1_package_update_requirement
624 (packageUpdateVersionID, package, minversion)
626 $statement = WCF
::getDB()->prepare($sql);
627 WCF
::getDB()->beginTransaction();
628 foreach ($requirementInserts as $requirement) {
629 $statement->execute([
630 $requirement['packageUpdateVersionID'],
631 $requirement['package'],
632 $requirement['minversion'],
635 WCF
::getDB()->commitTransaction();
637 $sql = "INSERT INTO wcf1_package_update_exclusion
638 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
640 $statement = WCF
::getDB()->prepare($sql);
641 WCF
::getDB()->beginTransaction();
642 foreach ($excludedPackagesParameters as $excludedPackage) {
643 $statement->execute([
644 $excludedPackage['packageUpdateVersionID'],
645 $excludedPackage['excludedPackage'],
646 $excludedPackage['excludedPackageVersion'],
649 WCF
::getDB()->commitTransaction();
651 $sql = "INSERT INTO wcf1_package_update_fromversion
652 (packageUpdateVersionID, fromversion)
654 $statement = WCF
::getDB()->prepare($sql);
655 WCF
::getDB()->beginTransaction();
656 foreach ($fromversionInserts as $fromversion) {
657 $statement->execute([
658 $fromversion['packageUpdateVersionID'],
659 $fromversion['fromversion'],
662 WCF
::getDB()->commitTransaction();
666 * Returns a list of available updates for installed packages.
669 * @throws SystemException
671 public function getAvailableUpdates(bool $removeRequirements = true, bool $removeOlderMinorReleases = false): array
675 // get update server data
676 $updateServers = PackageUpdateServer
::getActiveUpdateServers();
677 $packageUpdateServerIDs = \array_keys
($updateServers);
678 if (empty($packageUpdateServerIDs)) {
682 // get existing packages and their versions
683 $existingPackages = [];
684 $sql = "SELECT packageID, package, packageDescription, packageName,
685 packageVersion, packageDate, author, authorURL, isApplication
687 $statement = WCF
::getDB()->prepare($sql);
688 $statement->execute();
689 while ($row = $statement->fetchArray()) {
690 $existingPackages[$row['package']][] = $row;
692 if (empty($existingPackages)) {
696 // get all update versions
697 $conditions = new PreparedStatementConditionBuilder();
698 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
699 $conditions->add("package IN (
700 SELECT DISTINCT package
704 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
705 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
706 FROM wcf1_package_update pu
707 INNER JOIN wcf1_package_update_version puv
708 ON puv.packageUpdateID = pu.packageUpdateID
709 AND puv.isAccessible = 1
711 $statement = WCF
::getDB()->prepare($sql);
712 $statement->execute($conditions->getParameters());
713 while ($row = $statement->fetchArray()) {
714 if (!isset($existingPackages[$row['package']])) {
715 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
716 throw new SystemException("Invalid package update data, identifier '" . $row['package'] . "' does not match any installed package (case-mismatch).");
719 // case-mismatch, skip the update
724 foreach ($existingPackages[$row['package']] as $existingVersion) {
725 if (Package
::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
727 if (!isset($updates[$existingVersion['packageID']])) {
728 $existingVersion['versions'] = [];
729 $updates[$existingVersion['packageID']] = $existingVersion;
733 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
734 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
735 'packageDate' => $row['packageDate'],
736 'packageVersion' => $row['packageVersion'],
742 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
743 'packageUpdateID' => $row['packageUpdateID'],
744 'packageUpdateServerID' => $row['packageUpdateServerID'],
745 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
746 'filename' => $row['filename'],
752 // sort package versions
753 // and remove old versions
754 foreach ($updates as $packageID => $data) {
755 \
uksort($updates[$packageID]['versions'], ['wcf\data\package\Package', 'compareVersion']);
756 $updates[$packageID]['version'] = \
end($updates[$packageID]['versions']);
759 // remove requirements of application packages
760 if ($removeRequirements) {
761 foreach ($existingPackages as $instances) {
762 foreach ($instances as $instance) {
763 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
764 $updates = $this->removeUpdateRequirements(
766 $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']
773 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
774 if ($removeOlderMinorReleases) {
775 foreach ($updates as &$updateData) {
776 $highestVersions = [];
777 foreach ($updateData['versions'] as $versionNumber => $dummy) {
778 if (\
preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
779 $major = $matches[1];
780 if (isset($highestVersions[$major])) {
781 if (Package
::compareVersion($highestVersions[$major], $versionNumber, '<')) {
782 // version is newer, discard current version
783 unset($updateData['versions'][$highestVersions[$major]]);
784 $highestVersions[$major] = $versionNumber;
786 // version is lower, discard
787 unset($updateData['versions'][$versionNumber]);
790 $highestVersions[$major] = $versionNumber;
802 * Removes unnecessary updates of requirements from the list of available updates.
804 private function removeUpdateRequirements(array $updates, int $packageUpdateVersionID): array
806 $sql = "SELECT pur.package, pur.minversion, p.packageID
807 FROM wcf1_package_update_requirement pur
808 LEFT JOIN wcf1_package p
809 ON p.package = pur.package
810 WHERE pur.packageUpdateVersionID = ?";
811 $statement = WCF
::getDB()->prepare($sql);
812 $statement->execute([$packageUpdateVersionID]);
813 while ($row = $statement->fetchArray()) {
814 if (isset($updates[$row['packageID']])) {
815 $updates = $this->removeUpdateRequirements(
817 $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']
820 Package
::compareVersion(
822 $updates[$row['packageID']]['version']['packageVersion'],
826 unset($updates[$row['packageID']]);
835 * Returns package update versions of the specified package.
837 * @throws SystemException
839 public function getPackageUpdateVersions(string $package, string $version = ''): array
841 $packageUpdateServerIDs = [];
842 foreach (PackageUpdateServer
::getActiveUpdateServers() as $packageUpdateServer) {
843 $packageUpdateServerIDs[] = $packageUpdateServer->packageUpdateServerID
;
846 // get newest package version
847 if (empty($version)) {
848 $version = $this->getNewestPackageVersion($package);
851 if ($version === null) {
852 throw new SystemException("Cannot find the package '" . $package . "'");
856 $conditions = new PreparedStatementConditionBuilder();
857 $conditions->add('pu.package = ?', [$package]);
858 $conditions->add('puv.packageVersion = ?', [$version]);
859 $conditions->add('puv.isAccessible = ?', [1]);
860 $conditions->add('pus.packageUpdateServerID IN (?)', [$packageUpdateServerIDs]);
862 $sql = "SELECT puv.*, pu.*, pus.serverURL, pus.loginUsername, pus.loginPassword
863 FROM wcf1_package_update_version puv
864 LEFT JOIN wcf1_package_update pu
865 ON pu.packageUpdateID = puv.packageUpdateID
866 LEFT JOIN wcf1_package_update_server pus
867 ON pus.packageUpdateServerID = pu.packageUpdateServerID
869 $statement = WCF
::getDB()->prepare($sql);
870 $statement->execute($conditions->getParameters());
871 $versions = $statement->fetchAll(\PDO
::FETCH_ASSOC
);
873 if (empty($versions)) {
874 throw new SystemException("Cannot find the package '" . $package . "' in version '" . $version . "'");
881 * Returns the newest available version of a package.
883 public function getNewestPackageVersion(string $package): ?
string
887 $sql = "SELECT packageVersion
888 FROM wcf1_package_update_version
889 WHERE packageUpdateID IN (
890 SELECT packageUpdateID
891 FROM wcf1_package_update
894 $statement = WCF
::getDB()->prepare($sql);
895 $statement->execute([$package]);
896 while ($row = $statement->fetchArray()) {
897 $versions[$row['packageVersion']] = $row['packageVersion'];
900 // sort by version number
901 \
usort($versions, [Package
::class, 'compareVersion']);
903 // take newest (last)
904 return \array_pop
($versions);
908 * Stores the filename of a download in session.
910 public function cacheDownload(string $package, string $version, string $filename): void
912 $cachedDownloads = WCF
::getSession()->getVar('cachedPackageUpdateDownloads');
913 if (!\
is_array($cachedDownloads)) {
914 $cachedDownloads = [];
918 $cachedDownloads[$package . '@' . $version] = $filename;
919 WCF
::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);