Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.46% covered (warning)
69.46%
389 / 560
62.50% covered (warning)
62.50%
15 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
PFValuesUtils
69.46% covered (warning)
69.46%
389 / 560
62.50% covered (warning)
62.50%
15 / 24
1025.15
0.00% covered (danger)
0.00%
0 / 1
 getSMWPropertyValues
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 getCategoriesForPage
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
5
 getAllCategories
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getAllValuesForProperty
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getAllValuesFromWikidata
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
7
 getAllValuesForCargoField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValuesForCargoField
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
6.97
 getAllPagesForCategory
95.29% covered (success)
95.29%
81 / 85
0.00% covered (danger)
0.00%
0 / 1
22
 fixedMultiSort
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getAllPagesForConcept
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
462
 getAllPagesForNamespace
51.14% covered (warning)
51.14%
45 / 88
0.00% covered (danger)
0.00%
0 / 1
97.92
 getAutocompleteValues
46.43% covered (danger)
46.43%
13 / 28
0.00% covered (danger)
0.00%
0 / 1
21.45
 getAutocompletionTypeAndSource
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
17
 getRemoteDataTypeAndPossiblySetAutocompleteValues
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
14
 setAutocompleteValues
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getValuesArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getValueLabelMap
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getValuesFromExternalURL
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 getSQLConditionForAutocompleteInColumn
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
6.01
 getAllPagesForQuery
16.67% covered (danger)
16.67%
3 / 18
0.00% covered (danger)
0.00%
0 / 1
8.21
 processSemanticQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getMaxValuesToRetrieve
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 standardizeNamespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 labelToValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Static functions for handling lists of values and labels.
4 *
5 * @author Yaron Koren
6 * @file
7 * @ingroup PF
8 */
9
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Title\Title;
12
13class PFValuesUtils {
14
15    /**
16     * Helper function to handle getPropertyValues().
17     *
18     * @param Store $store
19     * @param Title $subject
20     * @param string $propID
21     * @param \SMW\RequestOptions|null $requestOptions
22     * @return array
23     * @suppress PhanUndeclaredTypeParameter For Store
24     */
25    public static function getSMWPropertyValues( $store, $subject, $propID, $requestOptions = null ) {
26        // If SMW is not installed, exit out.
27        if ( !class_exists( '\SMW\DIWikiPage' ) ) {
28            return [];
29        }
30        if ( $subject === null ) {
31            $page = null;
32        } else {
33            $page = \SMW\DIWikiPage::newFromTitle( $subject );
34        }
35        $property = \SMW\DIProperty::newFromUserLabel( $propID );
36        $res = $store->getPropertyValues( $page, $property, $requestOptions );
37        $values = [];
38        foreach ( $res as $value ) {
39            if ( $value instanceof \SMWDIUri ) {
40                $values[] = $value->getURI();
41            } elseif ( $value instanceof \SMW\DIWikiPage ) {
42                $realValue = str_replace( '_', ' ', $value->getDBKey() );
43                if ( $value->getNamespace() != 0 ) {
44                    $realValue = PFUtils::getCanonicalName( $value->getNamespace() ) . ":$realValue";
45                }
46                $values[] = $realValue;
47            } else {
48                // getSortKey() seems to return the correct
49                // value for all the other data types.
50                $values[] = str_replace( '_', ' ', $value->getSortKey() );
51            }
52        }
53        return $values;
54    }
55
56    /**
57     * Helper function - gets names of categories for a page;
58     * based on Title::getParentCategories(), but simpler.
59     *
60     * @param Title $title
61     * @return array
62     */
63    public static function getCategoriesForPage( $title ) {
64        $dbr = PFUtils::getReadDB();
65
66        $categories = [];
67        $titlekey = $title->getArticleID();
68        if ( $titlekey == 0 ) {
69            // Something's wrong - exit
70            return $categories;
71        }
72
73        if ( !$dbr->fieldExists( 'categorylinks', 'cl_to', __METHOD__ ) ) {
74            // MW 1.45+
75            $res = $dbr->newSelectQueryBuilder()
76                ->select( 'lt_title' )
77                ->from( 'categorylinks' )
78                ->join( 'linktarget', null, 'cl_target_id = lt_id' )
79                ->where( [ 'cl_from' => $titlekey, 'lt_namespace' => NS_CATEGORY ] )
80                ->caller( __METHOD__ )
81                ->fetchResultSet();
82            foreach ( $res as $row ) {
83                $categories[] = $row->lt_title;
84            }
85            return $categories;
86        }
87
88        $conditions = [ 'cl_from' => $titlekey ];
89        $res = $dbr->select(
90            'categorylinks',
91            'DISTINCT cl_to',
92            $conditions,
93            __METHOD__
94        );
95        while ( $row = $res->fetchRow() ) {
96            $categories[] = $row['cl_to'];
97        }
98        $res->free();
99        return $categories;
100    }
101
102    /**
103     * Helper function - returns names of all the categories.
104     * @return array
105     */
106    public static function getAllCategories() {
107        $categories = [];
108        $db = PFUtils::getReadDB();
109        $res = $db->select(
110            'category',
111            'cat_title',
112             null,
113            __METHOD__
114        );
115        while ( $row = $res->fetchRow() ) {
116            $categories[] = $row['cat_title'];
117        }
118        $res->free();
119        return $categories;
120    }
121
122    /**
123     * This function, unlike the others, doesn't take in a substring
124     * because it uses the SMW data store, which can't perform
125     * case-insensitive queries; for queries with a substring, the
126     * function PFAutocompleteAPI::getAllValuesForProperty() exists.
127     *
128     * @param string $property_name
129     * @return array
130     */
131    public static function getAllValuesForProperty( $property_name ) {
132        $store = PFUtils::getSMWStore();
133        if ( $store == null ) {
134            return [];
135        }
136        $requestoptions = new \SMW\RequestOptions();
137        $requestoptions->limit = self::getMaxValuesToRetrieve();
138        $values = self::getSMWPropertyValues( $store, null, $property_name, $requestoptions );
139        sort( $values );
140        return $values;
141    }
142
143    /**
144     * This function is used for fetching the values from wikidata based on the provided
145     * annotations. For queries with substring, the function returns all the values which
146     * have the substring in it.
147     *
148     * @param string $query
149     * @param string|null $substring
150     * @return array
151     */
152    public static function getAllValuesFromWikidata( $query, $substring = null ) {
153        $endpointUrl = "https://query.wikidata.org/sparql";
154        global $wgLanguageCode;
155
156        $query = urldecode( $query );
157
158        $filter_strings = explode( '&', $query );
159        $filters = [];
160
161        foreach ( $filter_strings as $filter ) {
162            $temp = explode( "=", $filter );
163            $filters[ $temp[ 0 ] ] = $temp[ 1 ];
164        }
165
166        $attributesQuery = "";
167        $count = 0;
168        foreach ( $filters as $key => $val ) {
169            $attributesQuery .= "wdt:" . $key;
170            if ( is_numeric( str_replace( "Q", "", $val ) ) ) {
171                $attributesQuery .= " wd:" . $val . ";";
172            } else {
173                $attributesQuery .= "?customLabel" . $count . " .
174                ?customLabel" . $count . " rdfs:label \"" . $val . "\"@" . $wgLanguageCode . " . ";
175                $count++;
176                $attributesQuery .= "?value ";
177            }
178        }
179        unset( $count );
180        $attributesQuery = rtrim( $attributesQuery, ";" );
181        $attributesQuery = rtrim( $attributesQuery, ". ?value " );
182
183        $sparqlQueryString = "
184SELECT DISTINCT ?valueLabel WHERE {
185{
186SELECT ?value  WHERE {
187?value " . $attributesQuery . " .
188?value rdfs:label ?valueLabel .
189FILTER(LANG(?valueLabel) = \"" . $wgLanguageCode . "\") .
190FILTER(REGEX(LCASE(?valueLabel), \"\\\\b" . strtolower( $substring ?? '' ) . "\"))
191} ";
192        $maxValues = self::getMaxValuesToRetrieve( $substring );
193        $sparqlQueryString .= "LIMIT " . ( $maxValues + 10 );
194        $sparqlQueryString .= "}
195SERVICE wikibase:label { bd:serviceParam wikibase:language \"" . $wgLanguageCode . "\". }
196}";
197        $sparqlQueryString .= "LIMIT " . $maxValues;
198        $opts = [
199            'http' => [
200                'method' => 'GET',
201                'header' => [
202                    'Accept: application/sparql-results+json',
203                    'User-Agent: PageForms_API PHP/8.0'
204                ],
205            ],
206        ];
207        $context = stream_context_create( $opts );
208
209        $url = $endpointUrl . '?query=' . urlencode( $sparqlQueryString );
210        $response = file_get_contents( $url, false, $context );
211        $apiResults = json_decode( $response, true );
212        $results = [];
213        if ( $apiResults != null ) {
214            $apiResults = $apiResults[ 'results' ][ 'bindings' ];
215            foreach ( $apiResults as $result ) {
216                foreach ( $result as $key => $val ) {
217                    array_push( $results, $val[ 'value' ] );
218                }
219            }
220        }
221        return $results;
222    }
223
224    /**
225     * Used with the Cargo extension.
226     * @param string $tableName
227     * @param string $fieldName
228     * @return array
229     */
230    public static function getAllValuesForCargoField( $tableName, $fieldName ) {
231        return self::getValuesForCargoField( $tableName, $fieldName );
232    }
233
234    /**
235     * Used with the Cargo extension.
236     * @param string $tableName
237     * @param string $fieldName
238     * @param string|null $whereStr
239     * @return array
240     */
241    public static function getValuesForCargoField( $tableName, $fieldName, $whereStr = null ) {
242        global $wgPageFormsMaxLocalAutocompleteValues;
243
244        // The limit should be greater than the maximum number of local
245        // autocomplete values, so that form inputs also know whether
246        // to switch to remote autocompletion.
247        // (We increment by 10, to be on the safe side, since some values
248        // can be null, etc.)
249        $limitStr = max( 100, $wgPageFormsMaxLocalAutocompleteValues + 10 );
250
251        try {
252            $sqlQuery = CargoSQLQuery::newFromValues( $tableName, $fieldName, $whereStr, $joinOnStr = null, $fieldName, $havingStr = null, $fieldName, $limitStr, $offsetStr = 0 );
253        // @phan-suppress-next-line PhanUnusedVariableCaughtException
254        } catch ( Exception $e ) {
255            return [];
256        }
257
258        $queryResults = $sqlQuery->run();
259        $values = [];
260        // Field names starting with a '_' are special fields -
261        // all other fields will have had their underscores
262        // replaced with spaces in $queryResults.
263        if ( $fieldName[0] == '_' ) {
264            $fieldAlias = $fieldName;
265        } else {
266            $fieldAlias = str_replace( '_', ' ', $fieldName );
267        }
268        foreach ( $queryResults as $row ) {
269            if ( !isset( $row[$fieldAlias] ) ) {
270                continue;
271            }
272            // Cargo HTML-encodes everything - decode the quotes and
273            // angular brackets.
274            $values[] = html_entity_decode( $row[$fieldAlias] );
275        }
276        return $values;
277    }
278
279    /**
280     * Get all the pages that belong to a category and all its
281     * subcategories, down a certain number of levels - heavily based on
282     * SMW's SMWInlineQuery::includeSubcategories().
283     *
284     * @param string $top_category
285     * @param int $num_levels
286     * @param string|null $substring
287     * @return string[]
288     */
289    public static function getAllPagesForCategory( $top_category, $num_levels, $substring = null ) {
290        if ( $num_levels == 0 ) {
291            return [ $top_category ];
292        }
293        global $wgPageFormsUseDisplayTitle;
294
295        $db = PFUtils::getReadDB();
296        $top_category = str_replace( ' ', '_', $top_category );
297        $categories = [ $top_category ];
298        $checkcategories = [ $top_category ];
299        $pages = [];
300        $sortkeys = [];
301        for ( $level = $num_levels; $level > 0; $level-- ) {
302            $newcategories = [];
303            foreach ( $checkcategories as $category ) {
304                $tables = [ 'categorylinks', 'page' ];
305                $columns = [ 'page_title', 'page_namespace' ];
306                $conditions = [];
307                $join = [];
308                $join['categorylinks'] = [ 'JOIN', 'cl_from = page_id' ];
309                if ( $db->fieldExists( 'categorylinks', 'cl_to', __METHOD__ ) ) {
310                    $conditions['cl_to'] = $category;
311                } else {
312                    // MW 1.45+
313                    $tables[] = 'linktarget';
314                    $join['linktarget'] = [ 'JOIN', 'cl_target_id = lt_id' ];
315                    $conditions['lt_title'] = $category;
316                }
317                if ( $wgPageFormsUseDisplayTitle ) {
318                    $tables['pp_displaytitle'] = 'page_props';
319                    $tables['pp_defaultsort'] = 'page_props';
320                    $columns['pp_displaytitle_value'] = 'pp_displaytitle.pp_value';
321                    $columns['pp_defaultsort_value'] = 'pp_defaultsort.pp_value';
322                    $join['pp_displaytitle'] = [
323                        'LEFT JOIN', [
324                            'pp_displaytitle.pp_page = page_id',
325                            'pp_displaytitle.pp_propname = \'displaytitle\''
326                        ]
327                    ];
328                    $join['pp_defaultsort'] = [
329                        'LEFT JOIN', [
330                            'pp_defaultsort.pp_page = page_id',
331                            'pp_defaultsort.pp_propname = \'defaultsort\''
332                        ]
333                    ];
334                    if ( $substring != null ) {
335                        $conditions[] = '((pp_displaytitle.pp_value IS NULL OR pp_displaytitle.pp_value = \'\') AND (' .
336                            self::getSQLConditionForAutocompleteInColumn( 'page_title', $substring ) .
337                            ')) OR ' .
338                            self::getSQLConditionForAutocompleteInColumn( 'pp_displaytitle.pp_value', $substring, false ) .
339                            ' OR page_namespace = ' . NS_CATEGORY;
340                    }
341                } else {
342                    if ( $substring != null ) {
343                        $conditions[] = self::getSQLConditionForAutocompleteInColumn( 'page_title', $substring ) . ' OR page_namespace = ' . NS_CATEGORY;
344                    }
345                }
346                // Make the query.
347                $res = $db->select(
348                    $tables,
349                    $columns,
350                    $conditions,
351                    __METHOD__,
352                    $options = [
353                        'ORDER BY' => 'cl_type, cl_sortkey',
354                        'LIMIT' => self::getMaxValuesToRetrieve( $substring )
355                    ],
356                    $join );
357                if ( $res ) {
358                    while ( $res && $row = $res->fetchRow() ) {
359                        if ( !array_key_exists( 'page_title', $row ) ) {
360                            continue;
361                        }
362                        $page_namespace = $row['page_namespace'];
363                        $page_name = $row[ 'page_title' ];
364                        if ( $page_namespace == NS_CATEGORY ) {
365                            if ( !in_array( $page_name, $categories ) ) {
366                                $newcategories[] = $page_name;
367                            }
368                        } else {
369                            $cur_title = Title::makeTitleSafe( $page_namespace, $page_name );
370                            if ( $cur_title === null ) {
371                                // This can happen if it's
372                                // a "phantom" page, in a
373                                // namespace that no longer exists.
374                                continue;
375                            }
376                            $cur_value = $cur_title->getPrefixedText();
377                            if ( !in_array( $cur_value, $pages ) ) {
378                                if ( array_key_exists( 'pp_displaytitle_value', $row ) &&
379                                    ( $row[ 'pp_displaytitle_value' ] ) !== null &&
380                                    trim( str_replace( '&#160;', '', strip_tags( $row[ 'pp_displaytitle_value' ] ) ) ) !== '' ) {
381                                    $pages[ $cur_value . '@' ] = htmlspecialchars_decode( $row[ 'pp_displaytitle_value'] );
382                                } else {
383                                    $pages[ $cur_value . '@' ] = $cur_value;
384                                }
385                                if ( array_key_exists( 'pp_defaultsort_value', $row ) &&
386                                    ( $row[ 'pp_defaultsort_value' ] ) !== null ) {
387                                    $sortkeys[ $cur_value ] = $row[ 'pp_defaultsort_value'];
388                                } else {
389                                    $sortkeys[ $cur_value ] = $cur_value;
390                                }
391                            }
392                        }
393                    }
394                    $res->free();
395                }
396            }
397            if ( count( $newcategories ) == 0 ) {
398                return self::fixedMultiSort( $sortkeys, $pages );
399            } else {
400                $categories = array_merge( $categories, $newcategories );
401            }
402            $checkcategories = array_diff( $newcategories, [] );
403        }
404        return self::fixedMultiSort( $sortkeys, $pages );
405    }
406
407    /**
408     * array_multisort() unfortunately messes up array keys that are
409     * numeric - they get converted to 0, 1, etc. There are a few ways to
410     * get around this, but I (Yaron) couldn't get those working, so
411     * instead we're going with this hack, where all key values get
412     * appended with a '@' before sorting, which is then removed after
413     * sorting. It's inefficient, but it's probably good enough.
414     *
415     * @param string[] $sortkeys
416     * @param string[] $pages
417     * @return string[] a sorted version of $pages, sorted via $sortkeys
418     */
419    static function fixedMultiSort( $sortkeys, $pages ) {
420        array_multisort( $sortkeys, $pages );
421        $newPages = [];
422        foreach ( $pages as $key => $value ) {
423            $fixedKey = rtrim( $key, '@' );
424            $newPages[$fixedKey] = $value;
425        }
426        return $newPages;
427    }
428
429    /**
430     * @param string $conceptName
431     * @param string|null $substring
432     * @return string[]
433     */
434    public static function getAllPagesForConcept( $conceptName, $substring = null ) {
435        global $wgPageFormsAutocompleteOnAllChars;
436
437        $store = PFUtils::getSMWStore();
438        if ( $store == null ) {
439            return [];
440        }
441
442        $conceptTitle = Title::makeTitleSafe( SMW_NS_CONCEPT, $conceptName );
443
444        if ( $substring !== null ) {
445            $substring = strtolower( $substring );
446        }
447
448        // Escape if there's no such concept.
449        if ( $conceptTitle == null || !$conceptTitle->exists() ) {
450            throw new MWException( wfMessage( 'pf-missingconcept', wfEscapeWikiText( $conceptName ) ) );
451        }
452
453        global $wgPageFormsUseDisplayTitle;
454        $conceptDI = \SMW\DIWikiPage::newFromTitle( $conceptTitle );
455        $desc = new \SMW\Query\Language\ConceptDescription( $conceptDI );
456        $printout = new \SMW\Query\PrintRequest( \SMW\Query\PrintRequest::PRINT_THIS, "" );
457        $desc->addPrintRequest( $printout );
458        $query = new SMWQuery( $desc );
459        $query_result = $store->getQueryResult( $query );
460        $pages = [];
461        $sortkeys = [];
462        $titles = [];
463        while ( $res = $query_result->getNext() ) {
464            $page = $res[0]->getNextText( SMW_OUTPUT_WIKI );
465            if ( $wgPageFormsUseDisplayTitle ) {
466                $title = Title::newFromText( $page );
467                if ( $title !== null ) {
468                    $titles[] = $title;
469                }
470            } else {
471                $pages[$page] = $page;
472                $sortkeys[$page] = $page;
473            }
474        }
475
476        if ( $wgPageFormsUseDisplayTitle ) {
477            $properties = MediaWikiServices::getInstance()->getPageProps()->getProperties(
478                $titles, [ 'displaytitle', 'defaultsort' ]
479            );
480            foreach ( $titles as $title ) {
481                if ( array_key_exists( $title->getArticleID(), $properties ) ) {
482                    $titleprops = $properties[$title->getArticleID()];
483                } else {
484                    $titleprops = [];
485                }
486
487                $titleText = $title->getPrefixedText();
488                if ( array_key_exists( 'displaytitle', $titleprops ) &&
489                    trim( str_replace( '&#160;', '', strip_tags( $titleprops['displaytitle'] ) ) ) !== '' ) {
490                    $pages[$titleText] = htmlspecialchars_decode( $titleprops['displaytitle'] );
491                } else {
492                    $pages[$titleText] = $titleText;
493                }
494                if ( array_key_exists( 'defaultsort', $titleprops ) ) {
495                    $sortkeys[$titleText] = $titleprops['defaultsort'];
496                } else {
497                    $sortkeys[$titleText] = $titleText;
498                }
499            }
500        }
501
502        if ( $substring !== null ) {
503            $filtered_pages = [];
504            $filtered_sortkeys = [];
505            foreach ( $pages as $index => $pageName ) {
506                // Filter on the substring manually. It would
507                // be better to do this filtering in the
508                // original SMW query, but that doesn't seem
509                // possible yet.
510                // @TODO - this will miss a lot of results for
511                // concepts with > 1000 pages. Instead, this
512                // code should loop through all the pages,
513                // using "offset".
514                $lowercasePageName = strtolower( $pageName );
515                $position = strpos( $lowercasePageName, $substring );
516                if ( $position !== false ) {
517                    if ( $wgPageFormsAutocompleteOnAllChars ) {
518                        if ( $position >= 0 ) {
519                            $filtered_pages[$index] = $pageName;
520                            $filtered_sortkeys[$index] = $sortkeys[$index];
521                        }
522                    } else {
523                        if ( $position === 0 ||
524                            strpos( $lowercasePageName, ' ' . $substring ) > 0 ) {
525                            $filtered_pages[$index] = $pageName;
526                            $filtered_sortkeys[$index] = $sortkeys[$index];
527                        }
528                    }
529                }
530            }
531            $pages = $filtered_pages;
532            $sortkeys = $filtered_sortkeys;
533        }
534        array_multisort( $sortkeys, $pages );
535        // Set the "limit" here.
536        $pages = array_slice( $pages, 0, self::getMaxValuesToRetrieve( $substring ), true );
537
538        return $pages;
539    }
540
541    public static function getAllPagesForNamespace( $namespaceStr, $substring = null ) {
542        global $wgLanguageCode, $wgPageFormsUseDisplayTitle;
543
544        if ( $namespaceStr === '_contentNamespaces' ) {
545            global $wgContentNamespaces;
546            $queriedNamespaces = $wgContentNamespaces;
547            $namespaceConditions = array_map( static function ( $ns ) {
548                return "page_namespace = $ns";
549            }, $queriedNamespaces );
550            $namespaceNames = [];
551        } else {
552            $namespaceNames = explode( ',', $namespaceStr );
553            $queriedNamespaces = [];
554            $namespaceConditions = [];
555
556        }
557
558        $allNamespaces = PFUtils::getContLang()->getNamespaces();
559
560        if ( $wgLanguageCode !== 'en' ) {
561            $englishLang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
562            $allEnglishNamespaces = $englishLang->getNamespaces();
563        }
564
565        foreach ( $namespaceNames as $namespace_name ) {
566            $namespace_name = self::standardizeNamespace( $namespace_name );
567            // Cycle through all the namespace names for this language, and
568            // if one matches the namespace specified in the form, get the
569            // names of all the pages in that namespace.
570
571            // Switch to blank for the string 'Main'.
572            if ( $namespace_name == 'Main' || $namespace_name == 'main' ) {
573                $namespace_name = '';
574            }
575            $matchingNamespaceCode = null;
576            foreach ( $allNamespaces as $curNSCode => $curNSName ) {
577                if ( $curNSName == $namespace_name ) {
578                    $matchingNamespaceCode = $curNSCode;
579                }
580            }
581
582            // If that didn't find anything, and we're in a language
583            // other than English, check English as well.
584            if ( $matchingNamespaceCode === null && $wgLanguageCode != 'en' ) {
585                foreach ( $allEnglishNamespaces as $curNSCode => $curNSName ) {
586                    if ( $curNSName == $namespace_name ) {
587                        $matchingNamespaceCode = $curNSCode;
588                    }
589                }
590            }
591
592            if ( $matchingNamespaceCode === null ) {
593                throw new MWException( wfMessage( 'pf-missingnamespace', wfEscapeWikiText( $namespace_name ) ) );
594            }
595
596            $queriedNamespaces[] = $matchingNamespaceCode;
597            $namespaceConditions[] = "page_namespace = $matchingNamespaceCode";
598        }
599
600        $db = PFUtils::getReadDB();
601        $conditions = [];
602        $conditions[] = implode( ' OR ', $namespaceConditions );
603        $tables = [ 'page' ];
604        $columns = [ 'page_title' ];
605        if ( count( $namespaceConditions ) > 1 ) {
606            $columns[] = 'page_namespace';
607        }
608        if ( $wgPageFormsUseDisplayTitle ) {
609            $tables['pp_displaytitle'] = 'page_props';
610            $tables['pp_defaultsort'] = 'page_props';
611            $columns['pp_displaytitle_value'] = 'pp_displaytitle.pp_value';
612            $columns['pp_defaultsort_value'] = 'pp_defaultsort.pp_value';
613            $join = [
614                'pp_displaytitle' => [
615                    'LEFT JOIN', [
616                        'pp_displaytitle.pp_page = page_id',
617                        'pp_displaytitle.pp_propname = \'displaytitle\''
618                    ]
619                ],
620                'pp_defaultsort' => [
621                    'LEFT JOIN', [
622                        'pp_defaultsort.pp_page = page_id',
623                        'pp_defaultsort.pp_propname = \'defaultsort\''
624                    ]
625                ]
626            ];
627            if ( $substring != null ) {
628                $substringCondition = '(pp_displaytitle.pp_value IS NULL AND (' .
629                    self::getSQLConditionForAutocompleteInColumn( 'page_title', $substring ) .
630                    ')) OR ' .
631                    self::getSQLConditionForAutocompleteInColumn( 'pp_displaytitle.pp_value', $substring, false );
632                if ( !in_array( NS_CATEGORY, $queriedNamespaces ) ) {
633                    $substringCondition .= ' OR page_namespace = ' . NS_CATEGORY;
634                }
635                $conditions[] = $substringCondition;
636            }
637        } else {
638            $join = [];
639            if ( $substring != null ) {
640                $conditions[] = self::getSQLConditionForAutocompleteInColumn( 'page_title', $substring );
641            }
642        }
643        $options = [
644            'LIMIT' => self::getMaxValuesToRetrieve( $substring )
645        ];
646        $res = $db->select( $tables, $columns, $conditions, __METHOD__, $options, $join );
647
648        $pages = [];
649        $sortkeys = [];
650        while ( $row = $res->fetchRow() ) {
651            // If there's more than one namespace, include the
652            // namespace prefix in the results - otherwise, don't.
653            if ( array_key_exists( 'page_namespace', $row ) ) {
654                $actualTitle = Title::newFromText( $row['page_title'], $row['page_namespace'] );
655                $title = $actualTitle->getPrefixedText();
656            } else {
657                $title = str_replace( '_', ' ', $row['page_title'] );
658            }
659            if ( array_key_exists( 'pp_displaytitle_value', $row ) &&
660                ( $row[ 'pp_displaytitle_value' ] ) !== null &&
661                trim( str_replace( '&#160;', '', strip_tags( $row[ 'pp_displaytitle_value' ] ) ) ) !== '' ) {
662                $pages[ $title . '@' ] = htmlspecialchars_decode( $row[ 'pp_displaytitle_value'], ENT_QUOTES );
663            } else {
664                $pages[ $title . '@' ] = $title;
665            }
666            if ( array_key_exists( 'pp_defaultsort_value', $row ) &&
667                ( $row[ 'pp_defaultsort_value' ] ) !== null ) {
668                $sortkeys[ $title ] = $row[ 'pp_defaultsort_value'];
669            } else {
670                $sortkeys[ $title ] = $title;
671            }
672        }
673        $res->free();
674
675        return self::fixedMultiSort( $sortkeys, $pages );
676    }
677
678    /**
679     * Creates an array of values that match the specified source name and
680     * type, for use by both Javascript autocompletion and comboboxes.
681     *
682     * @param string|null $source_name
683     * @param string $source_type
684     * @return string[]
685     */
686    public static function getAutocompleteValues( $source_name, $source_type ) {
687        if ( $source_name === null ) {
688            return [];
689        }
690
691        // The query depends on whether this is a Cargo field, SMW
692        // property, category, SMW concept or namespace.
693        if ( $source_type == 'cargo field' ) {
694            $arr = explode( '|', $source_name );
695            if ( count( $arr ) == 3 ) {
696                $names_array = self::getValuesForCargoField( $arr[0], $arr[1], $arr[2] );
697            } else {
698                [ $table_name, $field_name ] = explode( '|', $source_name, 2 );
699                $names_array = self::getAllValuesForCargoField( $table_name, $field_name );
700            }
701            // Remove blank/null values from the array.
702            $names_array = array_values( array_filter( $names_array ) );
703        } elseif ( $source_type == 'property' ) {
704            $names_array = self::getAllValuesForProperty( $source_name );
705        } elseif ( $source_type == 'category' ) {
706            $names_array = self::getAllPagesForCategory( $source_name, 10 );
707        } elseif ( $source_type == 'concept' ) {
708            $names_array = self::getAllPagesForConcept( $source_name );
709        } elseif ( $source_type == 'query' ) {
710            // Get rid of the "@", which is a placeholder for the substring,
711            // since there is no substring here.
712            // May not cover all possible use cases.
713            $baseQuery = str_replace(
714                [ "~*@*", "~@*", "~*@", "~@", "like:*@*", "like:@*", "like:*@", "like:@", "&lt;@", "&gt;@", "@" ],
715                "+",
716                $source_name
717            );
718            $smwQuery = self::processSemanticQuery( $baseQuery );
719            $names_array = self::getAllPagesForQuery( $smwQuery );
720        } elseif ( $source_type == 'wikidata' ) {
721            $names_array = self::getAllValuesFromWikidata( $source_name );
722            sort( $names_array );
723        } else {
724            // i.e., $source_type == 'namespace'
725            $names_array = self::getAllPagesForNamespace( $source_name );
726        }
727        return $names_array;
728    }
729
730    public static function getAutocompletionTypeAndSource( &$field_args ) {
731        global $wgCapitalLinks;
732
733        if ( array_key_exists( 'values from property', $field_args ) ) {
734            $autocompletionSource = $field_args['values from property'];
735            $autocompleteFieldType = 'property';
736        } elseif ( array_key_exists( 'values from category', $field_args ) ) {
737            $autocompleteFieldType = 'category';
738            $autocompletionSource = $field_args['values from category'];
739        } elseif ( array_key_exists( 'values from concept', $field_args ) ) {
740            $autocompleteFieldType = 'concept';
741            $autocompletionSource = $field_args['values from concept'];
742        } elseif ( array_key_exists( 'values from namespace', $field_args ) ) {
743            $autocompleteFieldType = 'namespace';
744            $autocompletionSource = $field_args['values from namespace'];
745        } elseif ( array_key_exists( 'values from content namespaces', $field_args ) ) {
746            $autocompleteFieldType = 'namespace';
747            $autocompletionSource = "_contentNamespaces";
748        } elseif ( array_key_exists( 'values from url', $field_args ) ) {
749            $autocompleteFieldType = 'external_url';
750            $autocompletionSource = $field_args['values from url'];
751        } elseif ( array_key_exists( 'values from wikidata', $field_args ) ) {
752            $autocompleteFieldType = 'wikidata';
753            $autocompletionSource = $field_args['values from wikidata'];
754        } elseif ( array_key_exists( 'values from query', $field_args ) ) {
755            $autocompletionSource = $field_args['values from query'];
756            $autocompleteFieldType = 'semantic_query';
757        } elseif ( array_key_exists( 'values', $field_args ) ) {
758            global $wgPageFormsFieldNum;
759            $autocompleteFieldType = 'values';
760            $autocompletionSource = "values-$wgPageFormsFieldNum";
761        } elseif ( array_key_exists( 'autocomplete field type', $field_args ) ) {
762            $autocompleteFieldType = $field_args['autocomplete field type'];
763            $autocompletionSource = $field_args['autocompletion source'];
764        } elseif ( array_key_exists( 'full_cargo_field', $field_args ) ) {
765            $autocompletionSource = $field_args['full_cargo_field'];
766            $autocompleteFieldType = 'cargo field';
767        } elseif ( array_key_exists( 'cargo field', $field_args ) ) {
768            $fieldName = $field_args['cargo field'];
769            $tableName = $field_args['cargo table'];
770            $autocompletionSource = "$tableName|$fieldName";
771            $autocompleteFieldType = 'cargo field';
772            if ( array_key_exists( 'cargo where', $field_args ) ) {
773                $whereStr = $field_args['cargo where'];
774                $autocompletionSource .= "|$whereStr";
775            }
776        } elseif ( array_key_exists( 'semantic_property', $field_args ) ) {
777            $autocompletionSource = $field_args['semantic_property'];
778            $autocompleteFieldType = 'property';
779        } else {
780            $autocompleteFieldType = null;
781            $autocompletionSource = null;
782        }
783
784        if ( $wgCapitalLinks && in_array( $autocompleteFieldType, [ 'category', 'concept', 'namespace', 'property' ] ) ) {
785            $autocompletionSource = PFUtils::getContLang()->ucfirst( $autocompletionSource );
786        }
787
788        return [ $autocompleteFieldType, $autocompletionSource ];
789    }
790
791    public static function getRemoteDataTypeAndPossiblySetAutocompleteValues( $autocompleteFieldType, $autocompletionSource, $field_args, $autocompleteSettings ) {
792        global $wgPageFormsMaxLocalAutocompleteValues, $wgPageFormsAutocompleteValues;
793
794        if ( $autocompleteFieldType == 'external_url'
795            || $autocompleteFieldType == 'wikidata'
796            || ( array_key_exists( 'reverselookup', $field_args ) && $autocompleteFieldType !== 'values' )
797        ) {
798            // Autocompletion from URL is always done remotely.
799            return $autocompleteFieldType;
800        }
801        if ( $autocompletionSource == '' ) {
802            // No autocompletion.
803            return null;
804        }
805        // @TODO - that empty() check shouldn't be necessary.
806        if ( array_key_exists( 'possible_values', $field_args ) &&
807        !empty( $field_args['possible_values'] ) ) {
808            $autocompleteValues = $field_args['possible_values'];
809        } elseif ( $autocompleteFieldType == 'values' ) {
810            $autocompleteValues = explode( ',', $field_args['values'] );
811        } else {
812            $autocompleteValues = self::getAutocompleteValues( $autocompletionSource, $autocompleteFieldType );
813        }
814
815        if ( count( $autocompleteValues ) > $wgPageFormsMaxLocalAutocompleteValues &&
816            $autocompleteFieldType != 'values' &&
817            !array_key_exists( 'values dependent on', $field_args ) &&
818            !array_key_exists( 'mapping template', $field_args ) &&
819            !array_key_exists( 'mapping property', $field_args )
820        ) {
821            return $autocompleteFieldType;
822        } else {
823            $wgPageFormsAutocompleteValues[$autocompleteSettings] = $autocompleteValues;
824            return null;
825        }
826    }
827
828    /**
829     * Get all autocomplete-related values, plus delimiter value
830     * (it's needed also for the 'uploadable' link, if there is one).
831     *
832     * @param array $field_args
833     * @param bool $is_list
834     * @return string[]
835     */
836    public static function setAutocompleteValues( $field_args, $is_list ) {
837        [ $autocompleteFieldType, $autocompletionSource ] =
838            self::getAutocompletionTypeAndSource( $field_args );
839        $autocompleteSettings = $autocompletionSource;
840        if ( $is_list ) {
841            $autocompleteSettings .= ',list';
842            if ( array_key_exists( 'delimiter', $field_args ) ) {
843                $delimiter = $field_args['delimiter'];
844                $autocompleteSettings .= ',' . $delimiter;
845            } else {
846                $delimiter = ',';
847            }
848        } else {
849            $delimiter = null;
850        }
851
852        $remoteDataType = self::getRemoteDataTypeAndPossiblySetAutocompleteValues( $autocompleteFieldType, $autocompletionSource, $field_args, $autocompleteSettings );
853        return [ $autocompleteSettings, $remoteDataType, $delimiter ];
854    }
855
856    /**
857     * Helper function to get an array of values out of what may be either
858     * an array or a delimited string.
859     *
860     * The values returned are always the "raw" internal values (e.g. page
861     * names), never their display labels. Mapping internal values to display
862     * labels (display titles, "mapping property", etc.) is the responsibility
863     * of the individual input, which keeps the value/label association intact
864     * - see PFValuesUtils::getValueLabelMap() and
865     * PFMappingUtils::getMappedValuesForInput(). Converting to labels here was
866     * lossy: it discarded the internal value and corrupted any value whose
867     * label contained the delimiter (e.g. a display title "Hoi, Hoi" with a
868     * "," delimiter). (T427758)
869     *
870     * @param string[]|string $value
871     * @param string $delimiter
872     * @return string[]
873     */
874    public static function getValuesArray( $value, $delimiter ) {
875        if ( is_array( $value ) ) {
876            return $value;
877        } elseif ( $value == null ) {
878            return [];
879        } else {
880            return array_map( 'trim', explode( $delimiter, $value ) );
881        }
882    }
883
884    /**
885     * Normalize a field's "possible_values" into an associative
886     * [ internalValue => displayLabel ] map.
887     *
888     * Mapped fields (display titles, "mapping property"/"mapping template"/
889     * Cargo mapping) already provide an associative [ pageName => label ]
890     * array. Unmapped fields provide an indexed list of values, with labels
891     * supplied separately via "value_labels". This collapses both shapes into
892     * a single representation so inputs can render value=internal, text=label
893     * consistently. (T427758)
894     *
895     * @param array $possibleValues
896     * @param array|null $valueLabels
897     * @return array
898     */
899    public static function getValueLabelMap( array $possibleValues, ?array $valueLabels = null ) {
900        $map = [];
901        foreach ( $possibleValues as $key => $value ) {
902            if ( is_string( $key ) ) {
903                // Associative: already [ internalValue => label ].
904                $map[$key] = $value;
905            } else {
906                // Indexed: $value is the internal value; look up its label.
907                $map[$value] = ( $valueLabels !== null && array_key_exists( $value, $valueLabels ) )
908                    ? $valueLabels[$value]
909                    : $value;
910            }
911        }
912        return $map;
913    }
914
915    public static function getValuesFromExternalURL( $external_url_alias, $substring ) {
916        global $wgPageFormsAutocompletionURLs;
917        if ( empty( $wgPageFormsAutocompletionURLs ) ) {
918            return wfMessage( 'pf-nocompletionurls' );
919        }
920        if ( !array_key_exists( $external_url_alias, $wgPageFormsAutocompletionURLs ) ) {
921            return wfMessage( 'pf-invalidexturl' );
922        }
923        $url = $wgPageFormsAutocompletionURLs[$external_url_alias];
924        if ( empty( $url ) ) {
925            return wfMessage( 'pf-blankexturl' );
926        }
927        $url = str_replace( '<substr>', urlencode( $substring ), $url );
928        $page_contents = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( $url, [], __METHOD__ );
929        if ( empty( $page_contents ) ) {
930            return wfMessage( 'pf-externalpageempty' );
931        }
932        $data = json_decode( $page_contents );
933        if ( empty( $data ) ) {
934            return wfMessage( 'pf-externalpagebadjson' );
935        }
936        $return_values = [];
937        foreach ( $data->pfautocomplete as $val ) {
938            $return_values[$val->title] = $val->displaytitle ?? $val->title;
939        }
940        return $return_values;
941    }
942
943    /**
944     * Returns a SQL condition for autocompletion substring value in a column.
945     *
946     * @param string $column Value column name
947     * @param string $substring Substring to look for
948     * @param bool $replaceSpaces
949     * @return string SQL condition for use in WHERE clause
950     */
951    public static function getSQLConditionForAutocompleteInColumn( $column, $substring, $replaceSpaces = true ) {
952        global $wgPageFormsAutocompleteOnAllChars;
953
954        $db = PFUtils::getReadDB();
955
956        // CONVERT() is also supported in PostgreSQL, but it doesn't
957        // seem to work the same way.
958        if ( $db->getType() == 'mysql' ) {
959            $column_value = "LOWER(CONVERT($column USING utf8))";
960        } else {
961            $column_value = "LOWER($column)";
962        }
963
964        $substring = strtolower( $substring );
965        if ( $replaceSpaces ) {
966            $substring = str_replace( ' ', '_', $substring );
967        }
968
969        if ( $wgPageFormsAutocompleteOnAllChars ) {
970            return $column_value . $db->buildLike( $db->anyString(), $substring, $db->anyString() );
971        } else {
972            $sqlCond = $column_value . $db->buildLike( $substring, $db->anyString() );
973            $spaceRepresentation = $replaceSpaces ? '_' : ' ';
974            $wordSeparators = [ $spaceRepresentation, '/', '(', ')', '-', '\'', '\"' ];
975            foreach ( $wordSeparators as $wordSeparator ) {
976                $sqlCond .= ' OR ' . $column_value .
977                    $db->buildLike( $db->anyString(), $wordSeparator . $substring, $db->anyString() );
978            }
979            return $sqlCond;
980        }
981    }
982
983    /**
984     * Returns an array of the names of pages that are the result of an SMW query.
985     *
986     * @param string $rawQuery the query string like [[Category:Trees]][[age::>1000]]
987     * @return array
988     */
989    public static function getAllPagesForQuery( $rawQuery ) {
990        global $wgPageFormsMaxAutocompleteValues;
991        global $wgPageFormsUseDisplayTitle;
992
993        $rawQuery .= "|named args=yes|link=none|limit=$wgPageFormsMaxAutocompleteValues|searchlabel=";
994        $rawQueryArray = explode( "|", $rawQuery );
995        [ $queryString, $processedParams, $printouts ] = SMWQueryProcessor::getComponentsFromFunctionParams( $rawQueryArray, false );
996        SMWQueryProcessor::addThisPrintout( $printouts, $processedParams );
997        $processedParams = SMWQueryProcessor::getProcessedParams( $processedParams, $printouts );
998
999        // Run query and get results.
1000        $queryObj = SMWQueryProcessor::createQuery( $queryString,
1001            $processedParams,
1002            SMWQueryProcessor::SPECIAL_PAGE, '', $printouts );
1003        $res = PFUtils::getSMWStore()->getQueryResult( $queryObj );
1004        $rows = $res->getResults();
1005        $titles = [];
1006        $pages = [];
1007
1008        foreach ( $rows as $diWikiPage ) {
1009            $pages[] = $diWikiPage->getDbKey();
1010            $titles[] = $diWikiPage->getTitle();
1011        }
1012
1013        if ( $wgPageFormsUseDisplayTitle ) {
1014            $pages = PFMappingUtils::getDisplayTitles( $titles );
1015        }
1016
1017        return $pages;
1018    }
1019
1020    public static function processSemanticQuery( $query, $substr = '' ) {
1021        $query = str_replace(
1022            [ "&lt;", "&gt;", "(", ")", '%', '@' ],
1023            [ "<", ">", "[", "]", '|', $substr ],
1024            $query
1025        );
1026        return $query;
1027    }
1028
1029    public static function getMaxValuesToRetrieve( $substring = null ) {
1030        // $wgPageFormsMaxAutocompleteValues is currently misnamed,
1031        // or mis-used - it's actually used for those cases where
1032        // autocomplete *isn't* used, i.e. to populate a radiobutton
1033        // input, where it makes sense to have a very large limit
1034        // (current value: 1,000). For actual autocompletion, though,
1035        // with a substring, a limit like 20 makes more sense. It
1036        // would be good use the variable for this purpose instead,
1037        // with a default like 20, and then create a new global
1038        // variable, like $wgPageFormsMaxNonAutocompleteValues, to
1039        // hold the much larger number.
1040        if ( $substring == null ) {
1041            global $wgPageFormsMaxAutocompleteValues;
1042            return $wgPageFormsMaxAutocompleteValues;
1043        } else {
1044            return 20;
1045        }
1046    }
1047
1048    /**
1049     * Get the exact canonical namespace string, given a user-created string
1050     *
1051     * @param string $namespaceStr
1052     * @return string
1053     */
1054    public static function standardizeNamespace( $namespaceStr ) {
1055        $dummyTitle = Title::newFromText( "$namespaceStr:ABC" );
1056        return $dummyTitle ? $dummyTitle->getNsText() : $namespaceStr;
1057    }
1058
1059    /**
1060     * Map a label back to a value.
1061     * @param string $label
1062     * @param array $values
1063     * @return string
1064     */
1065    public static function labelToValue( $label, $values ) {
1066        $value = array_search( $label, $values );
1067        if ( $value === false ) {
1068            return $label;
1069        } else {
1070            return $value;
1071        }
1072    }
1073}