Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageBundleContent.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageBundleTranslation;
5
6use FormatJson;
7use JsonContent;
8use Message;
9use Status;
10use User;
11use WikiPage;
12
18class MessageBundleContent extends JsonContent {
19 public const CONTENT_MODEL_ID = 'translate-messagebundle';
20 // List of supported metadata keys
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
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
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}