2 namespace wcf\system\package\validation
;
3 use wcf\data\package\Package
;
4 use wcf\data\package\PackageList
;
5 use wcf\system\database\util\PreparedStatementConditionBuilder
;
6 use wcf\system\package\PackageArchive
;
10 * Recursively validates the package archive and it's delivered requirements.
12 * @author Alexander Ebert
13 * @copyright 2001-2018 WoltLab GmbH
14 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
15 * @package WoltLabSuite\Core\System\Package\Validation
17 class PackageValidationArchive
implements \RecursiveIterator
{
19 * list of excluded packages grouped by package
22 protected static $excludedPackages = [];
25 * package archive object
31 * list of direct requirements delivered by this package
32 * @var PackageValidationArchive[]
34 protected $children = [];
43 * exception occurred during validation
49 * associated package object
55 * parent package validation archive object
56 * @var PackageValidationArchive
64 private $position = 0;
67 * Creates a new package validation archive instance.
69 * @param string $archive
70 * @param PackageValidationArchive $parent
71 * @param integer $depth
73 public function __construct($archive, PackageValidationArchive
$parent = null, $depth = 0) {
74 $this->archive
= new PackageArchive($archive);
75 $this->parent
= $parent;
76 $this->depth
= $depth;
80 * Validates this package and optionally it's delivered requirements. The set validation
81 * mode will toggle between different checks.
83 * @param integer $validationMode
84 * @param string $requiredVersion
87 public function validate($validationMode, $requiredVersion = '') {
88 if ($validationMode !== PackageValidationManager
::VALIDATION_EXCLUSION
) {
90 // try to read archive
91 $this->archive
->openArchive();
93 // check if package is installable or suitable for an update
94 $this->validateInstructions($requiredVersion, $validationMode);
96 catch (PackageValidationException
$e) {
97 $this->exception
= $e;
103 $package = $this->archive
->getPackageInfo('name');
105 if ($validationMode === PackageValidationManager
::VALIDATION_RECURSIVE
) {
107 PackageValidationManager
::getInstance()->addVirtualPackage($package, $this->archive
->getPackageInfo('version'));
109 // cache excluded packages
110 self
::$excludedPackages[$package] = [];
111 $excludedPackages = $this->archive
->getExcludedPackages();
112 for ($i = 0, $count = count($excludedPackages); $i < $count; $i++
) {
113 if (!isset(self
::$excludedPackages[$package][$excludedPackages[$i]['name']])) {
114 self
::$excludedPackages[$package][$excludedPackages[$i]['name']] = [];
117 self
::$excludedPackages[$package][$excludedPackages[$i]['name']][] = $excludedPackages[$i]['version'];
120 // traverse open requirements
121 foreach ($this->archive
->getOpenRequirements() as $requirement) {
122 $virtualPackageVersion = PackageValidationManager
::getInstance()->getVirtualPackage($requirement['name']);
123 if ($virtualPackageVersion === null || Package
::compareVersion($virtualPackageVersion, $requirement['minversion'], '<')) {
124 if (empty($requirement['file'])) {
125 // check if package is known
127 FROM wcf".WCF_N
."_package
129 $statement = WCF
::getDB()->prepareStatement($sql);
130 $statement->execute([$requirement['name']]);
131 $package = $statement->fetchObject(Package
::class);
133 throw new PackageValidationException(PackageValidationException
::MISSING_REQUIREMENT
, [
134 'package' => $package,
135 'packageName' => $requirement['name'],
136 'packageVersion' => $requirement['minversion']
140 $archive = $this->archive
->extractTar($requirement['file']);
142 $index = count($this->children
);
143 $this->children
[$index] = new PackageValidationArchive($archive, $this, $this->depth +
1);
144 if (!$this->children
[$index]->validate(PackageValidationManager
::VALIDATION_RECURSIVE
, $requirement['minversion'])) {
148 PackageValidationManager
::getInstance()->addVirtualPackage(
149 $this->children
[$index]->getArchive()->getPackageInfo('name'),
150 $this->children
[$index]->getArchive()->getPackageInfo('version')
155 catch (PackageValidationException
$e) {
156 $this->exception
= $e;
161 else if ($validationMode === PackageValidationManager
::VALIDATION_EXCLUSION
) {
163 $this->validateExclusion($package);
165 for ($i = 0, $count = count($this->children
); $i < $count; $i++
) {
166 if (!$this->children
[$i]->validate(PackageValidationManager
::VALIDATION_EXCLUSION
)) {
171 catch (PackageValidationException
$e) {
172 $this->exception
= $e;
183 * Validates if the package has suitable install or update instructions
185 * @param string $requiredVersion
186 * @param integer $validationMode
187 * @throws PackageValidationException
189 protected function validateInstructions($requiredVersion, $validationMode) {
190 $package = $this->getPackage();
192 // delivered package does not provide the minimum required version
193 if (Package
::compareVersion($requiredVersion, $this->archive
->getPackageInfo('version'), '>')) {
194 throw new PackageValidationException(PackageValidationException
::INSUFFICIENT_VERSION
, [
195 'packageName' => $this->archive
->getPackageInfo('name'),
196 'packageVersion' => $requiredVersion,
197 'deliveredPackageVersion' => $this->archive
->getPackageInfo('version')
201 // check if this package exposes compatible api versions
202 $compatibleVersions = $this->archive
->getCompatibleVersions();
203 if (!empty($compatibleVersions)) {
204 $isCompatible = $isOlderVersion = false;
205 foreach ($compatibleVersions as $version) {
206 if (WCF
::isSupportedApiVersion($version)) {
207 $isCompatible = true;
210 else if ($version < WSC_API_VERSION
) {
211 $isOlderVersion = true;
215 if (!$isCompatible) {
216 throw new PackageValidationException(PackageValidationException
::INCOMPATIBLE_API_VERSION
, ['isOlderVersion' => $isOlderVersion]);
219 else if (ENABLE_DEBUG_MODE
&& ENABLE_DEVELOPER_TOOLS
&& ($package === null ||
$package->package
!== 'com.woltlab.wcf')) {
220 throw new PackageValidationException(PackageValidationException
::MISSING_API_VERSION
);
223 // package is not installed yet
224 if ($package === null) {
225 $instructions = $this->archive
->getInstallInstructions();
226 if (empty($instructions)) {
227 throw new PackageValidationException(PackageValidationException
::NO_INSTALL_PATH
, ['packageName' => $this->archive
->getPackageInfo('name')]);
230 if ($validationMode == PackageValidationManager
::VALIDATION_RECURSIVE
) {
231 $this->validatePackageInstallationPlugins('install', $instructions);
235 // package is already installed, check update path
236 if (!$this->archive
->isValidUpdate($package)) {
237 $deliveredPackageVersion = $this->archive
->getPackageInfo('version');
239 // check if the package is already installed with the same exact version
240 if ($package->packageVersion
=== $deliveredPackageVersion) {
241 throw new PackageValidationException(PackageValidationException
::ALREADY_INSTALLED
, [
242 'packageName' => $package->packageName
,
243 'packageVersion' => $package->packageVersion
247 throw new PackageValidationException(PackageValidationException
::NO_UPDATE_PATH
, [
248 'packageName' => $package->packageName
,
249 'packageVersion' => $package->packageVersion
,
250 'deliveredPackageVersion' => $deliveredPackageVersion
255 if ($validationMode === PackageValidationManager
::VALIDATION_RECURSIVE
) {
256 $this->validatePackageInstallationPlugins('update', $this->archive
->getUpdateInstructions());
262 * Validates install or update instructions against the corresponding PIP, unknown PIPs will be silently ignored.
264 * @param string $type
265 * @param mixed[][] $instructions
266 * @throws PackageValidationException
268 protected function validatePackageInstallationPlugins($type, array $instructions) {
269 for ($i = 0, $length = count($instructions); $i < $length; $i++
) {
270 $instruction = $instructions[$i];
271 if (!PackageValidationManager
::getInstance()->validatePackageInstallationPluginInstruction($this->archive
, $instruction['pip'], $instruction['value'])) {
272 $defaultFilename = PackageValidationManager
::getInstance()->getDefaultFilenameForPackageInstallationPlugin($instruction['pip']);
274 throw new PackageValidationException(PackageValidationException
::MISSING_INSTRUCTION_FILE
, [
275 'pip' => $instruction['pip'],
277 'value' => $instruction['value'] ?
: $defaultFilename
284 * Validates if an installed package excludes the current package and vice versa.
286 * @param string $package
287 * @throws PackageValidationException
289 protected function validateExclusion($package) {
290 $packageVersion = $this->archive
->getPackageInfo('version');
292 // excluding packages: installed -> current
293 $sql = "SELECT package.*, package_exclusion.*
294 FROM wcf".WCF_N
."_package_exclusion package_exclusion
295 LEFT JOIN wcf".WCF_N
."_package package
296 ON (package.packageID = package_exclusion.packageID)
297 WHERE excludedPackage = ?";
298 $statement = WCF
::getDB()->prepareStatement($sql);
299 $statement->execute([$this->getArchive()->getPackageInfo('name')]);
300 $excludingPackages = [];
301 while ($row = $statement->fetchArray()) {
302 $excludingPackage = $row['package'];
304 // use exclusions of queued package
305 if (isset(self
::$excludedPackages[$excludingPackage])) {
306 if (isset(self
::$excludedPackages[$excludingPackage][$package])) {
307 for ($i = 0, $count = count(self
::$excludedPackages[$excludingPackage][$package]); $i < $count; $i++
) {
308 if (Package
::compareVersion($packageVersion, self
::$excludedPackages[$excludingPackage][$package][$i], '<')) {
312 $excludingPackages[] = new Package(null, $row);
319 if (Package
::compareVersion($packageVersion, $row['excludedPackageVersion'], '<')) {
323 $excludingPackages[] = new Package(null, $row);
327 if (!empty($excludingPackages)) {
328 throw new PackageValidationException(PackageValidationException
::EXCLUDING_PACKAGES
, ['packages' => $excludingPackages]);
331 // excluded packages: current -> installed
332 if (!empty(self
::$excludedPackages[$package])) {
333 // get installed packages
334 $conditions = new PreparedStatementConditionBuilder();
335 $conditions->add("package IN (?)", [array_keys(self
::$excludedPackages[$package])]);
337 FROM wcf".WCF_N
."_package
339 $statement = WCF
::getDB()->prepareStatement($sql);
340 $statement->execute($conditions->getParameters());
342 while ($row = $statement->fetchArray()) {
343 $packages[$row['package']] = new Package(null, $row);
346 $excludedPackages = [];
347 foreach ($packages as $excludedPackage => $packageObj) {
348 $version = PackageValidationManager
::getInstance()->getVirtualPackage($excludedPackage);
349 if ($version === null) {
350 $version = $packageObj->packageVersion
;
353 for ($i = 0, $count = count(self
::$excludedPackages[$package][$excludedPackage]); $i < $count; $i++
) {
354 if (Package
::compareVersion($version, self
::$excludedPackages[$package][$excludedPackage][$i], '<')) {
358 $excludedPackages[] = $packageObj;
362 if (!empty($excludedPackages)) {
363 throw new PackageValidationException(PackageValidationException
::EXCLUDED_PACKAGES
, ['packages' => $excludedPackages]);
369 * Returns the occurred exception.
373 public function getException() {
374 return $this->exception
;
378 * Returns the exception message.
382 public function getExceptionMessage() {
383 if ($this->exception
=== null) {
387 if ($this->exception
instanceof PackageValidationException
) {
388 return $this->exception
->getErrorMessage();
391 return $this->exception
->getMessage();
395 * Returns the package archive object.
397 * @return PackageArchive
399 public function getArchive() {
400 return $this->archive
;
404 * Returns the package object based on the package archive's package identifier or null
405 * if the package isn't already installed.
409 public function getPackage() {
410 if ($this->package
=== null) {
412 if ($packages === null) {
415 // Do not rely on PackageCache here, it may be outdated if a previous installation of a package has failed
416 // and the user attempts to install it again in a secondary browser tab!
417 $packageList = new PackageList();
418 $packageList->readObjects();
419 foreach ($packageList as $package) {
420 $packages[$package->package
] = $package;
424 $identifier = $this->archive
->getPackageInfo('name');
425 if (isset($packages[$identifier])) {
426 $this->package
= $packages[$identifier];
430 return $this->package
;
434 * Returns nesting depth.
438 public function getDepth() {
443 * Sets the children of this package validation archive.
445 * @param PackageValidationArchive[] $children
447 public function setChildren(array $children) {
448 $this->children
= $children;
454 public function rewind() {
461 public function valid() {
462 return isset($this->children
[$this->position
]);
468 public function next() {
475 public function current() {
476 return $this->children
[$this->position
];
482 public function key() {
483 return $this->position
;
489 public function getChildren() {
490 return $this->children
[$this->position
];
496 public function hasChildren() {
497 return count($this->children
) > 0;