Merge pull request #5987 from WoltLab/acp-dahsboard-box-hight
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageUpdateDispatcher.class.php
CommitLineData
11ade432 1<?php
a9229942 2
11ade432 3namespace wcf\system\package;
a9229942 4
d344d364 5use GuzzleHttp\Exception\ClientException;
94176871 6use GuzzleHttp\Psr7\Request;
d344d364 7use GuzzleHttp\RequestOptions;
94176871 8use Psr\Http\Client\ClientExceptionInterface;
a9229942 9use wcf\data\package\Package;
11ade432
AE
10use wcf\data\package\update\server\PackageUpdateServer;
11use wcf\data\package\update\server\PackageUpdateServerEditor;
b4ae02cc 12use wcf\event\package\PackageUpdateListChanged;
3aa69672 13use wcf\system\cache\builder\PackageUpdateCacheBuilder;
11ade432 14use wcf\system\database\util\PreparedStatementConditionBuilder;
c4a0547d 15use wcf\system\event\EventHandler;
11ade432 16use wcf\system\exception\SystemException;
94176871 17use wcf\system\io\HttpFactory;
d9aed3c6 18use wcf\system\package\validation\PackageValidationException;
adf0710f 19use wcf\system\SingletonFactory;
2bc9f31d 20use wcf\system\WCF;
549b92e7 21use wcf\util\JSON;
6bc77b67 22use wcf\util\StringUtil;
11ade432
AE
23use wcf\util\XML;
24
25/**
26 * Provides functions to manage package updates.
a9229942
TD
27 *
28 * @author Alexander Ebert
29 * @copyright 2001-2019 WoltLab GmbH
30 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
11ade432 31 */
345b80d1 32final class PackageUpdateDispatcher extends SingletonFactory
a9229942 33{
3c2150d3 34 private bool $hasAuthCode = false;
a9229942 35
3c2150d3 36 private array $purchasedVersions = [
a9229942
TD
37 'woltlab' => [],
38 'pluginstore' => [],
39 ];
40
41 /**
42 * Refreshes the package database.
43 *
44 * @param int[] $packageUpdateServerIDs
a9229942 45 */
3c2150d3 46 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], bool $ignoreCache = false)
a9229942
TD
47 {
48 // get update server data
49 $tmp = PackageUpdateServer::getActiveUpdateServers($packageUpdateServerIDs);
50
51 // loop servers
52 $updateServers = [];
a9229942
TD
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;
a9229942
TD
58 }
59
60 $updateServers[] = $updateServer;
61 }
62 }
63
64 if ($requirePurchasedVersions && PACKAGE_SERVER_AUTH_CODE) {
65 $this->getPurchasedVersions();
66 }
67
68 // loop servers
69 $refreshedPackageLists = false;
70 foreach ($updateServers as $updateServer) {
71 $errorMessage = '';
72
73 try {
74 $this->getPackageUpdateXML($updateServer);
75 $refreshedPackageLists = true;
76 } catch (SystemException $e) {
77 $errorMessage = $e->getMessage();
78 } catch (PackageUpdateUnauthorizedException $e) {
d344d364 79 $body = $e->getResponseMessage();
a9229942
TD
80
81 // Try to find the page <title>.
82 if (\preg_match('~<title>(?<title>.*?)</title>~', $body, $matches)) {
83 $errorMessage = $matches['title'];
84 } else {
85 $errorMessage = $body;
86 }
87
88 $errorMessage = \mb_substr(StringUtil::trim(\strip_tags($errorMessage)), 0, 65000);
89 }
90
91 if ($errorMessage) {
92 // save error status
93 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
94 $updateServerEditor->update([
95 'status' => 'offline',
96 'errorMessage' => $errorMessage,
97 ]);
98 }
99 }
100
101 if ($refreshedPackageLists) {
102 PackageUpdateCacheBuilder::getInstance()->reset();
c4a0547d 103 foreach ($updateServers as $updateServer) {
104 EventHandler::getInstance()->fire(
105 new PackageUpdateListChanged($updateServer)
106 );
107 }
a9229942
TD
108 }
109 }
110
641e66bf 111 private function getPurchasedVersions()
a9229942 112 {
94176871
TD
113 $client = HttpFactory::makeClientWithTimeout(5);
114 $request = new Request(
115 'POST',
a9229942 116 'https://api.woltlab.com/1.0/customer/license/list.json',
94176871
TD
117 [
118 'content-type' => 'application/x-www-form-urlencoded',
119 ],
120 \http_build_query([
121 'authCode' => PACKAGE_SERVER_AUTH_CODE,
122 ], '', '&', \PHP_QUERY_RFC1738)
a9229942
TD
123 );
124
125 try {
94176871
TD
126 $response = $client->send($request);
127
128 $reply = JSON::decode((string)$response->getBody());
129
a9229942
TD
130 if ($reply['status'] == 200) {
131 $this->hasAuthCode = true;
132 $this->purchasedVersions = [
133 'woltlab' => ($reply['woltlab'] ?? []),
134 'pluginstore' => ($reply['pluginstore'] ?? []),
135 ];
136 }
b7a9ec4d 137 } catch (ClientExceptionInterface | SystemException) {
a9229942
TD
138 // ignore
139 }
140 }
141
142 /**
143 * Fetches the package_update.xml from an update server.
144 *
a9229942
TD
145 * @throws PackageUpdateUnauthorizedException
146 * @throws SystemException
147 */
641e66bf 148 private function getPackageUpdateXML(PackageUpdateServer $updateServer)
a9229942 149 {
a9229942 150 $authData = $updateServer->getAuthData();
d344d364
MW
151 $options = [];
152 if (!empty($authData)) {
153 $options[RequestOptions::AUTH] = [
154 $authData['username'],
155 $authData['password'],
156 ];
a9229942 157 }
d344d364
MW
158 $client = HttpFactory::makeClient($options);
159 $headers = [];
a9229942 160
07b61d90
TD
161 $requestedVersion = \wcf\getMinorVersion();
162 if (PackageUpdateServer::isUpgradeOverrideEnabled()) {
163 $requestedVersion = WCF::AVAILABLE_UPGRADE_VERSION;
164 }
d344d364 165 $headers['requested-woltlab-suite-version'] = $requestedVersion;
07b61d90 166
a9229942
TD
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
170 if (
171 !\preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL)
172 || !PACKAGE_SERVER_AUTH_CODE
173 ) {
174 $metaData = $updateServer->getMetaData();
175 if (isset($metaData['list']['etag'])) {
d344d364 176 $headers['if-none-match'] = $metaData['list']['etag'];
a9229942
TD
177 }
178 if (isset($metaData['list']['lastModified'])) {
d344d364 179 $headers['if-modified-since'] = $metaData['list']['lastModified'];
a9229942
TD
180 }
181 }
182 }
183
d344d364
MW
184 $request = new Request(
185 'GET',
186 $updateServer->getListURL(),
187 $headers
188 );
a9229942 189
d344d364
MW
190 try {
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(),
197 $updateServer,
198 );
199 }
a9229942 200
d344d364 201 if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 304) {
a9229942 202 throw new SystemException(
d344d364 203 WCF::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' (' . $response->getStatusCode() . ')'
a9229942
TD
204 );
205 }
206
207 $data = [
208 'lastUpdateTime' => TIME_NOW,
209 'status' => 'online',
210 'errorMessage' => '',
211 ];
212
213 // check if server indicates support for a newer API
7a1be0cb
AE
214 if ($updateServer->apiVersion !== '3.1' && $response->getHeader('wcf-update-server-api') !== []) {
215 $apiVersions = \explode(' ', $response->getHeader('wcf-update-server-api')[0]);
a9229942
TD
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';
220 }
221 }
222
223 // parse given package update xml
224 $allNewPackages = false;
d344d364 225 if ($apiVersion === '2.0' || $response->getStatusCode() != 304) {
0b56c50c
C
226 if (!$response->getBody()->getSize()) {
227 throw new SystemException(WCF::getLanguage()->get('wcf.acp.package.update.error.listNotFound'));
228 }
229
d344d364 230 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $response->getBody(), $apiVersion);
a9229942
TD
231 }
232
233 $metaData = [];
234 if (\in_array($apiVersion, ['2.1', '3.1'])) {
7a1be0cb 235 if ($response->getHeader('etag') === [] && $response->getHeader('last-modified') === []) {
a9229942 236 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
a9229942
TD
237 }
238
239 $metaData['list'] = [];
7a1be0cb
AE
240 if ($response->getHeader('etag') !== []) {
241 $metaData['list']['etag'] = $response->getHeader('etag')[0];
a9229942 242 }
7a1be0cb
AE
243 if ($response->getHeader('last-modified') !== []) {
244 $metaData['list']['lastModified'] = $response->getHeader('last-modified')[0];
a9229942 245 }
a9229942
TD
246 }
247 $data['metaData'] = \serialize($metaData);
248
d344d364 249 unset($request, $response);
a9229942
TD
250
251 if ($allNewPackages !== false) {
252 // purge package list
4f12fcf5 253 $sql = "DELETE FROM wcf1_package_update
a9229942 254 WHERE packageUpdateServerID = ?";
4f12fcf5 255 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
256 $statement->execute([$updateServer->packageUpdateServerID]);
257
258 // save packages
259 if (!empty($allNewPackages)) {
260 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID);
261 }
262 unset($allNewPackages);
263 }
264
265 // update server status
266 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
267 $updateServerEditor->update($data);
268 }
269
270 /**
271 * Parses a stream containing info from a packages_update.xml.
272 *
a9229942
TD
273 * @throws SystemException
274 */
3c2150d3 275 private function parsePackageUpdateXML(PackageUpdateServer $updateServer, string $content, string $apiVersion): array
a9229942
TD
276 {
277 $isTrustedServer = $updateServer->isTrustedServer();
278
279 // load xml document
280 $xml = new XML();
281 $xml->loadXML('packageUpdateServer.xml', $content);
282 $xpath = $xml->xpath();
283
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.");
290 }
291
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.");
296 }
297
298 // silently ignore this package to avoid unexpected errors for regular users
299 continue;
300 }
301
302 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
303 }
304
305 return $allNewPackages;
306 }
307
308 /**
309 * Parses the xml structure from a packages_update.xml.
310 *
a9229942
TD
311 * @throws PackageValidationException
312 */
641e66bf 313 private function parsePackageUpdateXMLBlock(
a9229942
TD
314 PackageUpdateServer $updateServer,
315 \DOMXPath $xpath,
316 \DOMElement $package,
3c2150d3
TD
317 string $apiVersion
318 ): array {
a9229942
TD
319 // define default values
320 $packageInfo = [
321 'author' => '',
322 'authorURL' => '',
323 'isApplication' => 0,
324 'packageDescription' => '',
325 'versions' => [],
326 'pluginStoreFileID' => 0,
327 ];
328
329 // parse package information
330 $elements = $xpath->query('./ns:packageinformation/*', $package);
331 foreach ($elements as $element) {
332 switch ($element->tagName) {
333 case 'packagename':
334 $packageInfo['packageName'] = $element->nodeValue;
335 break;
336
337 case 'packagedescription':
338 $packageInfo['packageDescription'] = $element->nodeValue;
339 break;
340
341 case 'isapplication':
342 $packageInfo['isApplication'] = \intval($element->nodeValue);
343 break;
344
345 case 'pluginStoreFileID':
346 if ($updateServer->isWoltLabStoreServer()) {
347 $packageInfo['pluginStoreFileID'] = \intval($element->nodeValue);
348 }
349 break;
350 }
351 }
352
353 // parse author information
354 $elements = $xpath->query('./ns:authorinformation/*', $package);
355 foreach ($elements as $element) {
356 switch ($element->tagName) {
357 case 'author':
358 $packageInfo['author'] = $element->nodeValue;
359 break;
360
361 case 'authorurl':
362 $packageInfo['authorURL'] = $element->nodeValue;
363 break;
364 }
365 }
366
367 $key = '';
368 if ($this->hasAuthCode) {
369 if ($updateServer->isWoltLabUpdateServer()) {
370 $key = 'woltlab';
371 } elseif ($updateServer->isWoltLabStoreServer()) {
372 $key = 'pluginstore';
373 }
374 }
375
376 // parse versions
377 $elements = $xpath->query('./ns:versions/ns:version', $package);
378 /** @var \DOMElement $element */
379 foreach ($elements as $element) {
380 $versionNo = $element->getAttribute('name');
381
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] == '*') {
387 $isAccessible = 1;
388 } else {
389 $isAccessible = (Package::compareVersion(
390 $versionNo,
391 $this->purchasedVersions[$key][$packageName] . '.99',
392 '<='
393 ) ? 1 : 0);
394 }
395 } else {
396 $isAccessible = 0;
397 }
398 }
399
400 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
401
402 $children = $xpath->query('child::*', $element);
403 /** @var \DOMElement $child */
404 foreach ($children as $child) {
405 switch ($child->tagName) {
406 case 'fromversions':
407 $fromversions = $xpath->query('child::*', $child);
408 foreach ($fromversions as $fromversion) {
409 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue;
410 }
411 break;
412
413 case 'timestamp':
414 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue;
415 break;
416
417 case 'file':
418 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue;
419 break;
420
421 case 'requiredpackages':
422 $requiredPackages = $xpath->query('child::*', $child);
423
424 /** @var \DOMElement $requiredPackage */
425 foreach ($requiredPackages as $requiredPackage) {
426 $minVersion = $requiredPackage->getAttribute('minversion');
427 $required = $requiredPackage->nodeValue;
428
429 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
430 if (!empty($minVersion)) {
431 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
432 }
433 }
434 break;
435
436 case 'optionalpackages':
437 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
438
439 $optionalPackages = $xpath->query('child::*', $child);
440 foreach ($optionalPackages as $optionalPackage) {
441 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue;
442 }
443 break;
444
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');
451
cdffd4c7 452 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [
a990af76 453 'version' => $version,
cdffd4c7 454 ];
a9229942
TD
455 }
456 break;
457
458 case 'license':
459 $packageInfo['versions'][$versionNo]['license'] = [
460 'license' => $child->nodeValue,
461 'licenseURL' => $child->hasAttribute('url') ? $child->getAttribute('url') : '',
462 ];
463 break;
464
465 case 'compatibility':
466 if ($apiVersion !== '3.1') {
467 continue 2;
468 }
469
470 $packageInfo['versions'][$versionNo]['compatibility'] = [];
471
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]
481 );
482 } else {
483 continue;
484 }
485 }
486
487 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
488 }
489 }
490 break;
491 }
492 }
493 }
494
495 return $packageInfo;
496 }
497
498 /**
499 * Updates information parsed from a packages_update.xml into the database.
a9229942 500 */
3c2150d3 501 private function savePackageUpdates(array $allNewPackages, int $packageUpdateServerID): void
a9229942 502 {
ec7873a2 503 $excludedPackagesParameters = $requirementInserts = $fromversionInserts = [];
4f12fcf5 504 $sql = "INSERT INTO wcf1_package_update
a9229942
TD
505 (packageUpdateServerID, package, packageName, packageDescription, author, authorURL, isApplication, pluginStoreFileID)
506 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
4f12fcf5 507 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
508 WCF::getDB()->beginTransaction();
509 foreach ($allNewPackages as $identifier => $packageData) {
510 $statement->execute([
511 $packageUpdateServerID,
512 $identifier,
513 $packageData['packageName'],
514 $packageData['packageDescription'],
515 $packageData['author'],
516 $packageData['authorURL'],
517 $packageData['isApplication'],
518 $packageData['pluginStoreFileID'],
519 ]);
520 }
521 WCF::getDB()->commitTransaction();
522
523 $sql = "SELECT packageUpdateID, package
4f12fcf5 524 FROM wcf1_package_update
a9229942 525 WHERE packageUpdateServerID = ?";
4f12fcf5 526 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
527 $statement->execute([$packageUpdateServerID]);
528 $packageUpdateIDs = $statement->fetchMap('package', 'packageUpdateID');
529
4f12fcf5 530 $sql = "INSERT INTO wcf1_package_update_version
a9229942
TD
531 (filename, license, licenseURL, isAccessible, packageDate, packageUpdateID, packageVersion)
532 VALUES (?, ?, ?, ?, ?, ?, ?)";
4f12fcf5 533 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
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],
544 $packageVersion,
545 ]);
546 }
547 }
548 WCF::getDB()->commitTransaction();
549
550 $conditions = new PreparedStatementConditionBuilder();
551 $conditions->add('packageUpdateID IN (?)', [\array_values($packageUpdateIDs)]);
552 $sql = "SELECT packageUpdateVersionID, packageUpdateID, packageVersion
4f12fcf5
TD
553 FROM wcf1_package_update_version
554 {$conditions}";
555 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
556 $statement->execute($conditions->getParameters());
557 $packageUpdateVersions = [];
558 while ($row = $statement->fetchArray()) {
559 if (!isset($packageUpdateVersions[$row['packageUpdateID']])) {
560 $packageUpdateVersions[$row['packageUpdateID']] = [];
561 }
562
563 $packageUpdateVersions[$row['packageUpdateID']][$row['packageVersion']] = $row['packageUpdateVersionID'];
564 }
565
566 foreach ($allNewPackages as $package => $packageData) {
567 foreach ($packageData['versions'] as $packageVersion => $versionData) {
568 $packageUpdateID = $packageUpdateIDs[$package];
569 $packageUpdateVersionID = $packageUpdateVersions[$packageUpdateID][$packageVersion];
570
571 if (isset($versionData['requiredPackages'])) {
572 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
573 $requirementInserts[] = [
574 'packageUpdateVersionID' => $packageUpdateVersionID,
575 'package' => $requiredIdentifier,
576 'minversion' => $required['minversion'] ?? '',
577 ];
578 }
579 }
580
a9229942
TD
581 if (isset($versionData['excludedPackages'])) {
582 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
583 $excludedPackagesParameters[] = [
584 'packageUpdateVersionID' => $packageUpdateVersionID,
585 'excludedPackage' => $excludedIdentifier,
1996ec7a 586 'excludedPackageVersion' => $exclusion['version'],
a9229942
TD
587 ];
588 }
589 }
590
591 if (isset($versionData['fromversions'])) {
592 foreach ($versionData['fromversions'] as $fromversion) {
593 $fromversionInserts[] = [
594 'packageUpdateVersionID' => $packageUpdateVersionID,
595 'fromversion' => $fromversion,
596 ];
597 }
598 }
599
fd351144
TD
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'] = [];
a9229942 605 }
fd351144 606 $excludeCore60 = '6.0.0 Alpha 1';
a9229942 607
fd351144
TD
608 $coreExclude = null;
609 $versionData['excludedPackages'] = \array_filter(
610 $versionData['excludedPackages'],
611 static function ($excludedPackage, $excludedVersion) use (&$coreExclude) {
612 if ($excludedPackage === 'com.woltlab.wcf') {
613 $coreExclude = $excludedVersion;
a9229942 614
fd351144
TD
615 return false;
616 }
a9229942 617
fd351144
TD
618 return true;
619 },
620 \ARRAY_FILTER_USE_BOTH
621 );
a9229942 622
fd351144
TD
623 if ($coreExclude === null || Package::compareVersion($coreExclude, $excludeCore60, '>')) {
624 $versionData['excludedPackages'][] = [
625 'packageUpdateVersionID' => $packageUpdateVersionID,
626 'excludedPackage' => 'com.woltlab.wcf',
627 'excludedPackageVersion' => $excludeCore60,
628 ];
a9229942
TD
629 }
630 }
631 }
632 }
633
ec7873a2
TD
634 $sql = "INSERT INTO wcf1_package_update_requirement
635 (packageUpdateVersionID, package, minversion)
636 VALUES (?, ?, ?)";
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'],
644 ]);
a9229942 645 }
ec7873a2 646 WCF::getDB()->commitTransaction();
a9229942 647
ec7873a2
TD
648 $sql = "INSERT INTO wcf1_package_update_exclusion
649 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
650 VALUES (?, ?, ?)";
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'],
658 ]);
a9229942 659 }
ec7873a2 660 WCF::getDB()->commitTransaction();
a9229942 661
ec7873a2
TD
662 $sql = "INSERT INTO wcf1_package_update_fromversion
663 (packageUpdateVersionID, fromversion)
664 VALUES (?, ?)";
665 $statement = WCF::getDB()->prepare($sql);
666 WCF::getDB()->beginTransaction();
667 foreach ($fromversionInserts as $fromversion) {
668 $statement->execute([
669 $fromversion['packageUpdateVersionID'],
670 $fromversion['fromversion'],
671 ]);
a9229942 672 }
ec7873a2 673 WCF::getDB()->commitTransaction();
a9229942
TD
674 }
675
676 /**
677 * Returns a list of available updates for installed packages.
678 *
a9229942
TD
679 * @return array
680 * @throws SystemException
681 */
3c2150d3 682 public function getAvailableUpdates(bool $removeRequirements = true, bool $removeOlderMinorReleases = false): array
a9229942
TD
683 {
684 $updates = [];
685
686 // get update server data
687 $updateServers = PackageUpdateServer::getActiveUpdateServers();
688 $packageUpdateServerIDs = \array_keys($updateServers);
689 if (empty($packageUpdateServerIDs)) {
690 return $updates;
691 }
692
693 // get existing packages and their versions
694 $existingPackages = [];
695 $sql = "SELECT packageID, package, packageDescription, packageName,
696 packageVersion, packageDate, author, authorURL, isApplication
4f12fcf5
TD
697 FROM wcf1_package";
698 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
699 $statement->execute();
700 while ($row = $statement->fetchArray()) {
701 $existingPackages[$row['package']][] = $row;
702 }
703 if (empty($existingPackages)) {
704 return $updates;
705 }
706
707 // get all update versions
708 $conditions = new PreparedStatementConditionBuilder();
709 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
94d12f75
MS
710 $conditions->add("package IN (
711 SELECT DISTINCT package
4f12fcf5 712 FROM wcf1_package
94d12f75 713 )");
a9229942
TD
714
715 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
716 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
4f12fcf5
TD
717 FROM wcf1_package_update pu
718 INNER JOIN wcf1_package_update_version puv
c240c98a
MS
719 ON puv.packageUpdateID = pu.packageUpdateID
720 AND puv.isAccessible = 1
4f12fcf5
TD
721 {$conditions}";
722 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
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).");
728 }
729
730 // case-mismatch, skip the update
731 continue;
732 }
733
734 // test version
735 foreach ($existingPackages[$row['package']] as $existingVersion) {
736 if (Package::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
737 // package data
738 if (!isset($updates[$existingVersion['packageID']])) {
739 $existingVersion['versions'] = [];
740 $updates[$existingVersion['packageID']] = $existingVersion;
741 }
742
743 // version data
744 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
745 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
746 'packageDate' => $row['packageDate'],
747 'packageVersion' => $row['packageVersion'],
748 'servers' => [],
749 ];
750 }
751
752 // server data
753 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
754 'packageUpdateID' => $row['packageUpdateID'],
755 'packageUpdateServerID' => $row['packageUpdateServerID'],
756 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
757 'filename' => $row['filename'],
758 ];
759 }
760 }
761 }
762
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']);
768 }
769
770 // remove requirements of application packages
771 if ($removeRequirements) {
13b11e4c 772 foreach ($existingPackages as $instances) {
a9229942
TD
773 foreach ($instances as $instance) {
774 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
775 $updates = $this->removeUpdateRequirements(
776 $updates,
777 $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']
778 );
779 }
780 }
781 }
782 }
783
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;
796 } else {
797 // version is lower, discard
798 unset($updateData['versions'][$versionNumber]);
799 }
800 } else {
801 $highestVersions[$major] = $versionNumber;
802 }
803 }
804 }
805 }
806 unset($updateData);
807 }
808
809 return $updates;
810 }
811
812 /**
813 * Removes unnecessary updates of requirements from the list of available updates.
a9229942 814 */
3c2150d3 815 private function removeUpdateRequirements(array $updates, int $packageUpdateVersionID): array
a9229942
TD
816 {
817 $sql = "SELECT pur.package, pur.minversion, p.packageID
4f12fcf5
TD
818 FROM wcf1_package_update_requirement pur
819 LEFT JOIN wcf1_package p
c240c98a 820 ON p.package = pur.package
a9229942 821 WHERE pur.packageUpdateVersionID = ?";
4f12fcf5 822 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
823 $statement->execute([$packageUpdateVersionID]);
824 while ($row = $statement->fetchArray()) {
825 if (isset($updates[$row['packageID']])) {
826 $updates = $this->removeUpdateRequirements(
827 $updates,
828 $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']
829 );
830 if (
831 Package::compareVersion(
832 $row['minversion'],
833 $updates[$row['packageID']]['version']['packageVersion'],
834 '>='
835 )
836 ) {
837 unset($updates[$row['packageID']]);
838 }
839 }
840 }
841
842 return $updates;
843 }
844
a9229942
TD
845 /**
846 * Returns package update versions of the specified package.
847 *
a9229942
TD
848 * @throws SystemException
849 */
3c2150d3 850 public function getPackageUpdateVersions(string $package, string $version = ''): array
a9229942
TD
851 {
852 $packageUpdateServerIDs = [];
853 foreach (PackageUpdateServer::getActiveUpdateServers() as $packageUpdateServer) {
854 $packageUpdateServerIDs[] = $packageUpdateServer->packageUpdateServerID;
855 }
856
857 // get newest package version
858 if (empty($version)) {
859 $version = $this->getNewestPackageVersion($package);
860 }
861
4fc606ad
AE
862 if ($version === null) {
863 throw new SystemException("Cannot find the package '" . $package . "'");
864 }
865
a9229942
TD
866 // get versions
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]);
872
873 $sql = "SELECT puv.*, pu.*, pus.serverURL, pus.loginUsername, pus.loginPassword
4f12fcf5
TD
874 FROM wcf1_package_update_version puv
875 LEFT JOIN wcf1_package_update pu
c240c98a 876 ON pu.packageUpdateID = puv.packageUpdateID
4f12fcf5 877 LEFT JOIN wcf1_package_update_server pus
c240c98a 878 ON pus.packageUpdateServerID = pu.packageUpdateServerID
4f12fcf5
TD
879 {$conditions}";
880 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
881 $statement->execute($conditions->getParameters());
882 $versions = $statement->fetchAll(\PDO::FETCH_ASSOC);
883
884 if (empty($versions)) {
4fc606ad 885 throw new SystemException("Cannot find the package '" . $package . "' in version '" . $version . "'");
a9229942
TD
886 }
887
888 return $versions;
889 }
890
891 /**
892 * Returns the newest available version of a package.
a9229942 893 */
4fc606ad 894 public function getNewestPackageVersion(string $package): ?string
a9229942
TD
895 {
896 // get all versions
897 $versions = [];
898 $sql = "SELECT packageVersion
4f12fcf5 899 FROM wcf1_package_update_version
a9229942
TD
900 WHERE packageUpdateID IN (
901 SELECT packageUpdateID
4f12fcf5 902 FROM wcf1_package_update
a9229942
TD
903 WHERE package = ?
904 )";
4f12fcf5 905 $statement = WCF::getDB()->prepare($sql);
a9229942
TD
906 $statement->execute([$package]);
907 while ($row = $statement->fetchArray()) {
908 $versions[$row['packageVersion']] = $row['packageVersion'];
909 }
910
911 // sort by version number
912 \usort($versions, [Package::class, 'compareVersion']);
913
914 // take newest (last)
915 return \array_pop($versions);
916 }
917
918 /**
919 * Stores the filename of a download in session.
a9229942 920 */
3c2150d3 921 public function cacheDownload(string $package, string $version, string $filename): void
a9229942
TD
922 {
923 $cachedDownloads = WCF::getSession()->getVar('cachedPackageUpdateDownloads');
924 if (!\is_array($cachedDownloads)) {
925 $cachedDownloads = [];
926 }
927
928 // store in session
929 $cachedDownloads[$package . '@' . $version] = $filename;
930 WCF::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);
931 }
11ade432 932}