Apply PSR-12 code style (#3886)
[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
TD
4
5use wcf\data\package\Package;
11ade432
AE
6use wcf\data\package\update\server\PackageUpdateServer;
7use wcf\data\package\update\server\PackageUpdateServerEditor;
3aa69672 8use wcf\system\cache\builder\PackageUpdateCacheBuilder;
11ade432 9use wcf\system\database\util\PreparedStatementConditionBuilder;
dd3ee2d2 10use wcf\system\exception\HTTPUnauthorizedException;
11ade432 11use wcf\system\exception\SystemException;
1533edef 12use wcf\system\io\RemoteFile;
d9aed3c6 13use wcf\system\package\validation\PackageValidationException;
adf0710f 14use wcf\system\SingletonFactory;
2bc9f31d 15use wcf\system\WCF;
bfefbb8a 16use wcf\util\HTTPRequest;
549b92e7 17use wcf\util\JSON;
6bc77b67 18use wcf\util\StringUtil;
11ade432
AE
19use wcf\util\XML;
20
21/**
22 * Provides functions to manage package updates.
a9229942
TD
23 *
24 * @author Alexander Ebert
25 * @copyright 2001-2019 WoltLab GmbH
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package WoltLabSuite\Core\System\Package
11ade432 28 */
a9229942
TD
29class PackageUpdateDispatcher extends SingletonFactory
30{
31 protected $hasAuthCode = false;
32
33 protected $purchasedVersions = [
34 'woltlab' => [],
35 'pluginstore' => [],
36 ];
37
38 /**
39 * Refreshes the package database.
40 *
41 * @param int[] $packageUpdateServerIDs
42 * @param bool $ignoreCache
43 */
44 public function refreshPackageDatabase(array $packageUpdateServerIDs = [], $ignoreCache = false)
45 {
46 // get update server data
47 $tmp = PackageUpdateServer::getActiveUpdateServers($packageUpdateServerIDs);
48
49 // loop servers
50 $updateServers = [];
51 $foundWoltLabServer = false;
52 $requirePurchasedVersions = false;
53 foreach ($tmp as $updateServer) {
54 if ($ignoreCache || $updateServer->lastUpdateTime < TIME_NOW - 600) {
55 if (\preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL)) {
56 $requirePurchasedVersions = true;
57
58 // move a woltlab.com update server to the front of the queue to probe for SSL support
59 if (!$foundWoltLabServer) {
60 \array_unshift($updateServers, $updateServer);
61 $foundWoltLabServer = true;
62
63 continue;
64 }
65 }
66
67 $updateServers[] = $updateServer;
68 }
69 }
70
71 if ($requirePurchasedVersions && PACKAGE_SERVER_AUTH_CODE) {
72 $this->getPurchasedVersions();
73 }
74
75 // loop servers
76 $refreshedPackageLists = false;
77 foreach ($updateServers as $updateServer) {
78 $errorMessage = '';
79
80 try {
81 $this->getPackageUpdateXML($updateServer);
82 $refreshedPackageLists = true;
83 } catch (SystemException $e) {
84 $errorMessage = $e->getMessage();
85 } catch (PackageUpdateUnauthorizedException $e) {
86 $body = $e->getRequest()->getReply()['body'];
87
88 // Try to find the page <title>.
89 if (\preg_match('~<title>(?<title>.*?)</title>~', $body, $matches)) {
90 $errorMessage = $matches['title'];
91 } else {
92 $errorMessage = $body;
93 }
94
95 $errorMessage = \mb_substr(StringUtil::trim(\strip_tags($errorMessage)), 0, 65000);
96 }
97
98 if ($errorMessage) {
99 // save error status
100 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
101 $updateServerEditor->update([
102 'status' => 'offline',
103 'errorMessage' => $errorMessage,
104 ]);
105 }
106 }
107
108 if ($refreshedPackageLists) {
109 PackageUpdateCacheBuilder::getInstance()->reset();
110 }
111 }
112
113 protected function getPurchasedVersions()
114 {
115 if (!RemoteFile::supportsSSL()) {
116 return;
117 }
118
119 $request = new HTTPRequest(
120 'https://api.woltlab.com/1.0/customer/license/list.json',
121 ['timeout' => 5],
122 ['authCode' => PACKAGE_SERVER_AUTH_CODE]
123 );
124
125 try {
126 $request->execute();
127 $reply = JSON::decode($request->getReply()['body']);
128 if ($reply['status'] == 200) {
129 $this->hasAuthCode = true;
130 $this->purchasedVersions = [
131 'woltlab' => ($reply['woltlab'] ?? []),
132 'pluginstore' => ($reply['pluginstore'] ?? []),
133 ];
134 }
135 } catch (SystemException $e) {
136 // ignore
137 }
138 }
139
140 /**
141 * Fetches the package_update.xml from an update server.
142 *
143 * @param PackageUpdateServer $updateServer
144 * @param bool $forceHTTP
145 * @throws PackageUpdateUnauthorizedException
146 * @throws SystemException
147 */
148 protected function getPackageUpdateXML(PackageUpdateServer $updateServer, $forceHTTP = false)
149 {
150 $settings = [];
151 $authData = $updateServer->getAuthData();
152 if ($authData) {
153 $settings['auth'] = $authData;
154 }
155
156 $secureConnection = $updateServer->attemptSecureConnection();
157 if ($secureConnection && !$forceHTTP) {
158 $settings['timeout'] = 5;
159 }
160
161 $request = new HTTPRequest($updateServer->getListURL($forceHTTP), $settings);
162
163 $apiVersion = $updateServer->apiVersion;
164 if (\in_array($apiVersion, ['2.1', '3.1'])) {
165 // skip etag check for WoltLab servers when an auth code is provided
166 if (
167 !\preg_match('~^https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL)
168 || !PACKAGE_SERVER_AUTH_CODE
169 ) {
170 $metaData = $updateServer->getMetaData();
171 if (isset($metaData['list']['etag'])) {
172 $request->addHeader('if-none-match', $metaData['list']['etag']);
173 }
174 if (isset($metaData['list']['lastModified'])) {
175 $request->addHeader('if-modified-since', $metaData['list']['lastModified']);
176 }
177 }
178 }
179
180 try {
181 $request->execute();
182 $reply = $request->getReply();
183 } catch (HTTPUnauthorizedException $e) {
184 throw new PackageUpdateUnauthorizedException($request, $updateServer);
185 } catch (SystemException $e) {
186 $reply = $request->getReply();
187
188 $statusCode = \is_array($reply['statusCode']) ? \reset($reply['statusCode']) : $reply['statusCode'];
189 // status code 0 is a connection timeout
190 if (!$statusCode && $secureConnection) {
191 if (\preg_match('~https?://(?:update|store)\.woltlab\.com\/~', $updateServer->serverURL)) {
192 // woltlab.com servers are most likely to be available,
193 // thus we assume that SSL connections are dropped
194 RemoteFile::disableSSL();
195 }
196
197 // retry via http
198 $this->getPackageUpdateXML($updateServer, true);
199
200 return;
201 }
202
203 throw new SystemException(
204 WCF::getLanguage()->get('wcf.acp.package.update.error.listNotFound') . ' (' . $statusCode . ')'
205 );
206 }
207
208 $data = [
209 'lastUpdateTime' => TIME_NOW,
210 'status' => 'online',
211 'errorMessage' => '',
212 ];
213
214 // check if server indicates support for a newer API
215 if ($updateServer->apiVersion !== '3.1' && !empty($reply['httpHeaders']['wcf-update-server-api'])) {
216 $apiVersions = \explode(' ', \reset($reply['httpHeaders']['wcf-update-server-api']));
217 if (\in_array('3.1', $apiVersions)) {
218 $apiVersion = $data['apiVersion'] = '3.1';
219 } elseif (\in_array('2.1', $apiVersions)) {
220 $apiVersion = $data['apiVersion'] = '2.1';
221 }
222 }
223
224 // parse given package update xml
225 $allNewPackages = false;
226 if ($apiVersion === '2.0' || $reply['statusCode'] != 304) {
227 $allNewPackages = $this->parsePackageUpdateXML($updateServer, $reply['body'], $apiVersion);
228 }
229
230 $metaData = [];
231 if (\in_array($apiVersion, ['2.1', '3.1'])) {
232 if (empty($reply['httpHeaders']['etag']) && empty($reply['httpHeaders']['last-modified'])) {
233 throw new SystemException("Missing required HTTP headers 'etag' and 'last-modified'.");
234 } elseif (empty($reply['httpHeaders']['wcf-update-server-ssl'])) {
235 throw new SystemException("Missing required HTTP header 'wcf-update-server-ssl'.");
236 }
237
238 $metaData['list'] = [];
239 if (!empty($reply['httpHeaders']['etag'])) {
240 $metaData['list']['etag'] = \reset($reply['httpHeaders']['etag']);
241 }
242 if (!empty($reply['httpHeaders']['last-modified'])) {
243 $metaData['list']['lastModified'] = \reset($reply['httpHeaders']['last-modified']);
244 }
245
246 $metaData['ssl'] = (\reset($reply['httpHeaders']['wcf-update-server-ssl']) == 'true') ? true : false;
247 }
248 $data['metaData'] = \serialize($metaData);
249
250 unset($request, $reply);
251
252 if ($allNewPackages !== false) {
253 // purge package list
254 $sql = "DELETE FROM wcf" . WCF_N . "_package_update
255 WHERE packageUpdateServerID = ?";
256 $statement = WCF::getDB()->prepareStatement($sql);
257 $statement->execute([$updateServer->packageUpdateServerID]);
258
259 // save packages
260 if (!empty($allNewPackages)) {
261 $this->savePackageUpdates($allNewPackages, $updateServer->packageUpdateServerID);
262 }
263 unset($allNewPackages);
264 }
265
266 // update server status
267 $updateServerEditor = new PackageUpdateServerEditor($updateServer);
268 $updateServerEditor->update($data);
269 }
270
271 /**
272 * Parses a stream containing info from a packages_update.xml.
273 *
274 * @param PackageUpdateServer $updateServer
275 * @param string $content
276 * @param string $apiVersion
277 * @return array
278 * @throws SystemException
279 */
280 protected function parsePackageUpdateXML(PackageUpdateServer $updateServer, $content, $apiVersion)
281 {
282 $isTrustedServer = $updateServer->isTrustedServer();
283
284 // load xml document
285 $xml = new XML();
286 $xml->loadXML('packageUpdateServer.xml', $content);
287 $xpath = $xml->xpath();
288
289 $allNewPackages = [];
290 $packages = $xpath->query('/ns:section/ns:package');
291 /** @var \DOMElement $package */
292 foreach ($packages as $package) {
293 if (!Package::isValidPackageName($package->getAttribute('name'))) {
294 throw new SystemException("'" . $package->getAttribute('name') . "' is not a valid package name.");
295 }
296
297 $name = $package->getAttribute('name');
298 if (\strpos($name, 'com.woltlab.') === 0 && !$isTrustedServer) {
299 if (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS) {
300 throw new SystemException("The server '" . $updateServer->serverURL . "' attempted to provide an official WoltLab package, but is not authorized.");
301 }
302
303 // silently ignore this package to avoid unexpected errors for regular users
304 continue;
305 }
306
307 $allNewPackages[$name] = $this->parsePackageUpdateXMLBlock($updateServer, $xpath, $package, $apiVersion);
308 }
309
310 return $allNewPackages;
311 }
312
313 /**
314 * Parses the xml structure from a packages_update.xml.
315 *
316 * @param PackageUpdateServer $updateServer
317 * @param \DOMXPath $xpath
318 * @param \DOMElement $package
319 * @param string $apiVersion
320 * @return array
321 * @throws PackageValidationException
322 */
323 protected function parsePackageUpdateXMLBlock(
324 PackageUpdateServer $updateServer,
325 \DOMXPath $xpath,
326 \DOMElement $package,
327 $apiVersion
328 ) {
329 // define default values
330 $packageInfo = [
331 'author' => '',
332 'authorURL' => '',
333 'isApplication' => 0,
334 'packageDescription' => '',
335 'versions' => [],
336 'pluginStoreFileID' => 0,
337 ];
338
339 // parse package information
340 $elements = $xpath->query('./ns:packageinformation/*', $package);
341 foreach ($elements as $element) {
342 switch ($element->tagName) {
343 case 'packagename':
344 $packageInfo['packageName'] = $element->nodeValue;
345 break;
346
347 case 'packagedescription':
348 $packageInfo['packageDescription'] = $element->nodeValue;
349 break;
350
351 case 'isapplication':
352 $packageInfo['isApplication'] = \intval($element->nodeValue);
353 break;
354
355 case 'pluginStoreFileID':
356 if ($updateServer->isWoltLabStoreServer()) {
357 $packageInfo['pluginStoreFileID'] = \intval($element->nodeValue);
358 }
359 break;
360 }
361 }
362
363 // parse author information
364 $elements = $xpath->query('./ns:authorinformation/*', $package);
365 foreach ($elements as $element) {
366 switch ($element->tagName) {
367 case 'author':
368 $packageInfo['author'] = $element->nodeValue;
369 break;
370
371 case 'authorurl':
372 $packageInfo['authorURL'] = $element->nodeValue;
373 break;
374 }
375 }
376
377 $key = '';
378 if ($this->hasAuthCode) {
379 if ($updateServer->isWoltLabUpdateServer()) {
380 $key = 'woltlab';
381 } elseif ($updateServer->isWoltLabStoreServer()) {
382 $key = 'pluginstore';
383 }
384 }
385
386 // parse versions
387 $elements = $xpath->query('./ns:versions/ns:version', $package);
388 /** @var \DOMElement $element */
389 foreach ($elements as $element) {
390 $versionNo = $element->getAttribute('name');
391
392 $isAccessible = ($element->getAttribute('accessible') == 'true') ? 1 : 0;
393 if ($key && $element->getAttribute('requireAuth') == 'true') {
394 $packageName = $package->getAttribute('name');
395 if (isset($this->purchasedVersions[$key][$packageName])) {
396 if ($this->purchasedVersions[$key][$packageName] == '*') {
397 $isAccessible = 1;
398 } else {
399 $isAccessible = (Package::compareVersion(
400 $versionNo,
401 $this->purchasedVersions[$key][$packageName] . '.99',
402 '<='
403 ) ? 1 : 0);
404 }
405 } else {
406 $isAccessible = 0;
407 }
408 }
409
410 $packageInfo['versions'][$versionNo] = ['isAccessible' => $isAccessible];
411
412 $children = $xpath->query('child::*', $element);
413 /** @var \DOMElement $child */
414 foreach ($children as $child) {
415 switch ($child->tagName) {
416 case 'fromversions':
417 $fromversions = $xpath->query('child::*', $child);
418 foreach ($fromversions as $fromversion) {
419 $packageInfo['versions'][$versionNo]['fromversions'][] = $fromversion->nodeValue;
420 }
421 break;
422
423 case 'timestamp':
424 $packageInfo['versions'][$versionNo]['packageDate'] = $child->nodeValue;
425 break;
426
427 case 'file':
428 $packageInfo['versions'][$versionNo]['file'] = $child->nodeValue;
429 break;
430
431 case 'requiredpackages':
432 $requiredPackages = $xpath->query('child::*', $child);
433
434 /** @var \DOMElement $requiredPackage */
435 foreach ($requiredPackages as $requiredPackage) {
436 $minVersion = $requiredPackage->getAttribute('minversion');
437 $required = $requiredPackage->nodeValue;
438
439 $packageInfo['versions'][$versionNo]['requiredPackages'][$required] = [];
440 if (!empty($minVersion)) {
441 $packageInfo['versions'][$versionNo]['requiredPackages'][$required]['minversion'] = $minVersion;
442 }
443 }
444 break;
445
446 case 'optionalpackages':
447 $packageInfo['versions'][$versionNo]['optionalPackages'] = [];
448
449 $optionalPackages = $xpath->query('child::*', $child);
450 foreach ($optionalPackages as $optionalPackage) {
451 $packageInfo['versions'][$versionNo]['optionalPackages'][] = $optionalPackage->nodeValue;
452 }
453 break;
454
455 case 'excludedpackages':
456 $excludedpackages = $xpath->query('child::*', $child);
457 /** @var \DOMElement $excludedPackage */
458 foreach ($excludedpackages as $excludedPackage) {
459 $exclusion = $excludedPackage->nodeValue;
460 $version = $excludedPackage->getAttribute('version');
461
462 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion] = [];
463 if (!empty($version)) {
464 $packageInfo['versions'][$versionNo]['excludedPackages'][$exclusion]['version'] = $version;
465 }
466 }
467 break;
468
469 case 'license':
470 $packageInfo['versions'][$versionNo]['license'] = [
471 'license' => $child->nodeValue,
472 'licenseURL' => $child->hasAttribute('url') ? $child->getAttribute('url') : '',
473 ];
474 break;
475
476 case 'compatibility':
477 if ($apiVersion !== '3.1') {
478 continue 2;
479 }
480
481 $packageInfo['versions'][$versionNo]['compatibility'] = [];
482
483 /** @var \DOMElement $compatibleVersion */
484 foreach ($xpath->query('child::*', $child) as $compatibleVersion) {
485 if ($compatibleVersion->nodeName === 'api' && $compatibleVersion->hasAttribute('version')) {
486 $versionNumber = $compatibleVersion->getAttribute('version');
487 if (!\preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $versionNumber)) {
488 if (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS) {
489 throw new PackageValidationException(
490 PackageValidationException::INVALID_API_VERSION,
491 ['version' => $versionNumber]
492 );
493 } else {
494 continue;
495 }
496 }
497
498 $packageInfo['versions'][$versionNo]['compatibility'][] = $versionNumber;
499 }
500 }
501 break;
502 }
503 }
504 }
505
506 return $packageInfo;
507 }
508
509 /**
510 * Updates information parsed from a packages_update.xml into the database.
511 *
512 * @param array $allNewPackages
513 * @param int $packageUpdateServerID
514 */
515 protected function savePackageUpdates(array &$allNewPackages, $packageUpdateServerID)
516 {
517 $excludedPackagesParameters = $optionalInserts = $requirementInserts = $compatibilityInserts = [];
518 $sql = "INSERT INTO wcf" . WCF_N . "_package_update
519 (packageUpdateServerID, package, packageName, packageDescription, author, authorURL, isApplication, pluginStoreFileID)
520 VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
521 $statement = WCF::getDB()->prepareStatement($sql);
522 WCF::getDB()->beginTransaction();
523 foreach ($allNewPackages as $identifier => $packageData) {
524 $statement->execute([
525 $packageUpdateServerID,
526 $identifier,
527 $packageData['packageName'],
528 $packageData['packageDescription'],
529 $packageData['author'],
530 $packageData['authorURL'],
531 $packageData['isApplication'],
532 $packageData['pluginStoreFileID'],
533 ]);
534 }
535 WCF::getDB()->commitTransaction();
536
537 $sql = "SELECT packageUpdateID, package
538 FROM wcf" . WCF_N . "_package_update
539 WHERE packageUpdateServerID = ?";
540 $statement = WCF::getDB()->prepareStatement($sql);
541 $statement->execute([$packageUpdateServerID]);
542 $packageUpdateIDs = $statement->fetchMap('package', 'packageUpdateID');
543
544 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_version
545 (filename, license, licenseURL, isAccessible, packageDate, packageUpdateID, packageVersion)
546 VALUES (?, ?, ?, ?, ?, ?, ?)";
547 $statement = WCF::getDB()->prepareStatement($sql);
548 WCF::getDB()->beginTransaction();
549 foreach ($allNewPackages as $package => $packageData) {
550 foreach ($packageData['versions'] as $packageVersion => $versionData) {
551 $statement->execute([
552 $versionData['file'] ?? '',
553 $versionData['license']['license'] ?? '',
554 $versionData['license']['licenseURL'] ?? '',
555 $versionData['isAccessible'] ? 1 : 0,
556 $versionData['packageDate'],
557 $packageUpdateIDs[$package],
558 $packageVersion,
559 ]);
560 }
561 }
562 WCF::getDB()->commitTransaction();
563
564 $conditions = new PreparedStatementConditionBuilder();
565 $conditions->add('packageUpdateID IN (?)', [\array_values($packageUpdateIDs)]);
566 $sql = "SELECT packageUpdateVersionID, packageUpdateID, packageVersion
567 FROM wcf" . WCF_N . "_package_update_version
568 " . $conditions;
569 $statement = WCF::getDB()->prepareStatement($sql);
570 $statement->execute($conditions->getParameters());
571 $packageUpdateVersions = [];
572 while ($row = $statement->fetchArray()) {
573 if (!isset($packageUpdateVersions[$row['packageUpdateID']])) {
574 $packageUpdateVersions[$row['packageUpdateID']] = [];
575 }
576
577 $packageUpdateVersions[$row['packageUpdateID']][$row['packageVersion']] = $row['packageUpdateVersionID'];
578 }
579
580 foreach ($allNewPackages as $package => $packageData) {
581 foreach ($packageData['versions'] as $packageVersion => $versionData) {
582 $packageUpdateID = $packageUpdateIDs[$package];
583 $packageUpdateVersionID = $packageUpdateVersions[$packageUpdateID][$packageVersion];
584
585 if (isset($versionData['requiredPackages'])) {
586 foreach ($versionData['requiredPackages'] as $requiredIdentifier => $required) {
587 $requirementInserts[] = [
588 'packageUpdateVersionID' => $packageUpdateVersionID,
589 'package' => $requiredIdentifier,
590 'minversion' => $required['minversion'] ?? '',
591 ];
592 }
593 }
594
595 if (isset($versionData['optionalPackages'])) {
596 foreach ($versionData['optionalPackages'] as $optionalPackage) {
597 $optionalInserts[] = [
598 'packageUpdateVersionID' => $packageUpdateVersionID,
599 'package' => $optionalPackage,
600 ];
601 }
602 }
603
604 if (isset($versionData['excludedPackages'])) {
605 foreach ($versionData['excludedPackages'] as $excludedIdentifier => $exclusion) {
606 $excludedPackagesParameters[] = [
607 'packageUpdateVersionID' => $packageUpdateVersionID,
608 'excludedPackage' => $excludedIdentifier,
609 'excludedPackageVersion' => $exclusion['version'] ?? '',
610 ];
611 }
612 }
613
614 if (isset($versionData['fromversions'])) {
615 foreach ($versionData['fromversions'] as $fromversion) {
616 $fromversionInserts[] = [
617 'packageUpdateVersionID' => $packageUpdateVersionID,
618 'fromversion' => $fromversion,
619 ];
620 }
621 }
622
623 // @deprecated 5.2
624 if (isset($versionData['compatibility'])) {
625 foreach ($versionData['compatibility'] as $version) {
626 $compatibilityInserts[] = [
627 'packageUpdateVersionID' => $packageUpdateVersionID,
628 'version' => $version,
629 ];
630 }
631
632 // The API compatibility versions are deprecated, any package that exposes them must
633 // exclude at most `com.woltlab.wcf` in version `6.0.0 Alpha 1`.
634 if (!empty($compatibilityInserts)) {
635 if (!isset($versionData['excludedPackages'])) {
636 $versionData['excludedPackages'] = [];
637 }
638 $excludeCore60 = '6.0.0 Alpha 1';
639
640 $coreExclude = null;
641 $versionData['excludedPackages'] = \array_filter(
642 $versionData['excludedPackages'],
643 static function ($excludedPackage, $excludedVersion) use (&$coreExclude) {
644 if ($excludedPackage === 'com.woltlab.wcf') {
645 $coreExclude = $excludedVersion;
646
647 return false;
648 }
649
650 return true;
651 },
652 \ARRAY_FILTER_USE_BOTH
653 );
654
655 if ($coreExclude === null || Package::compareVersion($coreExclude, $excludeCore60, '>')) {
656 $versionData['excludedPackages'][] = [
657 'packageUpdateVersionID' => $packageUpdateVersionID,
658 'excludedPackage' => 'com.woltlab.wcf',
659 'excludedPackageVersion' => $excludeCore60,
660 ];
661 }
662 }
663 }
664 }
665 }
666
667 if (!empty($requirementInserts)) {
668 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_requirement
669 (packageUpdateVersionID, package, minversion)
670 VALUES (?, ?, ?)";
671 $statement = WCF::getDB()->prepareStatement($sql);
672 WCF::getDB()->beginTransaction();
673 foreach ($requirementInserts as $requirement) {
674 $statement->execute([
675 $requirement['packageUpdateVersionID'],
676 $requirement['package'],
677 $requirement['minversion'],
678 ]);
679 }
680 WCF::getDB()->commitTransaction();
681 }
682
683 if (!empty($optionalInserts)) {
684 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_optional
685 (packageUpdateVersionID, package)
686 VALUES (?, ?)";
687 $statement = WCF::getDB()->prepareStatement($sql);
688 WCF::getDB()->beginTransaction();
689 foreach ($optionalInserts as $requirement) {
690 $statement->execute([
691 $requirement['packageUpdateVersionID'],
692 $requirement['package'],
693 ]);
694 }
695 WCF::getDB()->commitTransaction();
696 }
697
698 if (!empty($excludedPackagesParameters)) {
699 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_exclusion
700 (packageUpdateVersionID, excludedPackage, excludedPackageVersion)
701 VALUES (?, ?, ?)";
702 $statement = WCF::getDB()->prepareStatement($sql);
703 WCF::getDB()->beginTransaction();
704 foreach ($excludedPackagesParameters as $excludedPackage) {
705 $statement->execute([
706 $excludedPackage['packageUpdateVersionID'],
707 $excludedPackage['excludedPackage'],
708 $excludedPackage['excludedPackageVersion'],
709 ]);
710 }
711 WCF::getDB()->commitTransaction();
712 }
713
714 if (!empty($fromversionInserts)) {
715 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_fromversion
716 (packageUpdateVersionID, fromversion)
717 VALUES (?, ?)";
718 $statement = WCF::getDB()->prepareStatement($sql);
719 WCF::getDB()->beginTransaction();
720 foreach ($fromversionInserts as $fromversion) {
721 $statement->execute([
722 $fromversion['packageUpdateVersionID'],
723 $fromversion['fromversion'],
724 ]);
725 }
726 WCF::getDB()->commitTransaction();
727 }
728
729 // @deprecated 5.2
730 if (!empty($compatibilityInserts)) {
731 $sql = "INSERT INTO wcf" . WCF_N . "_package_update_compatibility
732 (packageUpdateVersionID, version)
733 VALUES (?, ?)";
734 $statement = WCF::getDB()->prepareStatement($sql);
735 WCF::getDB()->beginTransaction();
736 foreach ($compatibilityInserts as $versionData) {
737 $statement->execute([
738 $versionData['packageUpdateVersionID'],
739 $versionData['version'],
740 ]);
741 }
742 WCF::getDB()->commitTransaction();
743 }
744 }
745
746 /**
747 * Returns a list of available updates for installed packages.
748 *
749 * @param bool $removeRequirements
750 * @param bool $removeOlderMinorReleases
751 * @return array
752 * @throws SystemException
753 */
754 public function getAvailableUpdates($removeRequirements = true, $removeOlderMinorReleases = false)
755 {
756 $updates = [];
757
758 // get update server data
759 $updateServers = PackageUpdateServer::getActiveUpdateServers();
760 $packageUpdateServerIDs = \array_keys($updateServers);
761 if (empty($packageUpdateServerIDs)) {
762 return $updates;
763 }
764
765 // get existing packages and their versions
766 $existingPackages = [];
767 $sql = "SELECT packageID, package, packageDescription, packageName,
768 packageVersion, packageDate, author, authorURL, isApplication
769 FROM wcf" . WCF_N . "_package";
770 $statement = WCF::getDB()->prepareStatement($sql);
771 $statement->execute();
772 while ($row = $statement->fetchArray()) {
773 $existingPackages[$row['package']][] = $row;
774 }
775 if (empty($existingPackages)) {
776 return $updates;
777 }
778
779 // get all update versions
780 $conditions = new PreparedStatementConditionBuilder();
781 $conditions->add("pu.packageUpdateServerID IN (?)", [$packageUpdateServerIDs]);
782 $conditions->add("package IN (SELECT DISTINCT package FROM wcf" . WCF_N . "_package)");
783
784 $sql = "SELECT pu.packageUpdateID, pu.packageUpdateServerID, pu.package,
785 puv.packageUpdateVersionID, puv.packageDate, puv.filename, puv.packageVersion
786 FROM wcf" . WCF_N . "_package_update pu
787 LEFT JOIN wcf" . WCF_N . "_package_update_version puv
788 ON (puv.packageUpdateID = pu.packageUpdateID AND puv.isAccessible = 1)
789 " . $conditions;
790 $statement = WCF::getDB()->prepareStatement($sql);
791 $statement->execute($conditions->getParameters());
792 while ($row = $statement->fetchArray()) {
793 if (!isset($existingPackages[$row['package']])) {
794 if (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS) {
795 throw new SystemException("Invalid package update data, identifier '" . $row['package'] . "' does not match any installed package (case-mismatch).");
796 }
797
798 // case-mismatch, skip the update
799 continue;
800 }
801
802 // test version
803 foreach ($existingPackages[$row['package']] as $existingVersion) {
804 if (Package::compareVersion($existingVersion['packageVersion'], $row['packageVersion'], '<')) {
805 // package data
806 if (!isset($updates[$existingVersion['packageID']])) {
807 $existingVersion['versions'] = [];
808 $updates[$existingVersion['packageID']] = $existingVersion;
809 }
810
811 // version data
812 if (!isset($updates[$existingVersion['packageID']]['versions'][$row['packageVersion']])) {
813 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']] = [
814 'packageDate' => $row['packageDate'],
815 'packageVersion' => $row['packageVersion'],
816 'servers' => [],
817 ];
818 }
819
820 // server data
821 $updates[$existingVersion['packageID']]['versions'][$row['packageVersion']]['servers'][] = [
822 'packageUpdateID' => $row['packageUpdateID'],
823 'packageUpdateServerID' => $row['packageUpdateServerID'],
824 'packageUpdateVersionID' => $row['packageUpdateVersionID'],
825 'filename' => $row['filename'],
826 ];
827 }
828 }
829 }
830
831 // sort package versions
832 // and remove old versions
833 foreach ($updates as $packageID => $data) {
834 \uksort($updates[$packageID]['versions'], ['wcf\data\package\Package', 'compareVersion']);
835 $updates[$packageID]['version'] = \end($updates[$packageID]['versions']);
836 }
837
838 // remove requirements of application packages
839 if ($removeRequirements) {
840 foreach ($existingPackages as $identifier => $instances) {
841 foreach ($instances as $instance) {
842 if ($instance['isApplication'] && isset($updates[$instance['packageID']])) {
843 $updates = $this->removeUpdateRequirements(
844 $updates,
845 $updates[$instance['packageID']]['version']['servers'][0]['packageUpdateVersionID']
846 );
847 }
848 }
849 }
850 }
851
852 // remove older minor releases from list, e.g. only display 1.0.2, even if 1.0.1 is available
853 if ($removeOlderMinorReleases) {
854 foreach ($updates as &$updateData) {
855 $highestVersions = [];
856 foreach ($updateData['versions'] as $versionNumber => $dummy) {
857 if (\preg_match('~^(\d+\.\d+)\.~', $versionNumber, $matches)) {
858 $major = $matches[1];
859 if (isset($highestVersions[$major])) {
860 if (Package::compareVersion($highestVersions[$major], $versionNumber, '<')) {
861 // version is newer, discard current version
862 unset($updateData['versions'][$highestVersions[$major]]);
863 $highestVersions[$major] = $versionNumber;
864 } else {
865 // version is lower, discard
866 unset($updateData['versions'][$versionNumber]);
867 }
868 } else {
869 $highestVersions[$major] = $versionNumber;
870 }
871 }
872 }
873 }
874 unset($updateData);
875 }
876
877 return $updates;
878 }
879
880 /**
881 * Removes unnecessary updates of requirements from the list of available updates.
882 *
883 * @param array $updates
884 * @param int $packageUpdateVersionID
885 * @return array $updates
886 */
887 protected function removeUpdateRequirements(array $updates, $packageUpdateVersionID)
888 {
889 $sql = "SELECT pur.package, pur.minversion, p.packageID
890 FROM wcf" . WCF_N . "_package_update_requirement pur
891 LEFT JOIN wcf" . WCF_N . "_package p
892 ON (p.package = pur.package)
893 WHERE pur.packageUpdateVersionID = ?";
894 $statement = WCF::getDB()->prepareStatement($sql);
895 $statement->execute([$packageUpdateVersionID]);
896 while ($row = $statement->fetchArray()) {
897 if (isset($updates[$row['packageID']])) {
898 $updates = $this->removeUpdateRequirements(
899 $updates,
900 $updates[$row['packageID']]['version']['servers'][0]['packageUpdateVersionID']
901 );
902 if (
903 Package::compareVersion(
904 $row['minversion'],
905 $updates[$row['packageID']]['version']['packageVersion'],
906 '>='
907 )
908 ) {
909 unset($updates[$row['packageID']]);
910 }
911 }
912 }
913
914 return $updates;
915 }
916
917 /**
918 * Creates a new package installation scheduler.
919 *
920 * @param array $selectedPackages
921 * @return PackageInstallationScheduler
922 */
923 public function prepareInstallation(array $selectedPackages)
924 {
925 return new PackageInstallationScheduler($selectedPackages);
926 }
927
928 /**
929 * Returns package update versions of the specified package.
930 *
931 * @param string $package package identifier
932 * @param string $version package version
933 * @return array package update versions
934 * @throws SystemException
935 */
936 public function getPackageUpdateVersions($package, $version = '')
937 {
938 $packageUpdateServerIDs = [];
939 foreach (PackageUpdateServer::getActiveUpdateServers() as $packageUpdateServer) {
940 $packageUpdateServerIDs[] = $packageUpdateServer->packageUpdateServerID;
941 }
942
943 // get newest package version
944 if (empty($version)) {
945 $version = $this->getNewestPackageVersion($package);
946 }
947
948 // get versions
949 $conditions = new PreparedStatementConditionBuilder();
950 $conditions->add('pu.package = ?', [$package]);
951 $conditions->add('puv.packageVersion = ?', [$version]);
952 $conditions->add('puv.isAccessible = ?', [1]);
953 $conditions->add('pus.packageUpdateServerID IN (?)', [$packageUpdateServerIDs]);
954
955 $sql = "SELECT puv.*, pu.*, pus.serverURL, pus.loginUsername, pus.loginPassword
956 FROM wcf" . WCF_N . "_package_update_version puv
957 LEFT JOIN wcf" . WCF_N . "_package_update pu
958 ON (pu.packageUpdateID = puv.packageUpdateID)
959 LEFT JOIN wcf" . WCF_N . "_package_update_server pus
960 ON (pus.packageUpdateServerID = pu.packageUpdateServerID)
961 " . $conditions;
962 $statement = WCF::getDB()->prepareStatement($sql);
963 $statement->execute($conditions->getParameters());
964 $versions = $statement->fetchAll(\PDO::FETCH_ASSOC);
965
966 if (empty($versions)) {
967 throw new SystemException("Cannot find package '" . $package . "' in version '" . $version . "'");
968 }
969
970 return $versions;
971 }
972
973 /**
974 * Returns the newest available version of a package.
975 *
976 * @param string $package package identifier
977 * @return string newest package version
978 */
979 public function getNewestPackageVersion($package)
980 {
981 // get all versions
982 $versions = [];
983 $sql = "SELECT packageVersion
984 FROM wcf" . WCF_N . "_package_update_version
985 WHERE packageUpdateID IN (
986 SELECT packageUpdateID
987 FROM wcf" . WCF_N . "_package_update
988 WHERE package = ?
989 )";
990 $statement = WCF::getDB()->prepareStatement($sql);
991 $statement->execute([$package]);
992 while ($row = $statement->fetchArray()) {
993 $versions[$row['packageVersion']] = $row['packageVersion'];
994 }
995
996 // sort by version number
997 \usort($versions, [Package::class, 'compareVersion']);
998
999 // take newest (last)
1000 return \array_pop($versions);
1001 }
1002
1003 /**
1004 * Stores the filename of a download in session.
1005 *
1006 * @param string $package package identifier
1007 * @param string $version package version
1008 * @param string $filename
1009 */
1010 public function cacheDownload($package, $version, $filename)
1011 {
1012 $cachedDownloads = WCF::getSession()->getVar('cachedPackageUpdateDownloads');
1013 if (!\is_array($cachedDownloads)) {
1014 $cachedDownloads = [];
1015 }
1016
1017 // store in session
1018 $cachedDownloads[$package . '@' . $version] = $filename;
1019 WCF::getSession()->register('cachedPackageUpdateDownloads', $cachedDownloads);
1020 }
11ade432 1021}