diff --git a/src/Classes/IndexController.php b/src/Classes/IndexController.php index 0721258a..38f01293 100644 --- a/src/Classes/IndexController.php +++ b/src/Classes/IndexController.php @@ -231,6 +231,14 @@ public function invokeModuleInfo() return $this->redirect('/'); } + if ($error = ModuleStatus::hasValidRequire($module)) { + Notification::pushFlashMessage([ + 'type' => 'error', + 'text' => 'Error in require in moduleinfo.json of ' + . $module->getArchiveName() . ' ' . $module->getVersion() . ' - ' . $error + ]); + } + return $this->render('ModuleInfo', [ 'module' => $module ]); diff --git a/src/Classes/Module.php b/src/Classes/Module.php index 5b803f94..50af7b11 100644 --- a/src/Classes/Module.php +++ b/src/Classes/Module.php @@ -516,7 +516,7 @@ public function isCompatibleWithPhp(): bool $phpVersionInstalled = phpversion(); $comparator = new Comparator(new Parser()); - return $comparator->satisfiesOr($phpVersionInstalled, $phpVersionContraint); + return $comparator->satisfies($phpVersionInstalled, $phpVersionContraint); } /** @@ -536,7 +536,7 @@ public function isCompatibleWithMmlc(): bool } $comparator = new Comparator(new Parser()); - return $comparator->satisfiesOr($mmlcVersionInstalled, $mmlcVersionContraint); + return $comparator->satisfies($mmlcVersionInstalled, $mmlcVersionContraint); } public function getTemplateFiles($file): array diff --git a/src/Classes/ModuleStatus.php b/src/Classes/ModuleStatus.php index 3c1f81ba..d32c88fb 100644 --- a/src/Classes/ModuleStatus.php +++ b/src/Classes/ModuleStatus.php @@ -14,6 +14,8 @@ namespace RobinTheHood\ModifiedModuleLoaderClient; use RobinTheHood\ModifiedModuleLoaderClient\Semver\Comparator; +use RobinTheHood\ModifiedModuleLoaderClient\Semver\ConstraintParser; +use RobinTheHood\ModifiedModuleLoaderClient\Semver\ParseErrorException; use RobinTheHood\ModifiedModuleLoaderClient\Semver\Parser; class ModuleStatus @@ -143,4 +145,17 @@ public static function isUpdatable(Module $module): bool return $module->isInstalled(); } + + public static function hasValidRequire(Module $module): string + { + $constraintParser = new ConstraintParser(new Parser()); + foreach ($module->getRequire() as $module => $constraintString) { + try { + $constraintParser->parse($constraintString); + } catch (ParseErrorException $e) { + return $e->getMessage(); + } + } + return ''; + } } diff --git a/src/Classes/Semver/Comparator.php b/src/Classes/Semver/Comparator.php index 86b1598e..869cf380 100644 --- a/src/Classes/Semver/Comparator.php +++ b/src/Classes/Semver/Comparator.php @@ -163,34 +163,67 @@ public function isCompatible(string $versionString1, string $versionString2): bo return $this->greaterThanOrEqualTo($versionString1, $versionString2); } - public function satisfies(string $versionString1, string $constrain): bool + public function satisfies(string $versionString1, string $constrainString): bool { - if ($constrain[0] == '^') { // Ist Buchstabe an Index 0 = ^ - $versionString2 = str_replace('^', '', $constrain); - return $this->isCompatible($versionString1, $versionString2); - } elseif ($constrain[0] == '<' && $constrain[1] == '=') { - $versionString2 = str_replace('<=', '', $constrain); - return $this->lessThanOrEqualTo($versionString1, $versionString2); - } else { - $versionString2 = $constrain; - return $this->equalTo($versionString1, $versionString2); + $constraintParser = new ConstraintParser($this->parser); + try { + $constraint = $constraintParser->parse($constrainString); + } catch (ParseErrorException $e) { + return false; + } + + if ($constraint->type === Constraint::TYPE_OR) { + return $this->satisfiesOr($versionString1, $constraint); + } + + if ($constraint->type === Constraint::TYPE_AND) { + return $this->satisfiesAnd($versionString1, $constraint); } + + if ($constraint->type === Constraint::TYPE_LESS_OR_EQUAL) { + return $this->lessThanOrEqualTo($versionString1, $constraint->versionString); + } elseif ($constraint->type === Constraint::TYPE_LESS) { + return $this->lessThan($versionString1, $constraint->versionString); + } elseif ($constraint->type === Constraint::TYPE_GREATER_OR_EQUAL) { + return $this->greaterThanOrEqualTo($versionString1, $constraint->versionString); + } elseif ($constraint->type === Constraint::TYPE_GREATER) { + return $this->greaterThan($versionString1, $constraint->versionString); + } elseif ($constraint->type === Constraint::TYPE_CARET) { + return $this->isCompatible($versionString1, $constraint->versionString); + } elseif ($constraint->type === Constraint::TYPE_EQUAL) { + return $this->equalTo($versionString1, $constraint->versionString); + } + + return false; } /** - * Can satisfy multiple constraints with OR / || + * Can satisfy multiple constraints with OR (||) * * Example: ^7.4 || ^8.0 */ - public function satisfiesOr(string $versionString1, string $constraintOrExpression): bool + public function satisfiesOr(string $versionString1, Constraint $constraint): bool { - $constraints = explode('||', $constraintOrExpression); - foreach ($constraints as $constraint) { - $constraint = trim($constraint); - if ($this->satisfies($versionString1, $constraint)) { + foreach ($constraint->constraints as $constraint) { + if ($this->satisfies($versionString1, $constraint->constraintString)) { return true; } } return false; } + + /** + * Can satisfy multiple constraints with AND (,) + * + * Example: ^7.4, ^8.0 + */ + public function satisfiesAnd(string $versionString1, Constraint $constraint): bool + { + foreach ($constraint->constraints as $constraint) { + if (!$this->satisfies($versionString1, $constraint->constraintString)) { + return false; + } + } + return true; + } } diff --git a/src/Classes/Semver/Constraint.php b/src/Classes/Semver/Constraint.php new file mode 100644 index 00000000..541d0f69 --- /dev/null +++ b/src/Classes/Semver/Constraint.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\Semver; + +class Constraint +{ + public const TYPE_EQUAL = '='; + public const TYPE_OR = '||'; + public const TYPE_AND = ','; + public const TYPE_CARET = '^'; + public const TYPE_LESS_OR_EQUAL = '<='; + public const TYPE_LESS = '<'; + public const TYPE_GREATER_OR_EQUAL = '>='; + public const TYPE_GREATER = '>'; + + /** @var string $type */ + public $type; + + /** @var string $constraintString */ + public $constraintString; + + /** @var Version $version */ + public $version; + + /** @var string $versionString */ + public $versionString; + + /** @var Constraint[] $constraints */ + public $constraints = []; + + public static function createConstraintFromConstraints(array $constraints): string + { + $constraint = ''; + foreach ($constraints as $version) { + if ($constraint === '') { + $constraint = $version; + } else { + $constraint .= ', ' . $version; + } + } + return $constraint; + } + + public static function resolveCaretRange(string $range): string + { + if (preg_match('/^\^(?\d+)(\.(?\d+))?(\.(?\d+))?(?.*)$/', $range, $matches)) { + $major = intval($matches['major']); + $minor = isset($matches['minor']) ? intval($matches['minor']) : 0; + $patch = isset($matches['patch']) ? intval($matches['patch']) : 0; + $suffix = $matches['suffix']; + + $lower = sprintf("%d.%d.%d%s", $major, $minor, $patch, $suffix); + + if ($major == 0) { + $upper = sprintf("%d.%d.%d", $major, $minor + 1, 0); + } else { + $upper = sprintf("%d.%d.%s", $major + 1, 0, '0'); + } + + return ">=$lower,<$upper"; + } + + return $range; + } + + public static function resolve(string $constaint): string + { + return self::resolveCaretRange($constaint); + } +} diff --git a/src/Classes/Semver/ConstraintParser.php b/src/Classes/Semver/ConstraintParser.php new file mode 100644 index 00000000..d9263859 --- /dev/null +++ b/src/Classes/Semver/ConstraintParser.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace RobinTheHood\ModifiedModuleLoaderClient\Semver; + +use RobinTheHood\ModifiedModuleLoaderClient\Semver\ParseErrorException; + +class ConstraintParser +{ + /** @var Parser $parser */ + private $parser; + + public function __construct(Parser $parser) + { + $this->parser = $parser; + } + + public function parse(string $constraintString): Constraint + { + $constraint = new Constraint(); + $constraint->constraintString = $constraintString; + + if (strpos($constraintString, '||')) { + $constraint->type = Constraint::TYPE_OR; + $subConstraintStrings = explode('||', $constraintString); + foreach ($subConstraintStrings as $subConstraintString) { + $subConstraintString = trim($subConstraintString); + $subConstraint = $this->parse($subConstraintString); + $constraint->constraints[] = $subConstraint; + } + return $constraint; + } elseif (strpos($constraintString, ',')) { + $constraint->type = Constraint::TYPE_AND; + $subConstraintStrings = explode(',', $constraintString); + foreach ($subConstraintStrings as $subConstraintString) { + $subConstraintString = trim($subConstraintString); + $subConstraint = $this->parse($subConstraintString); + $constraint->constraints[] = $subConstraint; + } + return $constraint; + } elseif (strpos($constraintString, '^') === 0) { + $constraint->type = Constraint::TYPE_CARET; + $versionString = substr($constraintString, 1); + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } elseif (strpos($constraintString, '<=') === 0) { + $constraint->type = Constraint::TYPE_LESS_OR_EQUAL; + $versionString = substr($constraintString, 2); + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } elseif (strpos($constraintString, '<') === 0) { + $constraint->type = Constraint::TYPE_LESS; + $versionString = substr($constraintString, 1); + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } elseif (strpos($constraintString, '>=') === 0) { + $constraint->type = Constraint::TYPE_GREATER_OR_EQUAL; + $versionString = substr($constraintString, 2); + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } elseif (strpos($constraintString, '>') === 0) { + $constraint->type = Constraint::TYPE_GREATER; + $versionString = substr($constraintString, 1); + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } + + try { + $constraint->type = Constraint::TYPE_EQUAL; + $versionString = $constraintString; + $version = $this->parser->parse($versionString); + $constraint->version = $version; + $constraint->versionString = $versionString; + return $constraint; + } catch (ParseErrorException $e) { + throw new ParseErrorException('Unsupported version constraint ' . $constraintString); + } + } +} diff --git a/src/Classes/Semver/Filter.php b/src/Classes/Semver/Filter.php index d89b9abe..3ddbeea9 100644 --- a/src/Classes/Semver/Filter.php +++ b/src/Classes/Semver/Filter.php @@ -52,7 +52,7 @@ public function byConstraint(string $constraint, array $versions): array { $fileredVersions = []; foreach ($versions as $version) { - if ($this->comparator->satisfiesOr($version, $constraint)) { + if ($this->comparator->satisfies($version, $constraint)) { $fileredVersions[] = $version; } } diff --git a/tests/unit/SemverTests/SemverComparatorTest.php b/tests/unit/SemverTests/SemverComparatorTest.php index 9b4137f7..2f0d6aee 100644 --- a/tests/unit/SemverTests/SemverComparatorTest.php +++ b/tests/unit/SemverTests/SemverComparatorTest.php @@ -130,7 +130,7 @@ public function testThatVersionASatisfiesConstraint() public function testThatVersionASatisfiesOrConstraint() { - $this->assertTrue($this->comparator->satisfiesOr('3.3.3', '^2.2.2 || ^3.3.3')); - $this->assertFalse($this->comparator->satisfiesOr('4.4.4', '^2.2.2 || ^3.3.3')); + $this->assertTrue($this->comparator->satisfies('3.3.3', '^2.2.2 || ^3.3.3')); + $this->assertFalse($this->comparator->satisfies('4.4.4', '^2.2.2 || ^3.3.3')); } }