content: Refactor and fix various bugs in JsonContent
[lhc/web/wiklou.git] / includes / content / JsonContent.php
1 <?php
2 /**
3 * JSON Content Model
4 *
5 * This class requires the root structure to be an object (not primitives or arrays).
6 *
7 * @file
8 *
9 * @author Ori Livneh <ori@wikimedia.org>
10 * @author Kunal Mehta <legoktm@gmail.com>
11 */
12
13 /**
14 * Represents the content of a JSON content.
15 * @since 1.24
16 */
17 class JsonContent extends TextContent {
18
19 /**
20 * @since 1.25
21 * @var Status
22 */
23 protected $jsonParse;
24
25 /**
26 * @param string $text JSON
27 */
28 public function __construct( $text ) {
29 parent::__construct( $text, CONTENT_MODEL_JSON );
30 }
31
32 /**
33 * Decodes the JSON into a PHP associative array.
34 *
35 * @deprecated since 1.25 Use getData instead.
36 * @return array|null
37 */
38 public function getJsonData() {
39 wfDeprecated( __METHOD__, '1.25' );
40 return FormatJson::decode( $this->getNativeData(), true );
41 }
42
43 /**
44 * Decodes the JSON string into a PHP object.
45 *
46 * @return Status
47 */
48 public function getData() {
49 if ( $this->jsonParse === null ) {
50 $this->jsonParse = FormatJson::parse( $this->getNativeData() );
51 }
52 return $this->jsonParse;
53 }
54
55 /**
56 * @return bool Whether content is valid.
57 */
58 public function isValid() {
59 return $this->getData()->isGood() && is_object( $this->getData()->getValue() );
60 }
61
62 /**
63 * Pretty-print JSON.
64 *
65 * If called before validation, it may return JSON "null".
66 *
67 * @return string
68 */
69 public function beautifyJSON() {
70 return FormatJson::encode( $this->getData()->getValue(), true );
71 }
72
73 /**
74 * Beautifies JSON prior to save.
75 *
76 * @param Title $title Title
77 * @param User $user User
78 * @param ParserOptions $popts
79 * @return JsonContent
80 */
81 public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
82 // FIXME: WikiPage::doEditContent invokes PST before validation. As such, native data
83 // may be invalid (though PST result is discarded later in that case).
84 if ( !$this->isValid() ) {
85 return $this;
86 }
87
88 return new static( $this->beautifyJSON() );
89 }
90
91 /**
92 * Set the HTML and add the appropriate styles.
93 *
94 * @param Title $title
95 * @param int $revId
96 * @param ParserOptions $options
97 * @param bool $generateHtml
98 * @param ParserOutput $output
99 */
100 protected function fillParserOutput( Title $title, $revId,
101 ParserOptions $options, $generateHtml, ParserOutput &$output
102 ) {
103 // FIXME: WikiPage::doEditContent generates parser output before validation.
104 // As such, native data may be invalid (though output is discarded later in that case).
105 if ( $generateHtml && $this->isValid() ) {
106 $output->setText( $this->objectTable( $this->getData()->getValue() ) );
107 $output->addModuleStyles( 'mediawiki.content.json' );
108 } else {
109 $output->setText( '' );
110 }
111 }
112
113 /**
114 * Construct an HTML representation of a JSON object.
115 *
116 * Called recursively via valueCell().
117 *
118 * @param stdClass $mapping
119 * @return string HTML
120 */
121 protected function objectTable( $mapping ) {
122 $rows = array();
123 $empty = true;
124
125 foreach ( $mapping as $key => $val ) {
126 $rows[] = $this->objectRow( $key, $val );
127 $empty = false;
128 }
129 if ( $empty ) {
130 $rows[] = Html::rawElement( 'tr', array(),
131 Html::element( 'td', array( 'class' => 'mw-json-empty' ),
132 wfMessage( 'content-json-empty-object' )->text()
133 )
134 );
135 }
136 return Html::rawElement( 'table', array( 'class' => 'mw-json' ),
137 Html::rawElement( 'tbody', array(), join( "\n", $rows ) )
138 );
139 }
140
141 /**
142 * Construct HTML representation of a single key-value pair.
143 * @param string $key
144 * @param mixed $val
145 * @return string HTML.
146 */
147 protected function objectRow( $key, $val ) {
148 $th = Xml::elementClean( 'th', array(), $key );
149 $td = self::valueCell( $val );
150 return Html::rawElement( 'tr', array(), $th . $td );
151 }
152
153 /**
154 * Constructs an HTML representation of a JSON array.
155 *
156 * Called recursively via valueCell().
157 *
158 * @param array $mapping
159 * @return string HTML
160 */
161 protected function arrayTable( $mapping ) {
162 $rows = array();
163 $empty = true;
164
165 foreach ( $mapping as $val ) {
166 $rows[] = $this->arrayRow( $val );
167 $empty = false;
168 }
169 if ( $empty ) {
170 $rows[] = Html::rawElement( 'tr', array(),
171 Html::element( 'td', array( 'class' => 'mw-json-empty' ),
172 wfMessage( 'content-json-empty-array' )->text()
173 )
174 );
175 }
176 return Html::rawElement( 'table', array( 'class' => 'mw-json' ),
177 Html::rawElement( 'tbody', array(), join( "\n", $rows ) )
178 );
179 }
180
181 /**
182 * Construct HTML representation of a single array value.
183 * @param mixed $val
184 * @return string HTML.
185 */
186 protected function arrayRow( $val ) {
187 $td = self::valueCell( $val );
188 return Html::rawElement( 'tr', array(), $td );
189 }
190
191 /**
192 * Construct HTML representation of a single value.
193 * @param mixed $val
194 * @return string HTML.
195 */
196 protected function valueCell( $val ) {
197 if ( is_object( $val ) ) {
198 return Html::rawElement( 'td', array(), self::objectTable( $val ) );
199 }
200 if ( is_array( $val ) ) {
201 return Html::rawElement( 'td', array(), self::arrayTable( $val ) );
202 }
203 if ( is_string( $val ) ) {
204 $val = '"' . $val . '"';
205 } else {
206 $val = FormatJson::encode( $val );
207 }
208
209 return Xml::elementClean( 'td', array( 'class' => 'value' ), $val );
210 }
211 }