From d55fedfba8ce80c76dd86b9696cb72c4adb01255 Mon Sep 17 00:00:00 2001 From: Erik Bernhardson Date: Wed, 17 Sep 2014 14:39:15 -0700 Subject: [PATCH] Introduce Message::plaintextParam I have run into numerous issues trying to utilize unsafe user provided content as an argument to a Message instance. Specific cases are enumerated in MessageTest.php Typically the solution to using user provided text is to use Message::rawParam, but this pushes escaping of the parameter to the caller. This patch introduces Message::plaintextParams which handles escaping of the string parameter to match the requested output format. The functionality is: * plain and text: exactly like rawParams() * escaped, parse and parseAsBlock: escape it but don't do brace expansion Additionaly, similar to Message::rawParam, plaintext parameters are not valid parser function arguments. Change-Id: I320645cd23c98fea4bfc32ab22b7ef8d320957cb --- includes/Message.php | 62 ++++++++++++++++++++++++++ tests/phpunit/includes/MessageTest.php | 49 ++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/includes/Message.php b/includes/Message.php index 59c51206ff..93a37cbb4d 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -540,6 +540,30 @@ class Message { return $this; } + /** + * Add parameters that are plaintext and will be passed through without + * the content being evaluated. Plaintext parameters are not valid as + * arguments to parser functions. This differs from self::rawParams in + * that the Message class handles escaping to match the output format. + * + * @since 1.25 + * + * @param string|string[] $param,... plaintext parameters, or a single argument that is + * an array of plaintext parameters. + * + * @return Message $this + */ + public function plaintextParams( /*...*/ ) { + $params = func_get_args(); + if ( isset( $params[0] ) && is_array( $params[0] ) ) { + $params = $params[0]; + } + foreach ( $params as $param ) { + $this->parameters[] = self::plaintextParam( $param ); + } + return $this; + } + /** * Set the language and the title from a context object * @@ -915,6 +939,17 @@ class Message { return array( 'bitrate' => $bitrate ); } + /** + * @since 1.25 + * + * @param string $plaintext + * + * @return string[] Array with a single "plaintext" key. + */ + public static function plaintextParam( $plaintext ) { + return array( 'plaintext' => $plaintext ); + } + /** * Substitutes any parameters into the message text. * @@ -964,6 +999,8 @@ class Message { return array( 'before', $this->language->formatSize( $param['size'] ) ); } elseif ( isset( $param['bitrate'] ) ) { return array( 'before', $this->language->formatBitrate( $param['bitrate'] ) ); + } elseif ( isset( $param['plaintext'] ) ) { + return array( 'after', $this->formatPlaintext( $param['plaintext'] ) ); } else { $warning = 'Invalid parameter for message "' . $this->getKey() . '": ' . htmlspecialchars( serialize( $param ) ); @@ -1049,6 +1086,31 @@ class Message { return $this->message; } + /** + * Formats a message parameter wrapped with 'plaintext'. Ensures that + * the entire string is displayed unchanged when displayed in the output + * format. + * + * @since 1.25 + * + * @param string $plaintext String to ensure plaintext output of + * + * @return string Input plaintext encoded for output to $this->format + */ + protected function formatPlaintext( $plaintext ) { + switch ( $this->format ) { + case 'text': + case 'plain': + return $plaintext; + + case 'parse': + case 'block-parse': + case 'escaped': + default: + return htmlspecialchars( $plaintext, ENT_QUOTES ); + + } + } } /** diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index 47560e613e..4c5424c5ec 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -277,6 +277,55 @@ class MessageTest extends MediaWikiLangTestCase { ); } + public function messagePlaintextParamsProvider() { + return array( + array( + 'one $2
foo
[[Bar]] {{Baz}} <', + 'plain', + ), + + array( + // expect + 'one $2
foo
[[Bar]] {{Baz}} <', + // format + 'text', + ), + array( + 'one $2 <div>foo</div> [[Bar]] {{Baz}} &lt;', + 'escaped', + ), + + array( + 'one $2 <div>foo</div> [[Bar]] {{Baz}} &lt;', + 'parse', + ), + + array( + "

one $2 <div>foo</div> [[Bar]] {{Baz}} &lt;\n

", + 'parseAsBlock', + ), + ); + } + + /** + * @dataProvider messagePlaintextParamsProvider + * @covers Message::plaintextParams + */ + public function testMessagePlaintextParams( $expect, $format ) { + $lang = Language::factory( 'en' ); + + $msg = new RawMessage( '$1 $2' ); + $params = array( + 'one $2', + '
foo
[[Bar]] {{Baz}} <', + ); + $this->assertEquals( + $expect, + $msg->inLanguage( $lang )->plaintextParams( $params )->$format(), + "Fail formatting for $format" + ); + } + /** * @covers Message::inContentLanguage */ -- 2.20.1