Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.93% covered (success)
98.93%
185 / 187
84.62% covered (warning)
84.62%
11 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFMappingUtils
98.93% covered (success)
98.93%
185 / 187
84.62% covered (warning)
84.62%
11 / 13
76
0.00% covered (danger)
0.00%
0 / 1
 getMappingType
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getMappedValuesForInput
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 isIndexedArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMappedValues
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 getValuesWithMappingProperty
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 getValuesWithMappingTemplate
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 getValuesWithMappingCargoField
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
6.01
 getValuesWithTranslateMapping
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getLabelsForTitles
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
14
 getDisplayTitles
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 removeNSPrefixFromLabel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 disambiguateLabels
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 createDisplayTitleLabels
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Methods for mapping values to labels
4 * @file
5 * @ingroup PF
6 */
7
8use MediaWiki\Context\RequestContext;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Parser\Parser;
11use MediaWiki\Parser\ParserOptions;
12use MediaWiki\Title\Title;
13
14class PFMappingUtils {
15
16    /**
17     * @param array $args
18     * @param bool $useDisplayTitle
19     * @return string|null
20     */
21    public static function getMappingType( array $args, bool $useDisplayTitle = false ) {
22        $mappingType = null;
23        if ( array_key_exists( 'mapping property', $args ) ) {
24            $mappingType = 'mapping property';
25        } elseif ( array_key_exists( 'mapping template', $args ) ) {
26            $mappingType = 'mapping template';
27        } elseif ( array_key_exists( 'mapping cargo table', $args ) &&
28        array_key_exists( 'mapping cargo field', $args ) ) {
29            // @todo: or use 'cargo field'?
30            $mappingType = 'mapping cargo field';
31        } elseif ( array_key_exists( 'mapping using translate', $args ) ) {
32            $mappingType = 'mapping using translate';
33        } elseif ( $useDisplayTitle ) {
34            $mappingType = 'displaytitle';
35        }
36        return $mappingType;
37    }
38
39    /**
40     * Map values if possible and return a named (associative) array
41     * @param array $values
42     * @param array $args
43     * @return array
44     */
45    public static function getMappedValuesForInput( array $values, array $args = [] ) {
46        global $wgPageFormsUseDisplayTitle;
47        $mappingType = self::getMappingType( $args, $wgPageFormsUseDisplayTitle );
48        if ( self::isIndexedArray( $values ) == false ) {
49            // already named associative
50            $pages = array_keys( $values );
51            $values = self::getMappedValues( $pages, $mappingType, $args, $wgPageFormsUseDisplayTitle );
52            $res = $values;
53        } elseif ( $mappingType !== null ) {
54            $res = self::getMappedValues( $values, $mappingType, $args, $wgPageFormsUseDisplayTitle );
55        } else {
56            $res = [];
57            foreach ( $values as $key => $value ) {
58                $res[$value] = $value;
59            }
60        }
61        return $res;
62    }
63
64    /**
65     * Check if array is indexed/sequential (true), else named/associative (false)
66     * @param array $arr
67     * @return bool
68     */
69    private static function isIndexedArray( $arr ) {
70        if ( array_keys( $arr ) == range( 0, count( $arr ) - 1 ) ) {
71            return true;
72        } else {
73            return false;
74        }
75    }
76
77    /**
78     * Return named array of mapped values
79     * Static version of PF_FormField::setMappedValues
80     * @param array $values
81     * @param string|null $mappingType
82     * @param array $args
83     * @param bool $useDisplayTitle
84     * @return array
85     */
86    public static function getMappedValues(
87        array $values,
88        ?string $mappingType,
89        array $args,
90        bool $useDisplayTitle
91    ) {
92        $mappedValues = null;
93        switch ( $mappingType ) {
94            case 'mapping property':
95                $mappingProperty = $args['mapping property'];
96                $mappedValues = self::getValuesWithMappingProperty( $values, $mappingProperty );
97                break;
98            case 'mapping template':
99                $mappingTemplate = $args['mapping template'];
100                $mappedValues = self::getValuesWithMappingTemplate( $values, $mappingTemplate );
101                break;
102            case 'mapping cargo field':
103                $mappingCargoField = $args['mapping cargo field'] ?? null;
104                $mappingCargoValueField = $args['mapping cargo value field'] ?? null;
105                $mappingCargoTable = $args['mapping cargo table'];
106                $mappedValues = self::getValuesWithMappingCargoField( $values, $mappingCargoField, $mappingCargoValueField, $mappingCargoTable, $useDisplayTitle );
107                break;
108            case 'mapping using translate':
109                $translateMapping = $args[ 'mapping using translate' ];
110                $mappedValues = self::getValuesWithTranslateMapping( $values, $translateMapping );
111                break;
112            case 'displaytitle':
113                $isReverseLookup = ( array_key_exists( 'reverselookup', $args ) && ( $args['reverselookup'] == 'true' ) );
114                $mappedValues = self::getLabelsForTitles( $values, $isReverseLookup );
115                // @todo - why just array_values ?
116                break;
117        }
118        $res = ( $mappedValues !== null ) ? self::disambiguateLabels( $mappedValues ) : $values;
119        return $res;
120    }
121
122    /**
123     * Helper function to get a named array of labels from
124     * an indexed array of values given a mapping property.
125     * Originally in PF_FormField
126     * @param array $values
127     * @param string $propertyName
128     * @return array
129     */
130    public static function getValuesWithMappingProperty(
131        array $values,
132        string $propertyName
133    ): array {
134        $store = PFUtils::getSMWStore();
135        if ( $store == null || empty( $values ) ) {
136            return [];
137        }
138        $res = [];
139        foreach ( $values as $index => $value ) {
140            // @todo - does this make sense?
141            // if ( $useDisplayTitle ) {
142            // $value = $index;
143            // }
144            $subject = Title::newFromText( $value );
145            if ( $subject != null ) {
146                $vals = PFValuesUtils::getSMWPropertyValues( $store, $subject, $propertyName );
147                if ( count( $vals ) > 0 ) {
148                    $res[$value] = trim( $vals[0] );
149                } else {
150                    // @todo - make this optional
151                    $label = self::removeNSPrefixFromLabel( trim( $value ) );
152                    $res[$value] = $label;
153                }
154            } else {
155                $res[$value] = $value;
156            }
157        }
158        return $res;
159    }
160
161    /**
162     * Helper function to get an array of labels from an array of values
163     * given a mapping template.
164     * @todo remove $useDisplayTitle?
165     * @param array $values
166     * @param string $mappingTemplate
167     * @param bool $useDisplayTitle
168     * @return array
169     */
170    public static function getValuesWithMappingTemplate(
171        array $values,
172        string $mappingTemplate,
173        bool $useDisplayTitle = false
174    ): array {
175        $title = Title::makeTitleSafe( NS_TEMPLATE, $mappingTemplate );
176        $templateExists = $title->exists();
177        $res = [];
178
179        if ( $templateExists ) {
180            $parser = PFUtils::getParser();
181            // If the form employs mapping templates to translate values to labels, PFMappingUtils
182            // fundamentally relies on the parser to render them. Since we might be outside a standard
183            // page rendering context (e.g. pfautocomplete API), we explicitly initialize an external
184            // parse sequence so that the template parsing succeeds without crashing.
185            if ( $parser->getOptions() === null ) {
186                wfDebugLog( 'PageForms', 'Parser uninitialized before mapping template. Initializing now.' );
187                $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
188                $parser->startExternalParse( Title::newMainPage(), $parserOptions, Parser::OT_HTML, true );
189            }
190        }
191
192        foreach ( $values as $index => $value ) {
193            // if ( $useDisplayTitle ) {
194            // $value = $index;
195            // }
196            if ( $templateExists ) {
197                $label = trim( $parser->recursiveTagParse( '{{' . $mappingTemplate .
198                    '|' . $value . '}}' ) );
199                if ( $label == '' ) {
200                    $res[$value] = $value;
201                } else {
202                    $res[$value] = $label;
203                }
204            } else {
205                $res[$value] = $value;
206            }
207        }
208        return $res;
209    }
210
211    /**
212     * Helper function to get an array of labels from an array of values
213     * given a mapping Cargo table/field.
214     * Derived from PFFormField::setValuesWithMappingCargoField
215     * @todo does $useDisplayTitle make sense here?
216     * @todo see if check for $mappingCargoValueField works
217     * @param array $values
218     * @param string|null $mappingCargoField
219     * @param string|null $mappingCargoValueField
220     * @param string|null $mappingCargoTable
221     * @param bool $useDisplayTitle
222     * @return array
223     */
224    public static function getValuesWithMappingCargoField(
225        $values,
226        $mappingCargoField,
227        $mappingCargoValueField,
228        $mappingCargoTable,
229        bool $useDisplayTitle = false
230    ) {
231        $labels = [];
232        foreach ( $values as $index => $value ) {
233            if ( $useDisplayTitle ) {
234                $value = $index;
235            }
236            $labels[$value] = $value;
237            // Check if this works
238            if ( $mappingCargoValueField !== null ) {
239                $valueField = $mappingCargoValueField;
240            } else {
241                $valueField = '_pageName';
242            }
243            $vals = PFValuesUtils::getValuesForCargoField(
244                $mappingCargoTable,
245                $mappingCargoField,
246                $valueField . '="' . $value . '"'
247            );
248            if ( count( $vals ) > 0 && trim( $vals[0] ) != '' ) {
249                $labels[$value] = html_entity_decode( trim( $vals[0] ) );
250            }
251        }
252        return $labels;
253    }
254
255    /**
256     * Mapping with the Translate extension
257     * @param array $values
258     * @param string $translateMapping
259     * @return array
260     */
261    public static function getValuesWithTranslateMapping(
262        array $values,
263        string $translateMapping
264    ) {
265        $res = [];
266        $parser = PFUtils::getParser();
267        if ( $parser->getOptions() === null ) {
268            $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
269            $parser->startExternalParse( Title::newMainPage(), $parserOptions, Parser::OT_HTML, true );
270        }
271
272        foreach ( $values as $key ) {
273            $res[$key] = $parser->recursiveTagParse( '{{int:' . $translateMapping . $key . '}}' );
274        }
275        return $res;
276    }
277
278    /**
279     * Get a named array of display titles
280     *
281     * @param array $values = pagenames
282     * @param bool $doReverseLookup
283     * @return array
284     */
285    public static function getLabelsForTitles(
286        array $values,
287        bool $doReverseLookup = false
288    ) {
289        $labels = [];
290        $pageNamesForValues = [];
291        $allTitles = [];
292        foreach ( $values as $k => $value ) {
293            if ( trim( $value ) === "" ) {
294                continue;
295            }
296
297            // In some (rare) cases the provided key is the actual page name, resulting
298            // in errors when saving the form (since the display title was only shown).
299            if ( is_string( $k ) ) {
300                $titleFromKey = Title::newFromText( $k );
301                if ( $titleFromKey instanceof Title && $titleFromKey->exists() ) {
302                    $allTitles[] = $titleFromKey;
303                    $pageNamesForValues[$k] = $titleFromKey->getPrefixedText();
304                    continue;
305                }
306            }
307
308            if ( $doReverseLookup ) {
309                // Before attempting to extract a page name from parentheses,
310                // check whether the entire value is itself an existing page.
311                // Pages like "Pagename (foobar)" have parentheses in their
312                // actual title; without this check the regex below would
313                // incorrectly extract "foobar" as the page name.
314                $titleDirect = Title::newFromText( $value );
315                if ( $titleDirect instanceof Title && $titleDirect->exists() ) {
316                    $pageNamesForValues[$value] = $titleDirect->getPrefixedText();
317                    $allTitles[] = $titleDirect;
318                    continue;
319                }
320
321                // The regex matches every 'real' page inside the last brackets; for example
322                //  'Privacy (doel) (Privacy (doel)concept)',
323                //  'Pagina (doel) (Pagina)',
324                // will match on (Privacy (doel)concept), (Pagina), ect
325                if ( !preg_match_all( '/\((?:[^)(]*(?R)?)*+\)/', $value, $matches ) ) {
326                    // If no matches were found, just leave the value as is
327                    continue;
328                } else {
329                    $firstMatch = reset( $matches );
330                    // The actual match is always in the last group
331                    $realPage = end( $firstMatch );
332                    // The match still contains the first ( and last ) character, remove them
333                    $realPage = substr( $realPage, 1 );
334                    // Finally set the actual value
335                    $value = substr( $realPage, 0, -1 );
336                }
337            }
338            $titleInstance = Title::newFromText( $value );
339            // If the title is invalid, just leave the value as is
340            if ( $titleInstance === null ) {
341                continue;
342            }
343            $pageNamesForValues[$value] = $titleInstance->getPrefixedText();
344            $allTitles[] = $titleInstance;
345        }
346
347        $allDisplayTitles = self::getDisplayTitles( $allTitles );
348        foreach ( $pageNamesForValues as $value => $pageName ) {
349            if ( isset( $allDisplayTitles[ $pageName ] )
350                && strtolower( $allDisplayTitles[ $pageName ] ) !== strtolower( $value )
351            ) {
352                $displayValue = sprintf( '%s (%s)', $allDisplayTitles[ $pageName ], $value );
353            } else {
354                $displayValue = $value;
355            }
356            $labels[$value] = $displayValue;
357        }
358
359        return $labels;
360    }
361
362    /**
363     * Returns pages each with their display title as the value.
364     * @param array $titlesUnfiltered
365     * @return array
366     */
367    public static function getDisplayTitles(
368        array $titlesUnfiltered
369    ) {
370        $pages = $titles = [];
371        foreach ( $titlesUnfiltered as $k => $title ) {
372            if ( $title instanceof Title ) {
373                $titles[ $k ] = $title;
374            }
375        }
376        $properties = MediaWikiServices::getInstance()->getPageProps()
377            ->getProperties( $titles, [ 'displaytitle', 'defaultsort' ] );
378        foreach ( $titles as $title ) {
379            if ( array_key_exists( $title->getArticleID(), $properties ) ) {
380                $titleprops = $properties[$title->getArticleID()];
381            } else {
382                $titleprops = [];
383            }
384            $titleText = $title->getPrefixedText();
385            if ( array_key_exists( 'displaytitle', $titleprops ) &&
386                trim( str_replace( '&#160;', '', strip_tags( $titleprops['displaytitle'] ) ) ) !== '' ) {
387                $pages[$titleText] = htmlspecialchars_decode( $titleprops['displaytitle'] );
388            } else {
389                $pages[$titleText] = $titleText;
390            }
391        }
392        return $pages;
393    }
394
395    /**
396     * Remove namespace prefix (if any) from label
397     * @param string $label
398     * @return string
399     */
400    private static function removeNSPrefixFromLabel( string $label ) {
401        $labelArr = explode( ':', trim( $label ) );
402        if ( count( $labelArr ) > 1 ) {
403            $prefix = array_shift( $labelArr );
404            $res = implode( ':', $labelArr );
405        } else {
406            $res = $label;
407        }
408        return $res;
409    }
410
411    /**
412     * Doing "mapping" on values can potentially lead to more than one
413     * value having the same "label". To avoid this, we find duplicate
414     * labels, if there are any, add on the real value, in parentheses,
415     * to all of them.
416     *
417     * @param array $labels
418     * @return array
419     */
420    public static function disambiguateLabels( array $labels ) {
421        if ( count( $labels ) == count( array_unique( $labels ) ) ) {
422            return $labels;
423        }
424        $fixed_labels = [];
425        foreach ( $labels as $value => $label ) {
426            $fixed_labels[$value] = $labels[$value];
427        }
428        $counts = array_count_values( $fixed_labels );
429        foreach ( $counts as $current_label => $count ) {
430            if ( $count > 1 ) {
431                $matching_keys = array_keys( $labels, $current_label );
432                foreach ( $matching_keys as $key ) {
433                    $fixed_labels[$key] .= ' (' . $key . ')';
434                }
435            }
436        }
437        if ( count( $fixed_labels ) == count( array_unique( $fixed_labels ) ) ) {
438            return $fixed_labels;
439        }
440        // If that didn't work, just add on " (value)" to *all* the
441        // labels. @TODO - is this necessary?
442        foreach ( $labels as $value => $label ) {
443            $labels[$value] .= ' (' . $value . ')';
444        }
445        return $labels;
446    }
447
448    /**
449     * Similar sort of concept as disambiguateLabels(), but this one has to
450     * do with display titles specifically.
451     *
452     * @param array $labels
453     * @return array
454     */
455    public static function createDisplayTitleLabels( array $labels ) {
456        foreach ( $labels as $value => $label ) {
457            if ( $label !== $value ) {
458                $labels[$value] .= ' (' . $value . ')';
459            }
460        }
461        return $labels;
462    }
463}