Validate config schema before loading it, fixes #10685
parent
10287fcea3
commit
8e93566c18
|
@ -40,6 +40,7 @@ use Composer\Downloader\TransportException;
|
||||||
use Composer\Json\JsonValidationException;
|
use Composer\Json\JsonValidationException;
|
||||||
use Composer\Repository\InstalledRepositoryInterface;
|
use Composer\Repository\InstalledRepositoryInterface;
|
||||||
use Seld\JsonLint\JsonParser;
|
use Seld\JsonLint\JsonParser;
|
||||||
|
use UnexpectedValueException;
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,18 +171,21 @@ class Factory
|
||||||
|
|
||||||
// determine and add main dirs to the config
|
// determine and add main dirs to the config
|
||||||
$home = self::getHomeDir();
|
$home = self::getHomeDir();
|
||||||
$config->merge(array('config' => array(
|
$config->merge(array(
|
||||||
|
'config' => array(
|
||||||
'home' => $home,
|
'home' => $home,
|
||||||
'cache-dir' => self::getCacheDir($home),
|
'cache-dir' => self::getCacheDir($home),
|
||||||
'data-dir' => self::getDataDir($home),
|
'data-dir' => self::getDataDir($home),
|
||||||
)), Config::SOURCE_DEFAULT);
|
)
|
||||||
|
), Config::SOURCE_DEFAULT);
|
||||||
|
|
||||||
// load global config
|
// load global config
|
||||||
$file = new JsonFile($config->get('home').'/config.json');
|
$file = new JsonFile($config->get('home').'/config.json');
|
||||||
if ($file->exists()) {
|
if ($file->exists()) {
|
||||||
if ($io && $io->isDebug()) {
|
if ($io) {
|
||||||
$io->writeError('Loading config file ' . $file->getPath());
|
$io->writeError('Loading config file ' . $file->getPath(), true, IOInterface::DEBUG);
|
||||||
}
|
}
|
||||||
|
self::validateJsonSchema($io, $file);
|
||||||
$config->merge($file->read(), $file->getPath());
|
$config->merge($file->read(), $file->getPath());
|
||||||
}
|
}
|
||||||
$config->setConfigSource(new JsonConfigSource($file));
|
$config->setConfigSource(new JsonConfigSource($file));
|
||||||
|
@ -205,28 +209,32 @@ class Factory
|
||||||
// load global auth file
|
// load global auth file
|
||||||
$file = new JsonFile($config->get('home').'/auth.json');
|
$file = new JsonFile($config->get('home').'/auth.json');
|
||||||
if ($file->exists()) {
|
if ($file->exists()) {
|
||||||
if ($io && $io->isDebug()) {
|
if ($io) {
|
||||||
$io->writeError('Loading config file ' . $file->getPath());
|
$io->writeError('Loading config file ' . $file->getPath(), true, IOInterface::DEBUG);
|
||||||
}
|
}
|
||||||
|
self::validateJsonSchema($io, $file, JsonFile::AUTH_SCHEMA);
|
||||||
$config->merge(array('config' => $file->read()), $file->getPath());
|
$config->merge(array('config' => $file->read()), $file->getPath());
|
||||||
}
|
}
|
||||||
$config->setAuthConfigSource(new JsonConfigSource($file, true));
|
$config->setAuthConfigSource(new JsonConfigSource($file, true));
|
||||||
|
|
||||||
// load COMPOSER_AUTH environment variable if set
|
// load COMPOSER_AUTH environment variable if set
|
||||||
if ($composerAuthEnv = Platform::getEnv('COMPOSER_AUTH')) {
|
if ($composerAuthEnv = Platform::getEnv('COMPOSER_AUTH')) {
|
||||||
$authData = json_decode($composerAuthEnv, true);
|
$authData = json_decode($composerAuthEnv);
|
||||||
|
|
||||||
if (null === $authData) {
|
if (null === $authData) {
|
||||||
if ($io) {
|
if ($io) {
|
||||||
$io->writeError('<error>COMPOSER_AUTH environment variable is malformed, should be a valid JSON object</error>');
|
$io->writeError('<error>COMPOSER_AUTH environment variable is malformed, should be a valid JSON object</error>');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($io && $io->isDebug()) {
|
if ($io) {
|
||||||
$io->writeError('Loading auth config from COMPOSER_AUTH');
|
$io->writeError('Loading auth config from COMPOSER_AUTH', true, IOInterface::DEBUG);
|
||||||
}
|
}
|
||||||
|
self::validateJsonSchema($io, $authData, JsonFile::AUTH_SCHEMA, 'COMPOSER_AUTH');
|
||||||
|
$authData = json_decode($composerAuthEnv, true);
|
||||||
|
if (null !== $authData) {
|
||||||
$config->merge(array('config' => $authData), 'COMPOSER_AUTH');
|
$config->merge(array('config' => $authData), 'COMPOSER_AUTH');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
@ -690,4 +698,26 @@ class Factory
|
||||||
|
|
||||||
return rtrim(strtr($home, '\\', '/'), '/');
|
return rtrim(strtr($home, '\\', '/'), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $fileOrData
|
||||||
|
* @param JsonFile::*_SCHEMA $schema
|
||||||
|
*/
|
||||||
|
private static function validateJsonSchema(?IOInterface $io, $fileOrData, int $schema = JsonFile::LAX_SCHEMA, ?string $source = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if ($fileOrData instanceof JsonFile) {
|
||||||
|
$fileOrData->validateSchema($schema);
|
||||||
|
} else {
|
||||||
|
JsonFile::validateJsonSchema($source, $fileOrData, $schema);
|
||||||
|
}
|
||||||
|
} catch (JsonValidationException $e) {
|
||||||
|
$msg = $e->getMessage().', this may result in errors and should be resolved:'.PHP_EOL.' - '.implode(PHP_EOL.' - ', $e->getErrors());
|
||||||
|
if ($io) {
|
||||||
|
$io->writeError('<warning>'.$msg.'</>');
|
||||||
|
} else {
|
||||||
|
throw new UnexpectedValueException($msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ class JsonFile
|
||||||
{
|
{
|
||||||
public const LAX_SCHEMA = 1;
|
public const LAX_SCHEMA = 1;
|
||||||
public const STRICT_SCHEMA = 2;
|
public const STRICT_SCHEMA = 2;
|
||||||
|
public const AUTH_SCHEMA = 3;
|
||||||
|
|
||||||
/** @deprecated Use \JSON_UNESCAPED_SLASHES */
|
/** @deprecated Use \JSON_UNESCAPED_SLASHES */
|
||||||
public const JSON_UNESCAPED_SLASHES = 64;
|
public const JSON_UNESCAPED_SLASHES = 64;
|
||||||
|
@ -186,7 +187,9 @@ class JsonFile
|
||||||
* @param string|null $schemaFile a path to the schema file
|
* @param string|null $schemaFile a path to the schema file
|
||||||
* @throws JsonValidationException
|
* @throws JsonValidationException
|
||||||
* @throws ParsingException
|
* @throws ParsingException
|
||||||
* @return bool true on success
|
* @return true true on success
|
||||||
|
*
|
||||||
|
* @phpstan-param self::*_SCHEMA $schema
|
||||||
*/
|
*/
|
||||||
public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schemaFile = null): bool
|
public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schemaFile = null): bool
|
||||||
{
|
{
|
||||||
|
@ -197,6 +200,11 @@ class JsonFile
|
||||||
self::validateSyntax($content, $this->path);
|
self::validateSyntax($content, $this->path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return self::validateJsonSchema($this->path, $data, $schema, $schemaFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateJsonSchema($source, $data, int $schema, ?string $schemaFile = null): bool
|
||||||
|
{
|
||||||
$isComposerSchemaFile = false;
|
$isComposerSchemaFile = false;
|
||||||
if (null === $schemaFile) {
|
if (null === $schemaFile) {
|
||||||
$isComposerSchemaFile = true;
|
$isComposerSchemaFile = true;
|
||||||
|
@ -216,6 +224,8 @@ class JsonFile
|
||||||
} elseif ($schema === self::STRICT_SCHEMA && $isComposerSchemaFile) {
|
} elseif ($schema === self::STRICT_SCHEMA && $isComposerSchemaFile) {
|
||||||
$schemaData->additionalProperties = false;
|
$schemaData->additionalProperties = false;
|
||||||
$schemaData->required = array('name', 'description');
|
$schemaData->required = array('name', 'description');
|
||||||
|
} elseif ($schema === self::AUTH_SCHEMA && $isComposerSchemaFile) {
|
||||||
|
$schemaData = (object) array('$ref' => $schemaFile.'#/properties/config', '$schema'=> "https://json-schema.org/draft-04/schema#");
|
||||||
}
|
}
|
||||||
|
|
||||||
$validator = new Validator();
|
$validator = new Validator();
|
||||||
|
@ -226,7 +236,7 @@ class JsonFile
|
||||||
foreach ((array) $validator->getErrors() as $error) {
|
foreach ((array) $validator->getErrors() as $error) {
|
||||||
$errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message'];
|
$errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message'];
|
||||||
}
|
}
|
||||||
throw new JsonValidationException('"'.$this->path.'" does not match the expected JSON schema', $errors);
|
throw new JsonValidationException('"'.$source.'" does not match the expected JSON schema', $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -239,6 +239,20 @@ class JsonFileTest extends TestCase
|
||||||
unlink($schema);
|
unlink($schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAuthSchemaValidationWithCustomDataSource(): void
|
||||||
|
{
|
||||||
|
$json = json_decode('{"github-oauth": "foo"}');
|
||||||
|
$expectedMessage = sprintf('"COMPOSER_AUTH" does not match the expected JSON schema');
|
||||||
|
$expectedError = 'github-oauth : String value found, but an object is required';
|
||||||
|
try {
|
||||||
|
JsonFile::validateJsonSchema('COMPOSER_AUTH', $json, JsonFile::AUTH_SCHEMA);
|
||||||
|
$this->fail('Expected exception to be thrown');
|
||||||
|
} catch (JsonValidationException $e) {
|
||||||
|
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||||
|
$this->assertSame([$expectedError], $e->getErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testParseErrorDetectMissingCommaMultiline(): void
|
public function testParseErrorDetectMissingCommaMultiline(): void
|
||||||
{
|
{
|
||||||
$json = '{
|
$json = '{
|
||||||
|
|
Loading…
Reference in New Issue