Skip to content

Commit 4ded2e2

Browse files
authored
fix: disallow string callables (#53)
1 parent 4a34f67 commit 4ded2e2

File tree

4 files changed

+97
-13
lines changed

4 files changed

+97
-13
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## 0.7.1 - 2022-05-23
9+
10+
### Added
11+
12+
- Nothing.
13+
14+
### Changed
15+
16+
- Nothing.
17+
18+
### Deprecated
19+
20+
- Nothing.
21+
22+
### Removed
23+
24+
- Nothing.
25+
26+
### Fixed
27+
28+
- Disallow the use of string callables in values passed to the formatter. Only array callables and closures are allowed.
29+
830
## 0.7.0 - 2022-02-11
931

1032
### Added

src/Intl/MessageFormat.php

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use function is_callable;
4343
use function is_int;
4444
use function is_numeric;
45+
use function is_string;
4546
use function preg_match;
4647
use function sprintf;
4748

@@ -97,8 +98,6 @@ public function format(string $pattern, array $values = []): string
9798
}
9899

99100
/**
100-
* @param array<array-key, float | int | string | callable(string):string> $values
101-
*
102101
* @throws Parser\Exception\IllegalParserUsageException
103102
* @throws Parser\Exception\InvalidArgumentException
104103
* @throws Parser\Exception\InvalidOffsetException
@@ -108,16 +107,27 @@ public function format(string $pattern, array $values = []): string
108107
* @throws Parser\Exception\UnableToParseMessageException
109108
* @throws UnableToFormatMessageException
110109
* @throws CollectionMismatchException
110+
*
111+
* @psalm-param array<array-key, float | int | string | callable(string=):string> $values
111112
*/
112113
private function applyPreprocessing(string $pattern, array &$values = []): string
113114
{
114-
$callbacks = array_filter($values, fn ($value): bool => is_callable($value));
115+
/** @var array<array-key, callable(string=):string> $callbacks */
116+
$callbacks = array_filter($values, fn ($value): bool => !is_string($value) && is_callable($value));
115117

116118
// Remove the callbacks from the values, since we will use them below.
117119
foreach (array_keys($callbacks) as $key) {
118120
unset($values[$key]);
119121
}
120122

123+
/**
124+
* This is to satisfy static analysis. At this point, $values should
125+
* not contain any callables.
126+
*
127+
* @var array<array-key, float | int | string> $valuesWithoutCallables
128+
*/
129+
$valuesWithoutCallables = &$values;
130+
121131
$parserOptions = new Parser\Options();
122132
$parserOptions->shouldParseSkeletons = true;
123133

@@ -130,15 +140,16 @@ private function applyPreprocessing(string $pattern, array &$values = []): strin
130140

131141
assert($parsed->val instanceof Parser\Type\ElementCollection);
132142

133-
return (new Printer())->printAst($this->processAst($parsed->val, $callbacks, $values));
143+
return (new Printer())->printAst($this->processAst($parsed->val, $callbacks, $valuesWithoutCallables));
134144
}
135145

136146
/**
137-
* @param array<array-key, callable(string):string> $callbacks
138-
* @param array<array-key, float | int | string | callable(string):string> $values
147+
* @param array<array-key, float | int | string> $values
139148
*
140149
* @throws CollectionMismatchException
141150
* @throws UnableToFormatMessageException
151+
*
152+
* @psalm-param array<array-key, callable(string=):string> $callbacks
142153
*/
143154
private function processAst(
144155
Parser\Type\ElementCollection $ast,
@@ -179,11 +190,12 @@ private function processAst(
179190
}
180191

181192
/**
182-
* @param array<array-key, callable(string):string> $callbacks
183-
* @param array<array-key, float | int | string | callable(string):string> $values
193+
* @param array<array-key, float | int | string> $values
184194
*
185195
* @throws CollectionMismatchException
186196
* @throws UnableToFormatMessageException
197+
*
198+
* @psalm-param array<array-key, callable(string=):string> $callbacks
187199
*/
188200
private function processTagElement(
189201
Parser\Type\TagElement $tagElement,
@@ -246,7 +258,7 @@ private function processTagElement(
246258
* @link https://tc39.es/ecma402/#sec-partitionnumberpattern
247259
* @link https://formatjs.io/docs/core-concepts/icu-syntax/#number-type
248260
*
249-
* @param array<array-key, float | int | string | callable(string):string> $values
261+
* @param array<array-key, float | int | string> $values
250262
*/
251263
private function processNumberElement(
252264
Parser\Type\NumberElement $numberElement,
@@ -267,10 +279,10 @@ private function processNumberElement(
267279
}
268280

269281
/**
270-
* @param array<array-key, callable(string):string> $callbacks
271-
*
272282
* @throws CollectionMismatchException
273283
* @throws UnableToFormatMessageException
284+
*
285+
* @psalm-param array<array-key, callable(string=):string> $callbacks
274286
*/
275287
private function processLiteralElement(
276288
Parser\Type\LiteralElement $literalElement,

src/Intl/MessageFormatInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ interface MessageFormatInterface
7171
* );
7272
* ```
7373
*
74-
* @param array<array-key, float | int | string | callable(string):string> $values
75-
*
7674
* @throws UnableToFormatMessageException
75+
*
76+
* @psalm-param array<array-key, float | int | string | callable(string=):string> $values
7777
*/
7878
public function format(string $pattern, array $values = []): string;
7979
}

tests/Intl/MessageFormatTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,4 +378,54 @@ public function testProcessesNumberWithoutStyle(): void
378378

379379
$this->assertSame($expected, $result);
380380
}
381+
382+
public function testArrayCallablesAndClosures(): void
383+
{
384+
$message = 'Hello, <firstName></firstName> <lastName></lastName>!';
385+
$expected = 'Hello, Jane Doe!';
386+
387+
$user = new class {
388+
public function getFirstName(): string
389+
{
390+
return 'Jane';
391+
}
392+
393+
public function getLastName(): string
394+
{
395+
return 'Doe';
396+
}
397+
};
398+
399+
$locale = new Locale('en-US');
400+
$formatter = new MessageFormat($locale);
401+
402+
$result = $formatter->format(
403+
$message,
404+
[
405+
'firstName' => [$user, 'getFirstName'],
406+
'lastName' => fn (): string => $user->getLastName(),
407+
],
408+
);
409+
410+
$this->assertSame($expected, $result);
411+
}
412+
413+
public function testStringsMustNotEvaluateAsCallables(): void
414+
{
415+
$message = 'Hello, {firstName} {lastName}!';
416+
$expected = 'Hello, Ceil Floor!';
417+
418+
$locale = new Locale('en-US');
419+
$formatter = new MessageFormat($locale);
420+
421+
$result = $formatter->format(
422+
$message,
423+
[
424+
'firstName' => 'Ceil',
425+
'lastName' => 'Floor',
426+
],
427+
);
428+
429+
$this->assertSame($expected, $result);
430+
}
381431
}

0 commit comments

Comments
 (0)