Skip to content

Commit 29843b3

Browse files
committed
bug symfony#44912 [Console] Allow OutputFormatter::escape() to be used for escaping URLs used in <href> (Seldaek)
This PR was merged into the 4.4 branch. Discussion ---------- [Console] Allow OutputFormatter::escape() to be used for escaping URLs used in <href> | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> I was trying to use escape() to make user-provided URLs safe in `<href=...>` but I realized it was really only good for avoid starting tags, and not for escaping the content of a tag. - escape() now escapes `>` as well as `<` - URLs containing escaped `<`, `>` are now rendered correctly - user-provided URLs should now be safe to use (as in they cannot break the formatting) as long as they're piped through `escape()` - possibly also resolves issues if you were trying to use user-provided colors i.e. `'<'.OutputFormatter::escape($color).'>'` where as in current released code it would not help you at all here. I haven't checked that yet I am happy to spend time adding tests but would like to first get feedback on the changes to know if it's reasonable or not to change `escape()` in this way. The rest of the changes I think are absolutely safe to merge and make sense regardless. Commits ------- cfa8910 Allow OutputFormatter::escape() to be used for escaping URLs used in <href>
2 parents 93ed7c3 + cfa8910 commit 29843b3

File tree

8 files changed

+25
-23
lines changed

8 files changed

+25
-23
lines changed

src/Symfony/Component/Console/Formatter/OutputFormatter.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ public function __clone()
3434
}
3535

3636
/**
37-
* Escapes "<" special char in given text.
37+
* Escapes "<" and ">" special chars in given text.
3838
*
3939
* @param string $text Text to escape
4040
*
4141
* @return string Escaped text
4242
*/
4343
public static function escape($text)
4444
{
45-
$text = preg_replace('/([^\\\\]?)</', '$1\\<', $text);
45+
$text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text);
4646

4747
return self::escapeTrailingBackslash($text);
4848
}
@@ -144,9 +144,10 @@ public function formatAndWrap(string $message, int $width)
144144
{
145145
$offset = 0;
146146
$output = '';
147-
$tagRegex = '[a-z][^<>]*+';
147+
$openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
148+
$closeTagRegex = '[a-z][^<>]*+';
148149
$currentLineLength = 0;
149-
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
150+
preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
150151
foreach ($matches[0] as $i => $match) {
151152
$pos = $match[1];
152153
$text = $match[0];
@@ -180,11 +181,7 @@ public function formatAndWrap(string $message, int $width)
180181

181182
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
182183

183-
if (str_contains($output, "\0")) {
184-
return strtr($output, ["\0" => '\\', '\\<' => '<']);
185-
}
186-
187-
return str_replace('\\<', '<', $output);
184+
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
188185
}
189186

190187
/**
@@ -218,7 +215,8 @@ private function createStyleFromString(string $string): ?OutputFormatterStyleInt
218215
} elseif ('bg' == $match[0]) {
219216
$style->setBackground(strtolower($match[1]));
220217
} elseif ('href' === $match[0]) {
221-
$style->setHref($match[1]);
218+
$url = preg_replace('{\\\\([<>])}', '$1', $match[1]);
219+
$style->setHref($url);
222220
} elseif ('options' === $match[0]) {
223221
preg_match_all('([^,;]+)', strtolower($match[1]), $options);
224222
$options = array_shift($options);

src/Symfony/Component/Console/Tests/Fixtures/command_2.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
command 2 description
33

44
<comment>Usage:</comment>
5-
descriptor:command2 [options] [--] \<argument_name>
6-
descriptor:command2 -o|--option_name \<argument_name>
7-
descriptor:command2 \<argument_name>
5+
descriptor:command2 [options] [--] \<argument_name\>
6+
descriptor:command2 -o|--option_name \<argument_name\>
7+
descriptor:command2 \<argument_name\>
88

99
<comment>Arguments:</comment>
1010
<info>argument_name</info>

src/Symfony/Component/Console/Tests/Fixtures/command_mbstring.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
command åèä description
33

44
<comment>Usage:</comment>
5-
descriptor:åèä [options] [--] \<argument_åèä>
6-
descriptor:åèä -o|--option_name \<argument_name>
7-
descriptor:åèä \<argument_name>
5+
descriptor:åèä [options] [--] \<argument_åèä\>
6+
descriptor:åèä -o|--option_name \<argument_name\>
7+
descriptor:åèä \<argument_name\>
88

99
<comment>Arguments:</comment>
1010
<info>argument_åèä</info>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<info>argument_name</info> argument description<comment> [default: "\<comment>style\</>"]</comment>
1+
<info>argument_name</info> argument description<comment> [default: "\<comment\>style\</\>"]</comment>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<info>-o, --option_name=OPTION_NAME</info> option description<comment> [default: "\<comment>style\</>"]</comment>
1+
<info>-o, --option_name=OPTION_NAME</info> option description<comment> [default: "\<comment\>style\</\>"]</comment>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<info>-o, --option_name=OPTION_NAME</info> option description<comment> [default: ["\<comment>Hello\</comment>","\<info>world\</info>"]]</comment><comment> (multiple values allowed)</comment>
1+
<info>-o, --option_name=OPTION_NAME</info> option description<comment> [default: ["\<comment\>Hello\</comment\>","\<info\>world\</info\>"]]</comment><comment> (multiple values allowed)</comment>

src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ public function testLGCharEscaping()
3232
$this->assertEquals('foo << bar \\', $formatter->format('foo << bar \\'));
3333
$this->assertEquals("foo << \033[32mbar \\ baz\033[39m \\", $formatter->format('foo << <info>bar \\ baz</info> \\'));
3434
$this->assertEquals('<info>some info</info>', $formatter->format('\\<info>some info\\</info>'));
35-
$this->assertEquals('\\<info>some info\\</info>', OutputFormatter::escape('<info>some info</info>'));
35+
$this->assertEquals('\\<info\\>some info\\</info\\>', OutputFormatter::escape('<info>some info</info>'));
36+
// every < and > gets escaped if not already escaped, but already escaped ones do not get escaped again
37+
// and escaped backslashes remain as such, same with backslashes escaping non-special characters
38+
$this->assertEquals('foo \\< bar \\< baz \\\\< foo \\> bar \\> baz \\\\> \\x', OutputFormatter::escape('foo < bar \\< baz \\\\< foo > bar \\> baz \\\\> \\x'));
3639

3740
$this->assertEquals(
3841
"\033[33mSymfony\\Component\\Console does work very well!\033[39m",
@@ -259,6 +262,7 @@ public function provideDecoratedAndNonDecoratedOutput()
259262
['<question>some question</question>', 'some question', "\033[30;46msome question\033[39;49m"],
260263
['<fg=red>some text with inline style</>', 'some text with inline style', "\033[31msome text with inline style\033[39m"],
261264
['<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>', 'some URL', "\033]8;;idea://open/?file=/path/SomeFile.php&line=12\033\\some URL\033]8;;\033\\"],
265+
['<href=https://example.com/\<woohoo\>>some URL with \<woohoo\></>', 'some URL with <woohoo>', "\033]8;;https://example.com/<woohoo>\033\\some URL with <woohoo>\033]8;;\033\\"],
262266
['<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>', 'some URL', 'some URL', 'JetBrains-JediTerm'],
263267
];
264268
}

src/Symfony/Component/Console/Tests/Helper/FormatterHelperTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ public function testFormatBlockLGEscaping()
8383
$formatter = new FormatterHelper();
8484

8585
$this->assertEquals(
86-
'<error> </error>'."\n".
87-
'<error> \<info>some info\</info> </error>'."\n".
88-
'<error> </error>',
86+
'<error> </error>'."\n".
87+
'<error> \<info\>some info\</info\> </error>'."\n".
88+
'<error> </error>',
8989
$formatter->formatBlock('<info>some info</info>', 'error', true),
9090
'::formatBlock() escapes \'<\' chars'
9191
);

0 commit comments

Comments
 (0)