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