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' => 'mw-templatedata-doc-desc' ), $data->description->$langCode ) . '