Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
86.86% |
152 / 175 |
|
46.15% |
6 / 13 |
CRAP | |
0.00% |
0 / 1 |
| PFMappingUtils | |
86.86% |
152 / 175 |
|
46.15% |
6 / 13 |
86.43 | |
0.00% |
0 / 1 |
| getMappingType | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
| getMappedValuesForInput | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
| isIndexedArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getMappedValues | |
44.00% |
11 / 25 |
|
0.00% |
0 / 1 |
27.56 | |||
| getValuesWithMappingProperty | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
6.01 | |||
| getValuesWithMappingTemplate | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
4.01 | |||
| getValuesWithMappingCargoField | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
5.01 | |||
| getValuesWithTranslateMapping | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getLabelsForTitles | |
97.14% |
34 / 35 |
|
0.00% |
0 / 1 |
14 | |||
| getDisplayTitles | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
7 | |||
| removeNSPrefixFromLabel | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
| disambiguateLabels | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 | |||
| createDisplayTitleLabels | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Methods for mapping values to labels |
| 4 | * @file |
| 5 | * @ingroup PF |
| 6 | */ |
| 7 | |
| 8 | use MediaWiki\MediaWikiServices; |
| 9 | use MediaWiki\Title\Title; |
| 10 | |
| 11 | class 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( ' ', '', 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 | } |