Merge "ImagePage: Use $this->getContext()->msg() instead of wfMessage"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 17 Dec 2014 23:26:35 +0000 (23:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 17 Dec 2014 23:26:35 +0000 (23:26 +0000)
includes/content/JsonContent.php
languages/i18n/en.json
languages/i18n/qqq.json
resources/src/mediawiki/mediawiki.content.json.css
tests/phpunit/includes/content/JsonContentTest.php

index 1ce25c2..ff3b25b 100644 (file)
@@ -2,6 +2,8 @@
 /**
  * JSON Content Model
  *
+ * This class requires the root structure to be an object (not primitives or arrays).
+ *
  * @file
  *
  * @author Ori Livneh <ori@wikimedia.org>
  */
 class JsonContent extends TextContent {
 
-       public function __construct( $text, $modelId = CONTENT_MODEL_JSON ) {
-               parent::__construct( $text, $modelId );
+       /**
+        * @since 1.25
+        * @var Status
+        */
+       protected $jsonParse;
+
+       /**
+        * @param string $text JSON
+        */
+       public function __construct( $text ) {
+               parent::__construct( $text, CONTENT_MODEL_JSON );
        }
 
        /**
         * Decodes the JSON into a PHP associative array.
-        * @return array
+        *
+        * @deprecated since 1.25 Use getData instead.
+        * @return array|null
         */
        public function getJsonData() {
+               wfDeprecated( __METHOD__, '1.25' );
                return FormatJson::decode( $this->getNativeData(), true );
        }
 
        /**
-        * @return bool Whether content is valid JSON.
+        * Decodes the JSON string into a PHP object.
+        *
+        * @return Status
+        */
+       public function getData() {
+               if ( $this->jsonParse === null ) {
+                       $this->jsonParse = FormatJson::parse( $this->getNativeData() );
+               }
+               return $this->jsonParse;
+       }
+
+       /**
+        * @return bool Whether content is valid.
         */
        public function isValid() {
-               return $this->getJsonData() !== null;
+               return $this->getData()->isGood() && is_object( $this->getData()->getValue() );
        }
 
        /**
-        * Pretty-print JSON
+        * Pretty-print JSON.
+        *
+        * If called before validation, it may return JSON "null".
         *
-        * @return bool|null|string
+        * @return string
         */
        public function beautifyJSON() {
-               $decoded = $this->getJsonData();
-               if ( !is_array( $decoded ) ) {
-                       return null;
-               }
-               return FormatJson::encode( $decoded, true );
-
+               return FormatJson::encode( $this->getData()->getValue(), true );
        }
 
        /**
         * Beautifies JSON prior to save.
+        *
         * @param Title $title Title
         * @param User $user User
         * @param ParserOptions $popts
         * @return JsonContent
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               // FIXME: WikiPage::doEditContent invokes PST before validation. As such, native data
+               // may be invalid (though PST result is discarded later in that case).
+               if ( !$this->isValid() ) {
+                       return $this;
+               }
+
                return new static( $this->beautifyJSON() );
        }
 
        /**
-        * Set the HTML and add the appropriate styles
-        *
+        * Set the HTML and add the appropriate styles.
         *
         * @param Title $title
         * @param int $revId
@@ -71,50 +100,112 @@ class JsonContent extends TextContent {
        protected function fillParserOutput( Title $title, $revId,
                ParserOptions $options, $generateHtml, ParserOutput &$output
        ) {
-               if ( $generateHtml ) {
-                       $output->setText( $this->objectTable( $this->getJsonData() ) );
+               // FIXME: WikiPage::doEditContent generates parser output before validation.
+               // As such, native data may be invalid (though output is discarded later in that case).
+               if ( $generateHtml && $this->isValid() ) {
+                       $output->setText( $this->objectTable( $this->getData()->getValue() ) );
                        $output->addModuleStyles( 'mediawiki.content.json' );
                } else {
                        $output->setText( '' );
                }
        }
+
        /**
-        * Constructs an HTML representation of a JSON object.
-        * @param array $mapping
+        * Construct an HTML representation of a JSON object.
+        *
+        * Called recursively via valueCell().
+        *
+        * @param stdClass $mapping
         * @return string HTML
         */
        protected function objectTable( $mapping ) {
                $rows = array();
+               $empty = true;
 
                foreach ( $mapping as $key => $val ) {
                        $rows[] = $this->objectRow( $key, $val );
+                       $empty = false;
                }
-               return Xml::tags( 'table', array( 'class' => 'mw-json' ),
-                       Xml::tags( 'tbody', array(), join( "\n", $rows ) )
+               if ( $empty ) {
+                       $rows[] = Html::rawElement( 'tr', array(),
+                               Html::element( 'td', array( 'class' => 'mw-json-empty' ),
+                                       wfMessage( 'content-json-empty-object' )->text()
+                               )
+                       );
+               }
+               return Html::rawElement( 'table', array( 'class' => 'mw-json' ),
+                       Html::rawElement( 'tbody', array(), join( "\n", $rows ) )
                );
        }
 
        /**
-        * Constructs HTML representation of a single key-value pair.
+        * Construct HTML representation of a single key-value pair.
         * @param string $key
         * @param mixed $val
         * @return string HTML.
         */
        protected function objectRow( $key, $val ) {
                $th = Xml::elementClean( 'th', array(), $key );
-               if ( is_array( $val ) ) {
-                       $td = Xml::tags( 'td', array(), self::objectTable( $val ) );
-               } else {
-                       if ( is_string( $val ) ) {
-                               $val = '"' . $val . '"';
-                       } else {
-                               $val = FormatJson::encode( $val );
-                       }
+               $td = self::valueCell( $val );
+               return Html::rawElement( 'tr', array(), $th . $td );
+       }
+
+       /**
+        * Constructs an HTML representation of a JSON array.
+        *
+        * Called recursively via valueCell().
+        *
+        * @param array $mapping
+        * @return string HTML
+        */
+       protected function arrayTable( $mapping ) {
+               $rows = array();
+               $empty = true;
 
-                       $td = Xml::elementClean( 'td', array( 'class' => 'value' ), $val );
+               foreach ( $mapping as $val ) {
+                       $rows[] = $this->arrayRow( $val );
+                       $empty = false;
                }
+               if ( $empty ) {
+                       $rows[] = Html::rawElement( 'tr', array(),
+                               Html::element( 'td', array( 'class' => 'mw-json-empty' ),
+                                       wfMessage( 'content-json-empty-array' )->text()
+                               )
+                       );
+               }
+               return Html::rawElement( 'table', array( 'class' => 'mw-json' ),
+                       Html::rawElement( 'tbody', array(), join( "\n", $rows ) )
+               );
+       }
 
-               return Xml::tags( 'tr', array(), $th . $td );
+       /**
+        * Construct HTML representation of a single array value.
+        * @param mixed $val
+        * @return string HTML.
+        */
+       protected function arrayRow( $val ) {
+               $td = self::valueCell( $val );
+               return Html::rawElement( 'tr', array(), $td );
        }
 
+       /**
+        * Construct HTML representation of a single value.
+        * @param mixed $val
+        * @return string HTML.
+        */
+       protected function valueCell( $val ) {
+               if ( is_object( $val ) ) {
+                       return Html::rawElement( 'td', array(), self::objectTable( $val ) );
+               }
+               if ( is_array( $val ) ) {
+                       return Html::rawElement( 'td', array(), self::arrayTable( $val ) );
+               }
+               if ( is_string( $val ) ) {
+                       $val = '"' . $val . '"';
+               } else {
+                       $val = FormatJson::encode( $val );
+               }
+
+               return Xml::elementClean( 'td', array( 'class' => 'value' ), $val );
+       }
 }
index 2ee3df2..da49fe9 100644 (file)
        "content-model-text": "plain text",
        "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
+       "content-model-json": "JSON",
+       "content-json-empty-object": "Empty object",
+       "content-json-empty-array": "Empty array",
        "duplicate-args-category": "Pages using duplicate arguments in template calls",
        "duplicate-args-category-desc": "The page contains template calls that use duplicates of arguments, such as <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-warning": "<strong>Warning:</strong> This page contains too many expensive parser function calls.\n\nIt should have less than $2 {{PLURAL:$2|call|calls}}, there {{PLURAL:$1|is now $1 call|are now $1 calls}}.",
index 813064d..42ae44c 100644 (file)
        "content-model-text": "Name for the plain text content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}\n{{Identical|Plain text}}",
        "content-model-javascript": "Name for the JavaScript content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}",
        "content-model-css": "Name for the CSS content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}",
+       "content-model-json": "Name for the JSON content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}",
+       "content-json-empty-object": "Used to represent an object with no properties on a JSON content model page.",
+       "content-json-empty-array": "Used to represent an array with no values on a JSON content model page.",
        "duplicate-args-category": "This message is used as a category name for a [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]] where pages are placed automatically if they contain template calls that use duplicates of arguments, such as <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "duplicate-args-category-desc": "Duplicate arguments category description. Shown on [[Special:TrackingCategories]].\n\nSee also:\n* {{msg-mw|Duplicate-args-category}}",
        "expensive-parserfunction-warning": "On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.\n\nParameters:\n* $1 - the current number of parser function calls\n* $2 - the allowed number of parser function calls\nSee also [[:mw:Manual:$wgExpensiveParserFunctionLimit|$wgExpensiveParserFunctionLimit in the MediaWiki manual]].\n\nSee also:\n* {{msg-mw|Expensive-parserfunction-category}}",
index d93e291..4afccda 100644 (file)
        padding: 0.5em 1em;
 }
 
-.mw-json td {
-       background-color: #eee;
-       font-style: italic;
-}
-
 .mw-json .value {
        background-color: #dcfae3;
        font-family: monospace, monospace;
        white-space: pre-wrap;
 }
 
+.mw-json-empty {
+       background-color: #fff;
+       font-style: italic;
+}
+
 .mw-json tr {
        margin-bottom: 0.5em;
+       background-color: #eee;
 }
 
 .mw-json th {
index d4151a5..0ad8ecc 100644 (file)
@@ -6,48 +6,76 @@
  */
 class JsonContentTest extends MediaWikiLangTestCase {
 
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( 'wgWellFormedXml', true );
+       }
+
        public static function provideValidConstruction() {
                return array(
-                       array( 'foo', CONTENT_MODEL_JSON, false, null ),
-                       array( FormatJson::encode( array() ), CONTENT_MODEL_JSON, true, array() ),
-                       array( FormatJson::encode( array( 'foo' ) ), CONTENT_MODEL_JSON, true, array( 'foo' ) ),
+                       array( 'foo', false, null ),
+                       array( '{}', true, (object)array() ),
+                       array( '{ "0": "bar" }', true, (object)array( 'bar' ) ),
                );
        }
 
        /**
         * @dataProvider provideValidConstruction
         */
-       public function testValidConstruct( $text, $modelId, $isValid, $expected ) {
-               $obj = new JsonContent( $text, $modelId );
+       public function testIsValid( $text, $isValid, $expected ) {
+               $obj = new JsonContent( $text, CONTENT_MODEL_JSON );
                $this->assertEquals( $isValid, $obj->isValid() );
-               $this->assertEquals( $expected, $obj->getJsonData() );
+               $this->assertEquals( $expected, $obj->getData()->getValue() );
        }
 
        public static function provideDataToEncode() {
                return array(
-                       array( array() ),
-                       array( array( 'foo' ) ),
-                       array( array( 'foo', 'bar' ) ),
-                       array( array( 'baz' => 'foo', 'bar' ) ),
-                       array( array( 'baz' => 1000, 'bar' ) ),
+                       array(
+                               // Round-trip empty array
+                               '[]',
+                               '[]',
+                       ),
+                       array(
+                               // Round-trip empty object
+                               '{}',
+                               '{}',
+                       ),
+                       array(
+                               // Round-trip empty array/object (nested)
+                               '{ "foo": {}, "bar": [] }',
+                               "{\n    \"foo\": {},\n    \"bar\": []\n}",
+                       ),
+                       array(
+                               '{ "foo": "bar" }',
+                               "{\n    \"foo\": \"bar\"\n}",
+                       ),
+                       array(
+                               '{ "foo": 1000 }',
+                               "{\n    \"foo\": 1000\n}",
+                       ),
+                       array(
+                               '{ "foo": 1000, "0": "bar" }',
+                               "{\n    \"foo\": 1000,\n    \"0\": \"bar\"\n}",
+                       ),
                );
        }
 
        /**
         * @dataProvider provideDataToEncode
         */
-       public function testBeautifyUsesFormatJson( $data ) {
-               $obj = new JsonContent( FormatJson::encode( $data ) );
-               $this->assertEquals( FormatJson::encode( $data, true ), $obj->beautifyJSON() );
+       public function testBeautifyJson( $input, $beautified ) {
+               $obj = new JsonContent( $input );
+               $this->assertEquals( $beautified, $obj->beautifyJSON() );
        }
 
        /**
         * @dataProvider provideDataToEncode
         */
-       public function testPreSaveTransform( $data ) {
-               $obj = new JsonContent( FormatJson::encode( $data ) );
+       public function testPreSaveTransform( $input, $transformed ) {
+               $obj = new JsonContent( $input );
                $newObj = $obj->preSaveTransform( $this->getMockTitle(), $this->getMockUser(), $this->getMockParserOptions() );
-               $this->assertTrue( $newObj->equals( new JsonContent( FormatJson::encode( $data, true ) ) ) );
+               $this->assertTrue( $newObj->equals( new JsonContent( $transformed ) ) );
        }
 
        private function getMockTitle() {
@@ -70,33 +98,33 @@ class JsonContentTest extends MediaWikiLangTestCase {
        public static function provideDataAndParserText() {
                return array(
                        array(
-                               array(),
-                               '<table class="mw-json"><tbody></tbody></table>'
+                               (object)array(),
+                               '<table class="mw-json"><tbody><tr><td class="mw-json-empty">Empty object</td></tr></tbody></table>'
                        ),
                        array(
-                               array( 'foo' ),
+                               (object)array( 'foo' ),
                                '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;foo&quot;</td></tr></tbody></table>'
                        ),
                        array(
-                               array( 'foo', 'bar' ),
+                               (object)array( 'foo', 'bar' ),
                                '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;foo&quot;</td></tr>' .
                                "\n" .
                                '<tr><th>1</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
                        ),
                        array(
-                               array( 'baz' => 'foo', 'bar' ),
+                               (object)array( 'baz' => 'foo', 'bar' ),
                                '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">&quot;foo&quot;</td></tr>' .
                                "\n" .
                                '<tr><th>0</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
                        ),
                        array(
-                               array( 'baz' => 1000, 'bar' ),
+                               (object)array( 'baz' => 1000, 'bar' ),
                                '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">1000</td></tr>' .
                                "\n" .
                                '<tr><th>0</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
                        ),
                        array(
-                               array( '<script>alert("evil!")</script>'),
+                               (object)array( '<script>alert("evil!")</script>'),
                                '<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;&lt;script&gt;alert(&quot;evil!&quot;)&lt;/script&gt;&quot;</td></tr></tbody></table>',
                        ),
                );