3 namespace wcf\system\package
;
5 use GuzzleHttp\Exception\ClientException
;
6 use GuzzleHttp\Psr7\Request
;
7 use GuzzleHttp\RequestOptions
;
8 use Psr\Http\Client\ClientExceptionInterface
;
9 use wcf\data\package\Package
;
10 use wcf\data\package\update\server\PackageUpdateServer
;
11 use wcf\data\package\update\server\PackageUpdateServerEditor
;
12 use wcf\event\package\PackageUpdateListChanged
;
13 use wcf\system\cache\builder\PackageUpdateCacheBuilder
;
14 use wcf\system\database\util\PreparedStatementConditionBuilder
;
15 use wcf\system\event\EventHandler
;
16 use wcf\system\exception\SystemException
;
17 use wcf\system\io\HttpFactory
;
18 use wcf\system\package\validation\PackageValidationException
;
19 use wcf\system\SingletonFactory
;
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->getResponseMessage();
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)
150 $authData = $updateServer->getAuthData();
152 if (!empty($authData)) {
153 $options[RequestOptions
::AUTH
] = [
154 $authData['username'],
155 $authData['password'],
158 $client = HttpFactory
::makeClient($options);
161 $requestedVersion = \wcf\
getMinorVersion();
162 if (PackageUpdateServer
::isUpgradeOverrideEnabled()) {
163 $requestedVersion = WCF
::AVAILABLE_UPGRADE_VERSION
;
165 $headers['requested-woltlab-suite-version'] = $requestedVersion;
167 $apiVersion = $updateServer->apiVersion
;
168 if (\
in_array($apiVersion, ['2.1', '3.1'])) {
169 // skip etag check for WoltLab servers when an auth code is provided
171 !\
preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)
172 ||
!PACKAGE_SERVER_AUTH_CODE
174 $metaData = $updateServer->getMetaData();
175 if (isset($metaData['list']['etag'])) {
176 $headers['if-none-match'] = $metaData['list']['etag'];
178 if (isset($metaData['list']['lastModified'])) {
179 $headers['if-modified-since'] = $metaData['list']['lastModified'];
184 $request = new Request(
186 $updateServer->getListURL(),
191 $response = $client->send($request);
192 } catch (ClientException
$e) {
193 throw new PackageUpdateUnauthorizedException(
194 $e->getResponse()->getStatusCode(),
195 $e->getResponse()->getHeaders(),
196 $e->getResponse()->getBody(),
201 if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 304) {
202 throw new SystemException(
203 WCF
::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' (' . $response->getStatusCode() . ')'
208 'lastUpdateTime' => TIME_NOW
,
209 'status' => 'online',
210 'errorMessage' => '',
213 // check if server indicates support for a newer API
214 if ($updateServer->apiVersion
!== '3.1' && $response->getHeader('wcf-update-server-api') !== []) {
215 $apiVersions = \
explode(' ', $response->getHeader('wcf-update-server-api')[0]);
216 if (\
in_array('3.1', $apiVersions)) {
217 $apiVersion = $data['apiVersion'] = '3.1';
218 } elseif (\
in_array('2.1', $apiVersions)) {
219 $apiVersion = $data['apiVersion'] = '2.1';
223 // parse given package update xml
224 $allNewPackages = false;
225 if ($apiVersion === '2.0' ||
$response->getStatusCode() != 304) {
226 if (!$response->getBody()->getSize()) {
227 throw new SystemException(WCF
::getLanguage()->get('wcf.acp.package.update.error.listNotFound'));
230 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $response->getBody(), $apiVersion);
234 if (\
in_array($apiVersion, ['2.1', '3.1'])) {
235 if ($response->getHeader('etag') === [] && $response->getHeader('last-modified') === []) {
236 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
239 $metaData['list'] = [];
240 if ($response->getHeader('etag') !== []) {
241 $metaData['list']['etag'] = $response->getHeader('etag')[0];
243 if ($response->getHeader('last-modified') !== []) {
244 $metaData['list']['lastModified'] = $response->getHeader('last-modified')[0];
247 $data['metaData'] = \
serialize($metaData);
249 unset($request, $response);
251 if ($allNewPackages !== false) {
252 // purge package list
253 $sql = "DELETE FROM wcf1_package_update
254 WHERE packageUpdateServerID = ?";
255 $statement = WCF
::getDB()->prepare($sql);
256 $statement->execute([$updateServer->packageUpdateServerID
]);
259 if (!empty($allNewPackages)) {
260 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID
);
262 unset($allNewPackages);
265 // update server status
266 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
267 $updateServerEditor->update($data);
271 * Parses a stream containing info from a packages_update.xml.
273 * @throws SystemException
275 private function parsePackageUpdateXML(PackageUpdateServer
$updateServer, string $content, string $apiVersion): array
277 $isTrustedServer = $updateServer->isTrustedServer();
281 $xml->loadXML('packageUpdateServer.xml', $content);
282 $xpath = $xml->xpath();
284 $allNewPackages = [];
285 $packages = $xpath->query('/ns:section/ns:package');
286 /** @var \DOMElement $package */
287 foreach ($packages as $package) {
288 if (!Package
::isValidPackageName($package->getAttribute('name'))) {
289 throw new SystemException("'" . $package->getAttribute('name') . "' is not a valid package name.");
292 $name = $package->getAttribute('name');
293 if (\
strpos($name, 'com.woltlab.') === 0 && !$isTrustedServer) {
294 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
295 throw new SystemException("The server '" . $updateServer->serverURL
. "' attempted to provide an official WoltLab package, but is not authorized.");
298 // silently ignore this package to avoid unexpected errors for regular users
302 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
305 return $allNewPackages;
309 * Parses the xml structure from a packages_update.xml.
311 * @throws PackageValidationException
313 private function parsePackageUpdateXMLBlock(
314 PackageUpdateServer
$updateServer,
316 \DOMElement
$package,
319 // define default values
323 'isApplication' => 0,
324 'packageDescription' => '',
326 'pluginStoreFileID' => 0,
329 // parse package information
330 $elements = $xpath->query('./ns:packageinformation/*', $package);
331 foreach ($elements as $element) {
332 switch ($element->tagName
) {
334 $packageInfo['packageName'] = $element->nodeValue
;
337 case 'packagedescription':
338 $packageInfo['packageDescription'] = $element->nodeValue
;
341 case 'isapplication':
342 $packageInfo['isApplication'] = \
intval($element->nodeValue
);
345 case 'pluginStoreFileID':
346 if ($updateServer->isWoltLabStoreServer()) {
347 $packageInfo['pluginStoreFileID'] = \
intval($element->nodeValue
);
353 // parse author information
354 $elements = $xpath->query('./ns:authorinformation/*', $package);
355 foreach ($elements as $element) {
356 switch ($element->tagName
) {
358 $packageInfo['author'] = $element->nodeValue
;
362 $packageInfo['authorURL'] = $element->nodeValue
;
368 if ($this->hasAuthCode
) {
369 if ($updateServer->isWoltLabUpdateServer()) {
371 } elseif ($updateServer->isWoltLabStoreServer()) {
372 $key = 'pluginstore';
377 $elements = $xpath->query('./ns:versions/ns:version', $package);
378 /** @var \DOMElement $element */
379 foreach ($elements as $element) {
380 $versionNo = $element->getAttribute('name');
382 $isAccessible = ($element->getAttribute('accessible') == 'true') ?
1 : 0;
383 if ($key && $element->getAttribute('requireAuth') == 'true') {
384 $packageName = $package->getAttribute('name');
385 if (isset($this->purchasedVersions
[$key][$packageName])) {
386 if ($this->purchasedVersions
[$key][$packageName] == '*') {
389 $isAccessible = (Package
::compareVersion(
391 $this->purchasedVersions
[$key][$packageName] . '.99',
400 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
402 $children = $xpath->query('child::*', $element);
403 /** @var \DOMElement $child */
404 foreach ($children as $child) {
405 switch ($child->tagName
) {
407 $fromversions = $xpath->query('child::*', $child);
408 foreach ($fromversions as $fromversion) {
409 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue
;
414 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue
;
418 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue
;
421 case 'requiredpackages':
422 $requiredPackages = $xpath->query('child::*', $child);
424 /** @var \DOMElement $requiredPackage */
425 foreach ($requiredPackages as $requiredPackage) {
426 $minVersion = $requiredPackage->getAttribute('minversion');
427 $required = $requiredPackage->nodeValue
;
429 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
430 if (!empty($minVersion)) {
431 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
436 case 'optionalpackages':
437 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
439 $optionalPackages = $xpath->query('child::*', $child);
440 foreach ($optionalPackages as $optionalPackage) {
441 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue
;
445 case 'excludedpackages':
446 $excludedpackages = $xpath->query('child::*', $child);
447 /** @var \DOMElement $excludedPackage */
448 foreach ($excludedpackages as $excludedPackage) {
449 $exclusion = $excludedPackage->nodeValue
;
450 $version = $excludedPackage->getAttribute('version');
452 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [
453 'version' => $version,
459 $packageInfo['versions'][$versionNo]['license'] = [
460 'license' => $child->nodeValue
,
461 'licenseURL' => $child->hasAttribute('url') ?
$child->getAttribute('url') : '',
465 case 'compatibility':
466 if ($apiVersion !== '3.1') {
470 $packageInfo['versions'][$versionNo]['compatibility'] = [];
472 /** @var \DOMElement $compatibleVersion */
473 foreach ($xpath->query('child::*', $child) as $compatibleVersion) {
474 if ($compatibleVersion->nodeName
=== 'api' && $compatibleVersion->hasAttribute('version')) {
475 $versionNumber = $compatibleVersion->getAttribute('version');
476 if (!\
preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $versionNumber)) {
477 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
478 throw new PackageValidationException(
479 PackageValidationException
::INVALID_API_VERSION
,
480 ['version' => $versionNumber]
487 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
499 * Updates information parsed from a packages_update.xml into the database.
501 private function savePackageUpdates(array $allNewPackages, int $packageUpdateServerID): void
503 $excludedPackagesParameters = $requirementInserts = $fromversionInserts = [];
504 $sql = "INSERT INTO wcf1_package_update
505 (packageUpdateServerID, package, packageName, packageDescription, author, authorURL, isApplication, pluginStoreFileID)
506 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
507 $statement = WCF
::getDB()->prepare($sql);
508 WCF
::getDB()->beginTransaction();
509 foreach ($allNewPackages as $identifier => $packageData) {
510 $statement->execute([
511 $packageUpdateServerID,
513 $packageData['packageName'],
514 $packageData['packageDescription'],
515 $packageData['author'],
516 $packageData['authorURL'],
517 $packageData['isApplication'],
518 $packageData['pluginStoreFileID'],
521 WCF
::getDB()->commitTransaction();
523 $sql = "SELECT packageUpdateID, package
524 FROM wcf1_package_update
525 WHERE packageUpdateServerID = ?";
526 $statement = WCF
::getDB()->prepare($sql);
527 $statement->execute([$packageUpdateServerID]);
528 $packageUpdateIDs = $statement->fetchMap('package', 'packageUpdateID');
530 $sql = "INSERT INTO wcf1_package_update_version
531 (filename, license, licenseURL, isAccessible, packageDate, packageUpdateID, packageVersion)
532 VALUES (?, ?, ?, ?, ?, ?, ?)";
533 $statement = WCF
::getDB()->prepare($sql);
534 WCF
::getDB()->beginTransaction();
535 foreach ($allNewPackages as $package => $packageData) {
536 foreach ($packageData['versions'] as $packageVersion => $versionData) {
537 $statement->execute([
538 $versionData['file'] ??
'',
539 $versionData['license']['license'] ??
'',
540 $versionData['license']['licenseURL'] ??
'',
541 $versionData['isAccessible'] ?
1 : 0,
542 $versionData['packageDate'],
543 $packageUpdateIDs[$package],
548 WCF
::getDB()->commitTransaction();
550 $conditions = new PreparedStatementConditionBuilder();
551 $conditions->add('packageUpdateID IN (?)', [\array_values
($packageUpdateIDs)]);
552 $sql = "SELECT packageUpdateVersionID, packageUpdateID, packageVersion
553 FROM wcf1_package_update_version
555 $statement = WCF
::getDB()->prepare($sql);
556 $statement->execute($conditions->getParameters());
557 $packageUpdateVersions = [];
558 while ($row = $statement->fetchArray()) {
559 if (!isset($packageUpdateVersions[$row['packageUpdateID']])) {
560 $packageUpdateVersions[$row['packageUpdateID']] = [];
563 $packageUpdateVersions[$row['packageUpdateID']][$row['packageVersion']] = $row['packageUpdateVersionID'];
566 foreach ($allNewPackages as $package => $packageData) {
567 foreach ($packageData['versions'] as $packageVersion => $versionData) {
568 $packageUpdateID = $packageUpdateIDs[$package];
569 $packageUpdateVersionID = $packageUpdateVersions[$packageUpdateID][$packageVersion];
571 if (isset($versionData['requiredPackages'])) {
572 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
573 $requirementInserts[] = [
574 'packageUpdateVersionID' => $packageUpdateVersionID,
575 'package' => $requiredIdentifier,
576 'minversion' => $required['minversion'] ??
'',
581 if (isset($versionData['excludedPackages'])) {
582 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
583 $excludedPackagesParameters[] = [
584 'packageUpdateVersionID' => $packageUpdateVersionID,
585 'excludedPackage' => $excludedIdentifier,
586 'excludedPackageVersion' => $exclusion['version'],
591 if (isset($versionData['fromversions'])) {
592 foreach ($versionData['fromversions'] as $fromversion) {
593 $fromversionInserts[] = [
594 'packageUpdateVersionID' => $packageUpdateVersionID,
595 'fromversion' => $fromversion,
600 // The API compatibility versions are deprecated, any package that exposes them must
601 // exclude at most `com.woltlab.wcf` in version `6.0.0 Alpha 1`.
602 if (!empty($versionData['compatibility'])) {
603 if (!isset($versionData['excludedPackages'])) {
604 $versionData['excludedPackages'] = [];
606 $excludeCore60 = '6.0.0 Alpha 1';
609 $versionData['excludedPackages'] = \array_filter
(
610 $versionData['excludedPackages'],
611 static function ($excludedPackage, $excludedVersion) use (&$coreExclude) {
612 if ($excludedPackage === 'com.woltlab.wcf') {
613 $coreExclude = $excludedVersion;
620 \ARRAY_FILTER_USE_BOTH
623 if ($coreExclude === null || Package
::compareVersion($coreExclude, $excludeCore60, '>')) {
624 $versionData['excludedPackages'][] = [
625 'packageUpdateVersionID' => $packageUpdateVersionID,
626 'excludedPackage' => 'com.woltlab.wcf',
627 'excludedPackageVersion' => $excludeCore60,
634 $sql = "INSERT INTO wcf1_package_update_requirement
635 (packageUpdateVersionID, package, minversion)
637 $statement = WCF
::getDB()->prepare($sql);
638 WCF
::getDB()->beginTransaction();
639 foreach ($requirementInserts as $requirement) {
640 $statement->execute([
641 $requirement['packageUpdateVersionID'],
642 $requirement['package'],
643 $requirement['minversion'],
646 WCF
::getDB()->commitTransaction();
648 $sql = "INSERT INTO wcf1_package_update_exclusion
649 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
651 $statement = WCF
::getDB()->prepare($sql);
652 WCF
::getDB()->beginTransaction();
653 foreach ($excludedPackagesParameters as $excludedPackage) {
654 $statement->execute([
655 $excludedPackage['packageUpdateVersionID'],
656 $excludedPackage['excludedPackage'],
657 $excludedPackage['excludedPackageVersion'],
660 WCF
::getDB()->commitTransaction();
662 $sql = "INSERT INTO wcf1_package_update_fromversion
663 (packageUpdateVersionID, fromversion)
665 $statement = WCF
::getDB()->prepare($sql);
666 WCF
::getDB()->beginTransaction();
667 foreach ($fromversionInserts as $fromversion) {
668 $statement->execute([
669 $fromversion['packageUpdateVersionID'],
670 $fromversion['fromversion'],
673 WCF
::getDB()->commitTransaction();
677 * Returns a list of available updates for installed packages.
680 * @throws SystemException
682 public function getAvailableUpdates(bool $removeRequirements = true, bool $removeOlderMinorReleases = false): array
686 // get update server data
687 $updateServers = PackageUpdateServer
::getActiveUpdateServers();
688 $packageUpdateServerIDs = \array_keys
($updateServers);
689 if (empty($packageUpdateServerIDs)) {
693 // get existing packages and their versions
694 $existingPackages = [];
695 $sql = "SELECT packageID, package, packageDescription, packageName,
696 packageVersion, packageDate, author, authorURL, isApplication
698 $statement = WCF
::getDB()->prepare($sql);
699 $statement->execute();
700 while ($row = $statement->fetchArray()) {
701 $existingPackages[$row['package']][] = $row;
703 if (empty($existingPackages)) {
707 // get all update versions
708 $conditions = new PreparedStatementConditionBuilder();
709 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
710 $conditions->add("package IN (
711 SELECT DISTINCT package
715 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
716 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
717 FROM wcf1_package_update pu
718 INNER JOIN wcf1_package_update_version puv
719 ON puv.packageUpdateID = pu.packageUpdateID
720 AND puv.isAccessible = 1
722 $statement = WCF
::getDB()->prepare($sql);
723 $statement->execute($conditions->getParameters());
724 while ($row = $statement->fetchArray()) {
725 if (!isset($existingPackages[$row['package']])) {
726 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
727 throw new SystemException("Invalid package update data, identifier '" . $row['package'] . "' does not match any installed package (case-mismatch).");
730 // case-mismatch, skip the update
735 foreach ($existingPackages[$row['package']] as $existingVersion) {
736 if (Package
::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
738 if (!isset($updates[$existingVersion['packageID']])) {
739 $existingVersion['versions'] = [];
740 $updates[$existingVersion['packageID']] = $existingVersion;
744 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
745 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
746 'packageDate' => $row['packageDate'],
747 'packageVersion' => $row['packageVersion'],
753 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
754 'packageUpdateID' => $row['packageUpdateID'],
755 'packageUpdateServerID' => $row['packageUpdateServerID'],
756 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
757 'filename' => $row['filename'],
763 // sort package versions
764 // and remove old versions
765 foreach ($updates as $packageID => $data) {
766 \
uksort($updates[$packageID]['versions'], ['wcf\data\package\Package', 'compareVersion']);
767 $updates[$packageID]['version'] = \
end($updates[$packageID]['versions']);
770 // remove requirements of application packages
771 if ($removeRequirements) {
772 foreach ($existingPackages as $instances) {
773 foreach ($instances as $instance) {
774 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
775 $updates = $this->removeUpdateRequirements(
777 $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']
784 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
785 if ($removeOlderMinorReleases) {
786 foreach ($updates as &$updateData) {
787 $highestVersions = [];
788 foreach ($updateData['versions'] as $versionNumber => $dummy) {
789 if (\
preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
790 $major = $matches[1];
791 if (isset($highestVersions[$major])) {
792 if (Package
::compareVersion($highestVersions[$major], $versionNumber, '<')) {
793 // version is newer, discard current version
794 unset($updateData['versions'][$highestVersions[$major]]);
795 $highestVersions[$major] = $versionNumber;
797 // version is lower, discard
798 unset($updateData['versions'][$versionNumber]);
801 $highestVersions[$major] = $versionNumber;
813 * Removes unnecessary updates of requirements from the list of available updates.
815 private function removeUpdateRequirements(array $updates, int $packageUpdateVersionID): array
817 $sql = "SELECT pur.package, pur.minversion, p.packageID
818 FROM wcf1_package_update_requirement pur
819 LEFT JOIN wcf1_package p
820 ON p.package = pur.package
821 WHERE pur.packageUpdateVersionID = ?";
822 $statement = WCF
::getDB()->prepare($sql);
823 $statement->execute([$packageUpdateVersionID]);
824 while ($row = $statement->fetchArray()) {
825 if (isset($updates[$row['packageID']])) {
826 $updates = $this->removeUpdateRequirements(
828 $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']
831 Package
::compareVersion(
833 $updates[$row['packageID']]['version']['packageVersion'],
837 unset($updates[$row['packageID']]);
846 * Returns package update versions of the specified package.
848 * @throws SystemException
850 public function getPackageUpdateVersions(string $package, string $version = ''): array
852 $packageUpdateServerIDs = [];
853 foreach (PackageUpdateServer
::getActiveUpdateServers() as $packageUpdateServer) {
854 $packageUpdateServerIDs[] = $packageUpdateServer->packageUpdateServerID
;
857 // get newest package version
858 if (empty($version)) {
859 $version = $this->getNewestPackageVersion($package);
862 if ($version === null) {
863 throw new SystemException("Cannot find the package '" . $package . "'");
867 $conditions = new PreparedStatementConditionBuilder();
868 $conditions->add('pu.package = ?', [$package]);
869 $conditions->add('puv.packageVersion = ?', [$version]);
870 $conditions->add('puv.isAccessible = ?', [1]);
871 $conditions->add('pus.packageUpdateServerID IN (?)', [$packageUpdateServerIDs]);
873 $sql = "SELECT puv.*, pu.*, pus.serverURL, pus.loginUsername, pus.loginPassword
874 FROM wcf1_package_update_version puv
875 LEFT JOIN wcf1_package_update pu
876 ON pu.packageUpdateID = puv.packageUpdateID
877 LEFT JOIN wcf1_package_update_server pus
878 ON pus.packageUpdateServerID = pu.packageUpdateServerID
880 $statement = WCF
::getDB()->prepare($sql);
881 $statement->execute($conditions->getParameters());
882 $versions = $statement->fetchAll(\PDO
::FETCH_ASSOC
);
884 if (empty($versions)) {
885 throw new SystemException("Cannot find the package '" . $package . "' in version '" . $version . "'");
892 * Returns the newest available version of a package.
894 public function getNewestPackageVersion(string $package): ?
string
898 $sql = "SELECT packageVersion
899 FROM wcf1_package_update_version
900 WHERE packageUpdateID IN (
901 SELECT packageUpdateID
902 FROM wcf1_package_update
905 $statement = WCF
::getDB()->prepare($sql);
906 $statement->execute([$package]);
907 while ($row = $statement->fetchArray()) {
908 $versions[$row['packageVersion']] = $row['packageVersion'];
911 // sort by version number
912 \
usort($versions, [Package
::class, 'compareVersion']);
914 // take newest (last)
915 return \array_pop
($versions);
919 * Stores the filename of a download in session.
921 public function cacheDownload(string $package, string $version, string $filename): void
923 $cachedDownloads = WCF
::getSession()->getVar('cachedPackageUpdateDownloads');
924 if (!\
is_array($cachedDownloads)) {
925 $cachedDownloads = [];
929 $cachedDownloads[$package . '@' . $version] = $filename;
930 WCF
::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);