Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.42% covered (danger)
42.42%
42 / 99
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageBundleContent
42.42% covered (danger)
42.42%
42 / 99
28.57% covered (danger)
28.57%
2 / 7
254.64
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
 prepareSave
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 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 FormatJson;
7use JsonContent;
8use Message;
9use Status;
10use User;
11use WikiPage;
12
13/**
14 * @author Niklas Laxström
15 * @license GPL-2.0-or-later
16 * @since 2021.05
17 */
18class 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}