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
;
13 * Represents the archive of a package.
16 * @copyright 2001-2014 WoltLab GmbH
17 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
18 * @package com.woltlab.wcf
19 * @subpackage system.package
20 * @category Community Framework
22 class PackageArchive
{
24 * path to package archive
27 protected $archive = null;
30 * package object of an existing package
31 * @var \wcf\data\package\Package
33 protected $package = null;
37 * @var \wcf\system\io\Tar
39 protected $tar = null;
42 * general package information
45 protected $packageInfo = array();
51 protected $authorInfo = array();
54 * list of requirements
57 protected $requirements = array();
60 * list of optional packages
63 protected $optionals = array();
66 * list of excluded packages
69 protected $excludedPackages = array();
72 * list of instructions
75 protected $instructions = array(
81 * list of php requirements
84 protected $phpRequirements = array();
87 * default name of the package.xml file
90 const INFO_FILE
= 'package.xml';
93 * Creates a new PackageArchive object.
95 * @param string $archive
96 * @param Package $package
98 public function __construct($archive, Package
$package = null) {
99 $this->archive
= $archive; // be careful: this is a string within this class,
100 // but an object in the packageStartInstallForm.class!
101 $this->package
= $package;
105 * Sets associated package object.
107 * @param \wcf\data\package\Package $package
109 public function setPackage(Package
$package) {
110 $this->package
= $package;
114 * Returns the name of the package archive.
118 public function getArchive() {
119 return $this->archive
;
123 * Returns the object of the package archive.
125 * @return \wcf\system\io\Tar
127 public function getTar() {
132 * Opens the package archive and reads package information.
134 public function openArchive() {
135 // check whether archive exists and is a TAR archive
136 if (!file_exists($this->archive
)) {
137 throw new PackageValidationException(PackageValidationException
::FILE_NOT_FOUND
, array('archive' => $this->archive
));
140 // open archive and read package information
141 $this->tar
= new Tar($this->archive
);
142 $this->readPackageInfo();
146 * Extracts information about this package (parses package.xml).
148 protected function readPackageInfo() {
149 // search package.xml in package archive
150 // throw error message if not found
151 if ($this->tar
->getIndexByFilename(self
::INFO_FILE
) === false) {
152 throw new PackageValidationException(PackageValidationException
::MISSING_PACKAGE_XML
, array('archive' => $this->archive
));
155 // extract package.xml, parse XML
156 // and compile an array with XML::getElementTree()
159 $xml->loadXML(self
::INFO_FILE
, $this->tar
->extractToString(self
::INFO_FILE
));
161 catch (\Exception
$e) { // bugfix to avoid file caching problems
162 $xml->loadXML(self
::INFO_FILE
, $this->tar
->extractToString(self
::INFO_FILE
));
166 $xpath = $xml->xpath();
167 $package = $xpath->query('/ns:package')->item(0);
170 $packageName = $package->getAttribute('name');
171 if (!Package
::isValidPackageName($packageName)) {
172 // package name is not a valid package identifier
173 throw new PackageValidationException(PackageValidationException
::INVALID_PACKAGE_NAME
, array('packageName' => $packageName));
176 $this->packageInfo
['name'] = $packageName;
178 // get package information
179 $packageInformation = $xpath->query('./ns:packageinformation', $package)->item(0);
180 $elements = $xpath->query('child::*', $packageInformation);
181 foreach ($elements as $element) {
182 switch ($element->tagName
) {
184 case 'packagedescription':
187 if (!isset($this->packageInfo
[$element->tagName
])) $this->packageInfo
[$element->tagName
] = array();
189 $languageCode = 'default';
190 if ($element->hasAttribute('language')) {
191 $languageCode = $element->getAttribute('language');
194 // fix case-sensitive names
195 $name = $element->tagName
;
196 if ($name == 'packagename') $name = 'packageName';
197 else if ($name == 'packagedescription') $name = 'packageDescription';
199 $this->packageInfo
[$name][$languageCode] = $element->nodeValue
;
202 case 'isapplication':
203 $this->packageInfo
['isApplication'] = intval($element->nodeValue
);
207 $this->packageInfo
['packageURL'] = $element->nodeValue
;
211 if (!Package
::isValidVersion($element->nodeValue
)) {
212 throw new PackageValidationException(PackageValidationException
::INVALID_PACKAGE_VERSION
, array('packageVersion' => $element->nodeValue
));
215 $this->packageInfo
['version'] = $element->nodeValue
;
219 DateUtil
::validateDate($element->nodeValue
);
221 $this->packageInfo
['date'] = @strtotime
($element->nodeValue
);
226 // get author information
227 $authorInformation = $xpath->query('./ns:authorinformation', $package)->item(0);
228 $elements = $xpath->query('child::*', $authorInformation);
229 foreach ($elements as $element) {
230 $tagName = ($element->tagName
== 'authorurl') ?
'authorURL' : $element->tagName
;
231 $this->authorInfo
[$tagName] = $element->nodeValue
;
234 // get required packages
235 $elements = $xpath->query('child::ns:requiredpackages/ns:requiredpackage', $package);
236 foreach ($elements as $element) {
237 if (!Package
::isValidPackageName($element->nodeValue
)) {
238 throw new PackageValidationException(PackageValidationException
::INVALID_PACKAGE_NAME
, array('packageName' => $element->nodeValue
));
242 $data = array('name' => $element->nodeValue
);
243 $attributes = $xpath->query('attribute::*', $element);
244 foreach ($attributes as $attribute) {
245 $data[$attribute->name
] = $attribute->value
;
248 $this->requirements
[$element->nodeValue
] = $data;
251 // get optional packages
252 $elements = $xpath->query('child::ns:optionalpackages/ns:optionalpackage', $package);
253 foreach ($elements as $element) {
254 if (!Package
::isValidPackageName($element->nodeValue
)) {
255 throw new PackageValidationException(PackageValidationException
::INVALID_PACKAGE_NAME
, array('packageName' => $element->nodeValue
));
259 $data = array('name' => $element->nodeValue
);
260 $attributes = $xpath->query('attribute::*', $element);
261 foreach ($attributes as $attribute) {
262 $data[$attribute->name
] = $attribute->value
;
265 $this->optionals
[] = $data;
268 // get excluded packages
269 $elements = $xpath->query('child::ns:excludedpackages/ns:excludedpackage', $package);
270 foreach ($elements as $element) {
271 if (!Package
::isValidPackageName($element->nodeValue
)) {
272 throw new PackageValidationException(PackageValidationException
::INVALID_PACKAGE_NAME
, array('packageName' => $element->nodeValue
));
276 $data = array('name' => $element->nodeValue
);
277 $attributes = $xpath->query('attribute::*', $element);
278 foreach ($attributes as $attribute) {
279 $data[$attribute->name
] = $attribute->value
;
282 $this->excludedPackages
[] = $data;
286 $elements = $xpath->query('./ns:instructions', $package);
287 foreach ($elements as $element) {
288 $instructionData = array();
289 $instructions = $xpath->query('./ns:instruction', $element);
290 foreach ($instructions as $instruction) {
292 $attributes = $xpath->query('attribute::*', $instruction);
293 foreach ($attributes as $attribute) {
294 $data[$attribute->name
] = $attribute->value
;
297 $instructionData[] = array(
298 'attributes' => $data,
299 'pip' => $instruction->getAttribute('type'),
300 'value' => $instruction->nodeValue
304 $fromVersion = $element->getAttribute('fromversion');
305 $type = $element->getAttribute('type');
307 if ($type == 'install') {
308 $this->instructions
['install'] = $instructionData;
311 $this->instructions
['update'][$fromVersion] = $instructionData;
315 // get php requirements
316 /*$requirements = $xpath->query('./ns:phprequirements', $package);
317 foreach ($requirements as $requirement) {
318 $elements = $xpath->query('child::*', $requirement);
319 foreach ($elements as $element) {
320 switch ($element->tagName) {
322 $this->phpRequirements['version'] = $element->nodeValue;
326 $this->phpRequirements['settings'][$element->getAttribute('name')] = $element->nodeValue;
330 $this->phpRequirements['extensions'][] = $element->nodeValue;
334 $this->phpRequirements['functions'][] = $element->nodeValue;
338 $this->phpRequirements['classes'][] = $element->nodeValue;
344 // add com.woltlab.wcf to package requirements
345 if (!isset($this->requirements
['com.woltlab.wcf']) && $this->packageInfo
['name'] != 'com.woltlab.wcf') {
346 $this->requirements
['com.woltlab.wcf'] = array('name' => 'com.woltlab.wcf');
349 if ($this->package
!= null) {
350 $this->filterUpdateInstructions();
353 // set default values
354 if (!isset($this->packageInfo
['isApplication'])) $this->packageInfo
['isApplication'] = 0;
355 if (!isset($this->packageInfo
['packageURL'])) $this->packageInfo
['packageURL'] = '';
359 * Filters update instructions.
361 protected function filterUpdateInstructions() {
362 $validFromVersion = null;
363 foreach ($this->instructions
['update'] as $fromVersion => $update) {
364 if (Package
::checkFromversion($this->package
->packageVersion
, $fromVersion)) {
365 $validFromVersion = $fromVersion;
370 if ($validFromVersion === null) {
371 $this->instructions
['update'] = array();
374 $this->instructions
['update'] = $this->instructions
['update'][$validFromVersion];
379 * Downloads the package archive.
381 * @return string path to the dowloaded file
383 public function downloadArchive() {
386 // file transfer via hypertext transfer protocol.
387 $this->archive
= FileUtil
::downloadFileFromHttp($this->archive
, $prefix);
390 $this->archive
= self
::unzipPackageArchive($this->archive
);
392 return $this->archive
;
396 * Closes and deletes the tar archive of this package.
398 public function deleteArchive() {
399 if ($this->tar
instanceof Tar
) {
403 @unlink
($this->archive
);
407 * Returns true if the package archive supports a new installation.
411 public function isValidInstall() {
412 return !empty($this->instructions
['install']);
416 * Checks if the new package is compatible with
417 * the package that is about to be updated.
419 * @param \wcf\data\package\Package $package
420 * @return boolean isValidUpdate
422 public function isValidUpdate(Package
$package = null) {
423 if ($this->package
=== null && $package !== null) {
424 $this->setPackage($package);
426 // re-evaluate update data
427 $this->filterUpdateInstructions();
430 // Check name of the installed package against the name of the update. Both must be identical.
431 if ($this->packageInfo
['name'] != $this->package
->package
) {
435 // Check if the version number of the installed package is lower than the version number to which
436 // it's about to be updated.
437 if (Package
::compareVersion($this->packageInfo
['version'], $this->package
->packageVersion
) != 1) {
441 // Check if the package provides an instructions block for the update from the installed package version
442 if (empty($this->instructions
['update'])) {
450 * Checks if the current package is already installed, as it is not
451 * possible to install non-applications multiple times within the
456 public function isAlreadyInstalled() {
457 $sql = "SELECT COUNT(*) AS count
458 FROM wcf".WCF_N
."_package
460 $statement = WCF
::getDB()->prepareStatement($sql);
461 $statement->execute(array($this->packageInfo
['name']));
462 $row = $statement->fetchArray();
464 return ($row['count'] > 0) ?
true : false;
468 * Returns true if the package is an application and has an unique abbrevation.
472 public function hasUniqueAbbreviation() {
473 if (!$this->packageInfo
['isApplication']) {
477 $sql = "SELECT COUNT(*)
478 FROM wcf".WCF_N
."_package
479 WHERE isApplication = ?
481 $statement = WCF
::getDB()->prepareStatement($sql);
482 $statement->execute(array(
484 '%.'.Package
::getAbbreviation($this->packageInfo
['name'])
487 return $statement->fetchColumn();
491 * Returns information about the author of this package archive.
493 * @param string $name name of the requested information
496 public function getAuthorInfo($name) {
497 if (isset($this->authorInfo
[$name])) return $this->authorInfo
[$name];
502 * Returns information about this package.
504 * @param string $name name of the requested information
507 public function getPackageInfo($name) {
508 if (isset($this->packageInfo
[$name])) return $this->packageInfo
[$name];
513 * Returns a localized information about this package.
515 * @param string $name
518 public function getLocalizedPackageInfo($name) {
519 if (isset($this->packageInfo
[$name][WCF
::getLanguage()->getFixedLanguageCode()])) {
520 return $this->packageInfo
[$name][WCF
::getLanguage()->getFixedLanguageCode()];
522 else if (isset($this->packageInfo
[$name]['default'])) {
523 return $this->packageInfo
[$name]['default'];
526 if (!empty($this->packageInfo
[$name])) {
527 return reset($this->packageInfo
[$name]);
534 * Returns a list of all requirements of this package.
538 public function getRequirements() {
539 return $this->requirements
;
543 * Returns a list of all delivered optional packages of this package.
547 public function getOptionals() {
548 return $this->optionals
;
552 * Returns a list of excluded packages.
556 public function getExcludedPackages() {
557 return $this->excludedPackages
;
561 * Returns the package installation instructions.
565 public function getInstallInstructions() {
566 return $this->instructions
['install'];
570 * Returns the package update instructions.
574 public function getUpdateInstructions() {
575 return $this->instructions
['update'];
579 * Checks which package requirements do already exist in right version.
580 * Returns a list with all existing requirements.
584 public function getAllExistingRequirements() {
585 $existingRequirements = array();
586 $existingPackages = array();
587 if ($this->package
!== null) {
588 $sql = "SELECT package.*
589 FROM wcf".WCF_N
."_package_requirement requirement
590 LEFT JOIN wcf".WCF_N
."_package package
591 ON (package.packageID = requirement.requirement)
592 WHERE requirement.packageID = ?";
593 $statement = WCF
::getDB()->prepareStatement($sql);
594 $statement->execute(array($this->package
->packageID
));
595 while ($row = $statement->fetchArray()) {
596 $existingRequirements[$row['package']] = $row;
601 $packageNames = array();
602 $requirements = $this->getRequirements();
603 foreach ($requirements as $requirement) {
604 if (isset($existingRequirements[$requirement['name']])) {
605 $existingPackages[$requirement['name']] = array();
606 $existingPackages[$requirement['name']][$existingRequirements[$requirement['name']]['packageID']] = $existingRequirements[$requirement['name']];
609 $packageNames[] = $requirement['name'];
613 // check whether the required packages do already exist
614 if (!empty($packageNames)) {
615 $conditions = new PreparedStatementConditionBuilder();
616 $conditions->add("package.package IN (?)", array($packageNames));
618 $sql = "SELECT package.*
619 FROM wcf".WCF_N
."_package package
621 $statement = WCF
::getDB()->prepareStatement($sql);
622 $statement->execute($conditions->getParameters());
623 while ($row = $statement->fetchArray()) {
624 // check required package version
625 if (isset($requirements[$row['package']]['minversion']) && Package
::compareVersion($row['packageVersion'], $requirements[$row['package']]['minversion']) == -1) {
629 if (!isset($existingPackages[$row['package']])) {
630 $existingPackages[$row['package']] = array();
633 $existingPackages[$row['package']][$row['packageID']] = $row;
637 return $existingPackages;
641 * Checks which package requirements do already exist in database.
642 * Returns a list with the existing requirements.
646 public function getExistingRequirements() {
648 $packageNames = array();
649 foreach ($this->requirements
as $requirement) {
650 $packageNames[] = $requirement['name'];
653 // check whether the required packages do already exist
654 $existingPackages = array();
655 if (!empty($packageNames)) {
656 $conditions = new PreparedStatementConditionBuilder();
657 $conditions->add("package IN (?)", array($packageNames));
660 FROM wcf".WCF_N
."_package
662 $statement = WCF
::getDB()->prepareStatement($sql);
663 $statement->execute($conditions->getParameters());
664 while ($row = $statement->fetchArray()) {
665 if (!isset($existingPackages[$row['package']])) {
666 $existingPackages[$row['package']] = array();
669 $existingPackages[$row['package']][$row['packageVersion']] = $row;
672 // sort multiple packages by version number
673 foreach ($existingPackages as $packageName => $instances) {
674 uksort($instances, array('wcf\data\package\Package', 'compareVersion'));
676 // get package with highest version number (get last package)
677 $existingPackages[$packageName] = array_pop($instances);
681 return $existingPackages;
685 * Returns a list of all open requirements of this package.
689 public function getOpenRequirements() {
690 // get all existing requirements
691 $existingPackages = $this->getExistingRequirements();
693 // check for open requirements
694 $openRequirements = array();
695 foreach ($this->requirements
as $requirement) {
696 if (isset($existingPackages[$requirement['name']])) {
697 // package does already exist
698 // maybe an update is necessary
699 if (isset($requirement['minversion'])) {
700 if (Package
::compareVersion($existingPackages[$requirement['name']]['packageVersion'], $requirement['minversion']) >= 0) {
701 // package does already exist in needed version
702 // skip installation of requirement
706 $requirement['existingVersion'] = $existingPackages[$requirement['name']]['packageVersion'];
713 $requirement['packageID'] = $existingPackages[$requirement['name']]['packageID'];
714 $requirement['action'] = 'update';
717 // package does not exist
718 // new installation is necessary
719 $requirement['packageID'] = 0;
720 $requirement['action'] = 'install';
723 $openRequirements[$requirement['name']] = $requirement;
726 return $openRequirements;
730 * Extracts the requested file in the package archive to the temp folder
731 * and returns the path to the extracted file.
733 * @param string $filename
734 * @param string $tempPrefix
737 public function extractTar($filename, $tempPrefix = 'package_') {
738 // search the requested tar archive in our package archive.
739 // throw error message if not found.
740 if (($fileIndex = $this->tar
->getIndexByFilename($filename)) === false) {
741 throw new PackageValidationException(PackageValidationException
::FILE_NOT_FOUND
, array(
742 'archive' => $this->archive
,
743 'targetArchive' => $filename
747 // requested tar archive was found
748 $fileInfo = $this->tar
->getFileInfo($fileIndex);
749 $filename = FileUtil
::getTemporaryFilename($tempPrefix, preg_replace('!^.*?(\.(?:tar\.gz|tgz|tar))$!i', '\\1', $fileInfo['filename']));
750 $this->tar
->extract($fileIndex, $filename);
756 * Unzips compressed package archives and returns the temporary file name.
758 * @param string $archive filename
761 public static function unzipPackageArchive($archive) {
762 if (!FileUtil
::isURL($archive)) {
763 $tar = new Tar($archive);
765 if ($tar->isZipped()) {
766 $tmpName = FileUtil
::getTemporaryFilename('package_');
767 if (FileUtil
::uncompressFile($archive, $tmpName)) {
777 * Returns a list of packages which exclude this package.
779 * @return array<\wcf\data\package\Package>
781 public function getConflictedExcludingPackages() {
782 $conflictedPackages = array();
783 $sql = "SELECT package.*, package_exclusion.*
784 FROM wcf".WCF_N
."_package_exclusion package_exclusion
785 LEFT JOIN wcf".WCF_N
."_package package
786 ON (package.packageID = package_exclusion.packageID)
787 WHERE excludedPackage = ?";
788 $statement = WCF
::getDB()->prepareStatement($sql);
789 $statement->execute(array($this->packageInfo
['name']));
790 while ($row = $statement->fetchArray()) {
791 if (!empty($row['excludedPackageVersion'])) {
792 if (Package
::compareVersion($this->packageInfo
['version'], $row['excludedPackageVersion'], '<')) {
797 $conflictedPackages[$row['packageID']] = new Package(null, $row);
800 return $conflictedPackages;
804 * Returns a list of packages which are excluded by this package.
806 * @return array<\wcf\data\package\Package>
808 public function getConflictedExcludedPackages() {
809 $conflictedPackages = array();
810 if (!empty($this->excludedPackages
)) {
811 $excludedPackages = array();
812 foreach ($this->excludedPackages
as $excludedPackageData) {
813 $excludedPackages[$excludedPackageData['name']] = $excludedPackageData['version'];
816 $conditions = new PreparedStatementConditionBuilder();
817 $conditions->add("package IN (?)", array(array_keys($excludedPackages)));
820 FROM wcf".WCF_N
."_package
822 $statement = WCF
::getDB()->prepareStatement($sql);
823 $statement->execute($conditions->getParameters());
824 while ($row = $statement->fetchArray()) {
825 if (!empty($excludedPackages[$row['package']])) {
826 if (Package
::compareVersion($row['packageVersion'], $excludedPackages[$row['package']], '<')) {
829 $row['excludedPackageVersion'] = $excludedPackages[$row['package']];
832 $conflictedPackages[$row['packageID']] = new Package(null, $row);
836 return $conflictedPackages;
840 * Returns a list of instructions for installation or update.
842 * @param string $type
845 public function getInstructions($type) {
846 if (isset($this->instructions
[$type])) {
847 return $this->instructions
[$type];
854 * Returns a list of php requirements for current package.
856 * @return array<array>
858 public function getPhpRequirements() {
859 return $this->phpRequirements
;