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