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