Add the new variable `FileProcessorFormField::$bigPreview` with getter and setter.
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageUpdateDispatcher.class.php
1 <?php
2
3 namespace wcf\system\package;
4
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;
19 use wcf\system\WCF;
20 use wcf\util\HTTPRequest;
21 use wcf\util\JSON;
22 use wcf\util\StringUtil;
23 use wcf\util\XML;
24
25 /**
26 * Provides functions to manage package updates.
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>
31 */
32 final class PackageUpdateDispatcher extends SingletonFactory
33 {
34 private bool $hasAuthCode = false;
35
36 private array $purchasedVersions = [
37 'woltlab' => [],
38 'pluginstore' => [],
39 ];
40
41 /**
42 * Refreshes the package database.
43 *
44 * @param int[] $packageUpdateServerIDs
45 */
46 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], bool $ignoreCache = false)
47 {
48 // get update server data
49 $tmp = PackageUpdateServer::getActiveUpdateServers($packageUpdateServerIDs);
50
51 // loop servers
52 $updateServers = [];
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;
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) {
79 $body = $e->getRequest()->getReply()['body'];
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();
103 foreach ($updateServers as $updateServer) {
104 EventHandler::getInstance()->fire(
105 new PackageUpdateListChanged($updateServer)
106 );
107 }
108 }
109 }
110
111 private function getPurchasedVersions()
112 {
113 $client = HttpFactory::makeClientWithTimeout(5);
114 $request = new Request(
115 'POST',
116 'https://api.woltlab.com/1.0/customer/license/list.json',
117 [
118 'content-type' => 'application/x-www-form-urlencoded',
119 ],
120 \http_build_query([
121 'authCode' => PACKAGE_SERVER_AUTH_CODE,
122 ], '', '&', \PHP_QUERY_RFC1738)
123 );
124
125 try {
126 $response = $client->send($request);
127
128 $reply = JSON::decode((string)$response->getBody());
129
130 if ($reply['status'] == 200) {
131 $this->hasAuthCode = true;
132 $this->purchasedVersions = [
133 'woltlab' => ($reply['woltlab'] ?? []),
134 'pluginstore' => ($reply['pluginstore'] ?? []),
135 ];
136 }
137 } catch (ClientExceptionInterface | SystemException) {
138 // ignore
139 }
140 }
141
142 /**
143 * Fetches the package_update.xml from an update server.
144 *
145 * @throws PackageUpdateUnauthorizedException
146 * @throws SystemException
147 */
148 private function getPackageUpdateXML(PackageUpdateServer $updateServer)
149 {
150 $settings = [];
151 $authData = $updateServer->getAuthData();
152 if ($authData) {
153 $settings['auth'] = $authData;
154 }
155
156 $request = new HTTPRequest($updateServer->getListURL(), $settings);
157
158 $requestedVersion = \wcf\getMinorVersion();
159 if (PackageUpdateServer::isUpgradeOverrideEnabled()) {
160 $requestedVersion = WCF::AVAILABLE_UPGRADE_VERSION;
161 }
162
163 $request->addHeader(
164 'requested-woltlab-suite-version',
165 $requestedVersion
166 );
167
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
171 if (
172 !\preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL)
173 || !PACKAGE_SERVER_AUTH_CODE
174 ) {
175 $metaData = $updateServer->getMetaData();
176 if (isset($metaData['list']['etag'])) {
177 $request->addHeader('if-none-match', $metaData['list']['etag']);
178 }
179 if (isset($metaData['list']['lastModified'])) {
180 $request->addHeader('if-modified-since', $metaData['list']['lastModified']);
181 }
182 }
183 }
184
185 try {
186 $request->execute();
187 $reply = $request->getReply();
188 } catch (HTTPUnauthorizedException $e) {
189 throw new PackageUpdateUnauthorizedException($request, $updateServer);
190 } catch (SystemException $e) {
191 $reply = $request->getReply();
192
193 $statusCode = \is_array($reply['statusCode']) ? \reset($reply['statusCode']) : $reply['statusCode'];
194
195 throw new SystemException(
196 WCF::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' (' . $statusCode . ')'
197 );
198 }
199
200 $data = [
201 'lastUpdateTime' => TIME_NOW,
202 'status' => 'online',
203 'errorMessage' => '',
204 ];
205
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';
213 }
214 }
215
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);
220 }
221
222 $metaData = [];
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'.");
226 }
227
228 $metaData['list'] = [];
229 if (!empty($reply['httpHeaders']['etag'])) {
230 $metaData['list']['etag'] = \reset($reply['httpHeaders']['etag']);
231 }
232 if (!empty($reply['httpHeaders']['last-modified'])) {
233 $metaData['list']['lastModified'] = \reset($reply['httpHeaders']['last-modified']);
234 }
235 }
236 $data['metaData'] = \serialize($metaData);
237
238 unset($request, $reply);
239
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]);
246
247 // save packages
248 if (!empty($allNewPackages)) {
249 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID);
250 }
251 unset($allNewPackages);
252 }
253
254 // update server status
255 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
256 $updateServerEditor->update($data);
257 }
258
259 /**
260 * Parses a stream containing info from a packages_update.xml.
261 *
262 * @throws SystemException
263 */
264 private function parsePackageUpdateXML(PackageUpdateServer $updateServer, string $content, string $apiVersion): array
265 {
266 $isTrustedServer = $updateServer->isTrustedServer();
267
268 // load xml document
269 $xml = new XML();
270 $xml->loadXML('packageUpdateServer.xml', $content);
271 $xpath = $xml->xpath();
272
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.");
279 }
280
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.");
285 }
286
287 // silently ignore this package to avoid unexpected errors for regular users
288 continue;
289 }
290
291 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
292 }
293
294 return $allNewPackages;
295 }
296
297 /**
298 * Parses the xml structure from a packages_update.xml.
299 *
300 * @throws PackageValidationException
301 */
302 private function parsePackageUpdateXMLBlock(
303 PackageUpdateServer $updateServer,
304 \DOMXPath $xpath,
305 \DOMElement $package,
306 string $apiVersion
307 ): array {
308 // define default values
309 $packageInfo = [
310 'author' => '',
311 'authorURL' => '',
312 'isApplication' => 0,
313 'packageDescription' => '',
314 'versions' => [],
315 'pluginStoreFileID' => 0,
316 ];
317
318 // parse package information
319 $elements = $xpath->query('./ns:packageinformation/*', $package);
320 foreach ($elements as $element) {
321 switch ($element->tagName) {
322 case 'packagename':
323 $packageInfo['packageName'] = $element->nodeValue;
324 break;
325
326 case 'packagedescription':
327 $packageInfo['packageDescription'] = $element->nodeValue;
328 break;
329
330 case 'isapplication':
331 $packageInfo['isApplication'] = \intval($element->nodeValue);
332 break;
333
334 case 'pluginStoreFileID':
335 if ($updateServer->isWoltLabStoreServer()) {
336 $packageInfo['pluginStoreFileID'] = \intval($element->nodeValue);
337 }
338 break;
339 }
340 }
341
342 // parse author information
343 $elements = $xpath->query('./ns:authorinformation/*', $package);
344 foreach ($elements as $element) {
345 switch ($element->tagName) {
346 case 'author':
347 $packageInfo['author'] = $element->nodeValue;
348 break;
349
350 case 'authorurl':
351 $packageInfo['authorURL'] = $element->nodeValue;
352 break;
353 }
354 }
355
356 $key = '';
357 if ($this->hasAuthCode) {
358 if ($updateServer->isWoltLabUpdateServer()) {
359 $key = 'woltlab';
360 } elseif ($updateServer->isWoltLabStoreServer()) {
361 $key = 'pluginstore';
362 }
363 }
364
365 // parse versions
366 $elements = $xpath->query('./ns:versions/ns:version', $package);
367 /** @var \DOMElement $element */
368 foreach ($elements as $element) {
369 $versionNo = $element->getAttribute('name');
370
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] == '*') {
376 $isAccessible = 1;
377 } else {
378 $isAccessible = (Package::compareVersion(
379 $versionNo,
380 $this->purchasedVersions[$key][$packageName] . '.99',
381 '<='
382 ) ? 1 : 0);
383 }
384 } else {
385 $isAccessible = 0;
386 }
387 }
388
389 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
390
391 $children = $xpath->query('child::*', $element);
392 /** @var \DOMElement $child */
393 foreach ($children as $child) {
394 switch ($child->tagName) {
395 case 'fromversions':
396 $fromversions = $xpath->query('child::*', $child);
397 foreach ($fromversions as $fromversion) {
398 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue;
399 }
400 break;
401
402 case 'timestamp':
403 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue;
404 break;
405
406 case 'file':
407 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue;
408 break;
409
410 case 'requiredpackages':
411 $requiredPackages = $xpath->query('child::*', $child);
412
413 /** @var \DOMElement $requiredPackage */
414 foreach ($requiredPackages as $requiredPackage) {
415 $minVersion = $requiredPackage->getAttribute('minversion');
416 $required = $requiredPackage->nodeValue;
417
418 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
419 if (!empty($minVersion)) {
420 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
421 }
422 }
423 break;
424
425 case 'optionalpackages':
426 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
427
428 $optionalPackages = $xpath->query('child::*', $child);
429 foreach ($optionalPackages as $optionalPackage) {
430 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue;
431 }
432 break;
433
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');
440
441 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [
442 'version' => $version,
443 ];
444 }
445 break;
446
447 case 'license':
448 $packageInfo['versions'][$versionNo]['license'] = [
449 'license' => $child->nodeValue,
450 'licenseURL' => $child->hasAttribute('url') ? $child->getAttribute('url') : '',
451 ];
452 break;
453
454 case 'compatibility':
455 if ($apiVersion !== '3.1') {
456 continue 2;
457 }
458
459 $packageInfo['versions'][$versionNo]['compatibility'] = [];
460
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]
470 );
471 } else {
472 continue;
473 }
474 }
475
476 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
477 }
478 }
479 break;
480 }
481 }
482 }
483
484 return $packageInfo;
485 }
486
487 /**
488 * Updates information parsed from a packages_update.xml into the database.
489 */
490 private function savePackageUpdates(array $allNewPackages, int $packageUpdateServerID): void
491 {
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,
501 $identifier,
502 $packageData['packageName'],
503 $packageData['packageDescription'],
504 $packageData['author'],
505 $packageData['authorURL'],
506 $packageData['isApplication'],
507 $packageData['pluginStoreFileID'],
508 ]);
509 }
510 WCF::getDB()->commitTransaction();
511
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');
518
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],
533 $packageVersion,
534 ]);
535 }
536 }
537 WCF::getDB()->commitTransaction();
538
539 $conditions = new PreparedStatementConditionBuilder();
540 $conditions->add('packageUpdateID IN (?)', [\array_values($packageUpdateIDs)]);
541 $sql = "SELECT packageUpdateVersionID, packageUpdateID, packageVersion
542 FROM wcf1_package_update_version
543 {$conditions}";
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']] = [];
550 }
551
552 $packageUpdateVersions[$row['packageUpdateID']][$row['packageVersion']] = $row['packageUpdateVersionID'];
553 }
554
555 foreach ($allNewPackages as $package => $packageData) {
556 foreach ($packageData['versions'] as $packageVersion => $versionData) {
557 $packageUpdateID = $packageUpdateIDs[$package];
558 $packageUpdateVersionID = $packageUpdateVersions[$packageUpdateID][$packageVersion];
559
560 if (isset($versionData['requiredPackages'])) {
561 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
562 $requirementInserts[] = [
563 'packageUpdateVersionID' => $packageUpdateVersionID,
564 'package' => $requiredIdentifier,
565 'minversion' => $required['minversion'] ?? '',
566 ];
567 }
568 }
569
570 if (isset($versionData['excludedPackages'])) {
571 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
572 $excludedPackagesParameters[] = [
573 'packageUpdateVersionID' => $packageUpdateVersionID,
574 'excludedPackage' => $excludedIdentifier,
575 'excludedPackageVersion' => $exclusion['version'],
576 ];
577 }
578 }
579
580 if (isset($versionData['fromversions'])) {
581 foreach ($versionData['fromversions'] as $fromversion) {
582 $fromversionInserts[] = [
583 'packageUpdateVersionID' => $packageUpdateVersionID,
584 'fromversion' => $fromversion,
585 ];
586 }
587 }
588
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'] = [];
594 }
595 $excludeCore60 = '6.0.0 Alpha 1';
596
597 $coreExclude = null;
598 $versionData['excludedPackages'] = \array_filter(
599 $versionData['excludedPackages'],
600 static function ($excludedPackage, $excludedVersion) use (&$coreExclude) {
601 if ($excludedPackage === 'com.woltlab.wcf') {
602 $coreExclude = $excludedVersion;
603
604 return false;
605 }
606
607 return true;
608 },
609 \ARRAY_FILTER_USE_BOTH
610 );
611
612 if ($coreExclude === null || Package::compareVersion($coreExclude, $excludeCore60, '>')) {
613 $versionData['excludedPackages'][] = [
614 'packageUpdateVersionID' => $packageUpdateVersionID,
615 'excludedPackage' => 'com.woltlab.wcf',
616 'excludedPackageVersion' => $excludeCore60,
617 ];
618 }
619 }
620 }
621 }
622
623 $sql = "INSERT INTO wcf1_package_update_requirement
624 (packageUpdateVersionID, package, minversion)
625 VALUES (?, ?, ?)";
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'],
633 ]);
634 }
635 WCF::getDB()->commitTransaction();
636
637 $sql = "INSERT INTO wcf1_package_update_exclusion
638 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
639 VALUES (?, ?, ?)";
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'],
647 ]);
648 }
649 WCF::getDB()->commitTransaction();
650
651 $sql = "INSERT INTO wcf1_package_update_fromversion
652 (packageUpdateVersionID, fromversion)
653 VALUES (?, ?)";
654 $statement = WCF::getDB()->prepare($sql);
655 WCF::getDB()->beginTransaction();
656 foreach ($fromversionInserts as $fromversion) {
657 $statement->execute([
658 $fromversion['packageUpdateVersionID'],
659 $fromversion['fromversion'],
660 ]);
661 }
662 WCF::getDB()->commitTransaction();
663 }
664
665 /**
666 * Returns a list of available updates for installed packages.
667 *
668 * @return array
669 * @throws SystemException
670 */
671 public function getAvailableUpdates(bool $removeRequirements = true, bool $removeOlderMinorReleases = false): array
672 {
673 $updates = [];
674
675 // get update server data
676 $updateServers = PackageUpdateServer::getActiveUpdateServers();
677 $packageUpdateServerIDs = \array_keys($updateServers);
678 if (empty($packageUpdateServerIDs)) {
679 return $updates;
680 }
681
682 // get existing packages and their versions
683 $existingPackages = [];
684 $sql = "SELECT packageID, package, packageDescription, packageName,
685 packageVersion, packageDate, author, authorURL, isApplication
686 FROM wcf1_package";
687 $statement = WCF::getDB()->prepare($sql);
688 $statement->execute();
689 while ($row = $statement->fetchArray()) {
690 $existingPackages[$row['package']][] = $row;
691 }
692 if (empty($existingPackages)) {
693 return $updates;
694 }
695
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
701 FROM wcf1_package
702 )");
703
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
710 {$conditions}";
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).");
717 }
718
719 // case-mismatch, skip the update
720 continue;
721 }
722
723 // test version
724 foreach ($existingPackages[$row['package']] as $existingVersion) {
725 if (Package::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
726 // package data
727 if (!isset($updates[$existingVersion['packageID']])) {
728 $existingVersion['versions'] = [];
729 $updates[$existingVersion['packageID']] = $existingVersion;
730 }
731
732 // version data
733 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
734 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
735 'packageDate' => $row['packageDate'],
736 'packageVersion' => $row['packageVersion'],
737 'servers' => [],
738 ];
739 }
740
741 // server data
742 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
743 'packageUpdateID' => $row['packageUpdateID'],
744 'packageUpdateServerID' => $row['packageUpdateServerID'],
745 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
746 'filename' => $row['filename'],
747 ];
748 }
749 }
750 }
751
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']);
757 }
758
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(
765 $updates,
766 $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']
767 );
768 }
769 }
770 }
771 }
772
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;
785 } else {
786 // version is lower, discard
787 unset($updateData['versions'][$versionNumber]);
788 }
789 } else {
790 $highestVersions[$major] = $versionNumber;
791 }
792 }
793 }
794 }
795 unset($updateData);
796 }
797
798 return $updates;
799 }
800
801 /**
802 * Removes unnecessary updates of requirements from the list of available updates.
803 */
804 private function removeUpdateRequirements(array $updates, int $packageUpdateVersionID): array
805 {
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(
816 $updates,
817 $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']
818 );
819 if (
820 Package::compareVersion(
821 $row['minversion'],
822 $updates[$row['packageID']]['version']['packageVersion'],
823 '>='
824 )
825 ) {
826 unset($updates[$row['packageID']]);
827 }
828 }
829 }
830
831 return $updates;
832 }
833
834 /**
835 * Returns package update versions of the specified package.
836 *
837 * @throws SystemException
838 */
839 public function getPackageUpdateVersions(string $package, string $version = ''): array
840 {
841 $packageUpdateServerIDs = [];
842 foreach (PackageUpdateServer::getActiveUpdateServers() as $packageUpdateServer) {
843 $packageUpdateServerIDs[] = $packageUpdateServer->packageUpdateServerID;
844 }
845
846 // get newest package version
847 if (empty($version)) {
848 $version = $this->getNewestPackageVersion($package);
849 }
850
851 if ($version === null) {
852 throw new SystemException("Cannot find the package '" . $package . "'");
853 }
854
855 // get versions
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]);
861
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
868 {$conditions}";
869 $statement = WCF::getDB()->prepare($sql);
870 $statement->execute($conditions->getParameters());
871 $versions = $statement->fetchAll(\PDO::FETCH_ASSOC);
872
873 if (empty($versions)) {
874 throw new SystemException("Cannot find the package '" . $package . "' in version '" . $version . "'");
875 }
876
877 return $versions;
878 }
879
880 /**
881 * Returns the newest available version of a package.
882 */
883 public function getNewestPackageVersion(string $package): ?string
884 {
885 // get all versions
886 $versions = [];
887 $sql = "SELECT packageVersion
888 FROM wcf1_package_update_version
889 WHERE packageUpdateID IN (
890 SELECT packageUpdateID
891 FROM wcf1_package_update
892 WHERE package = ?
893 )";
894 $statement = WCF::getDB()->prepare($sql);
895 $statement->execute([$package]);
896 while ($row = $statement->fetchArray()) {
897 $versions[$row['packageVersion']] = $row['packageVersion'];
898 }
899
900 // sort by version number
901 \usort($versions, [Package::class, 'compareVersion']);
902
903 // take newest (last)
904 return \array_pop($versions);
905 }
906
907 /**
908 * Stores the filename of a download in session.
909 */
910 public function cacheDownload(string $package, string $version, string $filename): void
911 {
912 $cachedDownloads = WCF::getSession()->getVar('cachedPackageUpdateDownloads');
913 if (!\is_array($cachedDownloads)) {
914 $cachedDownloads = [];
915 }
916
917 // store in session
918 $cachedDownloads[$package . '@' . $version] = $filename;
919 WCF::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);
920 }
921 }