Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Latest
## Changes
* [#18](https://github.com/cleverage/ui-process-bundle/issues/18) PHPStan level 10 (was 8), on `src` and `tests`: `LogProcessFilter::new()` `$label` typed as `TranslatableInterface|string|false|null` (the type of `setLabel()`), `LogRecord::$context` documented as `array<mixed>` (the Monolog context), conditional return type of `ProcessExecution::getReport()`.

## Fixes
* [#135](https://github.com/cleverage/ui-process-bundle/issues/135) HTTP API: a `context` given as a JSON string that does not decode to an array (`"1"`, `"true"`, `"\"abc\""`, `"null"`, `""`) is rejected with a `422` (`Context must be a JSON object or array.`); it gave a 500 (`TypeError`), whose message exposed the server paths with a synchronous execution. Add tests.

v3.1
------

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/06-http_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Parameters can be sent either as a JSON body, or as form data (`application/x-ww
|-----------|---------------------------|:--------:|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `code` | `string` | **X** | | Code of the process. It must exist and be public. |
| `input` | `string` or file | | `null` | Process input. With form data, it can be an uploaded file: the file is saved in the `upload_directory` directory (see [bundle configuration](01-bundle_configuration.md#container-parameters)) and its path is passed as input. |
| `context` | `object` or JSON `string` | | `{}` | Process context, as key/value pairs. With form data, send one field per value: `context[key]=value`. |
| `context` | `object` or JSON `string` | | `{}` | Process context, as key/value pairs. A JSON string must decode to an object or an array (`422` otherwise). With form data, send one field per value: `context[key]=value`. |
| `queue` | `bool` | | `true` | `true`: the process is queued to the `execute_process` transport (see [messenger](08-messenger.md)). `false`: the process is executed during the HTTP request. |

`code`, `input`, `context` and `queue` can also be passed in the query string, with form data or without body (the form
Expand All @@ -46,7 +46,7 @@ Responses
| `200` | `"Process has been added to queue. It will start as soon as possible."` | `queue` is `true`. |
| `200` | `"Process has been proceed well."` | `queue` is `false` and the process succeeded. |
| `500` | The exception message and `(process execution: <id>)`, as a JSON string | `queue` is `false` and the process failed. |
| `422` | Violation messages, e.g. `Process code is required.`, `The process "foo" does not exist.`, `The process "foo" is not public.` | Invalid parameters. A request body that cannot be parsed is handled as an empty request. |
| `422` | Violation messages, e.g. `Process code is required.`, `The process "foo" does not exist.`, `The process "foo" is not public.`, `Context must be a JSON object or array.` | Invalid parameters. A request body that cannot be parsed is handled as an empty request. |

Once the process has started, its execution is recorded in the [executions list](04-process_executions_and_logs.md), like any
other execution.
Expand Down
26 changes: 26 additions & 0 deletions src/Http/Model/HttpProcessExecution.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

use CleverAge\UiProcessBundle\Validator\IsValidProcessCode;
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Json;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Sequentially;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

final readonly class HttpProcessExecution
{
Expand All @@ -34,4 +36,28 @@ public function __construct(
public bool $queue = true,
) {
}

/**
* A JSON context must decode to an array: the Json constraint also accepts scalars ("1", "null"...) and "".
*/
#[Callback]
public function validateContext(ExecutionContextInterface $context): void
{
if (!\is_string($this->context)) {
return;
}
try {
$decoded = json_decode($this->context, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
if ('' !== $this->context) {
return; // Reported by the Json constraint
}
$decoded = null;
}
if (!\is_array($decoded)) {
$context->buildViolation('Context must be a JSON object or array.')
->atPath('context')
->addViolation();
}
}
}
30 changes: 30 additions & 0 deletions tests/Functional/HttpProcessExecuteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use CleverAge\UiProcessBundle\Twig\Extension\ProcessExtension;
use CleverAge\UiProcessBundle\Validator\IsValidProcessCodeValidator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\UsesClass;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;

Expand Down Expand Up @@ -154,6 +155,35 @@ public function testJsonBody(): void
self::assertSame(['key' => 'value'], $messages[0]->context);
}

/**
* A JSON context that does not decode to an array is rejected (it gave a 500: TypeError).
*/
#[DataProvider('provideScalarJsonContext')]
public function testScalarJsonContext(string $context, bool $queue): void
{
$this->client->request(
'POST',
'/http/process/execute',
server: ['HTTP_AUTHORIZATION' => 'Bearer '.self::TOKEN, 'CONTENT_TYPE' => 'application/json'],
content: json_encode(['code' => 'test.process', 'context' => $context, 'queue' => $queue], \JSON_THROW_ON_ERROR)
);

self::assertResponseStatusCodeSame(422);
self::assertSame([], $this->getDispatchedMessages());
self::assertSame([], $this->getEntityManager()->getRepository(ProcessExecution::class)->findAll());
}

/**
* @return iterable<string, array{string, bool}>
*/
public static function provideScalarJsonContext(): iterable
{
foreach (['1', 'true', '"abc"', 'null', ''] as $context) {
yield $context.' queued' => [$context, true];
yield $context.' synchronous' => [$context, false];
}
}

public function testUnknownProcess(): void
{
$this->execute(['code' => 'unknown']);
Expand Down
8 changes: 8 additions & 0 deletions tests/Http/Model/HttpProcessExecutionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ public static function provideValidation(): iterable
{
yield 'array context' => ['demo.process', ['key' => 'value'], []];
yield 'json context' => ['demo.process', '{"key":"value"}', []];
yield 'json list context' => ['demo.process', '["value"]', []];
yield 'empty json object context' => ['demo.process', '{}', []];
// Valid JSON accepted by the Json constraint, but not decoded to an array
yield 'json integer context' => ['demo.process', '1', ['context: Context must be a JSON object or array.']];
yield 'json boolean context' => ['demo.process', 'true', ['context: Context must be a JSON object or array.']];
yield 'json string context' => ['demo.process', '"abc"', ['context: Context must be a JSON object or array.']];
yield 'json null context' => ['demo.process', 'null', ['context: Context must be a JSON object or array.']];
yield 'empty string context' => ['demo.process', '', ['context: Context must be a JSON object or array.']];
yield 'missing code' => [null, [], ['code: Process code is required.']];
yield 'unknown code' => ['demo.unknown', [], ['code: The process "demo.unknown" does not exist.']];
}
Expand Down
Loading