Introduce Message::plaintextParam
authorErik Bernhardson <ebernhardson@wikimedia.org>
Wed, 17 Sep 2014 21:39:15 +0000 (14:39 -0700)
committerErik Bernhardson <ebernhardson@wikimedia.org>
Tue, 30 Sep 2014 19:51:29 +0000 (12:51 -0700)
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
tests/phpunit/includes/MessageTest.php

index 59c5120..93a37cb 100644 (file)
@@ -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 );
+
+               }
+       }
 }
 
 /**
index 47560e6..4c5424c 100644 (file)
@@ -277,6 +277,55 @@ class MessageTest extends MediaWikiLangTestCase {
                );
        }
 
+       public function messagePlaintextParamsProvider() {
+               return array(
+                       array(
+                               'one $2 <div>foo</div> [[Bar]] {{Baz}} &lt;',
+                               'plain',
+                       ),
+
+                       array(
+                               // expect
+                               'one $2 <div>foo</div> [[Bar]] {{Baz}} &lt;',
+                               // format
+                               'text',
+                       ),
+                       array(
+                               'one $2 &lt;div&gt;foo&lt;/div&gt; [[Bar]] {{Baz}} &amp;lt;',
+                               'escaped',
+                       ),
+
+                       array(
+                               'one $2 &lt;div&gt;foo&lt;/div&gt; [[Bar]] {{Baz}} &amp;lt;',
+                               'parse',
+                       ),
+
+                       array(
+                               "<p>one $2 &lt;div&gt;foo&lt;/div&gt; [[Bar]] {{Baz}} &amp;lt;\n</p>",
+                               'parseAsBlock',
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider messagePlaintextParamsProvider
+        * @covers Message::plaintextParams
+        */
+       public function testMessagePlaintextParams( $expect, $format ) {
+               $lang = Language::factory( 'en' );
+
+               $msg = new RawMessage( '$1 $2' );
+               $params = array(
+                       'one $2',
+                       '<div>foo</div> [[Bar]] {{Baz}} &lt;',
+               );
+               $this->assertEquals(
+                       $expect,
+                       $msg->inLanguage( $lang )->plaintextParams( $params )->$format(),
+                       "Fail formatting for $format"
+               );
+       }
+
        /**
         * @covers Message::inContentLanguage
         */