Merge branch 'next' into formBuilder
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageArchive.class.php
1 <?php
2 namespace wcf\system\package;
3 use wcf\data\package\Package;
4 use wcf\system\database\util\PreparedStatementConditionBuilder;
5 use wcf\system\package\validation\PackageValidationException;
6 use wcf\system\io\Tar;
7 use wcf\system\WCF;
8 use wcf\util\DateUtil;
9 use wcf\util\FileUtil;
10 use wcf\util\XML;
11
12 /**
13 * Represents the archive of a package.
14 *
15 * @author Marcel Werk
16 * @copyright 2001-2018 WoltLab GmbH
17 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
18 * @package WoltLabSuite\Core\System\Package
19 */
20 class PackageArchive {
21 /**
22 * path to package archive
23 * @var string
24 */
25 protected $archive;
26
27 /**
28 * package object of an existing package
29 * @var Package
30 */
31 protected $package;
32
33 /**
34 * tar archive object
35 * @var Tar
36 */
37 protected $tar;
38
39 /**
40 * general package information
41 * @var array
42 */
43 protected $packageInfo = [];
44
45 /**
46 * author information
47 * @var array
48 */
49 protected $authorInfo = [];
50
51 /**
52 * list of requirements
53 * @var array
54 */
55 protected $requirements = [];
56
57 /**
58 * list of optional packages
59 * @var array
60 */
61 protected $optionals = [];
62
63 /**
64 * list of excluded packages
65 * @var array
66 */
67 protected $excludedPackages = [];
68
69 /**
70 * list of compatible API versions
71 * @var integer[]
72 */
73 protected $compatibility = [];
74
75 /**
76 * list of instructions
77 * @var mixed[][]
78 */
79 protected $instructions = [
80 'install' => [],
81 'update' => []
82 ];
83
84 /**
85 * default name of the package.xml file
86 * @var string
87 */
88 const INFO_FILE = 'package.xml';
89
90 /**
91 * Creates a new PackageArchive object.
92 *
93 * @param string $archive
94 * @param Package $package
95 */
96 public function __construct($archive, Package $package = null) {
97 $this->archive = $archive; // be careful: this is a string within this class,
98 // but an object in the packageStartInstallForm.class!
99 $this->package = $package;
100 }
101
102 /**
103 * Sets associated package object.
104 *
105 * @param Package $package
106 */
107 public function setPackage(Package $package) {
108 $this->package = $package;
109 }
110
111 /**
112 * Returns the name of the package archive.
113 *
114 * @return string
115 */
116 public function getArchive() {
117 return $this->archive;
118 }
119
120 /**
121 * Returns the object of the package archive.
122 *
123 * @return Tar
124 */
125 public function getTar() {
126 return $this->tar;
127 }
128
129 /**
130 * Opens the package archive and reads package information.
131 */
132 public function openArchive() {
133 // check whether archive exists and is a TAR archive
134 if (!file_exists($this->archive)) {
135 throw new PackageValidationException(PackageValidationException::FILE_NOT_FOUND, ['archive' => $this->archive]);
136 }
137
138 // open archive and read package information
139 $this->tar = new Tar($this->archive);
140 $this->readPackageInfo();
141 }
142
143 /**
144 * Extracts information about this package (parses package.xml).
145 */
146 protected function readPackageInfo() {
147 // search package.xml in package archive
148 // throw error message if not found
149 if ($this->tar->getIndexByFilename(self::INFO_FILE) === false) {
150 throw new PackageValidationException(PackageValidationException::MISSING_PACKAGE_XML, ['archive' => $this->archive]);
151 }
152
153 // extract package.xml, parse XML
154 // and compile an array with XML::getElementTree()
155 $xml = new XML();
156 try {
157 $xml->loadXML(self::INFO_FILE, $this->tar->extractToString(self::INFO_FILE));
158 }
159 catch (\Exception $e) { // bugfix to avoid file caching problems
160 $xml->loadXML(self::INFO_FILE, $this->tar->extractToString(self::INFO_FILE));
161 }
162
163 // parse xml
164 $xpath = $xml->xpath();
165 /** @var \DOMElement $package */
166 $package = $xpath->query('/ns:package')->item(0);
167
168 // package name
169 $packageName = $package->getAttribute('name');
170 if (!Package::isValidPackageName($packageName)) {
171 // package name is not a valid package identifier
172 throw new PackageValidationException(PackageValidationException::INVALID_PACKAGE_NAME, ['packageName' => $packageName]);
173 }
174
175 $this->packageInfo['name'] = $packageName;
176
177 // get package information
178 $packageInformation = $xpath->query('./ns:packageinformation', $package)->item(0);
179 $elements = $xpath->query('child::*', $packageInformation);
180 /** @var \DOMElement $element */
181 foreach ($elements as $element) {
182 switch ($element->tagName) {
183 case 'packagename':
184 case 'packagedescription':
185 case 'readme':
186 case 'license':
187 if (!isset($this->packageInfo[$element->tagName])) $this->packageInfo[$element->tagName] = [];
188
189 $languageCode = 'default';
190 if ($element->hasAttribute('language')) {
191 $languageCode = $element->getAttribute('language');
192 }
193
194 // fix case-sensitive names
195 $name = $element->tagName;
196 if ($name == 'packagename') $name = 'packageName';
197 else if ($name == 'packagedescription') $name = 'packageDescription';
198
199 $this->packageInfo[$name][$languageCode] = $element->nodeValue;
200 break;
201
202 case 'isapplication':
203 $this->packageInfo['isApplication'] = intval($element->nodeValue);
204 break;
205
206 case 'applicationdirectory':
207 if (preg_match('~^[a-z0-9\-\_]+$~', $element->nodeValue)) {
208 $this->packageInfo['applicationDirectory'] = $element->nodeValue;
209 }
210 break;
211
212 case 'packageurl':
213 $this->packageInfo['packageURL'] = $element->nodeValue;
214 break;
215
216 case 'version':
217 if (!Package::isValidVersion($element->nodeValue)) {
218 throw new PackageValidationException(PackageValidationException::INVALID_PACKAGE_VERSION, ['packageVersion' => $element->nodeValue]);
219 }
220
221 $this->packageInfo['version'] = $element->nodeValue;
222 break;
223
224 case 'date':
225 DateUtil::validateDate($element->nodeValue);
226
227 $this->packageInfo['date'] = @strtotime($element->nodeValue);
228 break;
229 }
230 }
231
232 // get author information
233 $authorInformation = $xpath->query('./ns:authorinformation', $package)->item(0);
234 $elements = $xpath->query('child::*', $authorInformation);
235 foreach ($elements as $element) {
236 $tagName = ($element->tagName == 'authorurl') ? 'authorURL' : $element->tagName;
237 $this->authorInfo[$tagName] = $element->nodeValue;
238 }
239
240 // get required packages
241 $elements = $xpath->query('child::ns:requiredpackages/ns:requiredpackage', $package);
242 foreach ($elements as $element) {
243 if (!Package::isValidPackageName($element->nodeValue)) {
244 throw new PackageValidationException(PackageValidationException::INVALID_PACKAGE_NAME, ['packageName' => $element->nodeValue]);
245 }
246
247 // read attributes
248 $data = ['name' => $element->nodeValue];
249 $attributes = $xpath->query('attribute::*', $element);
250 foreach ($attributes as $attribute) {
251 $data[$attribute->name] = $attribute->value;
252 }
253
254 $this->requirements[$element->nodeValue] = $data;
255 }
256
257 // get optional packages
258 $elements = $xpath->query('child::ns:optionalpackages/ns:optionalpackage', $package);
259 foreach ($elements as $element) {
260 if (!Package::isValidPackageName($element->nodeValue)) {
261 throw new PackageValidationException(PackageValidationException::INVALID_PACKAGE_NAME, ['packageName' => $element->nodeValue]);
262 }
263
264 // read attributes
265 $data = ['name' => $element->nodeValue];
266 $attributes = $xpath->query('attribute::*', $element);
267 foreach ($attributes as $attribute) {
268 $data[$attribute->name] = $attribute->value;
269 }
270
271 $this->optionals[] = $data;
272 }
273
274 // get excluded packages
275 $elements = $xpath->query('child::ns:excludedpackages/ns:excludedpackage', $package);
276 foreach ($elements as $element) {
277 if (!Package::isValidPackageName($element->nodeValue)) {
278 throw new PackageValidationException(PackageValidationException::INVALID_PACKAGE_NAME, ['packageName' => $element->nodeValue]);
279 }
280
281 // read attributes
282 $data = ['name' => $element->nodeValue];
283 $attributes = $xpath->query('attribute::*', $element);
284 foreach ($attributes as $attribute) {
285 $data[$attribute->name] = $attribute->value;
286 }
287
288 $this->excludedPackages[] = $data;
289 }
290
291 // get api compatibility
292 $elements = $xpath->query('child::ns:compatibility/ns:api', $package);
293 foreach ($elements as $element) {
294 if (!$element->hasAttribute('version')) continue;
295
296 $version = $element->getAttribute('version');
297 if (!preg_match('~^(?:201[7-9]|20[2-9][0-9])$~', $version)) {
298 throw new PackageValidationException(PackageValidationException::INVALID_API_VERSION, ['version' => $version]);
299 }
300
301 $this->compatibility[] = $version;
302 }
303
304 // get instructions
305 $elements = $xpath->query('./ns:instructions', $package);
306 foreach ($elements as $element) {
307 $instructionData = [];
308 $instructions = $xpath->query('./ns:instruction', $element);
309 /** @var \DOMElement $instruction */
310 foreach ($instructions as $instruction) {
311 $data = [];
312 $attributes = $xpath->query('attribute::*', $instruction);
313 foreach ($attributes as $attribute) {
314 $data[$attribute->name] = $attribute->value;
315 }
316
317 $instructionData[] = [
318 'attributes' => $data,
319 'pip' => $instruction->getAttribute('type'),
320 'value' => $instruction->nodeValue
321 ];
322 }
323
324 $fromVersion = $element->getAttribute('fromversion');
325 $type = $element->getAttribute('type');
326
327 if ($type == 'install') {
328 $this->instructions['install'] = $instructionData;
329 }
330 else {
331 $this->instructions['update'][$fromVersion] = $instructionData;
332 }
333 }
334
335 // add com.woltlab.wcf to package requirements
336 if (!isset($this->requirements['com.woltlab.wcf']) && $this->packageInfo['name'] != 'com.woltlab.wcf') {
337 $this->requirements['com.woltlab.wcf'] = ['name' => 'com.woltlab.wcf'];
338 }
339
340 // during installations, `Package::$packageVersion` can be `null` which causes issues
341 // in `PackageArchive::filterUpdateInstructions()`; as update instructions are not needed
342 // for installations, not filtering update instructions is okay
343 if ($this->package !== null && $this->package->packageVersion !== null) {
344 $this->filterUpdateInstructions();
345 }
346
347 // set default values
348 if (!isset($this->packageInfo['isApplication'])) $this->packageInfo['isApplication'] = 0;
349 if (!isset($this->packageInfo['packageURL'])) $this->packageInfo['packageURL'] = '';
350 }
351
352 /**
353 * Filters update instructions.
354 */
355 protected function filterUpdateInstructions() {
356 $validFromVersion = null;
357 foreach ($this->instructions['update'] as $fromVersion => $update) {
358 if (Package::checkFromversion($this->package->packageVersion, $fromVersion)) {
359 $validFromVersion = $fromVersion;
360 break;
361 }
362 }
363
364 if ($validFromVersion === null) {
365 $this->instructions['update'] = [];
366 }
367 else {
368 $this->instructions['update'] = $this->instructions['update'][$validFromVersion];
369 }
370 }
371
372 /**
373 * Downloads the package archive.
374 *
375 * @return string path to the dowloaded file
376 */
377 public function downloadArchive() {
378 $prefix = 'package';
379
380 // file transfer via hypertext transfer protocol.
381 $this->archive = FileUtil::downloadFileFromHttp($this->archive, $prefix);
382
383 // unzip tar
384 $this->archive = self::unzipPackageArchive($this->archive);
385
386 return $this->archive;
387 }
388
389 /**
390 * Closes and deletes the tar archive of this package.
391 */
392 public function deleteArchive() {
393 if ($this->tar instanceof Tar) {
394 $this->tar->close();
395 }
396
397 @unlink($this->archive);
398 }
399
400 /**
401 * Returns true if the package archive supports a new installation.
402 *
403 * @return boolean
404 */
405 public function isValidInstall() {
406 return !empty($this->instructions['install']);
407 }
408
409 /**
410 * Checks if the new package is compatible with
411 * the package that is about to be updated.
412 *
413 * @param Package $package
414 * @return boolean isValidUpdate
415 */
416 public function isValidUpdate(Package $package = null) {
417 if ($this->package === null && $package !== null) {
418 $this->setPackage($package);
419
420 // re-evaluate update data
421 $this->filterUpdateInstructions();
422 }
423
424 // Check name of the installed package against the name of the update. Both must be identical.
425 if ($this->packageInfo['name'] != $this->package->package) {
426 return false;
427 }
428
429 // Check if the version number of the installed package is lower than the version number to which
430 // it's about to be updated.
431 if (Package::compareVersion($this->packageInfo['version'], $this->package->packageVersion) != 1) {
432 return false;
433 }
434
435 // Check if the package provides an instructions block for the update from the installed package version
436 if (empty($this->instructions['update'])) {
437 return false;
438 }
439
440 return true;
441 }
442
443 /**
444 * Checks if the current package is already installed, as it is not
445 * possible to install non-applications multiple times within the
446 * same environment.
447 *
448 * @return boolean
449 */
450 public function isAlreadyInstalled() {
451 $sql = "SELECT COUNT(*)
452 FROM wcf".WCF_N."_package
453 WHERE package = ?";
454 $statement = WCF::getDB()->prepareStatement($sql);
455 $statement->execute([$this->packageInfo['name']]);
456
457 return $statement->fetchSingleColumn() > 0;
458 }
459
460 /**
461 * Returns true if the package is an application and has an unique abbreviation.
462 *
463 * @return boolean
464 */
465 public function hasUniqueAbbreviation() {
466 if (!$this->packageInfo['isApplication']) {
467 return true;
468 }
469
470 $sql = "SELECT COUNT(*)
471 FROM wcf".WCF_N."_package
472 WHERE isApplication = ?
473 AND package LIKE ?";
474 $statement = WCF::getDB()->prepareStatement($sql);
475 $statement->execute([
476 1,
477 '%.'.Package::getAbbreviation($this->packageInfo['name'])
478 ]);
479
480 return $statement->fetchSingleColumn() > 0;
481 }
482
483 /**
484 * Returns information about the author of this package archive.
485 *
486 * @param string $name name of the requested information
487 * @return string
488 */
489 public function getAuthorInfo($name) {
490 if (isset($this->authorInfo[$name])) return $this->authorInfo[$name];
491 return null;
492 }
493
494 /**
495 * Returns information about this package.
496 *
497 * @param string $name name of the requested information
498 * @return mixed
499 */
500 public function getPackageInfo($name) {
501 if (isset($this->packageInfo[$name])) return $this->packageInfo[$name];
502 return null;
503 }
504
505 /**
506 * Returns a localized information about this package.
507 *
508 * @param string $name
509 * @return string
510 */
511 public function getLocalizedPackageInfo($name) {
512 if (isset($this->packageInfo[$name][WCF::getLanguage()->getFixedLanguageCode()])) {
513 return $this->packageInfo[$name][WCF::getLanguage()->getFixedLanguageCode()];
514 }
515 else if (isset($this->packageInfo[$name]['default'])) {
516 return $this->packageInfo[$name]['default'];
517 }
518
519 if (!empty($this->packageInfo[$name])) {
520 return reset($this->packageInfo[$name]);
521 }
522
523 return '';
524 }
525
526 /**
527 * Returns a list of all requirements of this package.
528 *
529 * @return array
530 */
531 public function getRequirements() {
532 return $this->requirements;
533 }
534
535 /**
536 * Returns a list of all delivered optional packages of this package.
537 *
538 * @return array
539 */
540 public function getOptionals() {
541 return $this->optionals;
542 }
543
544 /**
545 * Returns a list of excluded packages.
546 *
547 * @return array
548 */
549 public function getExcludedPackages() {
550 return $this->excludedPackages;
551 }
552
553 /**
554 * Returns the list of compatible API versions.
555 *
556 * @return integer[]
557 */
558 public function getCompatibleVersions() {
559 return $this->compatibility;
560 }
561
562 /**
563 * Returns the package installation instructions.
564 *
565 * @return array
566 */
567 public function getInstallInstructions() {
568 return $this->instructions['install'];
569 }
570
571 /**
572 * Returns the package update instructions.
573 *
574 * @return array
575 */
576 public function getUpdateInstructions() {
577 return $this->instructions['update'];
578 }
579
580 /**
581 * Checks which package requirements do already exist in right version.
582 * Returns a list with all existing requirements.
583 *
584 * @return array
585 */
586 public function getAllExistingRequirements() {
587 $existingRequirements = [];
588 $existingPackages = [];
589 if ($this->package !== null) {
590 $sql = "SELECT package.*
591 FROM wcf".WCF_N."_package_requirement requirement
592 LEFT JOIN wcf".WCF_N."_package package
593 ON (package.packageID = requirement.requirement)
594 WHERE requirement.packageID = ?";
595 $statement = WCF::getDB()->prepareStatement($sql);
596 $statement->execute([$this->package->packageID]);
597 while ($row = $statement->fetchArray()) {
598 $existingRequirements[$row['package']] = $row;
599 }
600 }
601
602 // build sql
603 $packageNames = [];
604 $requirements = $this->getRequirements();
605 foreach ($requirements as $requirement) {
606 if (isset($existingRequirements[$requirement['name']])) {
607 $existingPackages[$requirement['name']] = [];
608 $existingPackages[$requirement['name']][$existingRequirements[$requirement['name']]['packageID']] = $existingRequirements[$requirement['name']];
609 }
610 else {
611 $packageNames[] = $requirement['name'];
612 }
613 }
614
615 // check whether the required packages do already exist
616 if (!empty($packageNames)) {
617 $conditions = new PreparedStatementConditionBuilder();
618 $conditions->add("package.package IN (?)", [$packageNames]);
619
620 $sql = "SELECT package.*
621 FROM wcf".WCF_N."_package package
622 ".$conditions;
623 $statement = WCF::getDB()->prepareStatement($sql);
624 $statement->execute($conditions->getParameters());
625 while ($row = $statement->fetchArray()) {
626 // check required package version
627 if (isset($requirements[$row['package']]['minversion']) && Package::compareVersion($row['packageVersion'], $requirements[$row['package']]['minversion']) == -1) {
628 continue;
629 }
630
631 if (!isset($existingPackages[$row['package']])) {
632 $existingPackages[$row['package']] = [];
633 }
634
635 $existingPackages[$row['package']][$row['packageID']] = $row;
636 }
637 }
638
639 return $existingPackages;
640 }
641
642 /**
643 * Checks which package requirements do already exist in database.
644 * Returns a list with the existing requirements.
645 *
646 * @return array
647 */
648 public function getExistingRequirements() {
649 // build sql
650 $packageNames = [];
651 foreach ($this->requirements as $requirement) {
652 $packageNames[] = $requirement['name'];
653 }
654
655 // check whether the required packages do already exist
656 $existingPackages = [];
657 if (!empty($packageNames)) {
658 $conditions = new PreparedStatementConditionBuilder();
659 $conditions->add("package IN (?)", [$packageNames]);
660
661 $sql = "SELECT *
662 FROM wcf".WCF_N."_package
663 ".$conditions;
664 $statement = WCF::getDB()->prepareStatement($sql);
665 $statement->execute($conditions->getParameters());
666 while ($row = $statement->fetchArray()) {
667 if (!isset($existingPackages[$row['package']])) {
668 $existingPackages[$row['package']] = [];
669 }
670
671 $existingPackages[$row['package']][$row['packageVersion']] = $row;
672 }
673
674 // sort multiple packages by version number
675 foreach ($existingPackages as $packageName => $instances) {
676 uksort($instances, [Package::class, 'compareVersion']);
677
678 // get package with highest version number (get last package)
679 $existingPackages[$packageName] = array_pop($instances);
680 }
681 }
682
683 return $existingPackages;
684 }
685
686 /**
687 * Returns a list of all open requirements of this package.
688 *
689 * @return array
690 */
691 public function getOpenRequirements() {
692 // get all existing requirements
693 $existingPackages = $this->getExistingRequirements();
694
695 // check for open requirements
696 $openRequirements = [];
697 foreach ($this->requirements as $requirement) {
698 if (isset($existingPackages[$requirement['name']])) {
699 // package does already exist
700 // maybe an update is necessary
701 if (isset($requirement['minversion'])) {
702 if (Package::compareVersion($existingPackages[$requirement['name']]['packageVersion'], $requirement['minversion']) >= 0) {
703 // package does already exist in needed version
704 // skip installation of requirement
705 continue;
706 }
707 else {
708 $requirement['existingVersion'] = $existingPackages[$requirement['name']]['packageVersion'];
709 }
710 }
711 else {
712 continue;
713 }
714
715 $requirement['packageID'] = $existingPackages[$requirement['name']]['packageID'];
716 $requirement['action'] = 'update';
717 }
718 else {
719 // package does not exist
720 // new installation is necessary
721 $requirement['packageID'] = 0;
722 $requirement['action'] = 'install';
723 }
724
725 $openRequirements[$requirement['name']] = $requirement;
726 }
727
728 return $openRequirements;
729 }
730
731 /**
732 * Extracts the requested file in the package archive to the temp folder
733 * and returns the path to the extracted file.
734 *
735 * @param string $filename
736 * @param string $tempPrefix
737 * @return string
738 * @throws PackageValidationException
739 */
740 public function extractTar($filename, $tempPrefix = 'package_') {
741 // search the requested tar archive in our package archive.
742 // throw error message if not found.
743 if (($fileIndex = $this->tar->getIndexByFilename($filename)) === false) {
744 throw new PackageValidationException(PackageValidationException::FILE_NOT_FOUND, [
745 'archive' => $this->archive,
746 'targetArchive' => $filename
747 ]);
748 }
749
750 // requested tar archive was found
751 $fileInfo = $this->tar->getFileInfo($fileIndex);
752 $filename = FileUtil::getTemporaryFilename($tempPrefix, preg_replace('!^.*?(\.(?:tar\.gz|tgz|tar))$!i', '\\1', $fileInfo['filename']));
753 $this->tar->extract($fileIndex, $filename);
754
755 return $filename;
756 }
757
758 /**
759 * Unzips compressed package archives and returns the temporary file name.
760 *
761 * @param string $archive filename
762 * @return string
763 */
764 public static function unzipPackageArchive($archive) {
765 if (!FileUtil::isURL($archive)) {
766 $tar = new Tar($archive);
767 $tar->close();
768 if ($tar->isZipped()) {
769 $tmpName = FileUtil::getTemporaryFilename('package_');
770 if (FileUtil::uncompressFile($archive, $tmpName)) {
771 return $tmpName;
772 }
773 }
774 }
775
776 return $archive;
777 }
778
779 /**
780 * Returns a list of packages which exclude this package.
781 *
782 * @return Package[]
783 */
784 public function getConflictedExcludingPackages() {
785 $conflictedPackages = [];
786 $sql = "SELECT package.*, package_exclusion.*
787 FROM wcf".WCF_N."_package_exclusion package_exclusion
788 LEFT JOIN wcf".WCF_N."_package package
789 ON (package.packageID = package_exclusion.packageID)
790 WHERE excludedPackage = ?";
791 $statement = WCF::getDB()->prepareStatement($sql);
792 $statement->execute([$this->packageInfo['name']]);
793 while ($row = $statement->fetchArray()) {
794 if (!empty($row['excludedPackageVersion'])) {
795 if (Package::compareVersion($this->packageInfo['version'], $row['excludedPackageVersion'], '<')) {
796 continue;
797 }
798 }
799
800 $conflictedPackages[$row['packageID']] = new Package(null, $row);
801 }
802
803 return $conflictedPackages;
804 }
805
806 /**
807 * Returns a list of packages which are excluded by this package.
808 *
809 * @return Package[]
810 */
811 public function getConflictedExcludedPackages() {
812 $conflictedPackages = [];
813 if (!empty($this->excludedPackages)) {
814 $excludedPackages = [];
815 foreach ($this->excludedPackages as $excludedPackageData) {
816 $excludedPackages[$excludedPackageData['name']] = $excludedPackageData['version'];
817 }
818
819 $conditions = new PreparedStatementConditionBuilder();
820 $conditions->add("package IN (?)", [array_keys($excludedPackages)]);
821
822 $sql = "SELECT *
823 FROM wcf".WCF_N."_package
824 ".$conditions;
825 $statement = WCF::getDB()->prepareStatement($sql);
826 $statement->execute($conditions->getParameters());
827 while ($row = $statement->fetchArray()) {
828 if (!empty($excludedPackages[$row['package']])) {
829 if (Package::compareVersion($row['packageVersion'], $excludedPackages[$row['package']], '<')) {
830 continue;
831 }
832 $row['excludedPackageVersion'] = $excludedPackages[$row['package']];
833 }
834
835 $conflictedPackages[$row['packageID']] = new Package(null, $row);
836 }
837 }
838
839 return $conflictedPackages;
840 }
841
842 /**
843 * Returns a list of instructions for installation or update.
844 *
845 * @param string $type
846 * @return array
847 */
848 public function getInstructions($type) {
849 if (isset($this->instructions[$type])) {
850 return $this->instructions[$type];
851 }
852
853 return null;
854 }
855
856 /**
857 * Returns a list of php requirements for current package.
858 *
859 * @return mixed[][]
860 * @deprecated 3.0
861 */
862 public function getPhpRequirements() {
863 return [];
864 }
865 }