Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoQueryDisplayer
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 10
7656
0.00% covered (danger)
0.00%
0 / 1
 newFromSQLQuery
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAllFormatClasses
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFormatClass
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getFormatter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFormattedQueryResults
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
870
 formatFieldValue
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
462
 formatDateFieldValue
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 getTextSnippet
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 displayQueryResults
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 viewMoreResultsLink
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * CargoQueryDisplayer - class for displaying query results.
7 *
8 * @author Yaron Koren
9 * @ingroup Cargo
10 */
11
12class CargoQueryDisplayer {
13
14    public $mSQLQuery;
15    public $mFormat;
16    public $mDisplayParams = [];
17    public $mParser = null;
18    public $mFieldDescriptions;
19    public $mFieldTables;
20
21    public static function newFromSQLQuery( $sqlQuery ) {
22        $cqd = new CargoQueryDisplayer();
23        $cqd->mSQLQuery = $sqlQuery;
24        $cqd->mFieldDescriptions = $sqlQuery->mFieldDescriptions;
25        $cqd->mFieldTables = $sqlQuery->mFieldTables;
26        return $cqd;
27    }
28
29    public static function getAllFormatClasses() {
30        $formatClasses = [
31            'list' => 'CargoListFormat',
32            'ul' => 'CargoULFormat',
33            'ol' => 'CargoOLFormat',
34            'template' => 'CargoTemplateFormat',
35            'embedded' => 'CargoEmbeddedFormat',
36            'csv' => 'CargoCSVFormat',
37            'excel' => 'CargoExcelFormat',
38            'json' => 'CargoJSONFormat',
39            'outline' => 'CargoOutlineFormat',
40            'tree' => 'CargoTreeFormat',
41            'table' => 'CargoTableFormat',
42            'dynamic table' => 'CargoDynamicTableFormat',
43            'map' => 'CargoMapsFormat',
44            'googlemaps' => 'CargoGoogleMapsFormat',
45            'leaflet' => 'CargoLeafletFormat',
46            'openlayers' => 'CargoOpenLayersFormat',
47            'calendar' => 'CargoCalendarFormat',
48            'icalendar' => 'CargoICalendarFormat',
49            'timeline' => 'CargoTimelineFormat',
50            'gantt' => 'CargoGanttFormat',
51            'bpmn' => 'CargoBPMNFormat',
52            'category' => 'CargoCategoryFormat',
53            'bar chart' => 'CargoBarChartFormat',
54            'pie chart' => 'CargoPieChartFormat',
55            'gallery' => 'CargoGalleryFormat',
56            'slideshow' => 'CargoSlideshowFormat',
57            'tag cloud' => 'CargoTagCloudFormat',
58            'exhibit' => 'CargoExhibitFormat',
59            'bibtex' => 'CargoBibtexFormat',
60            'zip' => 'CargoZipFormat'
61        ];
62
63        // Let other extensions add their own formats - or even
64        // remove formats, if they want to.
65        Hooks::run( 'CargoSetFormatClasses', [ &$formatClasses ] );
66
67        return $formatClasses;
68    }
69
70    /**
71     * Given a format name, and a list of the fields, returns the name
72     * of the function to call for that format.
73     */
74    public function getFormatClass() {
75        $formatClasses = self::getAllFormatClasses();
76        if ( array_key_exists( $this->mFormat, $formatClasses ) ) {
77            return $formatClasses[$this->mFormat];
78        }
79
80        if ( count( $this->mFieldDescriptions ) > 1 ) {
81            $format = 'table';
82        } else {
83            $format = 'list';
84        }
85        return $formatClasses[$format];
86    }
87
88    public function getFormatter( $out, $parser = null ) {
89        $formatClass = $this->getFormatClass();
90        $formatObject = new $formatClass( $out, $parser );
91        return $formatObject;
92    }
93
94    public function getFormattedQueryResults( $queryResults ) {
95        // The assignment will do a copy.
96        global $wgScriptPath, $wgServer;
97        $queryResults = CargoUtils::replaceRedirectWithTarget( $queryResults, $this->mFieldDescriptions );
98        $formattedQueryResults = $queryResults;
99        foreach ( $queryResults as $rowNum => $row ) {
100            foreach ( $row as $fieldName => $value ) {
101                if ( $value === null || trim( $value ) === '' ) {
102                    continue;
103                }
104
105                if ( !array_key_exists( $fieldName, $this->mFieldDescriptions ) ) {
106                    continue;
107                }
108
109                $fieldDescription = $this->mFieldDescriptions[$fieldName];
110                if ( is_array( $this->mFieldTables ) && array_key_exists( $fieldName, $this->mFieldTables ) ) {
111                    $fieldTableName = $this->mFieldTables[$fieldName];
112                }
113                $fieldType = $fieldDescription->mType;
114
115                $text = '';
116                if ( $fieldDescription->mIsList ) {
117                    // There's probably an easier way to do
118                    // this, using array_map().
119                    $delimiter = $fieldDescription->getDelimiter();
120                    // We need to decode it in case the delimiter is ;
121                    $valueDecoded = html_entity_decode( $value );
122                    $fieldValues = explode( $delimiter, $valueDecoded );
123                    foreach ( $fieldValues as $i => $fieldValue ) {
124                        if ( trim( $fieldValue ) == '' ) {
125                            continue;
126                        }
127                        if ( $i > 0 ) {
128                            // Use a bullet point as
129                            // the list delimiter -
130                            // it's better than using
131                            // a comma, or the
132                            // defined delimiter,
133                            // because it's more
134                            // consistent and makes
135                            // it clearer whether
136                            // list parsing worked.
137                            $text .= ' <span class="CargoDelimiter">&bull;</span> ';
138                        }
139                        $text .= self::formatFieldValue( $fieldValue, $fieldType, $fieldDescription, $this->mParser );
140                    }
141                } elseif ( $fieldDescription->isDateOrDatetime() ) {
142                    $datePrecisionField = $fieldName . '__precision';
143                    if ( $fieldName[0] == '_' ) {
144                        // Special handling for pre-specified fields.
145                        $datePrecision = ( $fieldType == 'Datetime' ) ? CargoStore::DATE_AND_TIME : CargoStore::DATE_ONLY;
146                    } elseif ( array_key_exists( $datePrecisionField, $row ) ) {
147                        $datePrecision = $row[$datePrecisionField];
148                    } else {
149                        $fullDatePrecisionField = $fieldTableName . '.' . $datePrecisionField;
150                        if ( array_key_exists( $fullDatePrecisionField, $row ) ) {
151                            $datePrecision = $row[$fullDatePrecisionField];
152                        } else {
153                            // This should never
154                            // happen, but if it
155                            // does - let's just
156                            // give up.
157                            $datePrecision = CargoStore::DATE_ONLY;
158                        }
159                    }
160                    $text = self::formatDateFieldValue( $value, $datePrecision, $fieldType );
161                } elseif ( $fieldType == 'Boolean' ) {
162                    // Displaying a check mark for "yes"
163                    // and an x mark for "no" would be
164                    // cool, but those are apparently far
165                    // from universal symbols.
166                    $text = ( $value == true ) ? wfMessage( 'htmlform-yes' )->text() : wfMessage( 'htmlform-no' )->text();
167                } elseif ( $fieldType == 'Searchtext' && $this->mSQLQuery && array_key_exists( $fieldName, $this->mSQLQuery->mSearchTerms ) ) {
168                    $searchTerms = $this->mSQLQuery->mSearchTerms[$fieldName];
169                    $text = Html::rawElement( 'span', [ 'class' => 'searchresult' ], self::getTextSnippet( $value, $searchTerms ) );
170                } elseif ( $fieldType == 'Rating' ) {
171                    $rate = $value * 20;
172                    $url = $wgServer . $wgScriptPath . '/' . 'extensions/Cargo/resources/images/star-rating-sprite-1.png';
173                    $text = '<span style="display: block; width: 65px; height: 13px; background: url(\'' . $url . '\') 0 0;">
174                        <span style="display: block; width: ' . $rate . '%; height: 13px; background: url(\'' . $url . '\') 0 -13px;"></span>';
175                } else {
176                    $text = self::formatFieldValue( $value, $fieldType, $fieldDescription, $this->mParser );
177                }
178
179                if ( array_key_exists( 'max display chars', $this->mDisplayParams ) && ( $fieldType == 'Text' || $fieldType == 'Wikitext' ) ) {
180                    $maxDisplayChars = $this->mDisplayParams['max display chars'];
181                    if ( strlen( $text ) > $maxDisplayChars && strlen( strip_tags( $text ) ) > $maxDisplayChars ) {
182                        $text = '<span class="cargoMinimizedText">' . $text . '</span>';
183                    }
184                }
185
186                if ( $text != '' ) {
187                    $formattedQueryResults[$rowNum][$fieldName] = $text;
188                }
189            }
190        }
191        return $formattedQueryResults;
192    }
193
194    public static function formatFieldValue( $value, $type, $fieldDescription, $parser ) {
195        if ( $type == 'Integer' ) {
196            global $wgCargoDecimalMark, $wgCargoDigitGroupingCharacter;
197            return number_format( $value, 0, $wgCargoDecimalMark, $wgCargoDigitGroupingCharacter );
198        } elseif ( $type == 'Float' ) {
199            global $wgCargoDecimalMark, $wgCargoDigitGroupingCharacter;
200            // Can we assume that the decimal mark will be a '.' in the database?
201            $locOfDecimalPoint = strrpos( $value, '.' );
202            if ( $locOfDecimalPoint === false ) {
203                // Better to show "17.0" than "17", if it's a Float.
204                $numDecimalPlaces = 1;
205            } else {
206                $numDecimalPlaces = strlen( $value ) - $locOfDecimalPoint - 1;
207            }
208            return number_format( $value, $numDecimalPlaces, $wgCargoDecimalMark,
209                $wgCargoDigitGroupingCharacter );
210        } elseif ( $type == 'Page' ) {
211            $title = Title::newFromText( $value );
212            if ( $title == null ) {
213                return null;
214            }
215            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
216            // Hide the namespace in the display?
217            global $wgCargoHideNamespaceName;
218            if ( in_array( $title->getNamespace(), $wgCargoHideNamespaceName ) ) {
219                return CargoUtils::makeLink( $linkRenderer, $title, htmlspecialchars( $title->getRootText() ) );
220            } else {
221                return CargoUtils::makeLink( $linkRenderer, $title );
222            }
223        } elseif ( $type == 'File' ) {
224            // 'File' values are basically pages in the File:
225            // namespace; they are displayed as thumbnails within
226            // queries.
227            $title = Title::newFromText( $value, NS_FILE );
228            if ( $title == null || !$title->exists() ) {
229                return $value;
230            }
231            if ( method_exists( MediaWikiServices::class, 'getRepoGroup' ) ) {
232                // MediaWiki 1.34+
233                $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $title );
234            } else {
235                $file = wfLocalFile( $title );
236            }
237            return Linker::makeThumbLinkObj(
238                $title,
239                $file,
240                $value,
241                ''
242            );
243        } elseif ( $type == 'URL' ) {
244            if ( array_key_exists( 'link text', $fieldDescription->mOtherParams ) ) {
245                return Html::element( 'a', [ 'href' => $value ],
246                        $fieldDescription->mOtherParams['link text'] );
247            } else {
248                // Otherwise, display the URL as a link.
249                global $wgNoFollowLinks;
250                $linkParams = [ 'href' => $value, 'class' => 'external free' ];
251                if ( $wgNoFollowLinks ) {
252                    $linkParams['rel'] = 'nofollow';
253                }
254                return Html::element( 'a', $linkParams, $value );
255            }
256        } elseif ( $type == 'Date' || $type == 'Datetime' ) {
257            // This should not get called - date fields
258            // have a separate formatting function.
259            return $value;
260        } elseif ( $type == 'Wikitext' || $type == 'Wikitext string' || $type == '' ) {
261            return CargoUtils::smartParse( $value, $parser );
262        } elseif ( $type == 'Searchtext' ) {
263            $value = htmlspecialchars( $value );
264            if ( strlen( $value ) > 300 ) {
265                return substr( $value, 0, 300 ) . ' ...';
266            } else {
267                return $value;
268            }
269        }
270
271        // If it's not any of these specially-handled types, just
272        // return the value.
273        return $value;
274    }
275
276    public static function formatDateFieldValue( $dateValue, $datePrecision, $type ) {
277        // Quick escape.
278        if ( $dateValue == '' ) {
279            return '';
280        }
281
282        $seconds = strtotime( $dateValue );
283        // 'Y' adds leading zeroes to years with fewer than four digits,
284        // so remove them.
285        $yearString = ltrim( date( 'Y', $seconds ), '0' );
286        if ( $datePrecision == CargoStore::YEAR_ONLY ) {
287            return $yearString;
288        } elseif ( $datePrecision == CargoStore::MONTH_ONLY ) {
289            return CargoDrilldownUtils::monthToString( date( 'm', $seconds ) ) .
290                " $yearString";
291        } else {
292            // CargoStore::DATE_AND_TIME or
293            // CargoStore::DATE_ONLY
294            global $wgAmericanDates;
295            if ( $wgAmericanDates ) {
296                // We use MediaWiki's representation of month
297                // names, instead of PHP's, because its i18n
298                // support is of course far superior.
299                $dateText = CargoDrilldownUtils::monthToString( date( 'm', $seconds ) );
300                $dateText .= ' ' . date( 'j', $seconds ) . "$yearString";
301            } else {
302                $dateText = "$yearString-" . date( 'm-d', $seconds );
303            }
304            // @TODO - remove the redundant 'Date' check at some
305            // point. It's here because the "precision" constants
306            // changed a ittle in version 0.8.
307            if ( $type == 'Date' || $datePrecision == CargoStore::DATE_ONLY ) {
308                return $dateText;
309            }
310
311            // It's a Datetime - add time as well.
312            global $wgCargo24HourTime;
313            if ( $wgCargo24HourTime ) {
314                $timeText = date( 'G:i:s', $seconds );
315            } else {
316                $timeText = date( 'g:i:s A', $seconds );
317            }
318            return "$dateText $timeText";
319        }
320    }
321
322    /**
323     * Based heavily on MediaWiki's SearchResult::getTextSnippet()
324     */
325    public function getTextSnippet( $text, $terms ) {
326        if ( defined( '\SearchHighlighter::DEFAULT_CONTEXT_LINES' ) ) {
327            // MW 1.34+
328            // TODO: once the else block is removed simply drop these vars
329            // SearchHighlighter methods take these same values as defaults.
330            $contextlines = SearchHighlighter::DEFAULT_CONTEXT_LINES;
331            $contextchars = SearchHighlighter::DEFAULT_CONTEXT_CHARS;
332        } else {
333            list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs();
334        }
335
336        foreach ( $terms as $i => $term ) {
337            // Try to map from a MySQL search to a PHP one -
338            // this code could probably be improved.
339            $term = str_replace( [ '"', "'", '+', '*' ], '', $term );
340            // What is the point of this...?
341            if ( strpos( $term, '*' ) !== false ) {
342                $term = '\b' . $term . '\b';
343            }
344            $terms[$i] = $term;
345        }
346
347        // Replace newlines, etc. with spaces for better readability.
348        $text = preg_replace( '/\s+/', ' ', $text );
349        $h = new SearchHighlighter();
350        if ( count( $terms ) > 0 ) {
351            // In the core MediaWiki equivalent of this code,
352            // there is a check here of the flag
353            // $wgAdvancedSearchHighlighting. Instead, we always
354            // call the more expensive function, highlightText()
355            // rather than highlightSimple(), because we're not
356            // that concerned about performance.
357            $snippet = $h->highlightText( $text, $terms, $contextlines, $contextchars );
358        } else {
359            $snippet = $h->highlightNone( $text, $contextlines, $contextchars );
360        }
361
362        // Why is this necessary for Cargo, but not for MediaWiki?
363        return html_entity_decode( $snippet );
364    }
365
366    public function displayQueryResults( $formatter, $queryResults ) {
367        if ( count( $queryResults ) == 0 ) {
368            if ( array_key_exists( 'default', $this->mDisplayParams ) ) {
369                return $this->mDisplayParams['default'];
370            } else {
371                return '<em>' . wfMessage( 'table_pager_empty' )->text() . '</em>'; // default
372            }
373        }
374
375        $formattedQueryResults = $this->getFormattedQueryResults( $queryResults );
376        $text = '';
377
378        // If this is the 'template' format, let the formatter print
379        // out the intro and outro, so they can be parsed at the same
380        // time as the main body. In theory, this should be done for
381        // every result format, but in practice, probably only with
382        // 'template' could there be complex formatting (like a table
383        // with a header and footer) where this approach to parsing
384        // would make a difference.
385        $formatClass = get_class( $formatter );
386        if ( array_key_exists( 'intro', $this->mDisplayParams ) && $formatClass !== 'CargoTemplateFormat' ) {
387            $text .= CargoUtils::smartParse( $this->mDisplayParams['intro'], $formatter->mParser );
388        }
389        try {
390            $text .= $formatter->display( $queryResults, $formattedQueryResults, $this->mFieldDescriptions,
391                $this->mDisplayParams );
392        } catch ( Exception $e ) {
393            return CargoUtils::formatError( $e->getMessage() );
394        }
395        if ( array_key_exists( 'outro', $this->mDisplayParams ) && $formatClass !== 'CargoTemplateFormat' ) {
396            $text .= CargoUtils::smartParse( $this->mDisplayParams['outro'], $formatter->mParser );
397        }
398        return $text;
399    }
400
401    /**
402     * Display the link to view more results, pointing to Special:ViewData.
403     */
404    public function viewMoreResultsLink( $displayHTML = true ) {
405        $vd = Title::makeTitleSafe( NS_SPECIAL, 'ViewData' );
406        if ( array_key_exists( 'more results text', $this->mDisplayParams ) ) {
407            $moreResultsText = htmlspecialchars( $this->mDisplayParams['more results text'] );
408            // If the value is blank, don't show a link at all.
409            if ( $moreResultsText == '' ) {
410                return '';
411            }
412        } else {
413            $moreResultsText = wfMessage( 'moredotdotdot' )->parse();
414        }
415
416        $queryStringParams = [];
417        $sqlQuery = $this->mSQLQuery;
418        $queryStringParams['tables'] = $sqlQuery->mTablesStr;
419        $queryStringParams['fields'] = $sqlQuery->mFieldsStr;
420        if ( $sqlQuery->mOrigWhereStr != '' ) {
421            $queryStringParams['where'] = $sqlQuery->mOrigWhereStr;
422        }
423        if ( $sqlQuery->mJoinOnStr != '' ) {
424            $queryStringParams['join_on'] = $sqlQuery->mJoinOnStr;
425        }
426        if ( $sqlQuery->mOrigGroupByStr != '' ) {
427            $queryStringParams['group_by'] = $sqlQuery->mOrigGroupByStr;
428        }
429        if ( $sqlQuery->mOrigHavingStr != '' ) {
430            $queryStringParams['having'] = $sqlQuery->mOrigHavingStr;
431        }
432        $queryStringParams['order_by'] = $sqlQuery->mOrderBy;
433        if ( $this->mFormat != '' ) {
434            $queryStringParams['format'] = $this->mFormat;
435        }
436        $queryStringParams['offset'] = $sqlQuery->mQueryLimit;
437        $queryStringParams['limit'] = 100; // Is that a reasonable number in all cases?
438
439        // Add format-specific params.
440        foreach ( $this->mDisplayParams as $key => $value ) {
441            $queryStringParams[$key] = $value;
442        }
443
444        if ( $displayHTML ) {
445            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
446            $link = CargoUtils::makeLink( $linkRenderer, $vd, $moreResultsText, [], $queryStringParams );
447            return Html::rawElement( 'p', null, $link );
448        } else {
449            // Display link as wikitext.
450            global $wgServer;
451            return '[' . $wgServer . $vd->getLinkURL( $queryStringParams ) . ' ' . $moreResultsText . ']';
452        }
453    }
454
455}