wiki.techinc.nl/includes/content/JsonContent.php
Timo Tijhof e4f84af980 content: Refactor and fix various bugs in JsonContent
Follows-up d2a82fcb60. These issues weren't previously exposed
as nothing uses JsonContent by default in core, and the extensions
using it (e.g. EventLogging) lock it down very early. As are
most of these methods were never really put to use (they were
called after the extension does its superset of checking, or
too early and WikiPage ignores it).

Bug fixes

* Empty JSON object was converted to an array by PST conversion.
  The beautifyJSON method is intended for prettify purposes but
  actually modified the content stored in the database and made
  it no longer roundtrip ({} != []).

  We can't change getJsonData to return an object since it's
  a public method and people use it as an array. So we can't cast
  it to a PHP object as that would break back-compat.

  Turns out the class doesn't even support non-objects anyway (a
  primitive in JSON can trivially cause a fatal as it wasn't
  consistently considered invalid, though it didn't actually fatal
  due to some lucky spaghetti code in WikiPage).

* Fix beautifyJSON by checking for empty objects to prevent
  implicit {} to [] conversion.

* Add isValid() check to fillParserOutput() as it's called early
  on. Otherwise it throws a warning that 'foreach' (in objectTable)
  iterates over null. In practice it doesn't matter since the
  entire parser output is rejected when WikiPage eventually
  checks isValid (through Content::prepareSave).

* Consider all non- (PHP) array values invalid instead of just
  non-null values.

Enhancements

* Display message "Empty object" instead of a completely blank page
  for an empty object.

* Display message "Empty object" or "Empty array" instead of an
  empty table cell.

* Render arrays as a list of values (without indices).

* Remove italics from table cells for values. The monospace font
  should be enough. It also offsets it from the "Empty"
  placeholders (which are italicised).

Refactoring and clean up

* Use FormatJson::parse so that we can use Status to distinguish
  between null parse result and thus reliably cache it.

  Ideally we wouldn't need to cache it, but right now this code
  is pulled apart and called in so many strange ways that we end
  up calling this several times.

* Improve fairly meaningless test (testBeautifyJson) that was
  calling FormatJson in its data provider, exactly what the method
  being tested did. It also provided the test with data that could
  never end up in normal usage (a PHP-style associated array with
  implied numerical indices).

* Document that this class rejects non-array values.

* Document the problem with WikiPage assumming PST can run on any
  content. WikiPage fundamentally still assumes wikitext, in that
  there's no concept of invalid content.

* Fix incorrect documentation for getJsonData's return value
  (It may return null.)

* Fix incorrect documentation for beautifyJSON's return value.
  (It never returned boolean.)

Bug: T76553
Change-Id: Ifed379ba4674a8289b554a95953951886bf2cbfd
2014-12-17 23:04:24 +00:00

211 lines
5 KiB
PHP

<?php
/**
* JSON Content Model
*
* This class requires the root structure to be an object (not primitives or arrays).
*
* @file
*
* @author Ori Livneh <ori@wikimedia.org>
* @author Kunal Mehta <legoktm@gmail.com>
*/
/**
* Represents the content of a JSON content.
* @since 1.24
*/
class JsonContent extends TextContent {
/**
* @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.
*
* @deprecated since 1.25 Use getData instead.
* @return array|null
*/
public function getJsonData() {
wfDeprecated( __METHOD__, '1.25' );
return FormatJson::decode( $this->getNativeData(), true );
}
/**
* 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->getData()->isGood() && is_object( $this->getData()->getValue() );
}
/**
* Pretty-print JSON.
*
* If called before validation, it may return JSON "null".
*
* @return string
*/
public function beautifyJSON() {
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.
*
* @param Title $title
* @param int $revId
* @param ParserOptions $options
* @param bool $generateHtml
* @param ParserOutput $output
*/
protected function fillParserOutput( Title $title, $revId,
ParserOptions $options, $generateHtml, ParserOutput &$output
) {
// 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( '' );
}
}
/**
* 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;
}
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 ) )
);
}
/**
* 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 );
$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;
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 ) )
);
}
/**
* 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 );
}
}