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