Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 424 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
| CargoExport | |
0.00% |
0 / 424 |
|
0.00% |
0 / 16 |
17030 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
506 | |||
| displayCalendarData | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
420 | |||
| displayGanttData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getGanttJSONData | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
156 | |||
| displayBPMNData | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
380 | |||
| displayTimelineData | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
210 | |||
| displayNVD3ChartData | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
132 | |||
| parseWikitextInQueryResults | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| displayCSVData | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
| displayExcelData | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
| displayJSONData | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
| displayBibtexData | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| displayIcalendarData | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| displayFeedData | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| outputFile | |
0.00% |
0 / 8 |
|
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 | |
| 11 | use MediaWiki\Title\Title; |
| 12 | |
| 13 | class 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 .= ' [[' . $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 | } |