Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.83% |
46 / 48 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
TemplateDataBlob | |
95.83% |
46 / 48 |
|
75.00% |
6 / 8 |
23 | |
0.00% |
0 / 1 |
newFromJSON | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
newFromDatabase | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getInterfaceTextInLanguage | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
getStatus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDataInLanguage | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
9 | |||
getJSONForDatabase | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\TemplateData; |
4 | |
5 | use MediaWiki\MainConfigNames; |
6 | use MediaWiki\MediaWikiServices; |
7 | use MediaWiki\Status\Status; |
8 | use stdClass; |
9 | use 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 | */ |
17 | class 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 | } |