Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
46 / 48
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateDataBlob
95.83% covered (success)
95.83%
46 / 48
75.00% covered (warning)
75.00%
6 / 8
23
0.00% covered (danger)
0.00%
0 / 1
 newFromJSON
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 newFromDatabase
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getInterfaceTextInLanguage
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataInLanguage
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
 getJSONForDatabase
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\TemplateData;
4
5use MediaWiki\MainConfigNames;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Status\Status;
8use stdClass;
9use Wikimedia\Rdbms\IReadableDatabase;
10
11/**
12 * Represents the information about a template,
13 * coming from the JSON blob in the <templatedata> tags
14 * on wiki pages.
15 * @license GPL-2.0-or-later
16 */
17class TemplateDataBlob {
18
19    protected string $json;
20    protected Status $status;
21
22    /**
23     * Parse and validate passed JSON and create a blob handling
24     * instance.
25     * Accepts and handles user-provided data.
26     *
27     * @param IReadableDatabase $db
28     * @param string $json
29     * @return TemplateDataBlob
30     */
31    public static function newFromJSON( IReadableDatabase $db, string $json ): TemplateDataBlob {
32        $lang = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LanguageCode );
33        if ( $db->getType() === 'mysql' ) {
34            $tdb = new TemplateDataCompressedBlob( $json, $lang );
35        } else {
36            $tdb = new TemplateDataBlob( $json, $lang );
37        }
38        return $tdb;
39    }
40
41    /**
42     * Parse and validate passed JSON (possibly gzip-compressed) and create a blob handling
43     * instance.
44     *
45     * @param IReadableDatabase $db
46     * @param string $json
47     * @return TemplateDataBlob
48     */
49    public static function newFromDatabase( IReadableDatabase $db, string $json ): TemplateDataBlob {
50        // Handle GZIP compression. \037\213 is the header for GZIP files.
51        if ( substr( $json, 0, 2 ) === "\037\213" ) {
52            $json = gzdecode( $json );
53        }
54        return self::newFromJSON( $db, $json );
55    }
56
57    protected function __construct( string $json, string $lang ) {
58        $deprecatedTypes = array_keys( TemplateDataNormalizer::DEPRECATED_PARAMETER_TYPES );
59        $validator = new TemplateDataValidator( $deprecatedTypes );
60        $this->status = $validator->validate( json_decode( $json ) );
61
62        // If data is invalid, replace with the minimal valid blob.
63        // This is to make sure that, if something forgets to check the status first,
64        // we don't end up with invalid data in the database.
65        $value = $this->status->getValue() ?? (object)[ 'params' => (object)[] ];
66
67        $normalizer = new TemplateDataNormalizer( $lang );
68        $normalizer->normalize( $value );
69
70        // Don't bother storing the decoded object, it will always be cloned anyway
71        $this->json = json_encode( $value );
72    }
73
74    /**
75     * Get a single localized string from an InterfaceText object.
76     *
77     * Uses the preferred language passed to this function, or one of its fallbacks,
78     * or the site content language, or its fallbacks.
79     *
80     * @param stdClass $text An InterfaceText object
81     * @param string $langCode Preferred language
82     * @return null|string Text value from the InterfaceText object or null if no suitable
83     *  match was found
84     */
85    private function getInterfaceTextInLanguage( stdClass $text, string $langCode ): ?string {
86        if ( isset( $text->$langCode ) ) {
87            return $text->$langCode;
88        }
89
90        [ $userlangs, $sitelangs ] = MediaWikiServices::getInstance()->getLanguageFallback()
91            ->getAllIncludingSiteLanguage( $langCode );
92
93        foreach ( $userlangs as $lang ) {
94            if ( isset( $text->$lang ) ) {
95                return $text->$lang;
96            }
97        }
98
99        foreach ( $sitelangs as $lang ) {
100            if ( isset( $text->$lang ) ) {
101                return $text->$lang;
102            }
103        }
104
105        // If none of the languages are found fallback to null. Alternatively we could fallback to
106        // reset( $text ) which will return whatever key there is, but we should't give the user a
107        // "random" language with no context (e.g. could be RTL/Hebrew for an LTR/Japanese user).
108        return null;
109    }
110
111    public function getStatus(): Status {
112        return $this->status;
113    }
114
115    /**
116     * @return stdClass
117     */
118    public function getData() {
119        // Return deep clone so callers can't modify data. Needed for getDataInLanguage().
120        return json_decode( $this->json );
121    }
122
123    /**
124     * Get data with all InterfaceText objects resolved to a single string to the
125     * appropriate language.
126     *
127     * @param string $langCode Preferred language
128     * @return stdClass
129     */
130    public function getDataInLanguage( string $langCode ): stdClass {
131        $data = $this->getData();
132
133        // Root.description
134        if ( $data->description !== null ) {
135            $data->description = $this->getInterfaceTextInLanguage( $data->description, $langCode );
136        }
137
138        foreach ( $data->params as $param ) {
139            // Param.label
140            if ( $param->label !== null ) {
141                $param->label = $this->getInterfaceTextInLanguage( $param->label, $langCode );
142            }
143
144            // Param.description
145            if ( $param->description !== null ) {
146                $param->description = $this->getInterfaceTextInLanguage( $param->description, $langCode );
147            }
148
149            // Param.default
150            if ( $param->default !== null ) {
151                $param->default = $this->getInterfaceTextInLanguage( $param->default, $langCode );
152            }
153
154            // Param.example
155            if ( $param->example !== null ) {
156                $param->example = $this->getInterfaceTextInLanguage( $param->example, $langCode );
157            }
158        }
159
160        foreach ( $data->sets as $setObj ) {
161            $label = $this->getInterfaceTextInLanguage( $setObj->label, $langCode );
162            if ( $label === null ) {
163                // Contrary to other InterfaceTexts, set label is not optional. If we're here it
164                // means the template data from the wiki doesn't contain either the user language,
165                // site language or any of its fallbacks. Wikis should fix data that is in this
166                // condition (TODO: Disallow during saving?). For now, fallback to whatever we can
167                // get that does exist in the text object.
168                $arr = (array)$setObj->label;
169                $label = reset( $arr );
170            }
171
172            $setObj->label = $label;
173        }
174
175        return $data;
176    }
177
178    /**
179     * @return string JSON
180     */
181    public function getJSONForDatabase(): string {
182        return $this->json;
183    }
184
185}