Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
PremadeMediaWikiExtensionGroups
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 9
2862
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultNamespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNamespace
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setGroupPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setNamespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 createMessageGroup
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
210
 parseFile
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
650
 processGroups
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupConfiguration;
5
6use FileDependency;
7use MediaWiki\Extension\Translate\FileFormatSupport\JsonFormat;
8use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher;
9use MediaWiki\Extension\Translate\TranslatorInterface\Insertable\MediaWikiInsertablesSuggester;
10use MediaWiki\Extension\Translate\TranslatorInterface\Insertable\UrlInsertablesSuggester;
11use MediaWikiExtensionMessageGroup;
12use MessageGroup;
13use MessageGroupBase;
14use RuntimeException;
15use UnexpectedValueException;
16
17/**
18 * Class which handles special definition format for %MediaWiki extensions and skins.
19 * @author Niklas Laxström
20 * @license GPL-2.0-or-later
21 */
22class PremadeMediaWikiExtensionGroups {
23    protected string $idPrefix = 'ext-';
24    protected ?int $namespace = null;
25    /** @see __construct */
26    protected string $path;
27    /** @see __construct */
28    protected string $definitionFile;
29
30    /**
31     * @param string $def Absolute path to the definition file. See
32     *   tests/data/mediawiki-extensions.txt for example.
33     * @param string $path General prefix to the file locations without
34     *   the extension specific part. Should start with %GROUPROOT%/ or
35     *   otherwise export path will be wrong. The export path is
36     *   constructed by replacing %GROUPROOT%/ with target directory.
37     */
38    public function __construct( string $def, string $path ) {
39        $this->definitionFile = $def;
40        $this->path = rtrim( $path, '/' );
41    }
42
43    /** Get the default namespace. Subclasses can override this. */
44    protected function getDefaultNamespace(): int {
45        return NS_MEDIAWIKI;
46    }
47
48    /** Get the namespace ID */
49    protected function getNamespace(): int {
50        if ( $this->namespace === null ) {
51            $this->namespace = $this->getDefaultNamespace();
52        }
53        return $this->namespace;
54    }
55
56    /** How to prefix message group ids. */
57    public function setGroupPrefix( string $value ): void {
58        $this->idPrefix = $value;
59    }
60
61    /** Which namespace holds the messages. */
62    public function setNamespace( int $value ): void {
63        $this->namespace = $value;
64    }
65
66    /** Hook: TranslatePostInitGroups */
67    public function register( array &$list, array &$deps ): void {
68        $groups = $this->parseFile();
69        $groups = $this->processGroups( $groups );
70        foreach ( $groups as $id => $g ) {
71            $list[$id] = $this->createMessageGroup( $id, $g );
72        }
73
74        $deps[] = new FileDependency( $this->definitionFile );
75    }
76
77    /**
78     * Creates MediaWikiExtensionMessageGroup objects from parsed data.
79     * @param string $id unique group id already prefixed
80     * @param array $info array of group info
81     */
82    protected function createMessageGroup( string $id, array $info ): MessageGroup {
83        $conf = [];
84        $conf['BASIC']['class'] = MediaWikiExtensionMessageGroup::class;
85        $conf['BASIC']['id'] = $id;
86        $conf['BASIC']['namespace'] = $this->getNamespace();
87        $conf['BASIC']['label'] = $info['name'];
88
89        if ( isset( $info['desc'] ) ) {
90            $conf['BASIC']['description'] = $info['desc'];
91        } else {
92            $conf['BASIC']['descriptionmsg'] = $info['descmsg'];
93        }
94
95        $conf['FILES']['class'] = JsonFormat::class;
96        $conf['FILES']['sourcePattern'] = $this->path . '/' . $info['file'];
97
98        // @todo Find a better way
99        if ( isset( $info['aliasfile'] ) ) {
100            $conf['FILES']['aliasFileSource'] = $this->path . '/' . $info['aliasfile'];
101            $conf['FILES']['aliasFile'] = $info['aliasfile'];
102        }
103        if ( isset( $info['magicfile'] ) ) {
104            $conf['FILES']['magicFileSource'] = $this->path . '/' . $info['magicfile'];
105            $conf['FILES']['magicFile'] = $info['magicfile'];
106        }
107
108        if ( isset( $info['prefix'] ) ) {
109            $conf['MANGLER']['class'] = StringMatcher::class;
110            $conf['MANGLER']['prefix'] = $info['prefix'];
111            $conf['MANGLER']['patterns'] = $info['mangle'];
112
113            $mangler = new StringMatcher( $info['prefix'], $info['mangle'] );
114            if ( isset( $info['ignored'] ) ) {
115                $info['ignored'] = $mangler->mangleList( $info['ignored'] );
116            }
117            if ( isset( $info['optional'] ) ) {
118                $info['optional'] = $mangler->mangleList( $info['optional'] );
119            }
120        }
121
122        $conf['VALIDATORS'] = [
123            [ 'id' => 'BraceBalance' ],
124            [ 'id' => 'MediaWikiLink' ],
125            [ 'id' => 'MediaWikiPageName' ],
126            [ 'id' => 'MediaWikiParameter' ],
127            [ 'id' => 'MediaWikiPlural' ],
128        ];
129
130        $conf['INSERTABLES'] = [
131            [ 'class' => MediaWikiInsertablesSuggester::class ],
132            [ 'class' => UrlInsertablesSuggester::class ]
133        ];
134
135        if ( isset( $info['optional'] ) ) {
136            $conf['TAGS']['optional'] = $info['optional'];
137        }
138        if ( isset( $info['ignored'] ) ) {
139            $conf['TAGS']['ignored'] = $info['ignored'];
140        }
141
142        if ( isset( $info['languages'] ) ) {
143            $conf['LANGUAGES'] = [
144                'include' => [],
145                'exclude' => [],
146            ];
147
148            foreach ( $info['languages'] as $tagSpec ) {
149                if ( preg_match( '/^([+-])?(.+)$/', $tagSpec, $m ) ) {
150                    [ , $sign, $tag ] = $m;
151                    if ( $sign === '+' ) {
152                        $conf['LANGUAGES']['include'][] = $tag;
153                    } elseif ( $sign === '-' ) {
154                        $conf['LANGUAGES']['exclude'][] = $tag;
155                    } else {
156                        $conf['LANGUAGES']['exclude'] = '*';
157                        $conf['LANGUAGES']['include'][] = $tag;
158                    }
159                }
160            }
161        }
162
163        return MessageGroupBase::factory( $conf );
164    }
165
166    protected function parseFile(): array {
167        $defines = file_get_contents( $this->definitionFile );
168        $linefeed = '(\r\n|\n)';
169        $sections = array_map(
170            'trim',
171            preg_split( "/$linefeed{2,}/", $defines, -1, PREG_SPLIT_NO_EMPTY )
172        );
173        $groups = [];
174
175        foreach ( $sections as $section ) {
176            $lines = array_map( 'trim', preg_split( "/$linefeed/", $section ) );
177            $newGroup = [];
178
179            foreach ( $lines as $line ) {
180                if ( $line === '' || $line[0] === '#' ) {
181                    continue;
182                }
183
184                if ( !str_contains( $line, '=' ) ) {
185                    if ( empty( $newGroup['name'] ) ) {
186                        $newGroup['name'] = $line;
187                    } else {
188                        throw new RuntimeException( 'Trying to define name twice: ' . $line );
189                    }
190                } else {
191                    [ $key, $value ] = array_map( 'trim', explode( '=', $line, 2 ) );
192                    switch ( $key ) {
193                        case 'aliasfile':
194                        case 'desc':
195                        case 'descmsg':
196                        case 'file':
197                        case 'id':
198                        case 'magicfile':
199                        case 'var':
200                            $newGroup[$key] = $value;
201                            break;
202                        case 'optional':
203                        case 'ignored':
204                        case 'languages':
205                            $values = array_map( 'trim', explode( ',', $value ) );
206                            if ( !isset( $newGroup[$key] ) ) {
207                                $newGroup[$key] = [];
208                            }
209                            $newGroup[$key] = array_merge( $newGroup[$key], $values );
210                            break;
211                        case 'prefix':
212                            [ $prefix, $messages ] = array_map(
213                                'trim',
214                                explode( '|', $value, 2 )
215                            );
216                            if ( isset( $newGroup['prefix'] ) && $newGroup['prefix'] !== $prefix ) {
217                                throw new RuntimeException(
218                                    "Only one prefix supported: {$newGroup['prefix']} !== $prefix"
219                                );
220                            }
221                            $newGroup['prefix'] = $prefix;
222
223                            if ( !isset( $newGroup['mangle'] ) ) {
224                                $newGroup['mangle'] = [];
225                            }
226
227                            $messages = array_map( 'trim', explode( ',', $messages ) );
228                            $newGroup['mangle'] = array_merge( $newGroup['mangle'], $messages );
229                            break;
230                        default:
231                            throw new UnexpectedValueException( 'Unknown key:' . $key );
232                    }
233                }
234            }
235
236            if ( count( $newGroup ) ) {
237                if ( empty( $newGroup['name'] ) ) {
238                    throw new RuntimeException( "Name missing\n" . print_r( $newGroup, true ) );
239                }
240                $groups[] = $newGroup;
241            }
242        }
243
244        return $groups;
245    }
246
247    protected function processGroups( array $groups ): array {
248        $fixedGroups = [];
249        foreach ( $groups as $g ) {
250            $name = $g['name'];
251
252            $id = $g['id'] ?? $this->idPrefix . preg_replace( '/\s+/', '', strtolower( $name ) );
253
254            if ( !isset( $g['file'] ) ) {
255                $file = preg_replace( '/\s+/', '', "$name/i18n/%CODE%.json" );
256            } else {
257                $file = $g['file'];
258            }
259
260            $descMsg = $g['descmsg'] ?? str_replace( $this->idPrefix, '', $id ) . '-desc';
261
262            $newGroup = [
263                'name' => $name,
264                'file' => $file,
265                'descmsg' => $descMsg,
266            ];
267
268            $copyVars = [
269                'aliasfile',
270                'desc',
271                'ignored',
272                'languages',
273                'magicfile',
274                'mangle',
275                'optional',
276                'prefix',
277                'var',
278            ];
279
280            foreach ( $copyVars as $var ) {
281                if ( isset( $g[$var] ) ) {
282                    $newGroup[$var] = $g[$var];
283                }
284            }
285
286            // Mark some fixed form optional messages automatically
287            if ( !isset( $newGroup['optional' ] ) ) {
288                $newGroup['optional'] = [];
289            }
290
291            // Mark extension name and skin names optional.
292            $newGroup['optional'][] = '*-extensionname';
293            $newGroup['optional'][] = 'skinname-*';
294
295            $fixedGroups[$id] = $newGroup;
296        }
297
298        return $fixedGroups;
299    }
300}
301
302class_alias( PremadeMediaWikiExtensionGroups::class, 'PremadeMediaWikiExtensionGroups' );