Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 543 |
|
0.00% |
0 / 23 |
CRAP | |
0.00% |
0 / 1 |
PFValuesUtils | |
0.00% |
0 / 543 |
|
0.00% |
0 / 23 |
26732 | |
0.00% |
0 / 1 |
getSMWPropertyValues | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
getCategoriesForPage | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
getAllCategories | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getAllValuesForProperty | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getAllValuesFromWikidata | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
56 | |||
getAllValuesForCargoField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getValuesForCargoField | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
getAllPagesForCategory | |
0.00% |
0 / 84 |
|
0.00% |
0 / 1 |
462 | |||
fixedMultiSort | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getAllPagesForConcept | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
462 | |||
getAllPagesForNamespace | |
0.00% |
0 / 84 |
|
0.00% |
0 / 1 |
600 | |||
getAutocompleteValues | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
90 | |||
getAutocompletionTypeAndSource | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
342 | |||
getRemoteDataTypeAndPossiblySetAutocompleteValues | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
156 | |||
setAutocompleteValues | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getValuesArray | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getValuesFromExternalURL | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
getSQLConditionForAutocompleteInColumn | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
getAllPagesForQuery | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
processSemanticQuery | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getMaxValuesToRetrieve | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
standardizeNamespace | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
labelToValue | |
0.00% |
0 / 4 |
|
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 | |
10 | use MediaWiki\MediaWikiServices; |
11 | |
12 | class 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 = " |
166 | SELECT DISTINCT ?valueLabel WHERE { |
167 | { |
168 | SELECT ?value WHERE { |
169 | ?value " . $attributesQuery . " . |
170 | ?value rdfs:label ?valueLabel . |
171 | FILTER(LANG(?valueLabel) = \"" . $wgLanguageCode . "\") . |
172 | FILTER(REGEX(LCASE(?valueLabel), \"\\\\b" . strtolower( $substring ) . "\")) |
173 | } "; |
174 | $maxValues = self::getMaxValuesToRetrieve( $substring ); |
175 | $sparqlQueryString .= "LIMIT " . ( $maxValues + 10 ); |
176 | $sparqlQueryString .= "} |
177 | SERVICE 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( ' ', '', 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( ' ', '', 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( ' ', '', 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:@", "<@", ">@", "@" ], |
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 | [ "<", ">", "(", ")", '%', '@' ], |
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 | } |