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