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