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\PackageUpdateEditor
;
7 use wcf\data\package\Package
;
8 use wcf\system\cache\builder\PackageUpdateCacheBuilder
;
9 use wcf\system\database\util\PreparedStatementConditionBuilder
;
10 use wcf\system\exception\HTTPUnauthorizedException
;
11 use wcf\system\exception\SystemException
;
12 use wcf\system\io\RemoteFile
;
13 use wcf\system\package\validation\PackageValidationException
;
14 use wcf\system\SingletonFactory
;
16 use wcf\util\HTTPRequest
;
21 * Provides functions to manage package updates.
23 * @author Alexander Ebert
24 * @copyright 2001-2018 WoltLab GmbH
25 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
26 * @package WoltLabSuite\Core\System\Package
28 class PackageUpdateDispatcher
extends SingletonFactory
{
29 protected $hasAuthCode = false;
30 protected $purchasedVersions = [
36 * Refreshes the package database.
38 * @param integer[] $packageUpdateServerIDs
39 * @param boolean $ignoreCache
41 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], $ignoreCache = false) {
42 // get update server data
43 $tmp = PackageUpdateServer
::getActiveUpdateServers($packageUpdateServerIDs);
47 $foundWoltLabServer = false;
48 $requirePurchasedVersions = false;
49 foreach ($tmp as $updateServer) {
50 if ($ignoreCache ||
$updateServer->lastUpdateTime
< TIME_NOW
- 600) {
51 if (preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)) {
52 $requirePurchasedVersions = true;
54 // move a woltlab.com update server to the front of the queue to probe for SSL support
55 if (!$foundWoltLabServer) {
56 array_unshift($updateServers, $updateServer);
57 $foundWoltLabServer = true;
63 $updateServers[] = $updateServer;
67 if ($requirePurchasedVersions && PACKAGE_SERVER_AUTH_CODE
) {
68 $this->getPurchasedVersions();
72 $refreshedPackageLists = false;
73 foreach ($updateServers as $updateServer) {
77 $this->getPackageUpdateXML($updateServer);
78 $refreshedPackageLists = true;
80 catch (SystemException
$e) {
81 $errorMessage = $e->getMessage();
83 catch (PackageUpdateUnauthorizedException
$e) {
84 $reply = $e->getRequest()->getReply();
85 list($errorMessage) = reset($reply['httpHeaders']);
90 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
91 $updateServerEditor->update([
92 'status' => 'offline',
93 'errorMessage' => $errorMessage
98 if ($refreshedPackageLists) {
99 PackageUpdateCacheBuilder
::getInstance()->reset();
103 protected function getPurchasedVersions() {
104 if (!RemoteFile
::supportsSSL()) {
108 $request = new HTTPRequest(
109 'https://api.woltlab.com/1.0/customer/license/list.json',
111 ['authCode' => PACKAGE_SERVER_AUTH_CODE
]
116 $reply = JSON
::decode($request->getReply()['body']);
117 if ($reply['status'] == 200) {
118 $this->hasAuthCode
= true;
119 $this->purchasedVersions
= [
120 'woltlab' => (isset($reply['woltlab']) ?
$reply['woltlab'] : []),
121 'pluginstore' => (isset($reply['pluginstore']) ?
$reply['pluginstore'] : [])
125 catch (SystemException
$e) {
131 * Fetches the package_update.xml from an update server.
133 * @param PackageUpdateServer $updateServer
134 * @param boolean $forceHTTP
135 * @throws PackageUpdateUnauthorizedException
136 * @throws SystemException
138 protected function getPackageUpdateXML(PackageUpdateServer
$updateServer, $forceHTTP = false) {
140 $authData = $updateServer->getAuthData();
141 if ($authData) $settings['auth'] = $authData;
143 $secureConnection = $updateServer->attemptSecureConnection();
144 if ($secureConnection && !$forceHTTP) $settings['timeout'] = 5;
146 $request = new HTTPRequest($updateServer->getListURL($forceHTTP), $settings);
148 $apiVersion = $updateServer->apiVersion
;
149 if (in_array($apiVersion, ['2.1', '3.1'])) {
150 // skip etag check for WoltLab servers when an auth code is provided
151 if (!preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
) ||
!PACKAGE_SERVER_AUTH_CODE
) {
152 $metaData = $updateServer->getMetaData();
153 if (isset($metaData['list']['etag'])) $request->addHeader('if-none-match', $metaData['list']['etag']);
154 if (isset($metaData['list']['lastModified'])) $request->addHeader('if-modified-since', $metaData['list']['lastModified']);
160 $reply = $request->getReply();
162 catch (HTTPUnauthorizedException
$e) {
163 throw new PackageUpdateUnauthorizedException($request, $updateServer);
165 catch (SystemException
$e) {
166 $reply = $request->getReply();
168 $statusCode = is_array($reply['statusCode']) ?
reset($reply['statusCode']) : $reply['statusCode'];
169 // status code 0 is a connection timeout
170 if (!$statusCode && $secureConnection) {
171 if (preg_match('~https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL
)) {
172 // woltlab.com servers are most likely to be available, thus we assume that SSL connections are dropped
173 RemoteFile
::disableSSL();
177 $this->getPackageUpdateXML($updateServer, true);
181 throw new SystemException(WCF
::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' ('.$statusCode.')');
185 'lastUpdateTime' => TIME_NOW
,
186 'status' => 'online',
190 // check if server indicates support for a newer API
191 if ($updateServer->apiVersion
!== '3.1' && !empty($reply['httpHeaders']['wcf-update-server-api'])) {
192 $apiVersions = explode(' ', reset($reply['httpHeaders']['wcf-update-server-api']));
193 if (in_array('3.1', $apiVersions)) {
194 $apiVersion = $data['apiVersion'] = '3.1';
196 else if (in_array('2.1', $apiVersions)) {
197 $apiVersion = $data['apiVersion'] = '2.1';
201 // parse given package update xml
202 $allNewPackages = false;
203 if ($apiVersion === '2.0' ||
$reply['statusCode'] != 304) {
204 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $reply['body'], $apiVersion);
208 if (in_array($apiVersion, ['2.1', '3.1'])) {
209 if (empty($reply['httpHeaders']['etag']) && empty($reply['httpHeaders']['last-modified'])) {
210 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
212 else if (empty($reply['httpHeaders']['wcf-update-server-ssl'])) {
213 throw new SystemException("Missing required HTTP header 'wcf-update-server-ssl'.");
216 $metaData['list'] = [];
217 if (!empty($reply['httpHeaders']['etag'])) $metaData['list']['etag'] = reset($reply['httpHeaders']['etag']);
218 if (!empty($reply['httpHeaders']['last-modified'])) $metaData['list']['lastModified'] = reset($reply['httpHeaders']['last-modified']);
220 $metaData['ssl'] = (reset($reply['httpHeaders']['wcf-update-server-ssl']) == 'true') ?
true : false;
222 $data['metaData'] = serialize($metaData);
224 unset($request, $reply);
226 if ($allNewPackages !== false) {
227 // purge package list
228 $sql = "DELETE FROM wcf".WCF_N
."_package_update
229 WHERE packageUpdateServerID = ?";
230 $statement = WCF
::getDB()->prepareStatement($sql);
231 $statement->execute([$updateServer->packageUpdateServerID
]);
234 if (!empty($allNewPackages)) {
235 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID
);
237 unset($allNewPackages);
240 // update server status
241 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
242 $updateServerEditor->update($data);
246 * Parses a stream containing info from a packages_update.xml.
248 * @param PackageUpdateServer $updateServer
249 * @param string $content
250 * @param string $apiVersion
252 * @throws SystemException
254 protected function parsePackageUpdateXML(PackageUpdateServer
$updateServer, $content, $apiVersion) {
255 $isTrustedServer = $updateServer->isTrustedServer();
259 $xml->loadXML('packageUpdateServer.xml', $content);
260 $xpath = $xml->xpath();
262 $allNewPackages = [];
263 $packages = $xpath->query('/ns:section/ns:package');
264 /** @var \DOMElement $package */
265 foreach ($packages as $package) {
266 if (!Package
::isValidPackageName($package->getAttribute('name'))) {
267 throw new SystemException("'".$package->getAttribute('name')."' is not a valid package name.");
270 $name = $package->getAttribute('name');
271 if (strpos($name, 'com.woltlab.') === 0 && !$isTrustedServer) {
272 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
273 throw new SystemException("The server '".$updateServer->serverURL
."' attempted to provide an official WoltLab package, but is not authorized.");
276 // silently ignore this package to avoid unexpected errors for regular users
280 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
283 return $allNewPackages;
287 * Parses the xml structure from a packages_update.xml.
289 * @param PackageUpdateServer $updateServer
290 * @param \DOMXPath $xpath
291 * @param \DOMElement $package
292 * @param string $apiVersion
294 * @throws PackageValidationException
296 protected function parsePackageUpdateXMLBlock(PackageUpdateServer
$updateServer, \DOMXPath
$xpath, \DOMElement
$package, $apiVersion) {
297 // define default values
301 'isApplication' => 0,
302 'packageDescription' => '',
304 'pluginStoreFileID' => 0
307 // parse package information
308 $elements = $xpath->query('./ns:packageinformation/*', $package);
309 foreach ($elements as $element) {
310 switch ($element->tagName
) {
312 $packageInfo['packageName'] = $element->nodeValue
;
315 case 'packagedescription':
316 $packageInfo['packageDescription'] = $element->nodeValue
;
319 case 'isapplication':
320 $packageInfo['isApplication'] = intval($element->nodeValue
);
323 case 'pluginStoreFileID':
324 if ($updateServer->isWoltLabStoreServer()) {
325 $packageInfo['pluginStoreFileID'] = intval($element->nodeValue
);
331 // parse author information
332 $elements = $xpath->query('./ns:authorinformation/*', $package);
333 foreach ($elements as $element) {
334 switch ($element->tagName
) {
336 $packageInfo['author'] = $element->nodeValue
;
340 $packageInfo['authorURL'] = $element->nodeValue
;
346 if ($this->hasAuthCode
) {
347 if ($updateServer->isWoltLabUpdateServer()) $key = 'woltlab';
348 else if ($updateServer->isWoltLabStoreServer()) $key = 'pluginstore';
352 $elements = $xpath->query('./ns:versions/ns:version', $package);
353 /** @var \DOMElement $element */
354 foreach ($elements as $element) {
355 $versionNo = $element->getAttribute('name');
357 $isAccessible = ($element->getAttribute('accessible') == 'true') ?
1 : 0;
358 if ($key && $element->getAttribute('requireAuth') == 'true') {
359 $packageName = $package->getAttribute('name');
360 if (isset($this->purchasedVersions
[$key][$packageName])) {
361 if ($this->purchasedVersions
[$key][$packageName] == '*') {
365 $isAccessible = (Package
::compareVersion($versionNo, $this->purchasedVersions
[$key][$packageName] . '99', '<=') ?
1 : 0);
373 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
375 $children = $xpath->query('child::*', $element);
376 /** @var \DOMElement $child */
377 foreach ($children as $child) {
378 switch ($child->tagName
) {
380 $fromversions = $xpath->query('child::*', $child);
381 foreach ($fromversions as $fromversion) {
382 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue
;
387 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue
;
391 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue
;
394 case 'requiredpackages':
395 $requiredPackages = $xpath->query('child::*', $child);
397 /** @var \DOMElement $requiredPackage */
398 foreach ($requiredPackages as $requiredPackage) {
399 $minVersion = $requiredPackage->getAttribute('minversion');
400 $required = $requiredPackage->nodeValue
;
402 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
403 if (!empty($minVersion)) {
404 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
409 case 'optionalpackages':
410 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
412 $optionalPackages = $xpath->query('child::*', $child);
413 foreach ($optionalPackages as $optionalPackage) {
414 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue
;
418 case 'excludedpackages':
419 $excludedpackages = $xpath->query('child::*', $child);
420 /** @var \DOMElement $excludedPackage */
421 foreach ($excludedpackages as $excludedPackage) {
422 $exclusion = $excludedPackage->nodeValue
;
423 $version = $excludedPackage->getAttribute('version');
425 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [];
426 if (!empty($version)) {
427 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion]['version'] = $version;
433 $packageInfo['versions'][$versionNo]['license'] = [
434 'license' => $child->nodeValue
,
435 'licenseURL' => $child->hasAttribute('url') ?
$child->getAttribute('url') : ''
439 case 'compatibility':
440 if ($apiVersion !== '3.1') continue;
442 $packageInfo['versions'][$versionNo]['compatibility'] = [];
444 /** @var \DOMElement $compatibleVersion */
445 foreach ($xpath->query('child::*', $child) as $compatibleVersion) {
446 if ($compatibleVersion->nodeName
=== 'api' && $compatibleVersion->hasAttribute('version')) {
447 $versionNumber = $compatibleVersion->getAttribute('version');
448 if (!preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $versionNumber)) {
449 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
450 throw new PackageValidationException(PackageValidationException
::INVALID_API_VERSION
, ['version' => $versionNumber]);
457 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
469 * Updates information parsed from a packages_update.xml into the database.
471 * @param array $allNewPackages
472 * @param integer $packageUpdateServerID
474 protected function savePackageUpdates(array &$allNewPackages, $packageUpdateServerID) {
476 $excludedPackagesParameters = $fromversionParameters = $insertParameters = $optionalInserts = $requirementInserts = $compatibilityInserts = [];
477 WCF
::getDB()->beginTransaction();
478 foreach ($allNewPackages as $identifier => $packageData) {
479 // create new database entry
480 $packageUpdate = PackageUpdateEditor
::create([
481 'packageUpdateServerID' => $packageUpdateServerID,
482 'package' => $identifier,
483 'packageName' => $packageData['packageName'],
484 'packageDescription' => $packageData['packageDescription'],
485 'author' => $packageData['author'],
486 'authorURL' => $packageData['authorURL'],
487 'isApplication' => $packageData['isApplication'],
488 'pluginStoreFileID' => $packageData['pluginStoreFileID']
491 $packageUpdateID = $packageUpdate->packageUpdateID
;
493 // register version(s) of this update package.
494 if (isset($packageData['versions'])) {
495 foreach ($packageData['versions'] as $packageVersion => $versionData) {
496 if (isset($versionData['file'])) $packageFile = $versionData['file'];
497 else $packageFile = '';
499 // create new database entry
500 $version = PackageUpdateVersionEditor
::create([
501 'filename' => $packageFile,
502 'license' => isset($versionData['license']['license']) ?
$versionData['license']['license'] : '',
503 'licenseURL' => isset($versionData['license']['license']) ?
$versionData['license']['licenseURL'] : '',
504 'isAccessible' => $versionData['isAccessible'] ?
1 : 0,
505 'packageDate' => $versionData['packageDate'],
506 'packageUpdateID' => $packageUpdateID,
507 'packageVersion' => $packageVersion
510 $packageUpdateVersionID = $version->packageUpdateVersionID
;
512 // register requirement(s) of this update package version.
513 if (isset($versionData['requiredPackages'])) {
514 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
515 $requirementInserts[] = [
516 'packageUpdateVersionID' => $packageUpdateVersionID,
517 'package' => $requiredIdentifier,
518 'minversion' => isset($required['minversion']) ?
$required['minversion'] : ''
523 // register optional packages of this update package version
524 if (isset($versionData['optionalPackages'])) {
525 foreach ($versionData['optionalPackages'] as $optionalPackage) {
526 $optionalInserts[] = [
527 'packageUpdateVersionID' => $packageUpdateVersionID,
528 'package' => $optionalPackage
533 // register excluded packages of this update package version.
534 if (isset($versionData['excludedPackages'])) {
535 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
536 $excludedPackagesParameters[] = [
537 'packageUpdateVersionID' => $packageUpdateVersionID,
538 'excludedPackage' => $excludedIdentifier,
539 'excludedPackageVersion' => isset($exclusion['version']) ?
$exclusion['version'] : ''
544 // register fromversions of this update package version.
545 if (isset($versionData['fromversions'])) {
546 foreach ($versionData['fromversions'] as $fromversion) {
547 $fromversionInserts[] = [
548 'packageUpdateVersionID' => $packageUpdateVersionID,
549 'fromversion' => $fromversion
554 // register compatibility versions of this update package version.
555 if (isset($versionData['compatibility'])) {
556 foreach ($versionData['compatibility'] as $version) {
557 $compatibilityInserts[] = [
558 'packageUpdateVersionID' => $packageUpdateVersionID,
559 'version' => $version
566 WCF
::getDB()->commitTransaction();
568 // save requirements, excluded packages and fromversions
569 // insert requirements
570 if (!empty($requirementInserts)) {
571 $sql = "INSERT INTO wcf".WCF_N
."_package_update_requirement
572 (packageUpdateVersionID, package, minversion)
574 $statement = WCF
::getDB()->prepareStatement($sql);
575 WCF
::getDB()->beginTransaction();
576 foreach ($requirementInserts as $requirement) {
577 $statement->execute([
578 $requirement['packageUpdateVersionID'],
579 $requirement['package'],
580 $requirement['minversion']
583 WCF
::getDB()->commitTransaction();
587 if (!empty($optionalInserts)) {
588 $sql = "INSERT INTO wcf".WCF_N
."_package_update_optional
589 (packageUpdateVersionID, package)
591 $statement = WCF
::getDB()->prepareStatement($sql);
592 WCF
::getDB()->beginTransaction();
593 foreach ($optionalInserts as $requirement) {
594 $statement->execute([
595 $requirement['packageUpdateVersionID'],
596 $requirement['package']
599 WCF
::getDB()->commitTransaction();
603 if (!empty($excludedPackagesParameters)) {
604 $sql = "INSERT INTO wcf".WCF_N
."_package_update_exclusion
605 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
607 $statement = WCF
::getDB()->prepareStatement($sql);
608 WCF
::getDB()->beginTransaction();
609 foreach ($excludedPackagesParameters as $excludedPackage) {
610 $statement->execute([
611 $excludedPackage['packageUpdateVersionID'],
612 $excludedPackage['excludedPackage'],
613 $excludedPackage['excludedPackageVersion']
616 WCF
::getDB()->commitTransaction();
619 // insert fromversions
620 if (!empty($fromversionInserts)) {
621 $sql = "INSERT INTO wcf".WCF_N
."_package_update_fromversion
622 (packageUpdateVersionID, fromversion)
624 $statement = WCF
::getDB()->prepareStatement($sql);
625 WCF
::getDB()->beginTransaction();
626 foreach ($fromversionInserts as $fromversion) {
627 $statement->execute([
628 $fromversion['packageUpdateVersionID'],
629 $fromversion['fromversion']
632 WCF
::getDB()->commitTransaction();
635 // insert compatibility versions
636 if (!empty($compatibilityInserts)) {
637 $sql = "INSERT INTO wcf".WCF_N
."_package_update_compatibility
638 (packageUpdateVersionID, version)
640 $statement = WCF
::getDB()->prepareStatement($sql);
641 WCF
::getDB()->beginTransaction();
642 foreach ($compatibilityInserts as $versionData) {
643 $statement->execute([
644 $versionData['packageUpdateVersionID'],
645 $versionData['version']
648 WCF
::getDB()->commitTransaction();
653 * Returns a list of available updates for installed packages.
655 * @param boolean $removeRequirements
656 * @param boolean $removeOlderMinorReleases
658 * @throws SystemException
660 public function getAvailableUpdates($removeRequirements = true, $removeOlderMinorReleases = false) {
663 // get update server data
664 $updateServers = PackageUpdateServer
::getActiveUpdateServers();
665 $packageUpdateServerIDs = array_keys($updateServers);
666 if (empty($packageUpdateServerIDs)) return $updates;
668 // get existing packages and their versions
669 $existingPackages = [];
670 $sql = "SELECT packageID, package, packageDescription, packageName,
671 packageVersion, packageDate, author, authorURL, isApplication
672 FROM wcf".WCF_N
."_package";
673 $statement = WCF
::getDB()->prepareStatement($sql);
674 $statement->execute();
675 while ($row = $statement->fetchArray()) {
676 $existingPackages[$row['package']][] = $row;
678 if (empty($existingPackages)) return $updates;
680 // get all update versions
681 $conditions = new PreparedStatementConditionBuilder();
682 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
683 $conditions->add("package IN (SELECT DISTINCT package FROM wcf".WCF_N
."_package)");
685 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
686 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
687 FROM wcf".WCF_N
."_package_update pu
688 LEFT JOIN wcf".WCF_N
."_package_update_version puv
689 ON (puv.packageUpdateID = pu.packageUpdateID AND puv.isAccessible = 1)
691 $statement = WCF
::getDB()->prepareStatement($sql);
692 $statement->execute($conditions->getParameters());
693 while ($row = $statement->fetchArray()) {
694 if (!isset($existingPackages[$row['package']])) {
695 if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
) {
696 throw new SystemException("Invalid package update data, identifier '" . $row['package'] . "' does not match any installed package (case-mismatch).");
699 // case-mismatch, skip the update
704 foreach ($existingPackages[$row['package']] as $existingVersion) {
705 if (Package
::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
707 if (!isset($updates[$existingVersion['packageID']])) {
708 $existingVersion['versions'] = [];
709 $updates[$existingVersion['packageID']] = $existingVersion;
713 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
714 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
715 'packageDate' => $row['packageDate'],
716 'packageVersion' => $row['packageVersion'],
722 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
723 'packageUpdateID' => $row['packageUpdateID'],
724 'packageUpdateServerID' => $row['packageUpdateServerID'],
725 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
726 'filename' => $row['filename']
732 // sort package versions
733 // and remove old versions
734 foreach ($updates as $packageID => $data) {
735 uksort($updates[$packageID]['versions'], ['wcf\data\package\Package', 'compareVersion']);
736 $updates[$packageID]['version'] = end($updates[$packageID]['versions']);
739 // remove requirements of application packages
740 if ($removeRequirements) {
741 foreach ($existingPackages as $identifier => $instances) {
742 foreach ($instances as $instance) {
743 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
744 $updates = $this->removeUpdateRequirements($updates, $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
750 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
751 if ($removeOlderMinorReleases) {
752 foreach ($updates as &$updateData) {
753 $highestVersions = [];
754 foreach ($updateData['versions'] as $versionNumber => $dummy) {
755 if (preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
756 $major = $matches[1];
757 if (isset($highestVersions[$major])) {
758 if (Package
::compareVersion($highestVersions[$major], $versionNumber, '<')) {
759 // version is newer, discard current version
760 unset($updateData['versions'][$highestVersions[$major]]);
761 $highestVersions[$major] = $versionNumber;
764 // version is lower, discard
765 unset($updateData['versions'][$versionNumber]);
769 $highestVersions[$major] = $versionNumber;
781 * Removes unnecessary updates of requirements from the list of available updates.
783 * @param array $updates
784 * @param integer $packageUpdateVersionID
785 * @return array $updates
787 protected function removeUpdateRequirements(array $updates, $packageUpdateVersionID) {
788 $sql = "SELECT pur.package, pur.minversion, p.packageID
789 FROM wcf".WCF_N
."_package_update_requirement pur
790 LEFT JOIN wcf".WCF_N
."_package p
791 ON (p.package = pur.package)
792 WHERE pur.packageUpdateVersionID = ?";
793 $statement = WCF
::getDB()->prepareStatement($sql);
794 $statement->execute([$packageUpdateVersionID]);
795 while ($row = $statement->fetchArray()) {
796 if (isset($updates[$row['packageID']])) {
797 $updates = $this->removeUpdateRequirements($updates, $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']);
798 if (Package
::compareVersion($row['minversion'], $updates[$row['packageID']]['version']['packageVersion'], '>=')) {
799 unset($updates[$row['packageID']]);
808 * Creates a new package installation scheduler.
810 * @param array $selectedPackages
811 * @return PackageInstallationScheduler
813 public function prepareInstallation(array $selectedPackages) {
814 return new PackageInstallationScheduler($selectedPackages);
818 * Returns package update versions of the specified package.
820 * @param string $package package identifier
821 * @param string $version package version
822 * @return array package update versions
823 * @throws SystemException
825 public function getPackageUpdateVersions($package, $version = '') {
826 // get newest package version
827 if (empty($version)) {
828 $version = $this->getNewestPackageVersion($package);
832 $sql = "SELECT puv.*, pu.*, pus.loginUsername, pus.loginPassword
833 FROM wcf".WCF_N
."_package_update_version puv
834 LEFT JOIN wcf".WCF_N
."_package_update pu
835 ON (pu.packageUpdateID = puv.packageUpdateID)
836 LEFT JOIN wcf".WCF_N
."_package_update_server pus
837 ON (pus.packageUpdateServerID = pu.packageUpdateServerID)
839 AND puv.packageVersion = ?
840 AND puv.isAccessible = ?
841 AND pus.isDisabled = ?";
842 $statement = WCF
::getDB()->prepareStatement($sql);
843 $statement->execute([
849 $versions = $statement->fetchAll(\PDO
::FETCH_ASSOC
);
851 if (empty($versions)) {
852 throw new SystemException("Cannot find package '".$package."' in version '".$version."'");
859 * Returns the newest available version of a package.
861 * @param string $package package identifier
862 * @return string newest package version
864 public function getNewestPackageVersion($package) {
867 $sql = "SELECT packageVersion
868 FROM wcf".WCF_N
."_package_update_version
869 WHERE packageUpdateID IN (
870 SELECT packageUpdateID
871 FROM wcf".WCF_N
."_package_update
874 $statement = WCF
::getDB()->prepareStatement($sql);
875 $statement->execute([$package]);
876 while ($row = $statement->fetchArray()) {
877 $versions[$row['packageVersion']] = $row['packageVersion'];
880 // sort by version number
881 usort($versions, [Package
::class, 'compareVersion']);
883 // take newest (last)
884 return array_pop($versions);
888 * Stores the filename of a download in session.
890 * @param string $package package identifier
891 * @param string $version package version
892 * @param string $filename
894 public function cacheDownload($package, $version, $filename) {
895 $cachedDownloads = WCF
::getSession()->getVar('cachedPackageUpdateDownloads');
896 if (!is_array($cachedDownloads)) {
897 $cachedDownloads = [];
901 $cachedDownloads[$package.'@'.$version] = $filename;
902 WCF
::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);