Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.53% covered (danger)
44.53%
57 / 128
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CustomIndexFieldsParser
44.53% covered (danger)
44.53%
57 / 128
63.64% covered (warning)
63.64%
7 / 11
358.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getCustomIndexFieldsConfiguration
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadCustomIndexFieldsConfiguration
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
272
 parseCustomIndexFields
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 parseCustomIndexFieldsForHeader
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parseCustomIndexFieldsForJs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parseCustomIndexField
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getCustomIndexFieldForDataKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parseCustomIndexFieldWithVariablesReplacedWithIndexEntries
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 parseCustomIndexFieldsAsTemplateParams
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getContentLanguage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace ProofreadPage\Index;
4
5use FormatJson;
6use OutOfBoundsException;
7use RequestContext;
8
9/**
10 * @license GPL-2.0-or-later
11 *
12 * Returns the custom index entries from an IndexContent
13 */
14class CustomIndexFieldsParser {
15
16    /** @var array */
17    private $configuration;
18
19    /**
20     * Loads custom configuration
21     * @param array|null $customIndexFieldsConfiguration (optional)
22     */
23    public function __construct( array $customIndexFieldsConfiguration = null ) {
24        $this->configuration = ( $customIndexFieldsConfiguration === null )
25            ? $this->loadCustomIndexFieldsConfiguration()
26            : $customIndexFieldsConfiguration;
27    }
28
29    /**
30     * @return array the configuration
31     * The configuration is a list of properties like this :
32     * array(
33     *     'ID' => array( //the property id
34     *         'type' => 'string', // the property type (for compatibility reasons the values have not
35     *              // to be of this type). Possible values: string, number, page
36     *         'size' => 1, // for type = string : the size of the form input
37     *         'default' => '', // the default value
38     *         'label' => 'ID', // the label of the property
39     *         'help' => '', // a short help text
40     *         'values' => null, // an array value => label that list the possible values
41     *              // (for compatibility reasons the stored values have not to be one of these)
42     *         'header' => false, // give the content of this property to
43     *              // MediaWiki:Proofreadpage_header_template as template parameter
44     *         'hidden' => false // don't show the property in the index pages form. Useful for data
45     *              // that have always the same value (as language=en for en Wikisource) or are
46     *              // only set at the <pages> tag level.
47     *         )
48     * );
49     *  NB: The values set are the default values
50     */
51    public function getCustomIndexFieldsConfiguration() {
52        return $this->configuration;
53    }
54
55    private function loadCustomIndexFieldsConfiguration() {
56        $data = wfMessage( 'proofreadpage_index_data_config.json' )->inContentLanguage();
57
58        // fallback to the legacy name - T263094
59        if ( !( $data->exists() && $data->plain() != '' ) ) {
60            $data = wfMessage( 'proofreadpage_index_data_config' )->inContentLanguage();
61        }
62
63        if ( $data->exists() &&    $data->plain() != '' ) {
64            $config = FormatJson::decode( $data->plain(), true );
65            if ( $config === null ) {
66                RequestContext::getMain()->getOutput()->showErrorPage(
67                    'proofreadpage_dataconfig_badformatted',
68                    'proofreadpage_dataconfig_badformattedtext'
69                );
70                $config = [];
71            }
72        } else {
73            $attributes = explode( "\n", wfMessage( 'proofreadpage_index_attributes' )
74                ->inContentLanguage()->text() );
75            $headerAttributes = explode( ' ', wfMessage( 'proofreadpage_js_attributes' )
76                ->inContentLanguage()->text() );
77            $config = [];
78            foreach ( $attributes as $attribute ) {
79                $m = explode( '|', $attribute );
80                $params = [
81                    'type' => 'string',
82                    'size' => 1,
83                    'default' => '',
84                    'label' => $m[0],
85                    'help' => '',
86                    'values' => null,
87                    'header' => false
88                ];
89
90                if ( $m[0] == 'Header' ) {
91                    $params['default'] = wfMessage( 'proofreadpage_default_header' )
92                        ->inContentLanguage()->plain();
93                }
94                if ( $m[0] == 'Footer' ) {
95                    $params['default'] = wfMessage( 'proofreadpage_default_footer' )
96                        ->inContentLanguage()->plain();
97                }
98                if ( isset( $m[1] ) && $m[1] !== '' ) {
99                    $params['label'] = $m[1];
100                }
101                if ( isset( $m[2] ) ) {
102                    $params['size'] = intval( $m[2] );
103                }
104                $config[$m[0]] = $params;
105            }
106
107            foreach ( $headerAttributes as $attribute ) {
108                if ( isset( $config[$attribute] ) ) {
109                    $config[$attribute]['header'] = true;
110                } else {
111                    $config[$attribute] = [
112                        'type' => 'string',
113                        'size' => 1,
114                        'default' => '',
115                        'label' => $attribute,
116                        'values' => null,
117                        'header' => true,
118                        'hidden' => true
119                    ];
120                }
121            }
122        }
123
124        if ( !array_key_exists( 'Header', $config ) ) {
125            $config['Header'] = [
126                'default' => wfMessage( 'proofreadpage_default_header' )
127                    ->inContentLanguage()->plain(),
128                'header' => true,
129                'hidden' => true
130            ];
131        }
132        if ( !array_key_exists( 'Footer', $config ) ) {
133            $config['Footer'] = [
134                'default' => wfMessage( 'proofreadpage_default_footer' )
135                    ->inContentLanguage()->plain(),
136                'header' => true,
137                'hidden' => true
138            ];
139        }
140
141        return $config;
142    }
143
144    /**
145     * @param IndexContent $content
146     * @return CustomIndexField[]
147     */
148    public function parseCustomIndexFields( IndexContent $content ) {
149        $contentFields = [];
150        foreach ( $content->getFields() as $key => $value ) {
151            $contentFields[strtolower( $key )] = $value;
152        }
153
154        $values = [];
155        foreach ( $this->configuration as $varName => $property ) {
156            $key = strtolower( $varName );
157            if ( array_key_exists( $key, $contentFields ) ) {
158                $values[$varName] = new CustomIndexField(
159                    $varName, $contentFields[$key]->getText(), $property
160                );
161            } else {
162                $values[$varName] = new CustomIndexField( $varName, '', $property );
163            }
164        }
165        return $values;
166    }
167
168    /**
169     * Return metadata from the index page that have to be given to header template.
170     * @param IndexContent $content
171     * @return CustomIndexField[]
172     */
173    public function parseCustomIndexFieldsForHeader( IndexContent $content ) {
174        $entries = $this->parseCustomIndexFields( $content );
175        $headerEntries = [];
176        foreach ( $entries as $entry ) {
177            if ( $entry->isHeader() ) {
178                $headerEntries[$entry->getKey()] = $entry;
179            }
180        }
181        return $headerEntries;
182    }
183
184    /**
185     * Returns index entries that are to be exposed via mw.config
186     * @param IndexContent $content
187     * @return CustomIndexField[]
188     */
189    public function parseCustomIndexFieldsForJs( IndexContent $content ) {
190        $indexFields = $this->parseCustomIndexFields( $content );
191        $jsIndexFields = [];
192        foreach ( $indexFields as $field ) {
193            if ( $field->isAllowedInJs() ) {
194                $jsIndexFields[$field->getKey()] = $field;
195            }
196        }
197        return $jsIndexFields;
198    }
199
200    /**
201     * Return the index entry with the same name or null if it's not found
202     * Note: the comparison is case insensitive
203     * @param IndexContent $content
204     * @param string $fieldName
205     * @return CustomIndexField
206     * @throws OutOfBoundsException
207     */
208    public function parseCustomIndexField( IndexContent $content, $fieldName ) {
209        $fieldName = strtolower( $fieldName );
210        $entries = $this->parseCustomIndexFields( $content );
211        foreach ( $entries as $entry ) {
212            if ( strtolower( $entry->getKey() ) === $fieldName ) {
213                return $entry;
214            }
215        }
216        throw new OutOfBoundsException( 'Custom index entry ' . $fieldName . ' does not exist.' );
217    }
218
219    /**
220     * Return the index entry with the same "data" attribute value
221     *
222     * @param IndexContent $content
223     * @param string $dataKey
224     * @return CustomIndexField
225     * @throws OutOfBoundsException
226     */
227    public function getCustomIndexFieldForDataKey( IndexContent $content, string $dataKey ): CustomIndexField {
228        $fieldName = strtolower( $dataKey );
229        $entries = $this->parseCustomIndexFields( $content );
230        foreach ( $entries as $entry ) {
231            if ( $entry->getData() === $fieldName ) {
232                return $entry;
233            }
234        }
235        throw new OutOfBoundsException( 'Custom index entry ' . $fieldName . ' does not exist.' );
236    }
237
238    /**
239     * Return the value of an entry as wikitext with variable replaced with index entries and
240     * $otherParams
241     * Example: if 'header' entry is 'Page of {{title}} number {{pagenum}}' with
242     * $otherParams = array( 'pagenum' => 23 )
243     * the function called for 'header' will returns 'Page page my book number 23'
244     * @param IndexContent $content
245     * @param string $fieldName
246     * @param array $otherParams associative array other possible values to replace
247     * @return string the value with variables replaced
248     * @throws OutOfBoundsException
249     */
250    public function parseCustomIndexFieldWithVariablesReplacedWithIndexEntries(
251        IndexContent $content, $fieldName, array $otherParams
252    ) {
253        $entry = $this->parseCustomIndexField( $content, $fieldName );
254
255        // we can't use the parser here because it replace tags like <references /> by strange UIDs
256        $params = $this->parseCustomIndexFieldsAsTemplateParams( $content ) + $otherParams;
257        return preg_replace_callback(
258            '/{\{\{(.*)(\|(.*))?\}\}\}/U',
259            static function ( $matches ) use ( $params ) {
260                $paramKey = trim( strtolower( $matches[1] ) );
261                if ( array_key_exists( $paramKey, $params ) ) {
262                    return $params[$paramKey];
263                } elseif ( array_key_exists( 3, $matches ) ) {
264                    return trim( $matches[3] );
265                } else {
266                    return $matches[0];
267                }
268            },
269            $entry->getStringValue()
270        );
271    }
272
273    /**
274     * Returns the index entries formatted in order to be transcluded in templates
275     *
276     * @param IndexContent $content
277     * @return string[]
278     */
279    private function parseCustomIndexFieldsAsTemplateParams( IndexContent $content ) {
280        $indexEntries = $this->parseCustomIndexFieldsForHeader( $content );
281        $params = [];
282        foreach ( $indexEntries as $entry ) {
283            $params[strtolower( $entry->getKey() )] = $entry->getStringValue();
284        }
285        return $params;
286    }
287
288    /**
289     * @param IndexContent $content
290     * @return string|null
291     */
292    public function getContentLanguage( IndexContent $content ) {
293        try {
294            $entry = $this->getCustomIndexFieldForDataKey( $content, 'language' );
295            return $entry->getType() === 'langcode' ? $entry->getStringValue() : null;
296        } catch ( OutOfBoundsException $e ) {
297            return null;
298        }
299    }
300
301}