added schema/syntax validation for composer.json
parent
a1619788ba
commit
6929c42848
151
bin/installer
151
bin/installer
|
@ -1,151 +0,0 @@
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Composer.
|
|
||||||
*
|
|
||||||
* (c) Nils Adermann <naderman@naderman.de>
|
|
||||||
* Jordi Boggiano <j.boggiano@seld.be>
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
process($argv);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* processes the installer
|
|
||||||
*/
|
|
||||||
function process($argv)
|
|
||||||
{
|
|
||||||
$check = in_array('--check', $argv);
|
|
||||||
$help = in_array('--help', $argv);
|
|
||||||
$force = in_array('--force', $argv);
|
|
||||||
|
|
||||||
if ($help) {
|
|
||||||
displayHelp();
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = checkPlatform();
|
|
||||||
|
|
||||||
if ($check && !$ok) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ok || $force) {
|
|
||||||
installComposer();
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* displays the help
|
|
||||||
*/
|
|
||||||
function displayHelp()
|
|
||||||
{
|
|
||||||
echo <<<EOF
|
|
||||||
Composer Installer
|
|
||||||
------------------
|
|
||||||
Options
|
|
||||||
--help this help
|
|
||||||
--check for checking environment only
|
|
||||||
--force forces the installation
|
|
||||||
|
|
||||||
EOF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check the platform for possible issues on running composer
|
|
||||||
*/
|
|
||||||
function checkPlatform()
|
|
||||||
{
|
|
||||||
$errors = array();
|
|
||||||
if (false !== ini_get('detect_unicode')) {
|
|
||||||
$errors['unicode'] = 'On';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ini_get('phar.readonly')) {
|
|
||||||
$errors['readonly'] = 'On';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ini_get('phar.require_hash')) {
|
|
||||||
$errors['require_hash'] = 'On';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($suhosin = ini_get('suhosin.executor.include.whitelist') && (isset($suhosin) && false === stripos($suhosin, 'phar'))) {
|
|
||||||
$errors['suhosin'] = $suhosin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PHP_VERSION < '5.3.2') {
|
|
||||||
$errors['php'] = PHP_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($errors)) {
|
|
||||||
out("Composer detected that you have enabled some settings in your `php.ini` file that can make Composer unable to work properly.".PHP_EOL, 'error');
|
|
||||||
|
|
||||||
echo PHP_EOL.'Make sure that you have changed options listed below:'.PHP_EOL;
|
|
||||||
foreach ($errors as $error => $actual) {
|
|
||||||
switch ($error) {
|
|
||||||
case 'unicode':
|
|
||||||
$text = " detect_unicode = Off (actual: {$actual})".PHP_EOL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'readonly':
|
|
||||||
$text = " phar.readonly = Off (actual: {$actual})".PHP_EOL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'require_hash':
|
|
||||||
$text = " phar.require_hash = Off (actual: {$actual})".PHP_EOL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'suhosin':
|
|
||||||
$text = " suhosin.executor.include.whitelist = phar (actual: {$actual})".PHP_EOL;
|
|
||||||
break;
|
|
||||||
case 'php':
|
|
||||||
$text = " PHP_VERSION (actual: {$actual})".PHP_EOL;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
out($text, 'info');
|
|
||||||
}
|
|
||||||
echo PHP_EOL;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
out("All settings correct for using Composer".PHP_EOL,'success');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* installs composer to the current working directory
|
|
||||||
*/
|
|
||||||
function installComposer()
|
|
||||||
{
|
|
||||||
$installDir = getcwd();
|
|
||||||
$file = $installDir . DIRECTORY_SEPARATOR . 'composer.phar';
|
|
||||||
|
|
||||||
if (is_readable($file)) {
|
|
||||||
@unlink($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$download = copy('http://getcomposer.org/composer.phar', $installDir.DIRECTORY_SEPARATOR.'composer.phar');
|
|
||||||
|
|
||||||
out(PHP_EOL."Composer successfully installed to: " . $file, 'success');
|
|
||||||
out(PHP_EOL."Use it: php composer.phar".PHP_EOL, 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* colorize output
|
|
||||||
*/
|
|
||||||
function out($text, $color = null)
|
|
||||||
{
|
|
||||||
$styles = array(
|
|
||||||
'success' => "\033[0;32m%s\033[0m",
|
|
||||||
'error' => "\033[31;31m%s\033[0m",
|
|
||||||
'info' => "\033[33;33m%s\033[0m"
|
|
||||||
);
|
|
||||||
|
|
||||||
echo sprintf(isset($styles[$color]) ? $styles[$color] : "%s", $text);
|
|
||||||
}
|
|
|
@ -19,6 +19,8 @@
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.0",
|
"php": ">=5.3.0",
|
||||||
|
"justinrainbow/json-schema": ">=1.1.0",
|
||||||
|
"seld/jsonlint": "*",
|
||||||
"symfony/console": "dev-master",
|
"symfony/console": "dev-master",
|
||||||
"symfony/finder": "dev-master",
|
"symfony/finder": "dev-master",
|
||||||
"symfony/process": "dev-master"
|
"symfony/process": "dev-master"
|
||||||
|
|
|
@ -35,8 +35,7 @@
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Package version, see http://packagist.org/about for more info on valid schemes.",
|
"description": "Package version, see http://packagist.org/about for more info on valid schemes."
|
||||||
"required": true
|
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -53,7 +53,7 @@ EOT
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JsonFile::parseJson(file_get_contents($file));
|
JsonFile::parseJson(file_get_contents($file), true);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$output->writeln('<error>'.$e->getMessage().'</error>');
|
$output->writeln('<error>'.$e->getMessage().'</error>');
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Factory
|
||||||
'vendor-dir' => 'vendor',
|
'vendor-dir' => 'vendor',
|
||||||
);
|
);
|
||||||
|
|
||||||
$packageConfig = $file->read();
|
$packageConfig = $file->read(true);
|
||||||
|
|
||||||
if (isset($packageConfig['config']) && is_array($packageConfig['config'])) {
|
if (isset($packageConfig['config']) && is_array($packageConfig['config'])) {
|
||||||
$packageConfig['config'] = array_merge($composerConfig, $packageConfig['config']);
|
$packageConfig['config'] = array_merge($composerConfig, $packageConfig['config']);
|
||||||
|
|
|
@ -14,6 +14,8 @@ namespace Composer\Json;
|
||||||
|
|
||||||
use Composer\Repository\RepositoryManager;
|
use Composer\Repository\RepositoryManager;
|
||||||
use Composer\Composer;
|
use Composer\Composer;
|
||||||
|
use JsonSchema\Validator;
|
||||||
|
use Seld\JsonLint\JsonParser;
|
||||||
use Composer\Util\StreamContextFactory;
|
use Composer\Util\StreamContextFactory;
|
||||||
|
|
||||||
if (!defined('JSON_UNESCAPED_SLASHES')) {
|
if (!defined('JSON_UNESCAPED_SLASHES')) {
|
||||||
|
@ -68,7 +70,7 @@ class JsonFile
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function read()
|
public function read($validate = false)
|
||||||
{
|
{
|
||||||
$ctx = StreamContextFactory::getContext(array(
|
$ctx = StreamContextFactory::getContext(array(
|
||||||
'http' => array(
|
'http' => array(
|
||||||
|
@ -80,7 +82,7 @@ class JsonFile
|
||||||
throw new \RuntimeException('Could not read '.$this->path.', you are probably offline');
|
throw new \RuntimeException('Could not read '.$this->path.', you are probably offline');
|
||||||
}
|
}
|
||||||
|
|
||||||
return static::parseJson($json);
|
return static::parseJson($json, $validate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -216,58 +218,63 @@ class JsonFile
|
||||||
* Parses json string and returns hash.
|
* Parses json string and returns hash.
|
||||||
*
|
*
|
||||||
* @param string $json json string
|
* @param string $json json string
|
||||||
|
* @param boolean $validateSchema wether to validate the json schema
|
||||||
*
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
static public function parseJson($json)
|
public static function parseJson($json, $validateSchema=false)
|
||||||
{
|
{
|
||||||
$data = json_decode($json, true);
|
$data = static::validateSyntax($json);
|
||||||
|
|
||||||
if (null === $data && 'null' !== strtolower($json)) {
|
if ($validateSchema) {
|
||||||
switch (json_last_error()) {
|
static::validateSchema($json);
|
||||||
case JSON_ERROR_NONE:
|
|
||||||
$msg = 'No error has occurred, is your composer.json file empty?';
|
|
||||||
break;
|
|
||||||
case JSON_ERROR_DEPTH:
|
|
||||||
$msg = 'The maximum stack depth has been exceeded';
|
|
||||||
break;
|
|
||||||
case JSON_ERROR_STATE_MISMATCH:
|
|
||||||
$msg = 'Invalid or malformed JSON';
|
|
||||||
break;
|
|
||||||
case JSON_ERROR_CTRL_CHAR:
|
|
||||||
$msg = 'Control character error, possibly incorrectly encoded';
|
|
||||||
break;
|
|
||||||
case JSON_ERROR_SYNTAX:
|
|
||||||
$msg = 'Syntax error';
|
|
||||||
$charOffset = 0;
|
|
||||||
if (preg_match('#["}\]]\s*(,)\s*[}\]]#', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', extra comma';
|
|
||||||
} elseif (preg_match('#((?<=[^\\\\])\\\\(?!["\\\\/bfnrt]|u[a-f0-9]{4}))#i', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', unescaped backslash (\\)';
|
|
||||||
} elseif (preg_match('#(["}\]])(?: *\r?\n *)+"#', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', missing comma';
|
|
||||||
$charOffset = 1;
|
|
||||||
} elseif (preg_match('#^ *([a-z0-9_-]+) *:#mi', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', you must use double quotes (") around keys';
|
|
||||||
} elseif (preg_match('#(\'.+?\' *:|: *\'.+?\')#', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', use double quotes (") instead of single quotes (\')';
|
|
||||||
} elseif (preg_match('#(\[".*?":.*?\])#', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"])';
|
|
||||||
} elseif (preg_match('#".*?"( *["{\[])#', $json, $match, PREG_OFFSET_CAPTURE)) {
|
|
||||||
$msg .= ', missing colon';
|
|
||||||
}
|
|
||||||
if (isset($match[1][1])) {
|
|
||||||
$preError = substr($json, 0, $match[1][1]);
|
|
||||||
$msg .= ' on line '.(substr_count($preError, "\n")+1).', char '.abs(strrpos($preError, "\n") - strlen($preError) - $charOffset);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case JSON_ERROR_UTF8:
|
|
||||||
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
throw new \UnexpectedValueException('JSON Parse Error: '.$msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates a composer.json against the schema
|
||||||
|
*
|
||||||
|
* @param string $json
|
||||||
|
* @return boolean
|
||||||
|
* @throws \UnexpectedValueException
|
||||||
|
*/
|
||||||
|
public static function validateSchema($json)
|
||||||
|
{
|
||||||
|
$data = json_decode($json);
|
||||||
|
$schema = json_decode(file_get_contents(__DIR__ . '/../../../res/composer-schema.json'));
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
|
||||||
|
$validator->check($data, $schema);
|
||||||
|
|
||||||
|
if (!$validator->isValid()) {
|
||||||
|
$msg = "\n";
|
||||||
|
foreach ((array) $validator->getErrors() as $error) {
|
||||||
|
$msg .= ($error['property'] ? $error['property'].' : ' : '').$error['message']."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \UnexpectedValueException('Your composer.json did not validate against the schema. The following mistakes were found:'.$msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validates the json syntax
|
||||||
|
*
|
||||||
|
* @param string $json
|
||||||
|
* @return array
|
||||||
|
* @throws \UnexpectedValueException
|
||||||
|
*/
|
||||||
|
public static function validateSyntax($json)
|
||||||
|
{
|
||||||
|
$parser = new JsonParser();
|
||||||
|
$result = $parser->lint($json);
|
||||||
|
|
||||||
|
if (null === $result) {
|
||||||
|
return json_decode($json, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
namespace Composer\Test\Json;
|
namespace Composer\Test\Json;
|
||||||
|
|
||||||
|
use Seld\JsonLint\ParsingException;
|
||||||
use Composer\Json\JsonFile;
|
use Composer\Json\JsonFile;
|
||||||
|
|
||||||
class JsonFileTest extends \PHPUnit_Framework_TestCase
|
class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
|
@ -21,7 +22,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
$json = '{
|
$json = '{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('extra comma on line 2, char 21', $json);
|
$this->expectParseException('Parse error on line 2', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectExtraCommaInArray()
|
public function testParseErrorDetectExtraCommaInArray()
|
||||||
|
@ -31,7 +32,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
"bar",
|
"bar",
|
||||||
]
|
]
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('extra comma on line 3, char 18', $json);
|
$this->expectParseException('Parse error on line 3', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectUnescapedBackslash()
|
public function testParseErrorDetectUnescapedBackslash()
|
||||||
|
@ -39,7 +40,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
$json = '{
|
$json = '{
|
||||||
"fo\o": "bar"
|
"fo\o": "bar"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('unescaped backslash (\\) on line 2, char 12', $json);
|
$this->expectParseException('Parse error on line 1', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorSkipsEscapedBackslash()
|
public function testParseErrorSkipsEscapedBackslash()
|
||||||
|
@ -48,7 +49,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
"fo\\\\o": "bar"
|
"fo\\\\o": "bar"
|
||||||
"a": "b"
|
"a": "b"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('missing comma on line 2, char 23', $json);
|
$this->expectParseException('Parse error on line 2', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectSingleQuotes()
|
public function testParseErrorDetectSingleQuotes()
|
||||||
|
@ -56,7 +57,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
$json = '{
|
$json = '{
|
||||||
\'foo\': "bar"
|
\'foo\': "bar"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('use double quotes (") instead of single quotes (\') on line 2, char 9', $json);
|
$this->expectParseException('Parse error on line 1', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectMissingQuotes()
|
public function testParseErrorDetectMissingQuotes()
|
||||||
|
@ -64,7 +65,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
$json = '{
|
$json = '{
|
||||||
foo: "bar"
|
foo: "bar"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('must use double quotes (") around keys on line 2, char 9', $json);
|
$this->expectParseException('Parse error on line 1', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectArrayAsHash()
|
public function testParseErrorDetectArrayAsHash()
|
||||||
|
@ -72,7 +73,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
$json = '{
|
$json = '{
|
||||||
"foo": ["bar": "baz"]
|
"foo": ["bar": "baz"]
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"]) on line 2, char 16', $json);
|
$this->expectParseException('Parse error on line 2', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectMissingComma()
|
public function testParseErrorDetectMissingComma()
|
||||||
|
@ -81,7 +82,18 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
"foo": "bar"
|
"foo": "bar"
|
||||||
"bar": "foo"
|
"bar": "foo"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('missing comma on line 2, char 21', $json);
|
$this->expectParseException('Parse error on line 2', $json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchemaValidation()
|
||||||
|
{
|
||||||
|
$json = file_get_contents(__DIR__.'/../../../../composer.json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->assertNull(JsonFile::validateSchema($json));
|
||||||
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
$this->fail('invalid schema');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectMissingCommaMultiline()
|
public function testParseErrorDetectMissingCommaMultiline()
|
||||||
|
@ -91,7 +103,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
|
|
||||||
"bar": "foo"
|
"bar": "foo"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('missing comma on line 2, char 24', $json);
|
$this->expectParseException('Parse error on line 2', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectMissingColon()
|
public function testParseErrorDetectMissingColon()
|
||||||
|
@ -100,7 +112,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
"bar" "foo"
|
"bar" "foo"
|
||||||
}';
|
}';
|
||||||
$this->expectParseException('missing colon on line 3, char 14', $json);
|
$this->expectParseException('Parse error on line 3', $json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSimpleJsonString()
|
public function testSimpleJsonString()
|
||||||
|
@ -166,7 +178,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
|
||||||
try {
|
try {
|
||||||
JsonFile::parseJson($json);
|
JsonFile::parseJson($json);
|
||||||
$this->fail();
|
$this->fail();
|
||||||
} catch (\UnexpectedValueException $e) {
|
} catch (ParsingException $e) {
|
||||||
$this->assertContains($text, $e->getMessage());
|
$this->assertContains($text, $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue