mediawiki-extensions-Templa.../TemplateDataBlob.php
Timo Tijhof f8eb557a64 Account for absence of template description in #getHtml
The description property is optional and is explicitly set to
null by the parser in normalisation if there is none.

The HTML rendering of parameter descriptions (as oppposed to the
description of the template as a whole) already covered for this.
Used the same logic for the template parameter.

Also fixed:
* Localize the text "no description"
* Use !== null instead of isset() since the property is always
  set, we just need to know whether it is null or an object.

Bug: 54422
Change-Id: I86a40dbd1225feb54123e77a856b4ad6d525461b
2013-09-25 20:23:46 +00:00

500 lines
12 KiB
PHP

<?php
/**
* @file
* @ingroup Extensions
*/
/**
* Represents the information about a template,
* coming from the JSON blob in the <templatedata> tags
* on wiki pages.
*
* @class
*/
class TemplateDataBlob {
// Size of MySQL 'blob' field; page_props table where the data is stored uses one.
const MAX_LENGTH = 65535;
/**
* @var stdClass
*/
private $data;
/**
* @var Status: Cache of TemplateDataBlob::parse
*/
private $status;
/**
* Parse and validate passed JSON and create a TemplateDataBlob object.
* Accepts and handles user-provided data.
*
* @param string $json
* @throws MWException
* @return TemplateDataBlob
*/
public static function newFromJSON( $json ) {
$tdb = new self( json_decode( $json ) );
$status = $tdb->parse();
if ( !$status->isOK() ) {
// Don't save invalid data, clear it.
$tdb->data = new stdClass();
}
$tdb->status = $status;
return $tdb;
}
/**
* Parse and validate passed JSON (possibly gzip-compressed) and create a TemplateDataBlob object.
*
* @param string $json
* @return TemplateDataBlob
*/
public static function newFromDatabase( $json ) {
// Handle GZIP compression. \037\213 is the header for GZIP files.
if ( substr( $json, 0, 2 ) === "\037\213" ) {
$json = gzdecode( $json );
}
return self::newFromJSON( $json );
}
/**
* Parse the data, normalise it and validate it.
*
* See spec.templatedata.json for the expected format of the JSON object.
* @return Status
*/
private function parse() {
$data = $this->data;
static $rootKeys = array(
'description',
'params',
'sets',
);
static $paramKeys = array(
'label',
'required',
'description',
'deprecated',
'aliases',
'default',
'inherits',
'type',
);
static $types = array(
'unknown',
'string',
'number',
'string/wiki-page-name',
'string/wiki-user-name',
'string/line',
);
if ( $data === null ) {
return Status::newFatal( 'templatedata-invalid-parse' );
}
if ( !is_object( $data ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'templatedata', 'object' );
}
foreach ( $data as $key => $value ) {
if ( !in_array( $key, $rootKeys ) ) {
return Status::newFatal( 'templatedata-invalid-unknown', $key );
}
}
// Root.description
if ( isset( $data->description ) ) {
if ( !is_object( $data->description ) && !is_string( $data->description ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'description', 'string|object' );
}
$data->description = self::normaliseInterfaceText( $data->description );
} else {
$data->description = null;
}
// Root.params
if ( !isset( $data->params ) ) {
return Status::newFatal( 'templatedata-invalid-missing', 'params', 'object' );
}
if ( !is_object( $data->params ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'params', 'object' );
}
// Deep clone
// We need this to determine whether a property was originally set
// to decide whether 'inherits' will add it or not.
$unnormalizedParams = unserialize( serialize( $data->params ) );
foreach ( $data->params as $paramName => $paramObj ) {
if ( !is_object( $paramObj ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}",
'object'
);
}
foreach ( $paramObj as $key => $value ) {
if ( !in_array( $key, $paramKeys ) ) {
return Status::newFatal(
'templatedata-invalid-unknown',
"params.{$paramName}.{$key}"
);
}
}
// Param.label
if ( isset( $paramObj->label ) ) {
if ( !is_object( $paramObj->label ) && !is_string( $paramObj->label ) ) {
// TODO: Also validate that the keys are valid lang codes and the values strings.
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.label",
'string|object'
);
}
$paramObj->label = self::normaliseInterfaceText( $paramObj->label );
} else {
$paramObj->label = null;
}
// Param.required
if ( isset( $paramObj->required ) ) {
if ( !is_bool( $paramObj->required ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.required",
'boolean'
);
}
} else {
$paramObj->required = false;
}
// Param.description
if ( isset( $paramObj->description ) ) {
if ( !is_object( $paramObj->description ) && !is_string( $paramObj->description ) ) {
// TODO: Also validate that the keys are valid lang codes and the values strings.
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.description",
'string|object'
);
}
$paramObj->description = self::normaliseInterfaceText( $paramObj->description );
} else {
$paramObj->description = null;
}
// Param.deprecated
if ( isset( $paramObj->deprecated ) ) {
if ( $paramObj->deprecated !== false && !is_string( $paramObj->deprecated ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.deprecated",
'boolean|string'
);
}
} else {
$paramObj->deprecated = false;
}
// Param.aliases
if ( isset( $paramObj->aliases ) ) {
if ( !is_array( $paramObj->aliases ) ) {
// TODO: Validate the array values.
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.aliases",
'array'
);
}
} else {
$paramObj->aliases = array();
}
// Param.default
if ( isset( $paramObj->default ) ) {
if ( !is_string( $paramObj->default ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.default",
'string'
);
}
} else {
$paramObj->default = '';
}
// Param.type
if ( isset( $paramObj->type ) ) {
if ( !is_string( $paramObj->type ) ) {
return Status::newFatal(
'templatedata-invalid-type',
"params.{$paramName}.type",
'string'
);
}
if ( !in_array( $paramObj->type, $types ) ) {
return Status::newFatal(
'templatedata-invalid-value',
'params.' . $paramName . '.type'
);
}
} else {
$paramObj->type = 'unknown';
}
}
// Param.inherits
// Done afterwards to avoid code duplication
foreach ( $data->params as $paramName => $paramObj ) {
if ( isset( $paramObj->inherits ) ) {
if ( !isset( $data->params->{ $paramObj->inherits } ) ) {
return Status::newFatal(
'templatedata-invalid-missing',
"params.{$paramObj->inherits}"
);
}
$parentParamObj = $data->params->{ $paramObj->inherits };
foreach ( $parentParamObj as $key => $value ) {
if ( !in_array( $key, $paramKeys ) ) {
return Status::newFatal( 'templatedata-invalid-unknown', $key );
}
if ( !isset( $unnormalizedParams->$paramName->$key ) ) {
$paramObj->$key = is_object( $parentParamObj->$key ) ?
clone $parentParamObj->$key :
$parentParamObj->$key;
}
}
unset( $paramObj->inherits );
}
}
// Root.sets
if ( isset( $data->sets ) ) {
if ( !is_array( $data->sets ) ) {
return Status::newFatal( 'templatedata-invalid-type', 'sets', 'array' );
}
} else {
$data->sets = array();
}
foreach ( $data->sets as $setNr => $setObj ) {
if ( !is_object( $setObj ) ) {
return Status::newFatal( 'templatedata-invalid-type', "sets.{$setNr}", 'object' );
}
if ( !isset( $setObj->label ) ) {
return Status::newFatal(
'templatedata-invalid-missing',
"sets.{$setNr}.label",
'string|object'
);
}
if ( !is_object( $setObj->label ) && !is_string( $setObj->label ) ) {
// TODO: Also validate that the keys are valid lang codes and the values strings.
return Status::newFatal(
'templatedata-invalid-type',
"sets.{$setNr}.label",
'string|object'
);
}
$setObj->label = self::normaliseInterfaceText( $setObj->label );
if ( !isset( $setObj->params ) ) {
return Status::newFatal( 'templatedata-invalid-missing', "sets.{$setNr}.params", 'array' );
}
if ( !is_array( $setObj->params ) ) {
return Status::newFatal( 'templatedata-invalid-type', "sets.{$setNr}.params", 'array' );
}
foreach ( $setObj->params as $param ) {
if ( !isset( $data->params->$param ) ) {
return Status::newFatal( 'templatedata-invalid-missing', "params.{$param}" );
}
}
}
$length = strlen( $this->getJSONForDatabase() );
if ( $length > self::MAX_LENGTH ) {
return Status::newFatal( 'templatedata-invalid-length', $length, self::MAX_LENGTH );
}
return Status::newGood();
}
/**
* Normalise a InterfaceText field in the TemplateData blob.
* @return stdClass|string $text
*/
protected static function normaliseInterfaceText( $text ) {
if ( is_string( $text ) ) {
global $wgContLang;
$ret = new stdClass();
$ret->{ $wgContLang->getCode() } = $text;
return $ret;
}
return $text;
}
public function getStatus() {
return $this->status;
}
public function getData() {
// Returned by reference. Data is a private member. Use clone instead?
return $this->data;
}
/**
* @return string JSON
*/
public function getJSON() {
return json_encode( $this->data );
}
/**
* @return string JSON, gzip-compressed
*/
public function getJSONForDatabase() {
return gzencode( $this->getJSON() );
}
public function getHtml( Language $lang ) {
global $wgContLang;
$langCode = $wgContLang->getCode();
$data = $this->data;
$html =
Html::openElement( 'div', array( 'class' => 'mw-templatedata-doc-wrap' ) )
. Html::element(
'p',
array(
'class' => array(
'mw-templatedata-doc-desc',
'mw-templatedata-doc-muted' => $data->description === null,
)
),
$data->description !== null ?
$data->description->$langCode :
wfMessage( 'templatedata-doc-desc-empty' )->inLanguage( $lang )
)
. '<table class="wikitable mw-templatedata-doc-params">'
. Html::element(
'caption',
array(),
wfMessage( 'templatedata-doc-params' )->inLanguage( $lang )
)
. '<thead><tr>'
. Html::element(
'th',
array( 'colspan' => 2 ),
wfMessage( 'templatedata-doc-param-name' )->inLanguage( $lang )
)
. Html::element(
'th',
array(),
wfMessage( 'templatedata-doc-param-desc' )->inLanguage( $lang )
)
. Html::element(
'th',
array(),
wfMessage( 'templatedata-doc-param-type' )->inLanguage( $lang )
)
. Html::element(
'th',
array(),
wfMessage( 'templatedata-doc-param-default' )->inLanguage( $lang )
)
. Html::element(
'th',
array(),
wfMessage( 'templatedata-doc-param-status' )->inLanguage( $lang )
)
. '</tr></thead>'
. '<tbody>';
foreach ( $data->params as $paramName => $paramObj ) {
$description = '';
$default = '';
$aliases = '';
if ( count( $paramObj->aliases ) ) {
foreach ( $paramObj->aliases as $alias ) {
$aliases .= Html::element( 'tt', array(
'class' => 'mw-templatedata-doc-param-alias'
), $alias );
}
}
$html .= '<tr>'
// Label
. Html::element( 'th', array(),
isset( $paramObj->label->$langCode ) ?
$paramObj->label->$langCode :
ucfirst( $paramName )
)
// Parameters and aliases
. Html::rawElement( 'td', array( 'class' => 'mw-templatedata-doc-param-name' ),
Html::element( 'tt', array(), $paramName ) . $aliases
)
// Description
. Html::element( 'td', array(
'class' => array(
'mw-templatedata-doc-muted' => (
!isset( $paramObj->description->$langCode ) && $paramObj->deprecated === false
)
)
),
$paramObj->description->$langCode !== null ?
$paramObj->description->$langCode :
wfMessage( 'templatedata-doc-param-desc-empty' )->inLanguage( $lang )
)
// Type
. Html::rawElement( 'td', array(
'class' => array(
'mw-templatedata-doc-param-type',
'mw-templatedata-doc-muted' => $paramObj->type === 'unknown'
)
),
Html::element( 'tt', array(), $paramObj->type )
)
// Default
. Html::element( 'td', array(
'class' => array(
'mw-templatedata-doc-muted' => $paramObj->default === ''
)
),
$paramObj->default !== '' ? $paramObj->default : 'empty'
)
// Status
. Html::element( 'td', array(),
$paramObj->deprecated ? 'deprecated' : (
$paramObj->required ? 'required' : 'optional'
)
)
. '</tr>';
}
$html .= '</tbody></table>'
. Html::closeElement( 'div' );
return $html;
}
private function __construct( $data = null ) {
$this->data = $data;
}
}