Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/Tools/core/get/iTopGetTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ? '>=' : '<=',
]
);
Expand Down
23 changes: 22 additions & 1 deletion tests/phpunit/ToolsFromTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,25 @@ public function testQuoteString(): void
$this->assertEquals("Underscore\\_Test", iTopGetTools::quoteString("Underscore_Test"));
$this->assertEquals("Backslash\\\\Test", iTopGetTools::quoteString("Backslash\\Test"));
}
}

/**
* 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");
}
}