Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 116 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
SimpleFormat | |
0.00% |
0 / 115 |
|
0.00% |
0 / 19 |
2550 | |
0.00% |
0 / 1 |
supportsFuzzy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFileExtensions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
setGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setWritePath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWritePath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
exists | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
read | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
readFromVariable | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
write | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
writeIntoVariable | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
writeReal | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
tryReadSource | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
tryReadFile | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
filterAuthors | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
isContentEqual | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
shouldOverwrite | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isGroupFfsReadable | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\FileFormatSupport; |
5 | |
6 | use Exception; |
7 | use FileBasedMessageGroup; |
8 | use InvalidArgumentException; |
9 | use LogicException; |
10 | use MediaWiki\Extension\Translate\MessageLoading\Message; |
11 | use MediaWiki\Extension\Translate\MessageLoading\MessageCollection; |
12 | use MediaWiki\Extension\Translate\Services; |
13 | use RuntimeException; |
14 | use StringUtils; |
15 | use UtfNormal\Validator; |
16 | |
17 | /** |
18 | * A very basic FileFormatSupport module that implements some basic functionality and |
19 | * a simple binary based file format. Other FFS classes can extend SimpleFormat and |
20 | * override suitable methods. |
21 | * @ingroup FileFormatSupport |
22 | * @author Niklas Laxström |
23 | */ |
24 | class SimpleFormat implements FileFormatSupport { |
25 | |
26 | public function supportsFuzzy(): string { |
27 | return 'no'; |
28 | } |
29 | |
30 | public function getFileExtensions(): array { |
31 | return []; |
32 | } |
33 | |
34 | protected FileBasedMessageGroup $group; |
35 | protected ?string $writePath = null; |
36 | /** |
37 | * Stores the FILES section of the YAML configuration, |
38 | * which can be accessed for extra FFS class specific options. |
39 | * @var mixed |
40 | */ |
41 | protected $extra; |
42 | |
43 | private const RECORD_SEPARATOR = "\0"; |
44 | private const PART_SEPARATOR = "\0\0\0\0"; |
45 | |
46 | public function __construct( FileBasedMessageGroup $group ) { |
47 | $this->setGroup( $group ); |
48 | $conf = $group->getConfiguration(); |
49 | $this->extra = $conf['FILES']; |
50 | } |
51 | |
52 | public function setGroup( FileBasedMessageGroup $group ): void { |
53 | $this->group = $group; |
54 | } |
55 | |
56 | public function getGroup(): FileBasedMessageGroup { |
57 | return $this->group; |
58 | } |
59 | |
60 | public function setWritePath( string $target ): void { |
61 | $this->writePath = $target; |
62 | } |
63 | |
64 | public function getWritePath(): string { |
65 | return $this->writePath; |
66 | } |
67 | |
68 | /** |
69 | * Returns true if the file for this message group in a given language |
70 | * exists. If no $code is given, the groups source language is assumed. |
71 | * NB: Some formats store all languages in the same file, and then this |
72 | * function will return true even if there are no translations to that |
73 | * language. |
74 | * |
75 | * @param string|bool $code |
76 | */ |
77 | public function exists( $code = false ): bool { |
78 | if ( $code === false ) { |
79 | $code = $this->group->getSourceLanguage(); |
80 | } |
81 | |
82 | $filename = $this->group->getSourceFilePath( $code ); |
83 | if ( $filename === null ) { |
84 | return false; |
85 | } |
86 | |
87 | return file_exists( $filename ); |
88 | } |
89 | |
90 | /** |
91 | * Reads messages from the file in a given language and returns an array |
92 | * of AUTHORS, MESSAGES and possibly other properties. |
93 | * |
94 | * @return array|bool False if the file does not exist |
95 | * @throws RuntimeException if the file is not readable or has bad encoding |
96 | */ |
97 | public function read( string $languageCode ) { |
98 | if ( !$this->isGroupFfsReadable() ) { |
99 | return []; |
100 | } |
101 | |
102 | if ( !$this->exists( $languageCode ) ) { |
103 | return false; |
104 | } |
105 | |
106 | $filename = $this->group->getSourceFilePath( $languageCode ); |
107 | $input = file_get_contents( $filename ); |
108 | if ( $input === false ) { |
109 | throw new RuntimeException( "Unable to read file $filename." ); |
110 | } |
111 | |
112 | if ( !StringUtils::isUtf8( $input ) ) { |
113 | throw new RuntimeException( "Contents of $filename are not valid utf-8." ); |
114 | } |
115 | |
116 | $input = Validator::cleanUp( $input ); |
117 | |
118 | // Strip BOM mark |
119 | $input = ltrim( $input, "\u{FEFF}" ); |
120 | |
121 | try { |
122 | return $this->readFromVariable( $input ); |
123 | } catch ( Exception $e ) { |
124 | throw new RuntimeException( "Parsing $filename failed: " . $e->getMessage() ); |
125 | } |
126 | } |
127 | |
128 | /** |
129 | * Parse the message data given as a string in the SimpleFormat format |
130 | * and return it as an array of AUTHORS and MESSAGES. |
131 | * |
132 | * @throws InvalidArgumentException |
133 | */ |
134 | public function readFromVariable( string $data ): array { |
135 | $parts = explode( self::PART_SEPARATOR, $data ); |
136 | |
137 | if ( count( $parts ) !== 2 ) { |
138 | throw new InvalidArgumentException( 'Wrong number of parts.' ); |
139 | } |
140 | |
141 | [ $authorsPart, $messagesPart ] = $parts; |
142 | $authors = explode( self::RECORD_SEPARATOR, $authorsPart ); |
143 | $messages = []; |
144 | |
145 | foreach ( explode( self::RECORD_SEPARATOR, $messagesPart ) as $line ) { |
146 | if ( $line === '' ) { |
147 | continue; |
148 | } |
149 | |
150 | $lineParts = explode( '=', $line, 2 ); |
151 | |
152 | if ( count( $lineParts ) !== 2 ) { |
153 | throw new InvalidArgumentException( "Wrong number of parts in line $line." ); |
154 | } |
155 | |
156 | [ $key, $message ] = $lineParts; |
157 | $key = trim( $key ); |
158 | $messages[$key] = $message; |
159 | } |
160 | |
161 | $messages = $this->group->getMangler()->mangleArray( $messages ); |
162 | |
163 | return [ |
164 | 'AUTHORS' => $authors, |
165 | 'MESSAGES' => $messages, |
166 | ]; |
167 | } |
168 | |
169 | /** Write the collection to file. */ |
170 | public function write( MessageCollection $collection ): void { |
171 | $writePath = $this->writePath; |
172 | |
173 | if ( $writePath === null ) { |
174 | throw new LogicException( 'Write path is not set. Set write path before calling write()' ); |
175 | } |
176 | |
177 | if ( !file_exists( $writePath ) ) { |
178 | throw new InvalidArgumentException( "Write path '$writePath' does not exist." ); |
179 | } |
180 | |
181 | if ( !is_writable( $writePath ) ) { |
182 | throw new InvalidArgumentException( "Write path '$writePath' is not writable." ); |
183 | } |
184 | |
185 | $targetFile = $writePath . '/' . $this->group->getTargetFilename( $collection->code ); |
186 | |
187 | $targetFileExists = file_exists( $targetFile ); |
188 | |
189 | if ( $targetFileExists ) { |
190 | $this->tryReadSource( $targetFile, $collection ); |
191 | } else { |
192 | $sourceFile = $this->group->getSourceFilePath( $collection->code ); |
193 | $this->tryReadSource( $sourceFile, $collection ); |
194 | } |
195 | |
196 | $output = $this->writeReal( $collection ); |
197 | if ( !$output ) { |
198 | return; |
199 | } |
200 | |
201 | // Some file formats might have changing parts, such as timestamp. |
202 | // This allows the file handler to skip updating files, where only |
203 | // the timestamp would change. |
204 | if ( $targetFileExists ) { |
205 | $oldContent = $this->tryReadFile( $targetFile ); |
206 | if ( $oldContent === null || !$this->shouldOverwrite( $oldContent, $output ) ) { |
207 | return; |
208 | } |
209 | } |
210 | |
211 | wfMkdirParents( dirname( $targetFile ), null, __METHOD__ ); |
212 | file_put_contents( $targetFile, $output ); |
213 | } |
214 | |
215 | /** Read a collection and return it as a SimpleFormat formatted string. */ |
216 | public function writeIntoVariable( MessageCollection $collection ): string { |
217 | $sourceFile = $this->group->getSourceFilePath( $collection->code ); |
218 | $this->tryReadSource( $sourceFile, $collection ); |
219 | |
220 | return $this->writeReal( $collection ); |
221 | } |
222 | |
223 | protected function writeReal( MessageCollection $collection ): string { |
224 | $output = ''; |
225 | |
226 | $authors = $collection->getAuthors(); |
227 | $authors = $this->filterAuthors( $authors, $collection->code ); |
228 | |
229 | $output .= implode( self::RECORD_SEPARATOR, $authors ); |
230 | $output .= self::PART_SEPARATOR; |
231 | |
232 | $mangler = $this->group->getMangler(); |
233 | |
234 | /** @var Message $m */ |
235 | foreach ( $collection as $key => $m ) { |
236 | $key = $mangler->unmangle( $key ); |
237 | $trans = $m->translation(); |
238 | $output .= "$key=$trans" . self::RECORD_SEPARATOR; |
239 | } |
240 | |
241 | return $output; |
242 | } |
243 | |
244 | /** |
245 | * This tries to pick up external authors in the source files so that they |
246 | * are not lost if those authors are not among those who have translated in |
247 | * the wiki. |
248 | * |
249 | * @todo Get rid of this |
250 | */ |
251 | protected function tryReadSource( string $filename, MessageCollection $collection ): void { |
252 | if ( !$this->isGroupFfsReadable() ) { |
253 | return; |
254 | } |
255 | |
256 | $sourceText = $this->tryReadFile( $filename ); |
257 | |
258 | // No need to do anything in SimpleFormat if it's null, |
259 | // it only reads author data from it. |
260 | if ( $sourceText !== null ) { |
261 | $sourceData = $this->readFromVariable( $sourceText ); |
262 | |
263 | if ( isset( $sourceData['AUTHORS'] ) ) { |
264 | $collection->addCollectionAuthors( $sourceData['AUTHORS'] ); |
265 | } |
266 | } |
267 | } |
268 | |
269 | /** |
270 | * Read the contents of $filename and return it as a string. |
271 | * Return null if the file doesn't exist. |
272 | * Throw an exception if the file isn't readable |
273 | * or if the reading fails strangely. |
274 | * @throws InvalidArgumentException |
275 | */ |
276 | protected function tryReadFile( string $filename ): ?string { |
277 | if ( $filename === '' || !file_exists( $filename ) ) { |
278 | return null; |
279 | } |
280 | |
281 | if ( !is_readable( $filename ) ) { |
282 | throw new InvalidArgumentException( "File $filename is not readable." ); |
283 | } |
284 | |
285 | $data = file_get_contents( $filename ); |
286 | if ( $data === false ) { |
287 | throw new InvalidArgumentException( "Unable to read file $filename." ); |
288 | } |
289 | |
290 | return $data; |
291 | } |
292 | |
293 | /** Remove excluded authors. */ |
294 | public function filterAuthors( array $authors, string $code ): array { |
295 | $groupId = $this->group->getId(); |
296 | $configHelper = Services::getInstance()->getConfigHelper(); |
297 | foreach ( $authors as $i => $v ) { |
298 | if ( $configHelper->isAuthorExcluded( $groupId, $code, (string)$v ) ) { |
299 | unset( $authors[$i] ); |
300 | } |
301 | } |
302 | |
303 | return array_values( $authors ); |
304 | } |
305 | |
306 | public function isContentEqual( ?string $a, ?string $b ): bool { |
307 | return $a === $b; |
308 | } |
309 | |
310 | public function shouldOverwrite( string $a, string $b ): bool { |
311 | return true; |
312 | } |
313 | |
314 | /** |
315 | * Check if the file format of the current group is readable by the file |
316 | * format system. This might happen if we are trying to export a JsonFormat |
317 | * or WikiPageMessage group to a GettextFormat. |
318 | */ |
319 | public function isGroupFfsReadable(): bool { |
320 | try { |
321 | $ffs = $this->group->getFFS(); |
322 | } catch ( RuntimeException $e ) { |
323 | if ( $e->getCode() === FileBasedMessageGroup::NO_FILE_FORMAT ) { |
324 | return false; |
325 | } |
326 | |
327 | throw $e; |
328 | } |
329 | |
330 | return get_class( $ffs ) === get_class( $this ); |
331 | } |
332 | } |
333 | |
334 | class_alias( SimpleFormat::class, 'SimpleFFS' ); |