Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 424
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoExport
0.00% covered (danger)
0.00%
0 / 424
0.00% covered (danger)
0.00%
0 / 16
17030
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
506
 displayCalendarData
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
420
 displayGanttData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGanttJSONData
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
156
 displayBPMNData
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
380
 displayTimelineData
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
210
 displayNVD3ChartData
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
132
 parseWikitextInQueryResults
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 displayCSVData
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 displayExcelData
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 displayJSONData
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 displayBibtexData
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 displayIcalendarData
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 displayFeedData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 outputFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Displays the results of a Cargo query in one of several possible
4 * structured data formats - in some cases for use by an Ajax-based
5 * display format.
6 *
7 * @author Yaron Koren
8 * @ingroup Cargo
9 */
10
11use MediaWiki\Title\Title;
12
13class CargoExport extends UnlistedSpecialPage {
14
15    /**
16     * Constructor
17     */
18    public function __construct() {
19        parent::__construct( 'CargoExport' );
20    }
21
22    public function execute( $query ) {
23        $this->getOutput()->disable();
24        $req = $this->getRequest();
25
26        // If no value has been set for 'tables', or 'table', just
27        // display a blank screen.
28        $tableArray = $req->getArray( 'tables' );
29        if ( $tableArray == null ) {
30            $tableArray = $req->getArray( 'table' );
31        }
32        if ( $tableArray == null ) {
33            return;
34        }
35        $fieldsArray = $req->getArray( 'fields' );
36        $whereArray = $req->getArray( 'where' );
37        $joinOnArray = $req->getArray( 'join_on' );
38        $groupByArray = $req->getArray( 'group_by' );
39        $havingArray = $req->getArray( 'having' );
40        $orderByArray = $req->getArray( 'order_by' );
41        $limitArray = $req->getArray( 'limit' );
42        $offsetArray = $req->getArray( 'offset' );
43
44        try {
45            $sqlQueries = [];
46            foreach ( $tableArray as $i => $table ) {
47                $fields = $fieldsArray[$i] ?? null;
48                $where = $whereArray[$i] ?? null;
49                $joinOn = $joinOnArray[$i] ?? null;
50                $groupBy = $groupByArray[$i] ?? null;
51                $having = $havingArray[$i] ?? null;
52                $orderBy = $orderByArray[$i] ?? null;
53                $limit = $limitArray[$i] ?? null;
54                $offset = $offsetArray[$i] ?? null;
55                $sqlQueries[] = CargoSQLQuery::newFromValues( $table,
56                    $fields, $where, $joinOn, $groupBy, $having,
57                    $orderBy, $limit, $offset );
58            }
59
60            $format = $req->getVal( 'format' );
61
62            if ( $format == 'fullcalendar' ) {
63                $this->displayCalendarData( $sqlQueries );
64            } elseif ( $format == 'timeline' ) {
65                $this->displayTimelineData( $sqlQueries );
66            } elseif ( $format == 'gantt' ) {
67                $this->displayGanttData( $sqlQueries );
68            } elseif ( $format == 'bpmn' ) {
69                $this->displayBPMNData( $sqlQueries );
70            } elseif ( $format == 'nvd3chart' ) {
71                $this->displayNVD3ChartData( $sqlQueries );
72            } elseif ( $format == 'csv' ) {
73                $delimiter = $req->getVal( 'delimiter' );
74                if ( $delimiter == '' ) {
75                    $delimiter = ',';
76                } elseif ( $delimiter == '\t' ) {
77                    $delimiter = "\t";
78                }
79                $filename = $req->getVal( 'filename' );
80                if ( $filename == '' ) {
81                    $filename = 'results.csv';
82                }
83                $parseValues = $req->getCheck( 'parse_values' );
84                $this->displayCSVData( $sqlQueries, $delimiter, $filename, $parseValues );
85            } elseif ( $format == 'excel' ) {
86                $filename = $req->getVal( 'filename' );
87                if ( $filename == '' ) {
88                    $filename = 'results.xlsx';
89                }
90                $parseValues = $req->getCheck( 'parse_values' );
91                $this->displayExcelData( $sqlQueries, $filename, $parseValues );
92            } elseif ( $format == 'json' ) {
93                $parseValues = $req->getCheck( 'parse_values' );
94                $this->displayJSONData( $sqlQueries, $parseValues );
95            } elseif ( $format == 'bibtex' ) {
96                $defaultEntryType = $req->getVal( 'default_entry_type' );
97                if ( $defaultEntryType == '' ) {
98                    $defaultEntryType = 'article';
99                }
100                $this->displayBibtexData( $sqlQueries, $defaultEntryType );
101            } elseif ( $format === 'icalendar' ) {
102                $this->displayIcalendarData( $sqlQueries );
103            } elseif ( $format === 'feed' ) {
104                $this->displayFeedData( $sqlQueries );
105            } else {
106                // Let other extensions display the data if they have defined their own "deferred"
107                // formats. This is an unusual hook in that functions that use it have to return false;
108                // otherwise the error message will be displayed.
109                $result = $this->getHookContainer()->run( 'CargoDisplayExportData', [ $format, $sqlQueries, $req ] );
110                if ( $result ) {
111                    print $this->msg( "cargo-query-missingformat" )->parse();
112                }
113            }
114        } catch ( Exception $e ) {
115            print $e->getMessage();
116        }
117    }
118
119    /**
120     * Used for calendar format
121     */
122    private function displayCalendarData( $sqlQueries ) {
123        $cdb = CargoUtils::getDB();
124        $req = $this->getRequest();
125
126        $colorArray = $req->getArray( 'color' );
127        $textColorArray = $req->getArray( 'text_color' );
128
129        $datesLowerLimit = $req->getVal( 'start' );
130        $datesUpperLimit = $req->getVal( 'end' );
131
132        $displayedArray = [];
133        foreach ( $sqlQueries as $i => $sqlQuery ) {
134            [ $startDateField, $endDateField ] = $sqlQuery->getMainStartAndEndDateFields();
135
136            $where = $sqlQuery->mWhereStr;
137            if ( $where != '' ) {
138                $where .= " AND ";
139            }
140            $where .= "(";
141            foreach ( $sqlQuery->mDateFieldPairs as $j => $dateFieldPair ) {
142                if ( $j > 0 ) {
143                    $where .= " OR ";
144                }
145                $startDateFieldName = $dateFieldPair['start'][0];
146                if ( array_key_exists( 'end', $dateFieldPair ) ) {
147                    $endDateFieldName = $dateFieldPair['end'][0];
148                } else {
149                    $endDateFieldName = $startDateFieldName;
150                }
151                $escapedLowerLimit = $cdb->addQuotes( $datesLowerLimit );
152                $escapedUpperLimit = $cdb->addQuotes( $datesUpperLimit );
153                $where .= "($endDateFieldName >= $escapedLowerLimit AND $startDateFieldName < $escapedUpperLimit)";
154            }
155            $where .= ")";
156            $sqlQuery->mWhereStr = $where;
157
158            $queryResults = $sqlQuery->run();
159
160            foreach ( $queryResults as $queryResult ) {
161                if ( array_key_exists( 'name', $queryResult ) ) {
162                    $eventTitle = $queryResult['name'];
163                } else {
164                    $eventTitle = reset( $queryResult );
165                }
166                // The FullCalendar JS library will HTML-encode
167                // titles, so avoid a double-encoding.
168                $eventTitle = html_entity_decode( $eventTitle );
169                if ( array_key_exists( 'color', $queryResult ) ) {
170                    $eventColor = $queryResult['color'];
171                } elseif ( $colorArray != null && array_key_exists( $i, $colorArray ) ) {
172                    $eventColor = $colorArray[$i];
173                } else {
174                    $eventColor = null;
175                }
176                if ( array_key_exists( 'text color', $queryResult ) ) {
177                    $eventTextColor = $queryResult['text color'];
178                } elseif ( $textColorArray != null && array_key_exists( $i, $textColorArray ) ) {
179                    $eventTextColor = $textColorArray[$i];
180                } else {
181                    $eventTextColor = null;
182                }
183                $eventStart = $queryResult[$startDateField];
184                $eventEnd = ( $endDateField !== null ) ? $queryResult[$endDateField] : null;
185                if ( array_key_exists( 'description', $queryResult ) ) {
186                    $eventDescription = $queryResult['description'];
187                } else {
188                    $eventDescription = null;
189                }
190
191                $startDatePrecisionField = $startDateField . '__precision';
192                // There might not be a precision field, if,
193                // for instance, the date field is an SQL
194                // function. Ideally we would figure out
195                // the right precision, but for now just
196                // go with "DATE_ONLY" - seems safe.
197                if ( array_key_exists( $startDatePrecisionField, $queryResult ) ) {
198                    $startDatePrecision = $queryResult[$startDatePrecisionField];
199                } else {
200                    $startDatePrecision = CargoStore::DATE_ONLY;
201                }
202                $curEvent = [
203                    // Get first field for the title - not
204                    // necessarily the page name.
205                    'title' => $eventTitle,
206                    'start' => $eventStart,
207                    'end' => $eventEnd,
208                    'color' => $eventColor,
209                    'textColor' => $eventTextColor,
210                    'description' => $eventDescription
211                ];
212                if ( array_key_exists( '_pageName', $queryResult ) ) {
213                    $title = Title::newFromText( $queryResult['_pageName'] );
214                    $curEvent['url'] = $title->getLocalURL();
215                } elseif ( array_key_exists( 'link', $queryResult ) ) {
216                    $title = Title::newFromText( $queryResult['link'] );
217                    $curEvent['url'] = $title->getLocalURL();
218                }
219                if ( $startDatePrecision != CargoStore::DATE_AND_TIME ) {
220                    $curEvent['allDay'] = true;
221                }
222                $displayedArray[] = $curEvent;
223            }
224        }
225
226        print json_encode( $displayedArray );
227    }
228
229    /**
230     * Used for gantt format
231     */
232    private function displayGanttData( $sqlQueries ) {
233        print self::getGanttJSONData( $sqlQueries );
234    }
235
236    public static function getGanttJSONData( $sqlQueries ) {
237        $displayedArray = [
238            'data' => [],
239            'links' => []
240        ];
241        foreach ( $sqlQueries as $sqlQuery ) {
242            [ $startDateField, $endDateField ] = $sqlQuery->getMainStartAndEndDateFields();
243
244            $queryResults = $sqlQuery->run();
245            $n = 1;
246            foreach ( $queryResults as $queryResult ) {
247                if ( array_key_exists( 'name', $queryResult ) ) {
248                    $eventTitle = $queryResult['name'];
249                } else {
250                    $eventTitle = reset( $queryResult );
251                }
252                if ( array_key_exists( '_pageID', $queryResult ) ) {
253                    $eventID = $queryResult['_pageID'];
254                } else {
255                    $eventID = $n;
256                    $n++;
257                }
258
259                if ( !isset( $queryResult[$startDateField] ) ) {
260                    continue;
261                }
262                $eventStart = $queryResult[$startDateField];
263                $eventEnd = ( $endDateField !== null && isset( $queryResult[$endDateField] ) ) ? $queryResult[$endDateField] : null;
264                if ( array_key_exists( 'duration', $queryResult ) ) {
265                    $eventDuration = $queryResult['duration'];
266                } else {
267                    $eventDuration = 1;
268                }
269                if ( array_key_exists( 'target', $queryResult ) ) {
270                    $target = $queryResult['target'];
271                } else {
272                    $target = null;
273                }
274
275                $data = [
276                    'id' => $eventID,
277                    'text' => $eventTitle,
278                    'start_date' => $eventStart,
279                    'end_date' => $eventEnd,
280                    'duration' => $eventDuration
281                ];
282                $links = [
283                    'id' => $eventID,
284                    'source' => $eventID,
285                    'target' => $target,
286                    'type' => "0"
287                ];
288                array_push( $displayedArray['data'], $data );
289                array_push( $displayedArray['links'], $links );
290            }
291            for ( $t = 0; $t < count( $displayedArray['links'] ); $t++ ) {
292                if ( $displayedArray['links'][$t]['target'] != null ) {
293                    $temp = $displayedArray['links'][$t]['target'];
294                    $key = array_search( $temp, array_column( $displayedArray['data'], 'text' ) );
295                    $displayedArray['links'][$t]['target'] = $displayedArray['links'][$key]['id'];
296                }
297            }
298        }
299        return json_encode( $displayedArray );
300    }
301
302    /**
303     * Used for bpmn format
304     */
305    private function displayBPMNData( $sqlQueries ) {
306        $sequenceFlows = [];
307        $elements = [];
308        $t = 1;
309        foreach ( $sqlQueries as $sqlQuery ) {
310            $queryResults = $sqlQuery->run();
311            foreach ( $queryResults as $queryResult ) {
312                // Type is mandatory.
313                if ( !array_key_exists( 'type', $queryResult ) ) {
314                    continue;
315                }
316
317                $curEvent = [
318                    'name' => $queryResult['name'] ?? reset( $queryResult ),
319                    'label' => $queryResult['label'] ?? "",
320                    'type' => $queryResult['type'],
321                    'source' => $queryResult['sources'] ?? "",
322                    'linkedpage' => $queryResult['linked'] ?? "",
323                    'flowLabels' => $queryResult['flowLabels'] ?? ""
324                ];
325
326                if ( str_contains( $curEvent['type'], 'Event' ) ) {
327                    $curEvent['height'] = "36";
328                    $curEvent['width'] = "36";
329                } elseif ( str_contains( $curEvent['type'], 'Gateway' ) ) {
330                    $curEvent['height'] = "50";
331                    $curEvent['width'] = "50";
332                } else {
333                    $curEvent['height'] = "80";
334                    $curEvent['width'] = "100";
335                }
336                $curEvent['id'] = $curEvent['type'] . $t;
337                $t++;
338                array_push( $elements, $curEvent );
339            }
340        }
341
342        header( 'Content-Type: text/xml' );
343        $XML = '<?xml version="1.0" encoding="UTF-8"?>';
344        // Needed to restore highlighting in vi - <?
345        $XML .= '<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
346        xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
347        xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
348        id="Definitions_18vnora"
349        targetNamespace="http://bpmn.io/schema/bpmn"
350        exporter="bpmn-js (https://demo.bpmn.io)"
351        exporterVersion="4.0.3">
352        <bpmn:process id="Process_1" isExecutable="false">';
353
354        // XML for BPMN Process
355        foreach ( $elements as $task ) {
356            if ( is_array( $task ) && $task['type'] != "" ) {
357                $XML .= '<bpmn:' . $task[ 'type' ] . ' id="' . $task['id'];
358                if ( $task['name'] != "" ) {
359                    if ( $task['label'] != "" ) {
360                        $XML .= '" name="' . $task['label'];
361                    } else {
362                        $XML .= '" name="' . $task['name'];
363                    }
364                }
365                if ( $task[ 'linkedpage' ] != "" ) {
366                    $XML .= '&#10;[[' . $task['linkedpage'] . ']]';
367                }
368                $XML .= '"></bpmn:' . $task['type'] . '>';
369            }
370        }
371
372        foreach ( $elements as $element ) {
373            if ( !array_key_exists( 'source', $element ) ) {
374                continue;
375            }
376            $sources = explode( ", ", $element['source'] );
377            $labels = explode( ", ", $element['flowLabels'] );
378
379            foreach ( $sources as $sourceNum => $sourceElementName ) {
380                $key = array_search( $sourceElementName, array_column( $elements, 'name' ) );
381                if ( $key === false ) {
382                    continue;
383                }
384                $sequenceFlows[] = [
385                    'type' => 'sequenceFlow',
386                    'source' => $elements[$key]['id'],
387                    'target' => $element['id'],
388                    'name' => $element['flowLabels'],
389                    'id' => 'sequenceFlow' . ( count( $sequenceFlows ) + 1 )
390                ];
391            }
392        }
393
394        foreach ( $sequenceFlows as $task ) {
395            if ( is_array( $task ) && $task['type'] == "sequenceFlow" ) {
396                $XML .= '<bpmn:sequenceFlow id="' . $task['id'] . '" sourceRef="' . $task['source'] .
397                    '" targetRef="' . $task['target'] . '" name="' . $task['name'] . '"/>';
398            }
399        }
400        $XML .= '</bpmn:process></bpmn:definitions>';
401        print $XML;
402    }
403
404    private function displayTimelineData( $sqlQueries ) {
405        $displayedArray = [];
406        foreach ( $sqlQueries as $sqlQuery ) {
407            [ $startDateField, $endDateField ] = $sqlQuery->getMainStartAndEndDateFields();
408
409            $queryResults = $sqlQuery->run();
410
411            foreach ( $queryResults as $queryResult ) {
412                $eventDescription = '';
413                $firstField = true;
414                foreach ( $sqlQuery->mFieldDescriptions as $fieldName => $fieldDescription ) {
415                    // Don't display the first field (it'll
416                    // be the title), or the main date fields.
417                    if ( $firstField ) {
418                        $firstField = false;
419                        continue;
420                    }
421                    if ( $fieldName == $startDateField || $fieldName == $endDateField ) {
422                        continue;
423                    }
424                    if ( !array_key_exists( $fieldName, $queryResult ) ) {
425                        continue;
426                    }
427                    $fieldValue = $queryResult[$fieldName];
428                    if ( trim( $fieldValue ) == '' ) {
429                        continue;
430                    }
431                    $eventDescription .= "<strong>$fieldName:</strong> $fieldValue<br />\n";
432                }
433
434                if ( array_key_exists( 'name', $queryResult ) ) {
435                    $eventTitle = $queryResult['name'];
436                } else {
437                    // Get first field for the 'title' - not
438                    // necessarily the page name.
439                    $eventTitle = reset( $queryResult );
440                }
441
442                if ( !isset( $queryResult[$startDateField] ) ) {
443                    continue;
444                }
445                $startDateValue = $queryResult[$startDateField];
446                if ( $endDateField !== null && isset( $queryResult[$endDateField] ) ) {
447                    $endDateValue = $queryResult[$endDateField];
448                } else {
449                    $endDateValue = $startDateValue;
450                }
451
452                $eventDisplayDetails = [
453                    'title' => $eventTitle,
454                    'description' => $eventDescription,
455                    'start' => $startDateValue,
456                    'end' => $endDateValue
457                ];
458
459                // If we have the name of the page on which
460                // the event is defined, link to that -
461                // otherwise, don't link to anything.
462                // (In most cases, the _pageName field will
463                // also be the title of the event.)
464                if ( array_key_exists( '_pageName', $queryResult ) ) {
465                    $title = Title::newFromText( $queryResult['_pageName'] );
466                    $eventDisplayDetails['link'] = $title->getFullURL();
467                }
468                $displayedArray[] = $eventDisplayDetails;
469            }
470        }
471        // Sort by date, ascending.
472        usort( $displayedArray, static function ( $a, $b ) {
473            return $a['start'] <=> $b['start'];
474        } );
475
476        $displayedArray = [ 'events' => $displayedArray ];
477        print json_encode( $displayedArray, JSON_HEX_TAG | JSON_HEX_QUOT );
478    }
479
480    private function displayNVD3ChartData( $sqlQueries ) {
481        // We'll only use the first query, if there's more than one.
482        $sqlQuery = $sqlQueries[0];
483        $queryResults = $sqlQuery->run();
484
485        // Handle date precision fields, which come alongside date fields.
486        foreach ( $queryResults as $i => $curRow ) {
487            foreach ( $curRow as $fieldName => $value ) {
488                if ( strpos( $fieldName, '__precision' ) == false ) {
489                    continue;
490                }
491                $dateField = str_replace( '__precision', '', $fieldName );
492                if ( !array_key_exists( $dateField, $curRow ) ) {
493                    continue;
494                }
495                $origDateValue = $curRow[$dateField];
496                // Years by themselves lead to a display
497                // problem, for some reason, so add a space.
498                $queryResults[$i][$dateField] = CargoQueryDisplayer::formatDateFieldValue( $origDateValue, $value, 'Date', $this->getLanguage() ) . ' ';
499                unset( $queryResults[$i][$fieldName] );
500            }
501        }
502
503        // @TODO - this array needs to be longer.
504        $colorsArray = [ '#60BD68', '#FAA43A', '#5DA6DA', '#CC333F' ];
505
506        // Initialize everything, using the field names.
507        $firstRow = reset( $queryResults );
508        $displayedArray = [];
509        $fieldNum = 0;
510        foreach ( $firstRow as $fieldName => $value ) {
511            if ( $fieldNum > 0 ) {
512                $curSeries = [
513                    'key' => $fieldName,
514                    'color' => $colorsArray[$fieldNum - 1],
515                    'values' => []
516                ];
517                $displayedArray[] = $curSeries;
518            }
519            $fieldNum++;
520        }
521
522        foreach ( $queryResults as $queryResult ) {
523            $fieldNum = 0;
524            $labelName = null;
525            foreach ( $queryResult as $value ) {
526                if ( $fieldNum == 0 ) {
527                    $labelName = $value;
528                    if ( trim( $value ) == '' ) {
529                        // Display blank labels as "None".
530                        $labelName = $this->msg( 'powersearch-togglenone' )->text();
531                    }
532                } else {
533                    $displayedArray[$fieldNum - 1]['values'][] = [
534                        'label' => $labelName,
535                        'value' => $value
536                    ];
537                }
538                $fieldNum++;
539            }
540        }
541
542        print json_encode( $displayedArray, JSON_NUMERIC_CHECK | JSON_HEX_TAG );
543    }
544
545    /**
546     * Turn all wikitext into HTML in a set of query results.
547     */
548    private function parseWikitextInQueryResults( $queryResults ) {
549        $parsedQueryResults = [];
550        foreach ( $queryResults as $rowNum => $rowValues ) {
551            $parsedQueryResults[$rowNum] = [];
552            foreach ( $rowValues as $colName => $value ) {
553                $parsedQueryResults[$rowNum][$colName] = CargoUtils::smartParse( $value, null );
554            }
555        }
556        return $parsedQueryResults;
557    }
558
559    private function displayCSVData( $sqlQueries, $delimiter, $filename, $parseValues ) {
560        header( 'Content-Encoding: UTF-16' );
561        header( "Content-Type: text/csv; charset=UTF-16" );
562        header( "Content-Disposition: attachment; filename=$filename" );
563
564        $queryResultsArray = [];
565        $allHeaders = [];
566        foreach ( $sqlQueries as $sqlQuery ) {
567            $queryResults = $sqlQuery->run();
568            if ( $parseValues ) {
569                $queryResults = $this->parseWikitextInQueryResults( $queryResults );
570            }
571            $allHeaders = array_merge( $allHeaders, array_keys( reset( $queryResults ) ) );
572            $queryResultsArray[] = $queryResults;
573        }
574
575        // Remove duplicates from headers array.
576        $allHeaders = array_unique( $allHeaders );
577
578        $out = fopen( 'php://output', 'w' );
579
580        // Display header row.
581        fputcsv( $out, $allHeaders, $delimiter );
582
583        // Display the data.
584        foreach ( $queryResultsArray as $queryResults ) {
585            foreach ( $queryResults as $queryResultRow ) {
586                // Put in a blank if this row doesn't contain
587                // a certain column (this will only happen
588                // for compound queries).
589                $displayedRow = [];
590                foreach ( $allHeaders as $header ) {
591                    if ( array_key_exists( $header, $queryResultRow ) ) {
592                        $displayedRow[$header] = $queryResultRow[$header];
593                    } else {
594                        $displayedRow[$header] = null;
595                    }
596                }
597                fputcsv( $out, $displayedRow, $delimiter );
598            }
599        }
600        fclose( $out );
601    }
602
603    private function displayExcelData( $sqlQueries, $filename, $parseValues ) {
604        // We'll only use the first query, if there's more than one.
605        $sqlQuery = $sqlQueries[0];
606        $queryResults = $sqlQuery->run();
607        if ( $parseValues ) {
608            $queryResults = $this->parseWikitextInQueryResults( $queryResults );
609        }
610
611        if ( class_exists( 'PhpOffice\PhpSpreadsheet\Spreadsheet' ) ) {
612            $file = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
613        } elseif ( class_exists( 'PHPExcel' ) ) {
614            $file = new PHPExcel();
615        } else {
616            die( "Error: Either the PHPExcel or the PhpSpreadsheet library must be installed for this format to work." );
617        }
618        $file->setActiveSheetIndex( 0 );
619
620        // Create array with header row and query results.
621        $header = [ array_keys( reset( $queryResults ) ) ];
622        $rows = array_merge( $header, $queryResults );
623
624        $file->getActiveSheet()->fromArray( $rows, null, 'A1' );
625        header( "Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" );
626        header( "Content-Disposition: attachment;filename=$filename" );
627        header( "Cache-Control: max-age=0" );
628
629        if ( class_exists( 'PhpOffice\PhpSpreadsheet\Spreadsheet' ) ) {
630            $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter( $file, 'Xlsx' );
631        } else { // if ( class_exists( 'PHPExcel' ) ) {
632            $writer = PHPExcel_IOFactory::createWriter( $file, 'Excel2007' );
633        }
634
635        $writer->save( 'php://output' );
636    }
637
638    private function displayJSONData( $sqlQueries, $parseValues ) {
639        $allQueryResults = [];
640        foreach ( $sqlQueries as $sqlQuery ) {
641            $queryResults = $sqlQuery->run();
642            if ( $parseValues ) {
643                $queryResults = $this->parseWikitextInQueryResults( $queryResults );
644            }
645
646            // Turn "List" fields into arrays.
647            foreach ( $sqlQuery->mFieldDescriptions as $alias => $fieldDescription ) {
648                if ( $fieldDescription->mIsList ) {
649                    $delimiter = $fieldDescription->getDelimiter();
650                    for ( $i = 0; $i < count( $queryResults ); $i++ ) {
651                        $curValue = $queryResults[$i][$alias] ?? '';
652                        if ( !is_array( $curValue ) ) {
653                            $queryResults[$i][$alias] = explode( $delimiter, $curValue );
654                        }
655                    }
656                }
657            }
658
659            $allQueryResults = array_merge( $allQueryResults, $queryResults );
660        }
661
662        if ( $parseValues ) {
663            $jsonOptions = JSON_PRETTY_PRINT;
664        } else {
665            $jsonOptions = JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_PRETTY_PRINT;
666        }
667        $json = json_encode( $allQueryResults, $jsonOptions );
668        $this->outputFile( 'application/json', 'export', 'json', $json );
669    }
670
671    private function displayBibtexData( $sqlQueries, $defaultEntryType ) {
672        $text = '';
673        foreach ( $sqlQueries as $sqlQuery ) {
674            $queryResults = $sqlQuery->run();
675            $text .= CargoBibtexFormat::generateBibtexEntries( $queryResults,
676                    $sqlQuery->mFieldDescriptions,
677                    [ 'default entry type' => $defaultEntryType ] );
678        }
679        $this->outputFile( 'text/plain', 'results.bib', 'bib', $text );
680    }
681
682    /**
683     * Output in the icalendar format.
684     *
685     * @param CargoSQLQuery[] $sqlQueries
686     */
687    private function displayIcalendarData( $sqlQueries ) {
688        $req = $this->getRequest();
689        $format = new CargoICalendarFormat( $this->getOutput() );
690        $calendar = $format->getCalendar( $req, $sqlQueries );
691
692        $filename = $req->getText( 'filename', 'export.ics' );
693        $this->outputFile( 'text/calendar', $filename, 'ics', $calendar );
694    }
695
696    /**
697     * Output an RSS or Atom feed.
698     *
699     * @param CargoSQLQuery[] $sqlQueries
700     */
701    private function displayFeedData( $sqlQueries ) {
702        $format = new CargoFeedFormat( $this->getOutput() );
703        $format->outputFeed( $this->getRequest(), $sqlQueries );
704    }
705
706    /**
707     * Output a file, with a normalized name and appropriate HTTP headers.
708     *
709     * @param string $contentType The MIME type of the file.
710     * @param string $filename The filename. It doesn't matter if it has the extension or not.
711     * @param string $fileExtension The file extension, without a leading dot.
712     * @param string $data The file contents.
713     * @param string $disposition Either 'inline' (the default) or 'attachment'.
714     */
715    private function outputFile( $contentType, $filename, $fileExtension, $data, $disposition = 'inline' ) {
716        // Clean the filename and make sure it has the correct extension.
717        $filenameTitle = Title::newFromText( wfStripIllegalFilenameChars( $filename ) );
718        $filename = $filenameTitle->getDBkey();
719        if ( substr( $filename, -strlen( '.' . $fileExtension ) ) !== '.' . $fileExtension ) {
720            $filename .= '.' . $fileExtension;
721        }
722
723        $disposition = in_array( $disposition, [ 'inline', 'attachment' ] ) ? $disposition : 'inline';
724        header( 'Content-Type: ' . $contentType );
725        header( 'Content-Disposition: ' . $disposition . ';filename=' . $filename );
726        file_put_contents( 'php://output', $data );
727    }
728}