diff --git a/libraries/src/Updater/ConstraintChecker.php b/libraries/src/Updater/ConstraintChecker.php new file mode 100644 index 0000000000000..70db5ffaf253e --- /dev/null +++ b/libraries/src/Updater/ConstraintChecker.php @@ -0,0 +1,196 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Updater; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\InputFilter; +use Joomla\CMS\Updater\Updater; +use Joomla\CMS\Version; + +/** + * ConstrainChecker Class + * + * @since __DEPLOY_VERSION__ + */ +class ConstraintChecker +{ + /** + * Checks whether the passed constraints are matched + * + * @param array $constraints The provided constraints to be checked + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + public function check(array $constraints) + { + if (!isset($constraints['targetplatform'])) + { + // targetplatform is required + return false; + } + + // Check targetplatform + if (!$this->checkTargetplatform($constraints['targetplatform'])) + { + return false; + } + + // Check php_minimum + if (isset($constraints['php_minimum']) && !$this->checkPhpMinimum($constraints['php_minimum'])) + { + return false; + } + + // Check supported databases + if (isset($constraints['supported_databases']) && !$this->checkSupportedDatabases($constraints['supported_databases'])) + { + return false; + } + + // Check stability + if (isset($constraints['tags']) && !$this->checkStability($constraints['tags'])) + { + return false; + } + + return true; + } + + /** + * Check the targetPlatform + * + * @param object $targetPlatform + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkTargetplatform(\stdClass $targetPlatform) + { + // Lower case and remove the exclamation mark + $product = strtolower(InputFilter::getInstance()->clean(Version::PRODUCT, 'cmd')); + + // Check that the product matches and that the version matches (optionally a regexp) + if ($product === $targetPlatform->name + && preg_match('/^' . $targetPlatform->version . '/', JVERSION)) + { + return true; + } + + return false; + } + + /** + * Check the minimum PHP version + * + * @param string $phpMinimum The minimum php version to check + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkPhpMinimum(string $phpMinimum) + { + // Check if PHP version supported via tag, assume true if tag isn't present + return version_compare(PHP_VERSION, $phpMinimum, '>='); + } + + /** + * Check the supported databases and versions + * + * @param object $supportedDatabases stdClass of supported databases and versions + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkSupportedDatabases(\stdClass $supportedDatabases) + { + $db = Factory::getDbo(); + $dbType = strtolower($db->getServerType()); + $dbVersion = $db->getVersion(); + + // MySQL and MariaDB use the same database driver but not the same version numbers + if ($dbType === 'mysql') + { + // Check whether we have a MariaDB version string and extract the proper version from it + if (stripos($dbVersion, 'mariadb') !== false) + { + // MariaDB: Strip off any leading '5.5.5-', if present + $dbVersion = preg_replace('/^5\.5\.5-/', '', $dbVersion); + $dbType = 'mariadb'; + } + } + + // Do we have an entry for the database? + if (\property_exists($supportedDatabases, $dbType)) + { + $minimumVersion = $supportedDatabases->$dbType; + + return version_compare($dbVersion, $minimumVersion, '>='); + } + + return false; + } + + /** + * Check the stability + * + * @param array $stabilityTags Stability tags to check + * + * @return bool + * + * @since __DEPLOY_VERSION__ + */ + protected function checkStability(array $stabilityTags) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $stabilityMatch = false; + + foreach ($stabilityTags as $tag) + { + $stability = $this->stabilityTagToInteger($tag); + + if (($stability >= $minimumStability)) + { + $stabilityMatch = true; + } + } + + return $stabilityMatch; + } + + /** + * Converts a tag to numeric stability representation. If the tag doesn't represent a known stability level (one of + * dev, alpha, beta, rc, stable) it is ignored. + * + * @param string $tag The tag string, e.g. dev, alpha, beta, rc, stable + * + * @return integer + * + * @since __DEPLOY_VERSION__ + */ + protected function stabilityTagToInteger($tag) + { + $constant = '\\Joomla\\CMS\\Updater\\Updater::STABILITY_' . strtoupper($tag); + + if (\defined($constant)) + { + return \constant($constant); + } + + return Updater::STABILITY_STABLE; + } +} diff --git a/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php new file mode 100644 index 0000000000000..0a2439f3d6156 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Updater/ConstraintCheckerTest.php @@ -0,0 +1,213 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms; + +use Joomla\CMS\Factory; +use Joomla\CMS\Updater\ConstraintChecker; +use Joomla\Database\DatabaseDriver; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for Version. + * + * @package Joomla.UnitTest + * @subpackage Version + * @since __DEPLOY_VERSION__ + */ +class ConstraintCheckerTest extends UnitTestCase +{ + /** + * @var ConstraintChecker + * @since 3.0 + */ + protected $checker; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function setUp():void + { + $this->checker = new ConstraintChecker(); + } + + /** + * Overrides the parent tearDown method. + * + * @return void + * + * @see \PHPUnit\Framework\TestCase::tearDown() + * @since __DEPLOY_VERSION__ + */ + protected function tearDown():void + { + unset($this->checker); + parent::tearDown(); + } + + public function testCheckMethodReturnsFalseIfPlatformIsMissing() + { + $constraint = []; + $this->assertFalse($this->checker->check($constraint)); + } + + public function testCheckMethodReturnsTrueIfPlatformIsOnlyConstraint() + { + $constraint = ['targetplatform' => (object) ["name" => "joomla", "version" => "4.*"]]; + $this->assertTrue($this->checker->check($constraint)); + } + + /** + * Tests the checkSupportedDatabases method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider supportedDatabasesDataProvider + */ + public function testCheckSupportedDatabases($currentDatabase, $supportedDatabases, $expectedResult) + { + $dbMock = $this->createMock(DatabaseDriver::class); + $dbMock->method('getServerType')->willReturn($currentDatabase['type']); + $dbMock->method('getVersion')->willReturn($currentDatabase['version']); + Factory::$database = $dbMock; + + $method = $this->getPublicMethod('checkSupportedDatabases'); + $result = $method->invoke($this->checker, $supportedDatabases); + + $this->assertSame($expectedResult, $result); + } + + /** + * Tests the checkPhpMinimum method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider targetplatformDataProvider + */ + public function testCheckPhpMinimumReturnFalseForFuturePhp() + { + $method = $this->getPublicMethod('checkPhpMinimum'); + + $this->assertFalse($method->invoke($this->checker, '99.9.9')); + } + + /** + * Tests the checkTargetplatform method + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @dataProvider targetplatformDataProvider + */ + public function testCheckTargetplatform($targetPlatform, $expectedResult) + { + $method = $this->getPublicMethod('checkTargetplatform'); + $result = $method->invoke($this->checker, $targetPlatform); + + $this->assertSame($expectedResult, $result); + } + + /** + * Data provider for testCheckSupportedDatabases method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function supportedDatabasesDataProvider() + { + return [ + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.6.0-log-cll-lve'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + [ + ['type' => 'mysql', 'version' => '5.7.37-log-cll-lve'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'pgsql', 'version' => '14.3'], + (object) ['mysql' => '5.8', 'mariadb' => '10.3'], + false + ], + [ + ['type' => 'mysql', 'version' => '10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.4'], + false + ], + [ + ['type' => 'mysql', 'version' => '5.5.5-10.3.34-MariaDB-0+deb10u1'], + (object) ['mysql' => '5.6', 'mariadb' => '10.3'], + true + ], + ]; + } + + /** + * Data provider for testCheckTargetplatform method + * + * @since __DEPLOY_VERSION__ + * + * @return array[] + */ + protected function targetplatformDataProvider() + { + return [ + [(object) ["name" => "foobar", "version" => "1.*"], false], + [(object) ["name" => "foobar", "version" => "4.*"], false], + [(object) ["name" => "joomla", "version" => "1.*"], false], + [(object) ["name" => "joomla", "version" => "3.1.2"], false], + [(object) ["name" => "joomla", "version" => ""], true], + [(object) ["name" => "joomla", "version" => ".*"], true], + [(object) ["name" => "joomla", "version" => JVERSION], true], + [(object) ["name" => "joomla", "version" => "4.*"], true], + ]; + } + + /** + * Internal helper method to get access to protected methods + * + * @since __DEPLOY_VERSION__ + * + * @param $method + * + * @return \ReflectionMethod + * @throws \ReflectionException + */ + protected function getPublicMethod($method) + { + $reflectionClass = new \ReflectionClass($this->checker); + $method = $reflectionClass->getMethod($method); + $method->setAccessible(true); + + return $method; + } +}