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