Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
42.42% |
42 / 99 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
MessageBundleContent | |
42.42% |
42 / 99 |
|
28.57% |
2 / 7 |
254.64 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isValid | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
validate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
prepareSave | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMessages | |
37.93% |
11 / 29 |
|
0.00% |
0 / 1 |
23.30 | |||
getMetadata | |
54.55% |
24 / 44 |
|
0.00% |
0 / 1 |
36.13 | |||
getRawData | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageBundleTranslation; |
5 | |
6 | use FormatJson; |
7 | use JsonContent; |
8 | use Message; |
9 | use Status; |
10 | use User; |
11 | use WikiPage; |
12 | |
13 | /** |
14 | * @author Niklas Laxström |
15 | * @license GPL-2.0-or-later |
16 | * @since 2021.05 |
17 | */ |
18 | class MessageBundleContent extends JsonContent { |
19 | public const CONTENT_MODEL_ID = 'translate-messagebundle'; |
20 | // List of supported metadata keys |
21 | /** @phpcs-require-sorted-array */ |
22 | public const METADATA_KEYS = [ |
23 | 'allowOnlyPriorityLanguages', |
24 | 'description', |
25 | 'label', |
26 | 'priorityLanguages', |
27 | 'sourceLanguage' |
28 | ]; |
29 | private ?array $messages = null; |
30 | private ?MessageBundleMetadata $metadata = null; |
31 | |
32 | public function __construct( $text, $modelId = self::CONTENT_MODEL_ID ) { |
33 | parent::__construct( $text, $modelId ); |
34 | } |
35 | |
36 | public function isValid(): bool { |
37 | try { |
38 | $this->getMessages(); |
39 | $this->getMetadata(); |
40 | return parent::isValid(); |
41 | } catch ( MalformedBundle $e ) { |
42 | return false; |
43 | } |
44 | } |
45 | |
46 | /** @throws MalformedBundle */ |
47 | public function validate(): void { |
48 | $this->getMessages(); |
49 | $this->getMetadata(); |
50 | } |
51 | |
52 | public function prepareSave( WikiPage $page, $flags, $parentRevId, User $user ) { |
53 | // TODO: Should be removed when it is no longer needed for backwards compatibility. |
54 | |
55 | // This will give an informative error message when trying to change the content model |
56 | try { |
57 | $this->getMessages(); |
58 | $this->getMetadata(); |
59 | return Status::newGood(); |
60 | } catch ( MalformedBundle $e ) { |
61 | // XXX: We have no context source nor is there Message::messageParam :( |
62 | return Status::newFatal( 'translate-messagebundle-validation-error', wfMessage( $e ) ); |
63 | } |
64 | } |
65 | |
66 | /** @throws MalformedBundle */ |
67 | public function getMessages(): array { |
68 | if ( isset( $this->messages ) ) { |
69 | return $this->messages; |
70 | } |
71 | |
72 | $data = $this->getRawData(); |
73 | // Remove the metadata since we are not concerned with it. |
74 | unset( $data['@metadata'] ); |
75 | |
76 | foreach ( $data as $key => $value ) { |
77 | if ( $key === '' ) { |
78 | throw new MalformedBundle( 'translate-messagebundle-error-key-empty' ); |
79 | } |
80 | |
81 | if ( strlen( $key ) > 100 ) { |
82 | throw new MalformedBundle( |
83 | 'translate-messagebundle-error-key-too-long', |
84 | [ $key ] |
85 | ); |
86 | } |
87 | |
88 | if ( !preg_match( '/^[a-zA-Z0-9-_.]+$/', $key ) ) { |
89 | throw new MalformedBundle( |
90 | 'translate-messagebundle-error-key-invalid-characters', |
91 | [ $key ] |
92 | ); |
93 | } |
94 | |
95 | if ( !is_string( $value ) ) { |
96 | throw new MalformedBundle( |
97 | 'translate-messagebundle-error-invalid-value', |
98 | [ $key ] |
99 | ); |
100 | } |
101 | |
102 | if ( trim( $value ) === '' ) { |
103 | throw new MalformedBundle( |
104 | 'translate-messagebundle-error-empty-value', |
105 | [ $key ] |
106 | ); |
107 | } |
108 | } |
109 | |
110 | $this->messages = $data; |
111 | return $this->messages; |
112 | } |
113 | |
114 | public function getMetadata(): MessageBundleMetadata { |
115 | if ( isset( $this->metadata ) ) { |
116 | return $this->metadata; |
117 | } |
118 | |
119 | $data = $this->getRawData(); |
120 | $metadata = $data['@metadata'] ?? []; |
121 | |
122 | if ( !is_array( $metadata ) ) { |
123 | throw new MalformedBundle( 'translate-messagebundle-error-metadata-type' ); |
124 | } |
125 | |
126 | foreach ( $metadata as $key => $value ) { |
127 | if ( !in_array( $key, self::METADATA_KEYS ) ) { |
128 | throw new MalformedBundle( |
129 | 'translate-messagebundle-error-invalid-metadata', |
130 | [ $key, Message::listParam( self::METADATA_KEYS ) ] |
131 | ); |
132 | } |
133 | } |
134 | |
135 | $sourceLanguage = $metadata['sourceLanguage'] ?? null; |
136 | if ( $sourceLanguage && !is_string( $sourceLanguage ) ) { |
137 | throw new MalformedBundle( |
138 | 'translate-messagebundle-error-invalid-sourcelanguage', [ $sourceLanguage ] |
139 | ); |
140 | } |
141 | |
142 | $priorityLanguageCodes = $metadata['priorityLanguages'] ?? null; |
143 | if ( $priorityLanguageCodes ) { |
144 | if ( !is_array( $priorityLanguageCodes ) ) { |
145 | throw new MalformedBundle( 'translate-messagebundle-error-invalid-prioritylanguage-format' ); |
146 | } |
147 | |
148 | $priorityLanguageCodes = array_unique( $priorityLanguageCodes ); |
149 | } |
150 | |
151 | $description = $metadata['description'] ?? null; |
152 | if ( $description !== null ) { |
153 | if ( !is_string( $description ) ) { |
154 | throw new MalformedBundle( |
155 | 'translate-messagebundle-error-invalid-description' |
156 | ); |
157 | } |
158 | |
159 | $description = trim( $description ) === '' ? null : trim( $description ); |
160 | } |
161 | |
162 | $label = $metadata['label'] ?? null; |
163 | if ( $label !== null ) { |
164 | if ( !is_string( $label ) ) { |
165 | throw new MalformedBundle( |
166 | 'translate-messagebundle-error-invalid-label' |
167 | ); |
168 | } |
169 | |
170 | $label = trim( $label ) === '' ? null : trim( $label ); |
171 | } |
172 | |
173 | $this->metadata = new MessageBundleMetadata( |
174 | $sourceLanguage, |
175 | $priorityLanguageCodes, |
176 | (bool)( $metadata['allowOnlyPriorityLanguages'] ?? false ), |
177 | $description, |
178 | $label |
179 | ); |
180 | return $this->metadata; |
181 | } |
182 | |
183 | private function getRawData(): array { |
184 | $status = FormatJson::parse( $this->getText(), FormatJson::FORCE_ASSOC ); |
185 | if ( !$status->isOK() ) { |
186 | throw new MalformedBundle( |
187 | 'translate-messagebundle-error-parsing', |
188 | [ $status->getMessage()->text() ] |
189 | ); |
190 | } |
191 | |
192 | $data = $status->getValue(); |
193 | // Crude check that we have an associative array (or empty array) |
194 | if ( !is_array( $data ) || ( $data !== [] && array_values( $data ) === $data ) ) { |
195 | throw new MalformedBundle( |
196 | 'translate-messagebundle-error-invalid-array', |
197 | [ gettype( $data ) ] |
198 | ); |
199 | } |
200 | |
201 | return $data; |
202 | } |
203 | } |