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