diff --git a/src/Tools/core/get/iTopGetTools.php b/src/Tools/core/get/iTopGetTools.php index e5133b1..dae3d42 100644 --- a/src/Tools/core/get/iTopGetTools.php +++ b/src/Tools/core/get/iTopGetTools.php @@ -105,11 +105,12 @@ protected function getPhoneFieldsFromPerson(): array #[McpTool(name: TOOL_PREFIX.'search-user-request-by-caller-status-start-date', annotations: new ToolAnnotations(null, true, false, true, false))] public function searchUserRequestByCallerStatusStartDate(#[Schema(format: 'email')] string $caller_email, array $statuses = [], string $start_date = '', string $condition = 'greater_than'): string { + $statuses = array_map(fn($status) => static::quoteString((string)$status), $statuses); return $this->runToolFromTemplates('searchUserRequestFromCallerStatusDate', 'UserRequest', [ 'caller_email' => static::quoteString($caller_email), - 'statuses' => count($statuses) > 0 ? "'".implode("','", $statuses)."'" : '', - 'start_date' => $start_date, + 'statuses' => count($statuses) > 0 ? "'".implode("','", $statuses)."'" : '', + 'start_date' => static::quoteString($start_date), 'condition' => $condition === 'greater_than' ? '>=' : '<=', ] ); diff --git a/tests/phpunit/ToolsFromTemplateTest.php b/tests/phpunit/ToolsFromTemplateTest.php index 6d1156d..624af4d 100644 --- a/tests/phpunit/ToolsFromTemplateTest.php +++ b/tests/phpunit/ToolsFromTemplateTest.php @@ -167,4 +167,25 @@ public function testQuoteString(): void $this->assertEquals("Underscore\\_Test", iTopGetTools::quoteString("Underscore_Test")); $this->assertEquals("Backslash\\\\Test", iTopGetTools::quoteString("Backslash\\Test")); } -} \ No newline at end of file + + /** + * Every caller-supplied value must stay inside its own OQL string literal: a quote in a + * status or in the start date must not close it and add conditions to the query. + * The OQL is checked once the JSON is decoded, as iTop does, so that the assertion does + * not depend on how Twig escapes each character. + */ + public function testSearchUserRequestQuotesStatusesAndStartDate(): void + { + $expectedKey = <<<'OQL' +SELECT UserRequest AS U JOIN Person AS P ON U.caller_id = P.id WHERE P.email='john@example.com' AND U.status IN('new','x\') OR (\'1\'=\'1') AND U.start_date >= '2026-01-01\' OR \'1\'=\'1' +OQL; + $mockiTopClient = $this->createMock(iTopClient::class); + $mockiTopClient->expects(self::once()) + ->method('postJsonToItop') + ->with($this->callback(fn(string $json) => (json_decode($json, true)['key'] ?? null) === $expectedKey)) + ->willReturn(json_encode(['objects' => null, 'code' => 0, 'message' => 'Found: 0'])); + + $tools = new iTopGetTools($this->twigEnvironment, $mockiTopClient, $this->logger, $this->datamodel); + $tools->searchUserRequestByCallerStatusStartDate('john@example.com', ['new', "x') OR ('1'='1"], "2026-01-01' OR '1'='1"); + } +}