diff --git a/bin/fetch-spdx-identifier b/bin/fetch-spdx-identifier new file mode 100755 index 000000000..6561593c3 --- /dev/null +++ b/bin/fetch-spdx-identifier @@ -0,0 +1,85 @@ +#!/usr/bin/env php +printStringArray($identifiers->getStrings()); + +/** + * SPDX Identifier List from the registry. + */ +class SPDXLicenseIdentifiersOnline +{ + const REGISTRY = 'http://www.spdx.org/licenses/'; + const EXPRESSION = '//*[@typeof="spdx:License"]/code[@property="spdx:licenseId"]/text()'; + + private $identifiers; + + /** + * @return string[] + */ + public function getStrings() + { + if ($this->identifiers) { + return $this->identifiers; + } + $this->identifiers = $this->importNodesFromURL( + self::REGISTRY, + self::EXPRESSION + ); + + return $this->identifiers; + } + + private function importNodesFromURL($url, $expressionTextNodes) + { + $doc = new DOMDocument(); + $doc->loadHTMLFile($url); + $xp = new DOMXPath($doc); + $codes = $xp->query($expressionTextNodes); + if (!$codes) { + throw new \Exception(sprintf('XPath query failed: %s', $expressionTextNodes)); + } + if ($codes->length < 20) { + throw new \Exception('Obtaining the license table failed, there can not be less than 20 identifiers.'); + } + $identifiers = array(); + foreach ($codes as $code) { + $identifiers[] = $code->nodeValue; + } + + return $identifiers; + } +} + +/** + * Print an array the way this script needs it. + */ +class JsonPrinter +{ + /** + * + * @param string[] $array + */ + public function printStringArray(array $array) + { + $lines = array(''); + $line = &$lines[0]; + $last = count($array) - 1; + foreach ($array as $item => $code) { + $code = sprintf('"%s"%s', $code, $item === $last ? '' : ', '); + $length = strlen($line) + strlen($code) - 1; + if ($length > 76) { + $line = rtrim($line); + unset($line); + $lines[] = $code; + $line = &$lines[count($lines) - 1]; + } else { + $line .= $code; + } + } + $json = sprintf("[%s]", implode("\n ", $lines)); + $json = str_replace(array("[\"", "\"]"), array("[\n \"", "\"\n]"), $json); + echo $json; + } +} \ No newline at end of file diff --git a/doc/04-schema.md b/doc/04-schema.md index 42678dda3..96d89289b 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -121,20 +121,52 @@ Optional. The license of the package. This can be either a string or an array of strings. -The recommended notation for the most common licenses is: - +The recommended notation for the most common licenses is (alphabetical): + + Apache-2.0 + BSD-2-Clause + BSD-3-Clause + BSD-4-Clause + GPL-2.0 + GPL-2.0+ + GPL-3.0 + GPL-3.0+ + LGPL-2.0 + LGPL-2.0+ + LGPL-3.0 + LGPL-3.0+ MIT - BSD-2 - BSD-3 - BSD-4 - GPLv2 - GPLv3 - LGPLv2 - LGPLv3 - Apache2 - WTFPL - -Optional, but it is highly recommended to supply this. + +Optional, but it is highly recommended to supply this. More identifiers are +listed at the [SPDX Open Source License Registry](http://www.spdx.org/licenses/). + +An Example: + + { + "license": "MIT" + } + + +For a package, when there is a choice between licenses (“disjunctive license”), +multiple can be specified as array. + +An Example for disjunctive licenses: + + { + "license": [ + "LGPL-2.0", + "GPL-3.0+" + ] + } + +Alternatively they can be separated with “or” and enclosed in brackets; + + { + "license": "(LGPL-2.0 or GPL-3.0+)" + } + +Similarly when multiple licenses need to be applied (“conjunctive license”), +they should be separated with “and” and enclosed in brackets. ### authors diff --git a/res/spdx-identifier.json b/res/spdx-identifier.json new file mode 100644 index 000000000..104d41a68 --- /dev/null +++ b/res/spdx-identifier.json @@ -0,0 +1,34 @@ +[ + "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "APL-1.0", + "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APSL-1.0", + "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0", "Artistic-2.0", "AAL", + "BSL-1.0", "BSD-2-Clause", "BSD-2-Clause-NetBSD", "BSD-2-Clause-FreeBSD", + "BSD-3-Clause", "BSD-4-Clause", "BSD-4-Clause-UC", "CECILL-1.0", + "CECILL-1.1", "CECILL-2.0", "CECILL-B", "CECILL-C", "ClArtistic", + "CNRI-Python-GPL-Compatible", "CNRI-Python", "CDDL-1.0", "CDDL-1.1", + "CPAL-1.0", "CPL-1.0", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", + "CC-BY-3.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", + "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0", + "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", + "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", + "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", "CC-BY-SA-3.0", "CC0-1.0", + "CUA-OPL-1.0", "EPL-1.0", "eCos-2.0", "ECL-1.0", "ECL-2.0", "EFL-1.0", + "EFL-2.0", "Entessa", "ErlPL-1.1", "EUDatagrid", "EUPL-1.0", "EUPL-1.1", + "Fair", "Frameworx-1.0", "AGPL-3.0", "GFDL-1.1", "GFDL-1.2", "GFDL-1.3", + "GPL-1.0", "GPL-1.0+", "GPL-2.0", "GPL-2.0+", + "GPL-2.0-with-autoconf-exception", "GPL-2.0-with-bison-exception", + "GPL-2.0-with-classpath-exception", "GPL-2.0-with-font-exception", + "GPL-2.0-with-GCC-exception", "GPL-3.0", "GPL-3.0+", + "GPL-3.0-with-autoconf-exception", "GPL-3.0-with-GCC-exception", "LGPL-2.1", + "LGPL-2.1+", "LGPL-3.0", "LGPL-3.0+", "LGPL-2.0", "LGPL-2.0+", "gSOAP-1.3b", + "HPND", "IPL-1.0", "IPA", "ISC", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", + "LPPL-1.3c", "Libpng", "LPL-1.0", "LPL-1.02", "MS-PL", "MS-RL", "MirOS", + "MIT", "Motosoto", "MPL-1.0", "MPL-1.1", "MPL-2.0", "Multics", "NASA-1.3", + "Naumen", "NGPL", "Nokia", "NPOSL-3.0", "NTP", "OCLC-2.0", "ODbL-1.0", + "PDDL-1.0", "OGTSL", "OSL-1.0", "OSL-2.0", "OSL-2.1", "OSL-3.0", + "OLDAP-2.8", "OpenSSL", "PHP-3.0", "PHP-3.01", "PostgreSQL", "Python-2.0", + "QPL-1.0", "RPSL-1.0", "RPL-1.5", "RHeCos-1.1", "RSCPL", "Ruby", "SAX-PD", + "OFL-1.0", "OFL-1.1", "SimPL-2.0", "Sleepycat", "SugarCRM-1.1.3", "SPL-1.0", + "Watcom-1.0", "NCSA", "VSL-1.0", "W3C", "WXwindows", "Xnet", "XFree86-1.1", + "YPL-1.0", "YPL-1.1", "Zimbra-1.3", "Zlib", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1" +] \ No newline at end of file diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index b65841d71..6e64a9cdb 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -18,67 +18,97 @@ use Symfony\Component\Console\Output\OutputInterface; use Composer\Json\JsonFile; use Composer\Json\JsonValidationException; use Composer\Util\RemoteFilesystem; +use Composer\Util\SPDXLicenseIdentifier; /** + * ValidateCommand + * * @author Robert Schönthal * @author Jordi Boggiano */ class ValidateCommand extends Command { + /** + * configure + */ protected function configure() { $this ->setName('validate') ->setDescription('Validates a composer.json') ->setDefinition(array( - new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json') - )) + new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json') + )) ->setHelp(<<getArgument('file'); if (!file_exists($file)) { - $output->writeln(''.$file.' not found.'); + $output->writeln('' . $file . ' not found.'); + return 1; } if (!is_readable($file)) { - $output->writeln(''.$file.' is not readable.'); + $output->writeln('' . $file . ' is not readable.'); + return 1; } $laxValid = false; try { $json = new JsonFile($file, new RemoteFilesystem($this->getIO())); - $json->read(); + $manifest = $json->read(); $json->validateSchema(JsonFile::LAX_SCHEMA); $laxValid = true; $json->validateSchema(); } catch (JsonValidationException $e) { if ($laxValid) { - $output->writeln(''.$file.' is valid for simple usage with composer but has'); + $output->writeln('' . $file . ' is valid for simple usage with composer but has'); $output->writeln('strict errors that make it unable to be published as a package:'); } else { - $output->writeln(''.$file.' is invalid, the following errors were found:'); + $output->writeln('' . $file . ' is invalid, the following errors were found:'); } foreach ($e->getErrors() as $message) { - $output->writeln(''.$message.''); + $output->writeln('' . $message . ''); } + return 1; } catch (\Exception $e) { - $output->writeln(''.$file.' contains a JSON Syntax Error:'); - $output->writeln(''.$e->getMessage().''); + $output->writeln('' . $file . ' contains a JSON Syntax Error:'); + $output->writeln('' . $e->getMessage() . ''); + return 1; } - $output->writeln(''.$file.' is valid'); + if (isset($manifest['license'])) { + try { + $identifier = new SPDXLicenseIdentifier($manifest['license']); + } catch (\InvalidArgumentException $e) { + $output->writeln(sprintf( + 'License "%s" is not a SPDX license identifier.', + print_r($manifest['license'], true) + )); + } + } else { + $output->writeln('No license specified.'); + } + + $output->writeln('' . $file . ' is valid'); + + return 0; } } diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index fabd43450..a1dd8b2d1 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -63,9 +63,18 @@ class Compiler foreach ($finder as $file) { $this->addFile($phar, $file); } - $this->addFile($phar, new \SplFileInfo(__DIR__.'/Autoload/ClassLoader.php'), false); - $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../res/composer-schema.json'), false); - $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../src/Composer/IO/hiddeninput.exe'), false); + $this->addFile($phar, new \SplFileInfo(__DIR__ . '/Autoload/ClassLoader.php'), false); + + $finder = new Finder(); + $finder->files() + ->name('*.json') + ->in(__DIR__ . '/../../res') + ; + + foreach ($finder as $file) { + $this->addFile($phar, $file, false); + } + $this->addFile($phar, new \SplFileInfo(__DIR__ . '/../../src/Composer/IO/hiddeninput.exe'), false); $finder = new Finder(); $finder->files() diff --git a/src/Composer/Util/SPDXLicenseIdentifier.php b/src/Composer/Util/SPDXLicenseIdentifier.php new file mode 100644 index 000000000..33bcb461d --- /dev/null +++ b/src/Composer/Util/SPDXLicenseIdentifier.php @@ -0,0 +1,228 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * SPDX License Identifier + * + * Supports composer array and SPDX tag notation for disjunctive/conjunctive + * licenses. + * + * @author Tom Klingenberg + */ +class SPDXLicenseIdentifier +{ + /** + * @var array + */ + private $identifiers; + /** + * @var array|string + */ + private $license; + + /** + * @param string|string[] $license + */ + public function __construct($license) + { + $this->initIdentifiers(); + $this->setLicense($license); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getLicense(); + } + + /** + * @return string + */ + public function getLicense() + { + return $this->license; + } + + /** + * @param array|string $license + * + * @throws \InvalidArgumentException + */ + public function setLicense($license) + { + if (is_array($license)) { + $license = $this->getLicenseFromArray($license); + } + if (!is_string($license)) { + throw new \InvalidArgumentException(sprintf( + 'Array or String expected, %s given.', gettype($license) + )); + } + if (!$this->isValidLicenseString($license)) { + throw new \InvalidArgumentException(sprintf( + 'Invalid license: "%s"', $license + )); + } + $this->license = $license; + } + + /** + * @param array $licenses + * + * @return string + */ + private function getLicenseFromArray(array $licenses) + { + $buffer = ''; + foreach ($licenses as $license) { + $buffer .= ($buffer ? ' or ' : '(') . (string)$license; + } + $buffer .= $buffer ? ')' : ''; + + return $buffer; + } + + /** + * init SPDX identifiers + */ + private function initIdentifiers() + { + $jsonFile = __DIR__ . '/../../../res/spdx-identifier.json'; + $this->identifiers = $this->arrayFromJSONFile($jsonFile); + } + + /** + * @param string $file + * + * @return array + * @throws \RuntimeException + */ + private function arrayFromJSONFile($file) + { + $data = json_decode(file_get_contents($file)); + if (!$data || !is_array($data)) { + throw new \RuntimeException(sprintf('Not a json array in file "%s"', $file)); + } + + return $data; + } + + /** + * @param string $identifier + * + * @return bool + */ + private function isValidLicenseIdentifier($identifier) + { + return in_array($identifier, $this->identifiers); + } + + /** + * @param string $license + * + * @return bool + * @throws \RuntimeException + */ + private function isValidLicenseString($license) + { + $tokens = array( + 'po' => '\(', + 'pc' => '\)', + 'op' => '(?:or|and)', + 'lix' => '(?:NONE|NOASSERTION)', + 'lir' => 'LicenseRef-\d+', + 'lic' => '[-+_.a-zA-Z0-9]{3,}', + 'ws' => '\s+', + '_' => '.', + ); + $next = function () use ($license, $tokens) + { + static $offset = 0; + if ($offset >= strlen($license)) { + return null; + } + foreach ($tokens as $name => $token) { + if (false === $r = preg_match("~$token~", $license, $matches, PREG_OFFSET_CAPTURE, $offset)) { + throw new \RuntimeException('Pattern for token %s failed (regex error).', $name); + } + if ($r === 0) { + continue; + } + if ($matches[0][1] !== $offset) { + continue; + } + $offset += strlen($matches[0][0]); + + return array($name, $matches[0][0]); + } + throw new \RuntimeException('At least the last pattern needs to match, but it did not (dot-match-all is missing?).'); + }; + $open = 0; + $require = 1; + $lastop = null; + while (list ($token, $string) = $next()) { + switch ($token) { + case 'po': + if ($open || !$require) { + return false; + } + $open = 1; + break; + case 'pc': + if ($open !== 1 || $require || !$lastop) { + return false; + } + $open = 2; + break; + case 'op': + if ($require || !$open) { + return false; + } + $lastop || $lastop = $string; + if ($lastop !== $string) { + return false; + } + $require = 1; + break; + case 'lix': + if ($open) { + return false; + } + goto lir; + case 'lic': + if (!$this->isValidLicenseIdentifier($string)) { + return false; + } + // Fall-through intended + case 'lir': + lir: + if (!$require) { + return false; + } + $require = 0; + break; + case 'ws': + break; + case '_': + return false; + default: + throw new \RuntimeException(sprintf('Unparsed token: %s.', print_r($token, true))); + } + } + + return !($open % 2 || $require); + } +} diff --git a/tests/Composer/Test/Util/SPDXLicenseIdentifierTest.php b/tests/Composer/Test/Util/SPDXLicenseIdentifierTest.php new file mode 100644 index 000000000..52bca9801 --- /dev/null +++ b/tests/Composer/Test/Util/SPDXLicenseIdentifierTest.php @@ -0,0 +1,83 @@ +assertInstanceOf('Composer\Util\SPDXLicenseIdentifier', $identifier); + } + + /** + * @dataProvider provideInvalidLicenses + * @expectedException InvalidArgumentException + * @param string|array $invalidLicense + */ + public function testInvalidLicenses($invalidLicense) + { + $identifier = new SPDXLicenseIdentifier($invalidLicense); + } + + public function testGetLicense() + { + $license = new SPDXLicenseIdentifier('NONE'); + $string = $license->getLicense(); + $this->assertInternalType('string', $string); + $string = (string)$license; + $this->assertInternalType('string', $string); + } +} \ No newline at end of file