diff --git a/lib/private/app/dependencyanalyzer.php b/lib/private/app/dependencyanalyzer.php index 172b8e88c0..af36637b67 100644 --- a/lib/private/app/dependencyanalyzer.php +++ b/lib/private/app/dependencyanalyzer.php @@ -44,13 +44,14 @@ class DependencyAnalyzer { * @returns array of missing dependencies */ public function analyze() { - $this->analysePhpVersion(); - $this->analyseSupportedDatabases(); - $this->analyseCommands(); + $this->analyzePhpVersion(); + $this->analyzeDatabases(); + $this->analyzeCommands(); + $this->analyzeLibraries(); return $this->missing; } - private function analysePhpVersion() { + private function analyzePhpVersion() { if (isset($this->dependencies['php']['@attributes']['min-version'])) { $minVersion = $this->dependencies['php']['@attributes']['min-version']; if (version_compare($this->platform->getPhpVersion(), $minVersion, '<')) { @@ -60,12 +61,12 @@ class DependencyAnalyzer { if (isset($this->dependencies['php']['@attributes']['max-version'])) { $maxVersion = $this->dependencies['php']['@attributes']['max-version']; if (version_compare($this->platform->getPhpVersion(), $maxVersion, '>')) { - $this->addMissing((string)$this->l->t('PHP with a version less then %s is required.', $maxVersion)); + $this->addMissing((string)$this->l->t('PHP with a version lower than %s is required.', $maxVersion)); } } } - private function analyseSupportedDatabases() { + private function analyzeDatabases() { if (!isset($this->dependencies['database'])) { return; } @@ -83,7 +84,7 @@ class DependencyAnalyzer { } } - private function analyseCommands() { + private function analyzeCommands() { if (!isset($this->dependencies['command'])) { return; } @@ -101,6 +102,39 @@ class DependencyAnalyzer { } } + private function analyzeLibraries() { + if (!isset($this->dependencies['lib'])) { + return; + } + + $libs = $this->dependencies['lib']; + foreach($libs as $lib) { + $libName = $this->getValue($lib); + $libVersion = $this->platform->getLibraryVersion($libName); + if (is_null($libVersion)) { + $this->addMissing((string)$this->l->t('The library %s is not available.', $libName)); + continue; + } + + if (is_array($lib)) { + if (isset($lib['@attributes']['min-version'])) { + $minVersion = $lib['@attributes']['min-version']; + if (version_compare($libVersion, $minVersion, '<')) { + $this->addMissing((string)$this->l->t('Library %s with a version higher than %s is required - available version %s.', + array($libName, $minVersion, $libVersion))); + } + } + if (isset($lib['@attributes']['max-version'])) { + $maxVersion = $lib['@attributes']['max-version']; + if (version_compare($libVersion, $maxVersion, '>')) { + $this->addMissing((string)$this->l->t('Library %s with a version lower than %s is required - available version %s.', + array($libName, $maxVersion, $libVersion))); + } + } + } + } + } + /** * @param $element * @return mixed diff --git a/lib/private/app/platform.php b/lib/private/app/platform.php index da515a235a..6279bb5f20 100644 --- a/lib/private/app/platform.php +++ b/lib/private/app/platform.php @@ -62,4 +62,10 @@ class Platform { $path = \OC_Helper::findBinaryPath($command); return ($path !== null); } + + public function getLibraryVersion($name) { + $repo = new PlatformRepository(); + $lib = $repo->findLibrary($name); + return $lib; + } } diff --git a/lib/private/app/platformrepository.php b/lib/private/app/platformrepository.php new file mode 100644 index 0000000000..96d04ec2e4 --- /dev/null +++ b/lib/private/app/platformrepository.php @@ -0,0 +1,210 @@ +packages = $this->initialize(); + } + + protected function initialize() { + $loadedExtensions = get_loaded_extensions(); + $packages = array(); + + // Extensions scanning + foreach ($loadedExtensions as $name) { + if (in_array($name, array('standard', 'Core'))) { + continue; + } + + $ext = new \ReflectionExtension($name); + try { + $prettyVersion = $ext->getVersion(); + } catch (\UnexpectedValueException $e) { + $prettyVersion = '0'; + } + try { + $prettyVersion = $this->normalizeVersion($prettyVersion); + } catch (\UnexpectedValueException $e) { + continue; + } + + $packages[$this->buildPackageName($name)] = $prettyVersion; + } + + foreach ($loadedExtensions as $name) { + $prettyVersion = null; + switch ($name) { + case 'curl': + $curlVersion = curl_version(); + $prettyVersion = $curlVersion['version']; + break; + + case 'iconv': + $prettyVersion = ICONV_VERSION; + break; + + case 'intl': + $name = 'ICU'; + if (defined('INTL_ICU_VERSION')) { + $prettyVersion = INTL_ICU_VERSION; + } else { + $reflector = new \ReflectionExtension('intl'); + + ob_start(); + $reflector->info(); + $output = ob_get_clean(); + + preg_match('/^ICU version => (.*)$/m', $output, $matches); + $prettyVersion = $matches[1]; + } + + break; + + case 'libxml': + $prettyVersion = LIBXML_DOTTED_VERSION; + break; + + case 'openssl': + $prettyVersion = preg_replace_callback('{^(?:OpenSSL\s*)?([0-9.]+)([a-z]?).*}', function ($match) { + return $match[1] . (empty($match[2]) ? '' : '.' . (ord($match[2]) - 96)); + }, OPENSSL_VERSION_TEXT); + break; + + case 'pcre': + $prettyVersion = preg_replace('{^(\S+).*}', '$1', PCRE_VERSION); + break; + + case 'uuid': + $prettyVersion = phpversion('uuid'); + break; + + case 'xsl': + $prettyVersion = LIBXSLT_DOTTED_VERSION; + break; + + default: + // None handled extensions have no special cases, skip + continue 2; + } + + try { + $prettyVersion = $this->normalizeVersion($prettyVersion); + } catch (\UnexpectedValueException $e) { + continue; + } + + $packages[$this->buildPackageName($name)] = $prettyVersion; + } + + return $packages; + } + + private function buildPackageName($name) { + return str_replace(' ', '-', $name); + } + + /** + * @param $name + * @return string + */ + public function findLibrary($name) { + $extName = $this->buildPackageName($name); + if (isset($this->packages[$extName])) { + return $this->packages[$extName]; + } + return null; + } + + private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + + /** + * Normalizes a version string to be able to perform comparisons on it + * + * https://github.com/composer/composer/blob/master/src/Composer/Package/Version/VersionParser.php#L94 + * + * @param string $version + * @param string $fullVersion optional complete version string to give more context + * @throws \UnexpectedValueException + * @return string + */ + public function normalizeVersion($version, $fullVersion = null) { + $version = trim($version); + if (null === $fullVersion) { + $fullVersion = $version; + } + // ignore aliases and just assume the alias is required instead of the source + if (preg_match('{^([^,\s]+) +as +([^,\s]+)$}', $version, $match)) { + $version = $match[1]; + } + // match master-like branches + if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { + return '9999999-dev'; + } + if ('dev-' === strtolower(substr($version, 0, 4))) { + return 'dev-' . substr($version, 4); + } + // match classical versioning + if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { // match date-based versioning + $version = preg_replace('{\D}', '-', $matches[1]); + $index = 2; + } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + $version = $matches[1] + . (!empty($matches[2]) ? $matches[2] : '.0') + . (!empty($matches[3]) ? $matches[3] : '.0') + . (!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + } + // add version modifiers if a version was matched + if (isset($index)) { + if (!empty($matches[$index])) { + if ('stable' === $matches[$index]) { + return $version; + } + $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? $matches[$index + 1] : ''); + } + if (!empty($matches[$index + 2])) { + $version .= '-dev'; + } + return $version; + } + $extraMessage = ''; + if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; + } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) { + $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; + } + throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage); + } + + private function expandStability($stability) { + $stability = strtolower($stability); + switch ($stability) { + case 'a': + return 'alpha'; + case 'b': + return 'beta'; + case 'p': + case 'pl': + return 'patch'; + case 'rc': + return 'RC'; + default: + return $stability; + } + } +} diff --git a/tests/data/app/expected-info.json b/tests/data/app/expected-info.json index fc0ab22497..a425622998 100644 --- a/tests/data/app/expected-info.json +++ b/tests/data/app/expected-info.json @@ -44,6 +44,21 @@ }, "@value": "notepad.exe" } + ], + "lib": [ + { + "@attributes" : { + "min-version": "1.2" + }, + "@value": "xml" + }, + { + "@attributes" : { + "max-version": "2.0" + }, + "@value": "intl" + }, + "curl" ] } } diff --git a/tests/data/app/valid-info.xml b/tests/data/app/valid-info.xml index f01f5fd55e..0ea15b63a4 100644 --- a/tests/data/app/valid-info.xml +++ b/tests/data/app/valid-info.xml @@ -25,5 +25,8 @@ mysql grep notepad.exe + xml + intl + curl diff --git a/tests/lib/app/dependencyanalyzer.php b/tests/lib/app/dependencyanalyzer.php index a21b53264b..872d5cfb2c 100644 --- a/tests/lib/app/dependencyanalyzer.php +++ b/tests/lib/app/dependencyanalyzer.php @@ -43,6 +43,14 @@ class DependencyAnalyzer extends \PHPUnit_Framework_TestCase { ->will( $this->returnCallback(function($command) { return ($command === 'grep'); })); + $this->platformMock->expects($this->any()) + ->method('getLibraryVersion') + ->will( $this->returnCallback(function($lib) { + if ($lib === 'curl') { + return "2.3.4"; + } + return null; + })); $this->l10nMock = $this->getMockBuilder('\OCP\IL10N') ->disableOriginalConstructor() @@ -112,6 +120,42 @@ class DependencyAnalyzer extends \PHPUnit_Framework_TestCase { $this->assertEquals($expectedMissing, $missing); } + /** + * @dataProvider providesLibs + * @param $expectedMissing + * @param $libs + */ + function testLibs($expectedMissing, $libs) { + $app = array( + 'dependencies' => array( + ) + ); + if (!is_null($libs)) { + $app['dependencies']['lib'] = $libs; + } + + $analyser = new \OC\App\DependencyAnalyzer($app, $this->platformMock, $this->l10nMock); + $missing = $analyser->analyze(); + + $this->assertTrue(is_array($missing)); + $this->assertEquals($expectedMissing, $missing); + } + + function providesLibs() { + return array( + // we expect curl to exist + array(array(), array('curl')), + // we expect abcde to exist + array(array('The library abcde is not available.'), array('abcde')), + // curl in version 100.0 does not exist + array(array('Library curl with a version higher than 100.0 is required - available version 2.3.4.'), + array(array('@attributes' => array('min-version' => '100.0'), '@value' => 'curl'))), + // curl in version 100.0 does not exist + array(array('Library curl with a version lower than 1.0.0 is required - available version 2.3.4.'), + array(array('@attributes' => array('max-version' => '1.0.0'), '@value' => 'curl'))) + ); + } + function providesCommands() { return array( array(array(), null), @@ -142,7 +186,7 @@ class DependencyAnalyzer extends \PHPUnit_Framework_TestCase { array(array(), null, '5.5'), array(array(), '5.4', '5.5'), array(array('PHP 5.4.4 or higher is required.'), '5.4.4', null), - array(array('PHP with a version less then 5.4.2 is required.'), null, '5.4.2'), + array(array('PHP with a version lower than 5.4.2 is required.'), null, '5.4.2'), ); } }