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