PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.1
VikBooking Hotel Booking Engine & PMS v1.7.1
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / report / report.php

report.php in VikBooking Hotel Booking Engine & PMS 1.7.1, at admin/helpers/report/report.php

1,469 lines 33.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * PMS Report abstract-parent class that all drivers should extend.
15 */
16 #[\AllowDynamicProperties]
17 abstract class VikBookingReport
18 {
19 /**
20 * @var string
21 */
22 protected $reportName = '';
23
24 /**
25 * @var string
26 */
27 protected $reportFile = '';
28
29 /**
30 * @var array
31 */
32 protected $reportFilters = [];
33
34 /**
35 * @var string
36 */
37 protected $reportScript = '';
38
39 /**
40 * @var string
41 */
42 protected $warning = '';
43
44 /**
45 * @var string
46 */
47 protected $error = '';
48
49 /**
50 * @var object
51 */
52 protected $dbo;
53
54 /**
55 * @var array
56 */
57 protected $cols = [];
58
59 /**
60 * @var array
61 */
62 protected $rows = [];
63
64 /**
65 * @var array
66 */
67 protected $footerRow = [];
68
69 /**
70 * An array of custom options to be passed to the report.
71 * Reports can use them before generating the report data.
72 *
73 * @var array
74 *
75 * @since 1.15.0 (J) - 1.5.0 (WP)
76 */
77 protected $options = [];
78
79 /**
80 * @var resource
81 *
82 * @since 1.16.1 (J) - 1.6.1 (WP)
83 */
84 protected $fp_export = null;
85
86 /**
87 * @var string
88 *
89 * @since 1.16.1 (J) - 1.6.1 (WP)
90 */
91 protected $csv_export_format = 'csv';
92
93 /**
94 * @var string
95 *
96 * @since 1.16.1 (J) - 1.6.1 (WP)
97 */
98 protected $csv_export_fname = '';
99
100 /**
101 * @var array
102 *
103 * @since 1.17.1 (J) - 1.7.1 (WP)
104 */
105 protected $actionData = [];
106
107 /**
108 * @var array
109 *
110 * @since 1.17.1 (J) - 1.7.1 (WP)
111 */
112 protected $resourceFiles = [];
113
114 /**
115 * @var ?string
116 *
117 * @since 1.17.1 (J) - 1.7.1 (WP)
118 */
119 protected $scope = null;
120
121 /**
122 * Class constructor should define the name of
123 * the report and the filters to be displayed.
124 */
125 public function __construct()
126 {
127 $this->dbo = JFactory::getDbo();
128 }
129
130 /**
131 * Extending Classes should define this method
132 * to get the name of the report.
133 */
134 abstract public function getName();
135
136 /**
137 * Extending Classes should define this method
138 * to get the name of class file.
139 */
140 abstract public function getFileName();
141
142 /**
143 * Extending Classes should define this method
144 * to get the filters of the report.
145 */
146 abstract public function getFilters();
147
148 /**
149 * Extending Classes should define this method
150 * to generate the report data (cols and rows).
151 */
152 abstract public function getReportData();
153
154 /**
155 * Main method to generate the report columns and rows. Stores on the current
156 * resource the CSV lines and forces the browser to download the file. In case
157 * of errors, the process is not terminated to let the View display the errors.
158 *
159 * @return void|bool script termination on success, false otherwise.
160 *
161 * @since 1.16.1 (J) - 1.6.1 (WP) drivers no longer need to implement it,
162 * unless the driver needs to override it.
163 */
164 public function exportCSV()
165 {
166 // grab all report lines to export
167 $csvlines = $this->getExportCSVLines();
168
169 if (!$csvlines) {
170 // no data to export
171 return false;
172 }
173
174 // force the download of the CSV file
175 $this->outputHeaders();
176
177 /**
178 * Trigger event to allow third-party plugins to set additional headers or contents to output.
179 *
180 * @since 1.16.8 (J) - 1.6.8 (WP)
181 */
182 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeExportOutputCSV', [$this, $this->csv_export_format]);
183
184 // send lines to output
185 $this->outputCSV($csvlines);
186
187 exit;
188 }
189
190 /**
191 * Returns the name to give to the CSV (or custom) file being exported.
192 *
193 * @param bool $cut_suffix if true the file name suffix will be cut off.
194 * @param string $suffix the optional file name suffix, hence the extension.
195 * @param bool $pretty if true, dashes will be converted to spaces and more.
196 *
197 * @return string
198 *
199 * @since 1.16.1 (J) - 1.6.1 (WP)
200 */
201 public function getExportCSVFileName($cut_suffix = false, $suffix = '.csv', $pretty = false)
202 {
203 if ($this->csv_export_fname) {
204 // use the exact report file name set
205 $export_fname = $this->csv_export_fname;
206 } else {
207 // use a generic file name
208 $export_fname = date('Y-m-d_H.i.s') . '-' . $this->reportFile . '.csv';
209 }
210
211 if ($cut_suffix) {
212 // cut off the file name suffix
213 if (!$suffix) {
214 // detect file extension
215 $suffix = substr($export_fname, strrpos($export_fname, '.'));
216 }
217 $export_fname = basename($export_fname, $suffix);
218 }
219
220 if ($pretty) {
221 // get the default date separator char
222 $datesep = VikBooking::getDateSeparator();
223 // add a dash between a range of dates
224 $export_fname = preg_replace("/([0-9])-([0-9])/i", '$1 - $2', $export_fname);
225 // add an empty space between report name and channel name
226 $export_fname = preg_replace("/([A-Z])-([A-Z])/i", '$1 $2', $export_fname);
227 // add an empty space between report name and date
228 $export_fname = preg_replace("/([A-Z0-9])-([0-9])/i", '$1 $2', $export_fname);
229 // convert the dashes to the actual date separator char
230 $export_fname = preg_replace("/([0-9])_([0-9])/i", '$1' . $datesep . '$2', $export_fname);
231 }
232
233 return $export_fname;
234 }
235
236 /**
237 * Sets the name to give to the CSV (or custom) file being exported.
238 *
239 * @param string $fname the full name of the file to export.
240 *
241 * @return self
242 *
243 * @since 1.16.1 (J) - 1.6.1 (WP)
244 */
245 public function setExportCSVFileName($fname)
246 {
247 $this->csv_export_fname = (string)$fname;
248
249 return $this;
250 }
251
252 /**
253 * Builds the list of the CSV lines to be exported from the current report data.
254 *
255 * @param bool $no_data true for actually not letting the report run.
256 *
257 * @return array list of CSV lines containing lists of CSV fields.
258 *
259 * @since 1.16.1 (J) - 1.6.1 (WP)
260 */
261 public function getExportCSVLines($no_data = false)
262 {
263 if (!$no_data && !$this->getReportData()) {
264 // nothing to export
265 return [];
266 }
267
268 // list of CSV lines
269 $csvlines = [];
270
271 if (!strcasecmp($this->csv_export_format, 'excel')) {
272 // add instructions for Excel
273
274 // UTF-8 BOM (not needed because we convert the encoding to UTF-16LE, which has BOM)
275 // $csvlines[] = chr(0xEF) . chr(0xBB) . chr(0xBF);
276
277 // define the separator
278 $csvlines[] = "sep=;\n";
279 }
280
281 // push the head of the CSV file
282 $csvcols = [];
283 foreach ($this->cols as $col) {
284 if (!is_array($col) || isset($col['ignore_export'])) {
285 // skip column
286 continue;
287 }
288
289 // push column label
290 $csvcols[] = $this->encodeExportCSVField($col['label']);
291 }
292
293 // push all columns
294 $csvlines[] = $csvcols;
295
296 // push the rows + footer row of the CSV file
297 foreach (array_merge($this->rows, $this->footerRow) as $row) {
298 $csvrow = [];
299 foreach ($row as $field) {
300 if (!is_array($field) || isset($field['ignore_export'])) {
301 // skip value
302 continue;
303 }
304
305 // build value for export
306 $export_value = $field['value'];
307 if (!isset($field['no_export_callback']) && !isset($field['no_csv_callback'])) {
308 if (isset($field['export_callback']) && is_callable($field['export_callback'])) {
309 // trigger closure callback to prepare the value for export
310 $export_value = $field['export_callback']($field['value']);
311 } elseif (isset($field['callback']) && is_callable($field['callback'])) {
312 // trigger closure callback to prepare the value for export
313 $export_value = $field['callback']($field['value']);
314 }
315 }
316
317 // ensure no HTML is present
318 $export_value = strip_tags($export_value);
319
320 // apply encoding and transliteration, if needed
321 $enc_trans_value = $this->encodeExportCSVField($export_value);
322
323 // make sure transliteration or encoding did not break the string
324 if (empty($enc_trans_value) && !empty($export_value)) {
325 // fallback to original and raw value
326 $enc_trans_value = $export_value;
327 }
328
329 // push row value
330 $csvrow[] = $enc_trans_value;
331 }
332
333 // push the whole row
334 $csvlines[] = $csvrow;
335 }
336
337 // return the list of lines
338 return $csvlines;
339 }
340
341 /**
342 * Checks if a custom resource file pointer to export the file has been set.
343 *
344 * @return bool
345 *
346 * @since 1.16.1 (J) - 1.6.1 (WP)
347 */
348 public function hasExportHandler()
349 {
350 return is_resource($this->fp_export);
351 }
352
353 /**
354 * Returns the resource file pointer on which the CSV lines will be exported.
355 *
356 * @return resource
357 *
358 * @since 1.16.1 (J) - 1.6.1 (WP)
359 */
360 public function getExportCSVHandler()
361 {
362 if (is_null($this->fp_export)) {
363 // set the default resource file pointer (output)
364 $this->setExportCSVHandler();
365 }
366
367 return $this->fp_export;
368 }
369
370 /**
371 * Sets the resource file pointer on which the CSV lines will be exported.
372 * Useful in case the export should be made on a file rather than on output.
373 *
374 * @param string|resource $filename file path, identifier or resource.
375 * @param string $mode the mode for opening the file pointer.
376 *
377 * @return self
378 *
379 * @since 1.16.1 (J) - 1.6.1 (WP)
380 */
381 public function setExportCSVHandler($filename = 'php://output', $mode = 'w')
382 {
383 // set resource file pointer
384 if (is_resource($filename)) {
385 $this->fp_export = $filename;
386 } else {
387 $this->fp_export = fopen($filename, $mode);
388 }
389
390 return $this;
391 }
392
393 /**
394 * Sets the current type of CSV export format.
395 *
396 * @param string $format either "csv" or "excel".
397 *
398 * @return self
399 *
400 * @since 1.16.1 (J) - 1.6.1 (WP)
401 */
402 public function setExportCSVFormat($format)
403 {
404 $export_format = 'csv';
405 if (!strcasecmp($format, 'excel')) {
406 $export_format = 'excel';
407 }
408
409 // set format type
410 $this->csv_export_format = $export_format;
411
412 return $this;
413 }
414
415 /**
416 * Sends to output the necessary headers to download the export file.
417 *
418 * @param array $headers optional additional or actual headers.
419 * @param bool $replace if true, only the provided headers will be used.
420 *
421 * @return void
422 */
423 public function outputHeaders(array $headers = [], $replace = false)
424 {
425 foreach ($headers as $header) {
426 // send custom header
427 header($header);
428 }
429
430 if ($replace) {
431 // do not send any other header
432 return;
433 }
434
435 // force the download of the CSV file
436 if (!strcasecmp($this->csv_export_format, 'excel')) {
437 // file compatible with Excel
438 header('Content-type: text/csv; charset=UTF-16LE');
439 } else {
440 // regular CSV
441 header('Content-type: text/csv; charset=UTF-8');
442 }
443 header('Cache-Control: no-store, no-cache');
444 header('Content-Disposition: attachment; filename="' . addslashes(basename($this->getExportCSVFileName(), '.csv')) . '.csv"');
445 }
446
447 /**
448 * Writes the CSV lines to export onto the current resource handler.
449 *
450 * @return bool
451 *
452 * @since 1.16.1 (J) - 1.6.1 (WP)
453 */
454 public function outputCSV(array $csvlines)
455 {
456 // get resource file pointer
457 $fp = $this->getExportCSVHandler();
458
459 if (!$fp) {
460 // resource file pointer unavailable
461 return false;
462 }
463
464 // default CSV delimiter and enclosure
465 $separator = ',';
466 $enclosure = "\"";
467 if (!strcasecmp($this->csv_export_format, 'excel')) {
468 // for Excel we use the semicolon as separator and double quotes as enclosure
469 $separator = ';';
470 }
471
472 // send lines to output
473 foreach ($csvlines as $csvline) {
474 if (is_string($csvline)) {
475 // must be the first line instructions for the export format
476 fputs($fp, $csvline);
477
478 // go to the next line
479 continue;
480 }
481
482 // put the array of values as a new CSV line
483 fputcsv($fp, $csvline, $separator, $enclosure);
484 }
485
486 // close the file pointer
487 fclose($fp);
488
489 return true;
490 }
491
492 /**
493 * Loads a specific report class and returns its instance.
494 * Should be called for instantiating any report sub-class.
495 *
496 * @param string $report the report file name (i.e. "revenue").
497 *
498 * @return mixed false or requested report object.
499 */
500 public static function getInstanceOf($report)
501 {
502 if (empty($report) || !is_string($report)) {
503 return false;
504 }
505
506 if (substr($report, -4) != '.php') {
507 $report .= '.php';
508 }
509
510 $report_path = dirname(__FILE__) . DIRECTORY_SEPARATOR . $report;
511 $classname = 'VikBookingReport' . str_replace(' ', '', ucwords(str_replace('.php', '', str_replace('_', ' ', $report))));
512
513 if (!is_file($report_path)) {
514 /**
515 * Trigger event to let other plugins register additional drivers.
516 *
517 * @since 1.16.0 (J) - 1.6.0 (WP)
518 */
519 $list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadPmsReports');
520 foreach ($list as $chunk) {
521 if (!is_array($chunk) || !$chunk) {
522 continue;
523 }
524 foreach ($chunk as $thirdp_report) {
525 if (basename($thirdp_report) == $report) {
526 // driver found
527 $report_path = $thirdp_report;
528 break;
529 }
530 }
531 }
532 }
533
534 if (!is_file($report_path)) {
535 // report driver file not found
536 return false;
537 }
538
539 // load report
540 require_once $report_path;
541
542 if (class_exists($classname)) {
543 // return the instance of the report object found
544 return new $classname;
545 }
546
547 return false;
548 }
549
550 /**
551 * Injects request variables for the report like if some filters were set.
552 *
553 * @param array $vars associative list of request vars to inject.
554 *
555 * @return void
556 *
557 * @since 1.16.1 (J) - 1.6.1 (WP) static-context used to construct the report object later.
558 */
559 public static function setRequestVars(array $vars)
560 {
561 foreach ($vars as $key => $value) {
562 /**
563 * For more safety across different platforms and versions (J3/J4 or WP)
564 * we inject values in the super global array as well as in the input object.
565 */
566 VikRequest::setVar($key, $value, 'request');
567 VikRequest::setVar($key, $value);
568 }
569 }
570
571 /**
572 * Proxy for object-context to inject request variables for the report.
573 *
574 * @param array $params associative list of request vars to inject.
575 *
576 * @return self
577 */
578 public function injectParams($params)
579 {
580 if (is_array($params) && $params) {
581 self::setRequestVars($params);
582 }
583
584 return $this;
585 }
586
587 /**
588 * Loads Charts CSS/JS assets.
589 *
590 * @return self
591 */
592 public function loadChartsAssets()
593 {
594 $document = JFactory::getDocument();
595 $document->addStyleSheet(VBO_ADMIN_URI . 'resources/Chart.min.css', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
596 $document->addScript(VBO_ADMIN_URI . 'resources/Chart.min.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
597
598 return $this;
599 }
600
601 /**
602 * Loads the jQuery UI Datepicker.
603 * Method used only by sub-classes.
604 *
605 * @return self
606 */
607 protected function loadDatePicker()
608 {
609 $vbo_app = VikBooking::getVboApplication();
610 $vbo_app->loadDatePicker();
611
612 return $this;
613 }
614
615 /**
616 * Applies the proper encoding to the field being added to
617 * the CSV lines for export, depending on CSV or Excel.
618 *
619 * @param string $field the value being added to the export line.
620 *
621 * @return string either the original or the properly encoded field.
622 *
623 * @since 1.16.1 (J) - 1.6.1 (WP)
624 * @since 1.16.8 (J) - 1.6.8 (WP) introduced hook to allow to manipulate encoding.
625 */
626 protected function encodeExportCSVField($field)
627 {
628 /**
629 * Trigger event to allow third-party plugins to manipulate encoding,
630 * in case certain dependencies are not available, such as "mb" support,
631 * PECL intl for "transliterator_transliterate" or "iconv" to ASCII.
632 *
633 * @since 1.16.8 (J) - 1.6.8 (WP)
634 */
635 $apply_encoding = VBOFactory::getPlatform()->getDispatcher()->filter('onBeforeEncodingCSVField', [&$field, $this->csv_export_format]);
636
637 if (in_array(false, $apply_encoding, true)) {
638 // the hook ordered to not proceed with applying any encoding
639 return $field;
640 }
641
642 if (!is_string($field) || !strcasecmp($this->csv_export_format, 'csv')) {
643 // apply no encoding in case of regular CSV or if non-string data type
644 return $field;
645 }
646
647 // process the Excel-like string field
648 if (preg_match('/[\\x80-\\xff]/', $field)) {
649 // UTF-8 encoding detected
650 if (function_exists('transliterator_transliterate')) {
651 // if Transliterator is available (PECL intl >= 2.0.0), transliterate to ASCII
652 $field = transliterator_transliterate('Any-Latin; Latin-ASCII;', $field);
653 }
654
655 // attempt to convert UTF-8 to ASCII to support currencies
656 $field = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $field);
657 }
658
659 if (!function_exists('mb_convert_encoding')) {
660 // abort to prevent server errors
661 return $field;
662 }
663
664 // convert encoding to UTF-16LE (low-endian with BOM)
665 return mb_convert_encoding($field, 'UTF-16LE', ['ASCII', 'UTF-8', 'ISO-8859-1']);
666 }
667
668 /**
669 * Loads all the rooms in VBO and returns the array.
670 *
671 * @return array
672 */
673 protected function getRooms()
674 {
675 $q = "SELECT * FROM `#__vikbooking_rooms` ORDER BY `name` ASC;";
676 $this->dbo->setQuery($q);
677 $rooms = $this->dbo->loadAssocList();
678
679 return $rooms;
680 }
681
682 /**
683 * Loads all the rate plans in VBO and returns the array.
684 *
685 * @return array
686 *
687 * @since 1.15.0 (J) - 1.5.0 (WP)
688 */
689 protected function getRatePlans()
690 {
691 $q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
692 $this->dbo->setQuery($q);
693 $rplans = $this->dbo->loadAssocList();
694
695 return VikBooking::sortRatePlans($rplans);
696 }
697
698 /**
699 * Returns the number of total units for all rooms, or for a specific room.
700 * By default, the rooms unpublished are skipped, and all rooms are used.
701 *
702 * @param [mixed] $idroom int or array.
703 * @param [int] $published true or false.
704 *
705 * @return int
706 */
707 protected function countRooms($idroom = 0, $published = 1)
708 {
709 $clauses = [];
710 if (is_int($idroom) && $idroom > 0) {
711 $clauses[] = "`id`=".(int)$idroom;
712 } elseif (is_array($idroom) && $idroom) {
713 $clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
714 }
715 if ($published) {
716 $clauses[] = "`avail`=1";
717 }
718
719 $q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`".($clauses ? " WHERE ".implode(' AND ', $clauses) : "").";";
720 $this->dbo->setQuery($q);
721 $totrooms = (int)$this->dbo->loadResult();
722
723 return $totrooms;
724 }
725
726 /**
727 * Concatenates the JavaScript rules.
728 * Method used only by sub-classes.
729 *
730 * @param string $str
731 *
732 * @return self
733 */
734 protected function setScript($str)
735 {
736 $this->reportScript .= $str."\n";
737
738 return $this;
739 }
740
741 /**
742 * Gets the current script string.
743 *
744 * @return string
745 */
746 public function getScript()
747 {
748 return rtrim($this->reportScript, "\n");
749 }
750
751 /**
752 * Returns the date format in VBO for date, jQuery UI, Joomla/WordPress.
753 * The visibility of this method should be public for anyone who needs it.
754 *
755 * @param string $type
756 *
757 * @return string
758 */
759 public function getDateFormat($type = 'date')
760 {
761 $nowdf = VikBooking::getDateFormat();
762 if ($nowdf == "%d/%m/%Y") {
763 $df = 'd/m/Y';
764 $juidf = 'dd/mm/yy';
765 } elseif ($nowdf == "%m/%d/%Y") {
766 $df = 'm/d/Y';
767 $juidf = 'mm/dd/yy';
768 } else {
769 $df = 'Y/m/d';
770 $juidf = 'yy/mm/dd';
771 }
772
773 switch ($type) {
774 case 'jui':
775 return $juidf;
776 case 'joomla':
777 case 'wordpress':
778 return $nowdf;
779 default:
780 return $df;
781 }
782 }
783
784 /**
785 * Returns the translated weekday.
786 * Uses the back-end language definitions.
787 *
788 * @param int $wday
789 * @param string $type use 'long' for the full name of the week, short for the 3-char version
790 *
791 * @return string
792 */
793 protected function getWdayString($wday, $type = 'long')
794 {
795 $wdays_map_long = [
796 JText::translate('VBWEEKDAYZERO'),
797 JText::translate('VBWEEKDAYONE'),
798 JText::translate('VBWEEKDAYTWO'),
799 JText::translate('VBWEEKDAYTHREE'),
800 JText::translate('VBWEEKDAYFOUR'),
801 JText::translate('VBWEEKDAYFIVE'),
802 JText::translate('VBWEEKDAYSIX')
803 ];
804
805 $wdays_map_short = [
806 JText::translate('VBSUN'),
807 JText::translate('VBMON'),
808 JText::translate('VBTUE'),
809 JText::translate('VBWED'),
810 JText::translate('VBTHU'),
811 JText::translate('VBFRI'),
812 JText::translate('VBSAT')
813 ];
814
815 if ($type != 'long') {
816 return isset($wdays_map_short[(int)$wday]) ? $wdays_map_short[(int)$wday] : '';
817 }
818
819 return isset($wdays_map_long[(int)$wday]) ? $wdays_map_long[(int)$wday] : '';
820 }
821
822 /**
823 * Sets the columns for this report.
824 *
825 * @param array $arr
826 *
827 * @return self
828 */
829 public function setReportCols($arr)
830 {
831 $this->cols = $arr;
832
833 return $this;
834 }
835
836 /**
837 * Returns the columns for this report.
838 * Should be called after getReportData()
839 * or the returned array will be empty.
840 *
841 * @return array
842 */
843 public function getReportCols()
844 {
845 return $this->cols;
846 }
847
848 /**
849 * Sorts the rows of the report by key.
850 *
851 * @param string $krsort the key attribute of the array pairs
852 * @param string $krorder ascending (ASC) or descending (DESC)
853 *
854 * @return void
855 */
856 protected function sortRows($krsort, $krorder)
857 {
858 if (empty($krsort) || !$this->rows) {
859 return;
860 }
861
862 $map = [];
863 foreach ($this->rows as $k => $row) {
864 foreach ($row as $kk => $v) {
865 if (isset($v['key']) && $v['key'] == $krsort) {
866 $map[$k] = $v['value'];
867 }
868 }
869 }
870 if (!$map) {
871 return;
872 }
873
874 if ($krorder == 'ASC') {
875 asort($map);
876 } else {
877 arsort($map);
878 }
879
880 $sorted = [];
881 foreach ($map as $k => $v) {
882 $sorted[$k] = $this->rows[$k];
883 }
884
885 $this->rows = $sorted;
886 }
887
888 /**
889 * Sets the rows for this report.
890 *
891 * @param array $arr
892 *
893 * @return self
894 */
895 public function setReportRows($arr)
896 {
897 $this->rows = $arr;
898
899 return $this;
900 }
901
902 /**
903 * Returns the rows for this report.
904 * Should be called after getReportData()
905 * or the returned array will be empty.
906 *
907 * @return array
908 */
909 public function getReportRows()
910 {
911 return $this->rows;
912 }
913
914 /**
915 * This method returns one or more rows (given the depth) generated by
916 * the current report invoked. It is useful to clean up the callbacks
917 * of the various cell-rows, to obtain a parsable result.
918 * Can be called as first method, by skipping also getReportData().
919 *
920 * @param int $depth how many records to obtain, null for all.
921 *
922 * @return array the queried report value in the given depth.
923 *
924 * @uses getReportData()
925 */
926 public function getReportValues($depth = null)
927 {
928 if (!$this->rows && !$this->getReportData()) {
929 return [];
930 }
931
932 $report_values = [];
933
934 foreach ($this->rows as $rk => $row) {
935 $report_values[$rk] = [];
936 foreach ($row as $col => $coldata) {
937 $display_value = $coldata['value'];
938 if (isset($coldata['callback']) && is_callable($coldata['callback'])) {
939 // launch callback
940 $display_value = $coldata['callback']($coldata['value']);
941 }
942 // push column value
943 $report_values[$rk][$coldata['key']] = [
944 'value' => $coldata['value'],
945 'display_value' => $display_value,
946 ];
947 /**
948 * We also pass along any reserved key for this row-data.
949 *
950 * @since 1.15.0 (J) - 1.5.0 (WP)
951 */
952 foreach ($coldata as $res_key => $data_val) {
953 if (substr($res_key, 0, 1) == '_') {
954 // push this reserved key
955 $report_values[$rk][$coldata['key']][$res_key] = $data_val;
956 }
957 }
958 }
959 }
960
961 if (!$report_values) {
962 return [];
963 }
964
965 if ($depth === 1) {
966 // get an associative array with the first row calculated
967 return $report_values[0];
968 }
969
970 if (is_int($depth) && $depth > 0 && count($report_values) >= $depth) {
971 // get the requested portion of the array
972 return array_slice($report_values, 0, $depth);
973 }
974
975 return $report_values;
976 }
977
978 /**
979 * Maps the columns labels to an associative array to be used for the values.
980 *
981 * @return array associative list of column keys and related values.
982 */
983 public function getColumnsValues()
984 {
985 if (!$this->cols) {
986 return [];
987 }
988
989 $col_values = [];
990
991 foreach ($this->cols as $col) {
992 if (!isset($col['key'])) {
993 continue;
994 }
995 $col_values[$col['key']] = $col;
996 unset($col_values[$col['key']]['key']);
997 }
998
999 return $col_values;
1000 }
1001
1002 /**
1003 * Gets a property defined by the report. Useful to get custom
1004 * properties set up by a specific report maybe for the Chart.
1005 *
1006 * @param string $property the name of the property needed.
1007 * @param mixed $def default value to return.
1008 *
1009 * @return mixed false on failure, property requested otherwise.
1010 */
1011 public function getProperty($property, $def = false)
1012 {
1013 if (isset($this->{$property})) {
1014 return $this->{$property};
1015 }
1016
1017 return $def;
1018 }
1019
1020 /**
1021 * Counts the number of days of difference between two timestamps.
1022 *
1023 * @param int $to_ts the target end date timestamp.
1024 * @param int $from_ts the starting date timestamp.
1025 *
1026 * @return int the days of difference between from and to timestamps.
1027 */
1028 public function countDaysTo($to_ts, $from_ts = 0)
1029 {
1030 if (empty($from_ts)) {
1031 $from_ts = time();
1032 }
1033
1034 // whether DateTime can be used
1035 $usedt = false;
1036
1037 if (class_exists('DateTime')) {
1038 $from_date = new DateTime(date('Y-m-d', $from_ts));
1039 if (method_exists($from_date, 'diff')) {
1040 $usedt = true;
1041 }
1042 }
1043
1044 if ($usedt) {
1045 $to_date = new DateTime(date('Y-m-d', $to_ts));
1046 $daysdiff = (int)$from_date->diff($to_date)->format('%a');
1047 if ($to_ts < $from_ts) {
1048 // we need a negative integer number
1049 $daysdiff = $daysdiff - ($daysdiff * 2);
1050 }
1051 return $daysdiff;
1052 }
1053
1054 return (int)round(($to_ts - $from_ts) / 86400);
1055 }
1056
1057 /**
1058 * Counts the average difference between two integers.
1059 *
1060 * @param int $in_days_from days to the lowest timestamp.
1061 * @param int $in_days_to days to the highest timestamp.
1062 *
1063 * @return int the average number between the two values.
1064 */
1065 public function countAverageDays($in_days_from, $in_days_to)
1066 {
1067 return (int)floor(($in_days_from + $in_days_to) / 2);
1068 }
1069
1070 /**
1071 * Sets the footer row (the totals) for this report.
1072 *
1073 * @param array $arr
1074 *
1075 * @return self
1076 */
1077 protected function setReportFooterRow($arr)
1078 {
1079 $this->footerRow = $arr;
1080
1081 return $this;
1082 }
1083
1084 /**
1085 * Returns the footer row for this report.
1086 * Should be called after getReportData()
1087 * or the returned array will be empty.
1088 *
1089 * @return array
1090 */
1091 public function getReportFooterRow()
1092 {
1093 return $this->footerRow;
1094 }
1095
1096 /**
1097 * Sub-classes can extend this method to define the
1098 * the canvas HTML tag for rendenring the Chart.
1099 * Any necessary script shall be set within this method.
1100 * Data can be passed as a mixed value through the argument.
1101 * This is the first method to be called when working with the Chart.
1102 *
1103 * @param mixed $data any necessary value to render the Chart.
1104 *
1105 * @return string the HTML of the canvas element.
1106 */
1107 public function getChart($data = null)
1108 {
1109 return '';
1110 }
1111
1112 /**
1113 * Sub-classes can extend this method to define the
1114 * the title of the Chart to be rendered.
1115 *
1116 * @return string the title of the Chart.
1117 */
1118 public function getChartTitle()
1119 {
1120 return '';
1121 }
1122
1123 /**
1124 * Sub-classes can extend this method to define
1125 * the meta data for the Chart containing stats.
1126 * An array for each meta-data should be returned.
1127 *
1128 * @param mixed $position string for the meta-data position
1129 * in the Chart (top, right, bottom).
1130 * @param mixed $data some arguments to be passed.
1131 *
1132 * @return array
1133 */
1134 public function getChartMetaData($position = null, $data = null)
1135 {
1136 return [];
1137 }
1138
1139 /**
1140 * Sets an array of custom options for this report. Useful to inject
1141 * params before getting the report data and changing the behavior.
1142 *
1143 * @param array $options The associative options to set.
1144 *
1145 * @return self
1146 *
1147 * @since 1.15.0 (J) - 1.5.0 (WP)
1148 */
1149 public function setReportOptions(array $options = [])
1150 {
1151 $this->options = $options;
1152
1153 return $this;
1154 }
1155
1156 /**
1157 * Returns the custom options for the report. Useful to
1158 * behave differently depending on who calls the report.
1159 * By default, the method returns an instance of JObject
1160 * to easily access all custom options defined, if any.
1161 *
1162 * @param bool $registry true to get a JObject instance.
1163 *
1164 * @return mixed instance of JObject or raw array.
1165 *
1166 * @since 1.15.0 (J) - 1.5.0 (WP)
1167 */
1168 public function getReportOptions($registry = true)
1169 {
1170 if ($registry) {
1171 return new JObject($this->options);
1172 }
1173
1174 return $this->options;
1175 }
1176
1177 /**
1178 * Defines an associative list of action data.
1179 *
1180 * @param array $data Associative list of action data.
1181 *
1182 * @return self
1183 *
1184 * @since 1.17.1 (J) - 1.7.1 (WP)
1185 */
1186 public function setActionData(array $data)
1187 {
1188 $this->actionData = $data;
1189
1190 return $this;
1191 }
1192
1193 /**
1194 * Returns the action data, either raw or as a registry.
1195 *
1196 * @param bool $registry True to get a JObject instance.
1197 *
1198 * @return array|JObject
1199 *
1200 * @since 1.17.1 (J) - 1.7.1 (WP)
1201 */
1202 public function getActionData($registry = true)
1203 {
1204 if ($registry) {
1205 return new JObject($this->actionData);
1206 }
1207
1208 return $this->actionData;
1209 }
1210
1211 /**
1212 * Sets the global scope for the report.
1213 *
1214 * @param string $scope The scope to set.
1215 *
1216 * @return self
1217 *
1218 * @since 1.17.1 (J) - 1.7.1 (WP)
1219 */
1220 public function setScope($scope)
1221 {
1222 $this->scope = $scope;
1223
1224 return $this;
1225 }
1226
1227 /**
1228 * Returns the global scope of the invoked report.
1229 *
1230 * @return ?string
1231 *
1232 * @since 1.17.1 (J) - 1.7.1 (WP)
1233 */
1234 public function getScope()
1235 {
1236 return $this->scope;
1237 }
1238
1239 /**
1240 * Returns the path to the PMS media data directory.
1241 *
1242 * @return string
1243 *
1244 * @since 1.17.1 (J) - 1.7.1 (WP)
1245 */
1246 public function getDataMediaPath()
1247 {
1248 return implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'pmsdata']);
1249 }
1250
1251 /**
1252 * Returns the URL to the PMS media data directory.
1253 *
1254 * @return string
1255 *
1256 * @since 1.17.1 (J) - 1.7.1 (WP)
1257 */
1258 public function getDataMediaUrl()
1259 {
1260 return VBO_ADMIN_URI . 'resources/pmsdata/';
1261 }
1262
1263 /**
1264 * Returns the optional report custom setting fields.
1265 *
1266 * @return array
1267 *
1268 * @since 1.17.1 (J) - 1.7.1 (WP)
1269 */
1270 public function getSettingFields()
1271 {
1272 return [];
1273 }
1274
1275 /**
1276 * Returns the report custom settings defined.
1277 *
1278 * @return array
1279 *
1280 * @since 1.17.1 (J) - 1.7.1 (WP)
1281 */
1282 public function loadSettings()
1283 {
1284 return (array) VBOFactory::getConfig()->getArray('report_settings_' . $this->getFileName(), []);
1285 }
1286
1287 /**
1288 * Saves the report custom settings defined.
1289 *
1290 * @param array $data The associative list of settings to save.
1291 * @param bool $merge If true, the previous settings will be merged.
1292 *
1293 * @return void
1294 *
1295 * @since 1.17.1 (J) - 1.7.1 (WP)
1296 */
1297 public function saveSettings(array $data, $merge = true)
1298 {
1299 if ($merge) {
1300 $data = array_merge($this->loadSettings(), $data);
1301 }
1302
1303 VBOFactory::getConfig()->set('report_settings_' . $this->getFileName(), $data);
1304 }
1305
1306 /**
1307 * Returns a numeric list of scoped extra actions.
1308 *
1309 * @param string $scope Optional scope identifier (cron, web, etc..).
1310 * @param bool $visible If true, the hidden actions will not be returned.
1311 *
1312 * @return array
1313 *
1314 * @since 1.17.1 (J) - 1.7.1 (WP)
1315 */
1316 public function getScopedActions($scope = null, $visible = true)
1317 {
1318 return [];
1319 }
1320
1321 /**
1322 * Executes a custom scoped action within the report.
1323 *
1324 * @param string $action The custom action to invoke.
1325 * @param string $scope Optional scope identifier (cron, web, etc..).
1326 * @param array $data Optional data for the action to execute.
1327 *
1328 * @return mixed
1329 *
1330 * @throws Exception
1331 *
1332 * @since 1.17.1 (J) - 1.7.1 (WP)
1333 */
1334 public function executeAction($action, $scope = null, array $data = [])
1335 {
1336 if (is_null($scope)) {
1337 $scope = $this->getScope();
1338 }
1339
1340 if (!$data) {
1341 $data = $this->getActionData($registry = false);
1342 }
1343
1344 $callable = [$this, $action];
1345
1346 if (!is_callable($callable)) {
1347 throw new Exception('Could not call the requested report action.', 500);
1348 }
1349
1350 return call_user_func_array($callable, [$scope, $data]);
1351 }
1352
1353 /**
1354 * Proxy for executing a custom scoped action and returning a property.
1355 *
1356 * @param string $action The custom action to invoke.
1357 * @param string $return The action result property to return.
1358 * @param string $scope Optional scope identifier (cron, web, etc..).
1359 * @param array $data Optional data for the action to execute.
1360 *
1361 * @return mixed
1362 *
1363 * @throws Exception
1364 *
1365 * @since 1.17.1 (J) - 1.7.1 (WP)
1366 */
1367 public function _callActionReturn($action, $return, $scope = null, array $data = [])
1368 {
1369 if (is_null($scope)) {
1370 $scope = $this->getScope();
1371 }
1372
1373 if (!$data) {
1374 $data = $this->getActionData($registry = false);
1375 }
1376
1377 $result = (array) $this->executeAction($action, $scope, $data);
1378
1379 if (!isset($result[$return])) {
1380 throw new Exception('Could not return the requested action result property.', 500);
1381 }
1382
1383 return $result[$return];
1384 }
1385
1386 /**
1387 * Defines a new resource file generated through a custom action.
1388 *
1389 * @param array $data Resource file data to bind.
1390 *
1391 * @return self
1392 *
1393 * @since 1.17.1 (J) - 1.7.1 (WP)
1394 */
1395 protected function defineResourceFile(array $data)
1396 {
1397 $element = new VBOReportResourceElement($data);
1398
1399 if ($element->getUrl()) {
1400 // ensure the resource is available
1401 $this->resourceFiles[] = $element;
1402 }
1403
1404 return $this;
1405 }
1406
1407 /**
1408 * Returns the resource files generated through custom actions.
1409 *
1410 * @return array
1411 *
1412 * @since 1.17.1 (J) - 1.7.1 (WP)
1413 */
1414 public function getResourceFiles()
1415 {
1416 return $this->resourceFiles;
1417 }
1418
1419 /**
1420 * Sets warning messages by concatenating the existing ones.
1421 * Method used only by sub-classes.
1422 *
1423 * @param string $str
1424 *
1425 * @return self
1426 */
1427 protected function setWarning($str)
1428 {
1429 $this->warning .= $str."\n";
1430
1431 return $this;
1432 }
1433
1434 /**
1435 * Gets the current warning string.
1436 *
1437 * @return string
1438 */
1439 public function getWarning()
1440 {
1441 return rtrim($this->warning, "\n");
1442 }
1443
1444 /**
1445 * Sets errors by concatenating the existing ones.
1446 * Method used only by sub-classes.
1447 *
1448 * @param string $str
1449 *
1450 * @return self
1451 */
1452 protected function setError($str)
1453 {
1454 $this->error .= $str."\n";
1455
1456 return $this;
1457 }
1458
1459 /**
1460 * Gets the current error string.
1461 *
1462 * @return string
1463 */
1464 public function getError()
1465 {
1466 return rtrim($this->error, "\n");
1467 }
1468 }
1469