Skip to content

Commit

Permalink
Validate config schema before loading it, fixes #10685
Browse files Browse the repository at this point in the history
  • Loading branch information
Seldaek committed Apr 1, 2022
1 parent 10287fc commit 8e93566
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 16 deletions.
58 changes: 44 additions & 14 deletions src/Composer/Factory.php
Expand Up @@ -40,6 +40,7 @@
use Composer\Json\JsonValidationException;
use Composer\Repository\InstalledRepositoryInterface;
use Seld\JsonLint\JsonParser;
use UnexpectedValueException;
use ZipArchive;

/**
Expand Down Expand Up @@ -170,18 +171,21 @@ public static function createConfig(IOInterface $io = null, ?string $cwd = null)

// determine and add main dirs to the config
$home = self::getHomeDir();
$config->merge(array('config' => array(
'home' => $home,
'cache-dir' => self::getCacheDir($home),
'data-dir' => self::getDataDir($home),
)), Config::SOURCE_DEFAULT);
$config->merge(array(
'config' => array(
'home' => $home,
'cache-dir' => self::getCacheDir($home),
'data-dir' => self::getDataDir($home),
)
), Config::SOURCE_DEFAULT);

// load global config
$file = new JsonFile($config->get('home').'/config.json');
if ($file->exists()) {
if ($io && $io->isDebug()) {
$io->writeError('Loading config file ' . $file->getPath());
if ($io) {
$io->writeError('Loading config file ' . $file->getPath(), true, IOInterface::DEBUG);
}
self::validateJsonSchema($io, $file);
$config->merge($file->read(), $file->getPath());
}
$config->setConfigSource(new JsonConfigSource($file));
Expand All @@ -205,26 +209,30 @@ public static function createConfig(IOInterface $io = null, ?string $cwd = null)
// load global auth file
$file = new JsonFile($config->get('home').'/auth.json');
if ($file->exists()) {
if ($io && $io->isDebug()) {
$io->writeError('Loading config file ' . $file->getPath());
if ($io) {
$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->setAuthConfigSource(new JsonConfigSource($file, true));

// load COMPOSER_AUTH environment variable if set
if ($composerAuthEnv = Platform::getEnv('COMPOSER_AUTH')) {
$authData = json_decode($composerAuthEnv, true);

$authData = json_decode($composerAuthEnv);
if (null === $authData) {
if ($io) {
$io->writeError('<error>COMPOSER_AUTH environment variable is malformed, should be a valid JSON object</error>');
}
} else {
if ($io && $io->isDebug()) {
$io->writeError('Loading auth config from COMPOSER_AUTH');
if ($io) {
$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');
}
}

Expand Down Expand Up @@ -690,4 +698,26 @@ private static function getUserDir(): string

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);
}
}
}
}
14 changes: 12 additions & 2 deletions src/Composer/Json/JsonFile.php
Expand Up @@ -30,6 +30,7 @@ class JsonFile
{
public const LAX_SCHEMA = 1;
public const STRICT_SCHEMA = 2;
public const AUTH_SCHEMA = 3;

/** @deprecated Use \JSON_UNESCAPED_SLASHES */
public const JSON_UNESCAPED_SLASHES = 64;
Expand Down Expand Up @@ -186,7 +187,9 @@ private function filePutContentsIfModified(string $path, string $content)
* @param string|null $schemaFile a path to the schema file
* @throws JsonValidationException
* @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
{
Expand All @@ -197,6 +200,11 @@ public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schem
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;
if (null === $schemaFile) {
$isComposerSchemaFile = true;
Expand All @@ -216,6 +224,8 @@ public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schem
} elseif ($schema === self::STRICT_SCHEMA && $isComposerSchemaFile) {
$schemaData->additionalProperties = false;
$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();
Expand All @@ -226,7 +236,7 @@ public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schem
foreach ((array) $validator->getErrors() as $error) {
$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;
Expand Down
14 changes: 14 additions & 0 deletions tests/Composer/Test/Json/JsonFileTest.php
Expand Up @@ -239,6 +239,20 @@ public function testCustomSchemaValidationStrict(): void
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
{
$json = '{
Expand Down

0 comments on commit 8e93566

Please sign in to comment.