File "report.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/report/report.php
File size: 38.27 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @package VikBooking
* @subpackage com_vikbooking
* @author Alessio Gaggii - E4J srl
* @copyright Copyright (C) 2025 E4J srl. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
* @link https://vikwp.com
*/
defined('ABSPATH') or die('No script kiddies please!');
/**
* PMS Report abstract-parent class that all drivers should extend.
*/
#[\AllowDynamicProperties]
abstract class VikBookingReport
{
/**
* @var string
*/
protected $reportName = '';
/**
* @var string
*/
protected $reportFile = '';
/**
* @var array
*/
protected $reportFilters = [];
/**
* @var string
*/
protected $reportScript = '';
/**
* @var string
*/
protected $warning = '';
/**
* @var string
*/
protected $error = '';
/**
* @var object
*/
protected $dbo;
/**
* @var array
*/
protected $cols = [];
/**
* @var array
*/
protected $rows = [];
/**
* @var array
*/
protected $footerRow = [];
/**
* An array of custom options to be passed to the report.
* Reports can use them before generating the report data.
*
* @var array
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
protected $options = [];
/**
* @var resource
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
protected $fp_export = null;
/**
* @var string
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
protected $csv_export_format = 'csv';
/**
* @var string
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
protected $csv_export_fname = '';
/**
* @var array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
protected $actionData = [];
/**
* @var array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
protected $resourceFiles = [];
/**
* @var ?string
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
protected $scope = null;
/**
* Class constructor should define the name of
* the report and the filters to be displayed.
*/
public function __construct()
{
$this->dbo = JFactory::getDbo();
}
/**
* Extending Classes should define this method
* to get the name of the report.
*/
abstract public function getName();
/**
* Extending Classes should define this method
* to get the name of class file.
*/
abstract public function getFileName();
/**
* Extending Classes should define this method
* to get the filters of the report.
*/
abstract public function getFilters();
/**
* Extending Classes should define this method
* to generate the report data (cols and rows).
*/
abstract public function getReportData();
/**
* Main method to generate the report columns and rows. Stores on the current
* resource the CSV lines and forces the browser to download the file. In case
* of errors, the process is not terminated to let the View display the errors.
*
* @return void|bool script termination on success, false otherwise.
*
* @since 1.16.1 (J) - 1.6.1 (WP) drivers no longer need to implement it,
* unless the driver needs to override it.
*/
public function exportCSV()
{
// grab all report lines to export
$csvlines = $this->getExportCSVLines();
if (!$csvlines) {
// no data to export
return false;
}
// force the download of the CSV file
$this->outputHeaders();
/**
* Trigger event to allow third-party plugins to set additional headers or contents to output.
*
* @since 1.16.8 (J) - 1.6.8 (WP)
*/
VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeExportOutputCSV', [$this, $this->csv_export_format]);
// send lines to output
$this->outputCSV($csvlines);
exit;
}
/**
* Returns the name to give to the CSV (or custom) file being exported.
*
* @param bool $cut_suffix if true the file name suffix will be cut off.
* @param string $suffix the optional file name suffix, hence the extension.
* @param bool $pretty if true, dashes will be converted to spaces and more.
*
* @return string
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function getExportCSVFileName($cut_suffix = false, $suffix = '.csv', $pretty = false)
{
if ($this->csv_export_fname) {
// use the exact report file name set
$export_fname = $this->csv_export_fname;
} else {
// use a generic file name
$export_fname = date('Y-m-d_H.i.s') . '-' . $this->reportFile . '.csv';
}
if ($cut_suffix) {
// cut off the file name suffix
if (!$suffix) {
// detect file extension
$suffix = substr($export_fname, strrpos($export_fname, '.'));
}
$export_fname = basename($export_fname, $suffix);
}
if ($pretty) {
// get the default date separator char
$datesep = VikBooking::getDateSeparator();
// add a dash between a range of dates
$export_fname = preg_replace("/([0-9])-([0-9])/i", '$1 - $2', $export_fname);
// add an empty space between report name and channel name
$export_fname = preg_replace("/([A-Z])-([A-Z])/i", '$1 $2', $export_fname);
// add an empty space between report name and date
$export_fname = preg_replace("/([A-Z0-9])-([0-9])/i", '$1 $2', $export_fname);
// convert the dashes to the actual date separator char
$export_fname = preg_replace("/([0-9])_([0-9])/i", '$1' . $datesep . '$2', $export_fname);
}
return $export_fname;
}
/**
* Sets the name to give to the CSV (or custom) file being exported.
*
* @param string $fname the full name of the file to export.
*
* @return self
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function setExportCSVFileName($fname)
{
$this->csv_export_fname = (string)$fname;
return $this;
}
/**
* Builds the list of the CSV lines to be exported from the current report data.
*
* @param bool $no_data true for actually not letting the report run.
*
* @return array list of CSV lines containing lists of CSV fields.
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function getExportCSVLines($no_data = false)
{
if (!$no_data && !$this->getReportData()) {
// nothing to export
return [];
}
// list of CSV lines
$csvlines = [];
if (!strcasecmp($this->csv_export_format, 'excel')) {
// add instructions for Excel
// UTF-8 BOM (not needed because we convert the encoding to UTF-16LE, which has BOM)
// $csvlines[] = chr(0xEF) . chr(0xBB) . chr(0xBF);
// define the separator
$csvlines[] = "sep=;\n";
}
// push the head of the CSV file
$csvcols = [];
foreach ($this->cols as $col) {
if (!is_array($col) || isset($col['ignore_export'])) {
// skip column
continue;
}
// push column label
$csvcols[] = $this->encodeExportCSVField($col['label']);
}
// push all columns
$csvlines[] = $csvcols;
// push the rows + footer row of the CSV file
foreach (array_merge($this->rows, $this->footerRow) as $row) {
$csvrow = [];
foreach ($row as $field) {
if (!is_array($field) || isset($field['ignore_export'])) {
// skip value
continue;
}
// build value for export
$export_value = $field['value'];
if (!isset($field['no_export_callback']) && !isset($field['no_csv_callback'])) {
if (isset($field['export_callback']) && is_callable($field['export_callback'])) {
// trigger closure callback to prepare the value for export
$export_value = $field['export_callback']($field['value']);
} elseif (isset($field['callback']) && is_callable($field['callback'])) {
// trigger closure callback to prepare the value for export
$export_value = $field['callback']($field['value']);
}
}
// ensure no HTML is present
$export_value = strip_tags($export_value);
// apply encoding and transliteration, if needed
$enc_trans_value = $this->encodeExportCSVField($export_value);
// make sure transliteration or encoding did not break the string
if (empty($enc_trans_value) && !empty($export_value)) {
// fallback to original and raw value
$enc_trans_value = $export_value;
}
// push row value
$csvrow[] = $enc_trans_value;
}
// push the whole row
$csvlines[] = $csvrow;
}
// return the list of lines
return $csvlines;
}
/**
* Checks if a custom resource file pointer to export the file has been set.
*
* @return bool
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function hasExportHandler()
{
return is_resource($this->fp_export);
}
/**
* Returns the resource file pointer on which the CSV lines will be exported.
*
* @return resource
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function getExportCSVHandler()
{
if (is_null($this->fp_export)) {
// set the default resource file pointer (output)
$this->setExportCSVHandler();
}
return $this->fp_export;
}
/**
* Sets the resource file pointer on which the CSV lines will be exported.
* Useful in case the export should be made on a file rather than on output.
*
* @param string|resource $filename file path, identifier or resource.
* @param string $mode the mode for opening the file pointer.
*
* @return self
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function setExportCSVHandler($filename = 'php://output', $mode = 'w')
{
// set resource file pointer
if (is_resource($filename)) {
$this->fp_export = $filename;
} else {
$this->fp_export = fopen($filename, $mode);
}
return $this;
}
/**
* Sets the current type of CSV export format.
*
* @param string $format either "csv" or "excel".
*
* @return self
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function setExportCSVFormat($format)
{
$export_format = 'csv';
if (!strcasecmp($format, 'excel')) {
$export_format = 'excel';
}
// set format type
$this->csv_export_format = $export_format;
return $this;
}
/**
* Sends to output the necessary headers to download the export file.
*
* @param array $headers optional additional or actual headers.
* @param bool $replace if true, only the provided headers will be used.
*
* @return void
*/
public function outputHeaders(array $headers = [], $replace = false)
{
foreach ($headers as $header) {
// send custom header
header($header);
}
if ($replace) {
// do not send any other header
return;
}
// force the download of the CSV file
if (!strcasecmp($this->csv_export_format, 'excel')) {
// file compatible with Excel
header('Content-type: text/csv; charset=UTF-16LE');
} else {
// regular CSV
header('Content-type: text/csv; charset=UTF-8');
}
header('Cache-Control: no-store, no-cache');
header('Content-Disposition: attachment; filename="' . addslashes(basename($this->getExportCSVFileName(), '.csv')) . '.csv"');
}
/**
* Writes the CSV lines to export onto the current resource handler.
*
* @return bool
*
* @since 1.16.1 (J) - 1.6.1 (WP)
*/
public function outputCSV(array $csvlines)
{
// get resource file pointer
$fp = $this->getExportCSVHandler();
if (!$fp) {
// resource file pointer unavailable
return false;
}
// default CSV delimiter and enclosure
$separator = ',';
$enclosure = '"';
if (!strcasecmp($this->csv_export_format, 'excel')) {
// for Excel we use the semicolon as separator and double quotes as enclosure
$separator = ';';
}
// send lines to output
foreach ($csvlines as $csvline) {
if (is_string($csvline)) {
// must be the first line instructions for the export format
fputs($fp, $csvline);
// go to the next line
continue;
}
// put the array of values as a new CSV line
fputcsv($fp, $csvline, $separator, $enclosure, $escape = '');
}
// close the file pointer
fclose($fp);
return true;
}
/**
* Loads a specific report class and returns its instance.
* Should be called for instantiating any report sub-class.
*
* @param string $report the report file name (i.e. "revenue").
*
* @return mixed false or requested report object.
*/
public static function getInstanceOf($report)
{
if (empty($report) || !is_string($report)) {
return false;
}
if (substr($report, -4) != '.php') {
$report .= '.php';
}
$report_path = dirname(__FILE__) . DIRECTORY_SEPARATOR . $report;
$classname = 'VikBookingReport' . str_replace(' ', '', ucwords(str_replace('.php', '', str_replace('_', ' ', $report))));
if (!is_file($report_path)) {
/**
* Trigger event to let other plugins register additional drivers.
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
$list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadPmsReports');
foreach ($list as $chunk) {
if (!is_array($chunk) || !$chunk) {
continue;
}
foreach ($chunk as $thirdp_report) {
if (basename($thirdp_report) == $report) {
// driver found
$report_path = $thirdp_report;
break;
}
}
}
}
if (!is_file($report_path)) {
// report driver file not found
return false;
}
// load report
require_once $report_path;
if (class_exists($classname)) {
// return the instance of the report object found
return new $classname;
}
return false;
}
/**
* Injects request variables for the report like if some filters were set.
*
* @param array $vars associative list of request vars to inject.
*
* @return void
*
* @since 1.16.1 (J) - 1.6.1 (WP) static-context used to construct the report object later.
*/
public static function setRequestVars(array $vars)
{
foreach ($vars as $key => $value) {
/**
* For more safety across different platforms and versions (J3/J4 or WP)
* we inject values in the super global array as well as in the input object.
*/
VikRequest::setVar($key, $value, 'request');
VikRequest::setVar($key, $value);
}
}
/**
* Proxy for object-context to inject request variables for the report.
*
* @param array $params associative list of request vars to inject.
*
* @return self
*/
public function injectParams($params)
{
if (is_array($params) && $params) {
self::setRequestVars($params);
}
return $this;
}
/**
* Loads Charts CSS/JS assets.
*
* @return self
*/
public function loadChartsAssets()
{
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_ADMIN_URI . 'resources/Chart.min.css', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
$document->addScript(VBO_ADMIN_URI . 'resources/Chart.min.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
return $this;
}
/**
* Loads the jQuery UI Datepicker.
* Method used only by sub-classes.
*
* @return self
*/
protected function loadDatePicker()
{
$vbo_app = VikBooking::getVboApplication();
$vbo_app->loadDatePicker();
return $this;
}
/**
* Applies the proper encoding to the field being added to
* the CSV lines for export, depending on CSV or Excel.
*
* @param string $field the value being added to the export line.
*
* @return string either the original or the properly encoded field.
*
* @since 1.16.1 (J) - 1.6.1 (WP)
* @since 1.16.8 (J) - 1.6.8 (WP) introduced hook to allow to manipulate encoding.
*/
protected function encodeExportCSVField($field)
{
/**
* Trigger event to allow third-party plugins to manipulate encoding,
* in case certain dependencies are not available, such as "mb" support,
* PECL intl for "transliterator_transliterate" or "iconv" to ASCII.
*
* @since 1.16.8 (J) - 1.6.8 (WP)
*/
$apply_encoding = VBOFactory::getPlatform()->getDispatcher()->filter('onBeforeEncodingCSVField', [&$field, $this->csv_export_format]);
if (in_array(false, $apply_encoding, true)) {
// the hook ordered to not proceed with applying any encoding
return $field;
}
if (!is_string($field) || !strcasecmp($this->csv_export_format, 'csv')) {
// apply no encoding in case of regular CSV or if non-string data type
return $field;
}
// process the Excel-like string field
if (preg_match('/[\\x80-\\xff]/', $field)) {
// UTF-8 encoding detected
if (function_exists('transliterator_transliterate')) {
// if Transliterator is available (PECL intl >= 2.0.0), transliterate to ASCII
$field = transliterator_transliterate('Any-Latin; Latin-ASCII;', $field);
}
// attempt to convert UTF-8 to ASCII to support currencies
$field = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $field);
}
if (!function_exists('mb_convert_encoding')) {
// abort to prevent server errors
return $field;
}
// convert encoding to UTF-16LE (low-endian with BOM)
return mb_convert_encoding($field, 'UTF-16LE', ['ASCII', 'UTF-8', 'ISO-8859-1']);
}
/**
* Used to apply transliteration over UTF-8 characters for having only latins chars.
*
* @param string $value The original string value.
*
* @return string The transliterated string or the original string.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
protected function transliterateToAscii(string $value)
{
if (!preg_match('/[\\x80-\\xff]/', $value)) {
// no UTF-8 encoding (special character) detected
return $value;
}
// make a safe copy of the original string
$copy_value = $value;
if (function_exists('transliterator_transliterate')) {
// if Transliterator is available (PECL intl >= 2.0.0), transliterate to ASCII
$copy_value = transliterator_transliterate('Any-Latin; Latin-ASCII;', $copy_value);
}
if (function_exists('iconv')) {
// attempt to convert UTF-8 to ASCII
$copy_value = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $copy_value);
}
if (empty($copy_value)) {
// revert to the original string value
$copy_value = $value;
}
return $copy_value;
}
/**
* Loads all the rooms in VBO and returns the array.
*
* @return array
*/
protected function getRooms()
{
$q = "SELECT * FROM `#__vikbooking_rooms` ORDER BY `name` ASC;";
$this->dbo->setQuery($q);
$rooms = $this->dbo->loadAssocList();
return $rooms;
}
/**
* Loads all the rate plans in VBO and returns the array.
*
* @return array
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
protected function getRatePlans()
{
$q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
$this->dbo->setQuery($q);
$rplans = $this->dbo->loadAssocList();
return VikBooking::sortRatePlans($rplans);
}
/**
* Returns the number of total units for all rooms, or for a specific room.
* By default, the rooms unpublished are skipped, and all rooms are used.
*
* @param [mixed] $idroom int or array.
* @param [int] $published true or false.
*
* @return int
*/
protected function countRooms($idroom = 0, $published = 1)
{
$clauses = [];
if (is_int($idroom) && $idroom > 0) {
$clauses[] = "`id`=".(int)$idroom;
} elseif (is_array($idroom) && $idroom) {
$clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
}
if ($published) {
$clauses[] = "`avail`=1";
}
$q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`".($clauses ? " WHERE ".implode(' AND ', $clauses) : "").";";
$this->dbo->setQuery($q);
$totrooms = (int)$this->dbo->loadResult();
return $totrooms;
}
/**
* Concatenates the JavaScript rules.
* Method used only by sub-classes.
*
* @param string $str
*
* @return self
*/
protected function setScript($str)
{
$this->reportScript .= $str."\n";
return $this;
}
/**
* Gets the current script string.
*
* @return string
*/
public function getScript()
{
return rtrim($this->reportScript, "\n");
}
/**
* Returns the date format in VBO for date, jQuery UI, Joomla/WordPress.
* The visibility of this method should be public for anyone who needs it.
*
* @param string $type
*
* @return string
*/
public function getDateFormat($type = 'date')
{
$nowdf = VikBooking::getDateFormat();
if ($nowdf == "%d/%m/%Y") {
$df = 'd/m/Y';
$juidf = 'dd/mm/yy';
} elseif ($nowdf == "%m/%d/%Y") {
$df = 'm/d/Y';
$juidf = 'mm/dd/yy';
} else {
$df = 'Y/m/d';
$juidf = 'yy/mm/dd';
}
switch ($type) {
case 'jui':
return $juidf;
case 'joomla':
case 'wordpress':
return $nowdf;
default:
return $df;
}
}
/**
* Returns the translated weekday.
* Uses the back-end language definitions.
*
* @param int $wday
* @param string $type use 'long' for the full name of the week, short for the 3-char version
*
* @return string
*/
protected function getWdayString($wday, $type = 'long')
{
$wdays_map_long = [
JText::translate('VBWEEKDAYZERO'),
JText::translate('VBWEEKDAYONE'),
JText::translate('VBWEEKDAYTWO'),
JText::translate('VBWEEKDAYTHREE'),
JText::translate('VBWEEKDAYFOUR'),
JText::translate('VBWEEKDAYFIVE'),
JText::translate('VBWEEKDAYSIX')
];
$wdays_map_short = [
JText::translate('VBSUN'),
JText::translate('VBMON'),
JText::translate('VBTUE'),
JText::translate('VBWED'),
JText::translate('VBTHU'),
JText::translate('VBFRI'),
JText::translate('VBSAT')
];
if ($type != 'long') {
return isset($wdays_map_short[(int)$wday]) ? $wdays_map_short[(int)$wday] : '';
}
return isset($wdays_map_long[(int)$wday]) ? $wdays_map_long[(int)$wday] : '';
}
/**
* Sets the columns for this report.
*
* @param array $arr
*
* @return self
*/
public function setReportCols($arr)
{
$this->cols = $arr;
return $this;
}
/**
* Returns the columns for this report.
* Should be called after getReportData()
* or the returned array will be empty.
*
* @return array
*/
public function getReportCols()
{
return $this->cols;
}
/**
* Sorts the rows of the report by key.
*
* @param string $krsort the key attribute of the array pairs
* @param string $krorder ascending (ASC) or descending (DESC)
*
* @return void
*/
protected function sortRows($krsort, $krorder)
{
if (empty($krsort) || !$this->rows) {
return;
}
$map = [];
foreach ($this->rows as $k => $row) {
foreach ($row as $kk => $v) {
if (isset($v['key']) && $v['key'] == $krsort) {
$map[$k] = $v['value'];
}
}
}
if (!$map) {
return;
}
if ($krorder == 'ASC') {
asort($map);
} else {
arsort($map);
}
$sorted = [];
foreach ($map as $k => $v) {
$sorted[$k] = $this->rows[$k];
}
$this->rows = $sorted;
}
/**
* Sets the rows for this report.
*
* @param array $arr
*
* @return self
*/
public function setReportRows($arr)
{
$this->rows = $arr;
return $this;
}
/**
* Returns the rows for this report.
* Should be called after getReportData()
* or the returned array will be empty.
*
* @return array
*/
public function getReportRows()
{
return $this->rows;
}
/**
* This method returns one or more rows (given the depth) generated by
* the current report invoked. It is useful to clean up the callbacks
* of the various cell-rows, to obtain a parsable result.
* Can be called as first method, by skipping also getReportData().
*
* @param int $depth how many records to obtain, null for all.
*
* @return array the queried report value in the given depth.
*
* @uses getReportData()
*/
public function getReportValues($depth = null)
{
if (!$this->rows && !$this->getReportData()) {
return [];
}
$report_values = [];
foreach ($this->rows as $rk => $row) {
$report_values[$rk] = [];
foreach ($row as $col => $coldata) {
$display_value = $coldata['value'];
if (isset($coldata['callback']) && is_callable($coldata['callback'])) {
// launch callback
$display_value = $coldata['callback']($coldata['value']);
}
// push column value
$report_values[$rk][$coldata['key']] = [
'value' => $coldata['value'],
'display_value' => $display_value,
];
/**
* We also pass along any reserved key for this row-data.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
foreach ($coldata as $res_key => $data_val) {
if (substr($res_key, 0, 1) == '_') {
// push this reserved key
$report_values[$rk][$coldata['key']][$res_key] = $data_val;
}
}
}
}
if (!$report_values) {
return [];
}
if ($depth === 1) {
// get an associative array with the first row calculated
return $report_values[0];
}
if (is_int($depth) && $depth > 0 && count($report_values) >= $depth) {
// get the requested portion of the array
return array_slice($report_values, 0, $depth);
}
return $report_values;
}
/**
* Maps the columns labels to an associative array to be used for the values.
*
* @return array associative list of column keys and related values.
*/
public function getColumnsValues()
{
if (!$this->cols) {
return [];
}
$col_values = [];
foreach ($this->cols as $col) {
if (!isset($col['key'])) {
continue;
}
$col_values[$col['key']] = $col;
unset($col_values[$col['key']]['key']);
}
return $col_values;
}
/**
* Gets a property defined by the report. Useful to get custom
* properties set up by a specific report maybe for the Chart.
*
* @param string $property the name of the property needed.
* @param mixed $def default value to return.
*
* @return mixed false on failure, property requested otherwise.
*/
public function getProperty($property, $def = false)
{
if (isset($this->{$property})) {
return $this->{$property};
}
return $def;
}
/**
* Counts the number of days of difference between two timestamps.
*
* @param int $to_ts the target end date timestamp.
* @param int $from_ts the starting date timestamp.
*
* @return int the days of difference between from and to timestamps.
*/
public function countDaysTo($to_ts, $from_ts = 0)
{
if (empty($from_ts)) {
$from_ts = time();
}
// whether DateTime can be used
$usedt = false;
if (class_exists('DateTime')) {
$from_date = new DateTime(date('Y-m-d', $from_ts));
if (method_exists($from_date, 'diff')) {
$usedt = true;
}
}
if ($usedt) {
$to_date = new DateTime(date('Y-m-d', $to_ts));
$daysdiff = (int)$from_date->diff($to_date)->format('%a');
if ($to_ts < $from_ts) {
// we need a negative integer number
$daysdiff = $daysdiff - ($daysdiff * 2);
}
return $daysdiff;
}
return (int)round(($to_ts - $from_ts) / 86400);
}
/**
* Counts the average difference between two integers.
*
* @param int $in_days_from days to the lowest timestamp.
* @param int $in_days_to days to the highest timestamp.
*
* @return int the average number between the two values.
*/
public function countAverageDays($in_days_from, $in_days_to)
{
return (int)floor(($in_days_from + $in_days_to) / 2);
}
/**
* Sets the footer row (the totals) for this report.
*
* @param array $arr
*
* @return self
*/
protected function setReportFooterRow($arr)
{
$this->footerRow = $arr;
return $this;
}
/**
* Returns the footer row for this report.
* Should be called after getReportData()
* or the returned array will be empty.
*
* @return array
*/
public function getReportFooterRow()
{
return $this->footerRow;
}
/**
* Sub-classes can extend this method to define the
* the canvas HTML tag for rendenring the Chart.
* Any necessary script shall be set within this method.
* Data can be passed as a mixed value through the argument.
* This is the first method to be called when working with the Chart.
*
* @param mixed $data any necessary value to render the Chart.
*
* @return string the HTML of the canvas element.
*/
public function getChart($data = null)
{
return '';
}
/**
* Sub-classes can extend this method to define the
* the title of the Chart to be rendered.
*
* @return string the title of the Chart.
*/
public function getChartTitle()
{
return '';
}
/**
* Sub-classes can extend this method to define
* the meta data for the Chart containing stats.
* An array for each meta-data should be returned.
*
* @param mixed $position string for the meta-data position
* in the Chart (top, right, bottom).
* @param mixed $data some arguments to be passed.
*
* @return array
*/
public function getChartMetaData($position = null, $data = null)
{
return [];
}
/**
* Sets an array of custom options for this report. Useful to inject
* params before getting the report data and changing the behavior.
*
* @param array $options The associative options to set.
*
* @return self
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function setReportOptions(array $options = [])
{
$this->options = $options;
return $this;
}
/**
* Returns the custom options for the report. Useful to
* behave differently depending on who calls the report.
* By default, the method returns an instance of JObject
* to easily access all custom options defined, if any.
*
* @param bool $registry true to get a JObject instance.
*
* @return mixed instance of JObject or raw array.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function getReportOptions($registry = true)
{
if ($registry) {
return new JObject($this->options);
}
return $this->options;
}
/**
* Defines an associative list of action data.
*
* @param array $data Associative list of action data.
*
* @return self
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function setActionData(array $data)
{
$this->actionData = $data;
return $this;
}
/**
* Returns the action data, either raw or as a registry.
*
* @param bool $registry True to get a JObject instance.
*
* @return array|JObject
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getActionData($registry = true)
{
if ($registry) {
return new JObject($this->actionData);
}
return $this->actionData;
}
/**
* Sets the global scope for the report.
*
* @param string $scope The scope to set.
*
* @return self
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function setScope($scope)
{
$this->scope = $scope;
return $this;
}
/**
* Returns the global scope of the invoked report.
*
* @return ?string
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getScope()
{
return $this->scope;
}
/**
* Returns the path to the PMS media data directory.
*
* @return string
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getDataMediaPath()
{
return implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'pmsdata']);
}
/**
* Returns the URL to the PMS media data directory.
*
* @return string
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getDataMediaUrl()
{
return VBO_ADMIN_URI . 'resources/pmsdata/';
}
/**
* Returns the optional report custom setting fields.
*
* @return array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getSettingFields()
{
return [];
}
/**
* Tells whether the current report allows to save
* the settings across multiple profile identifiers.
*
* @return bool
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function allowsProfileSettings()
{
// report profile settings disabled by default
return false;
}
/**
* Returns the report currently active profile identifier, if any.
*
* @return string
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function getActiveProfile()
{
$profile = (string) VBOFactory::getConfig()->getString('report_active_profile_' . $this->getFileName(), '');
return $profile;
}
/**
* Sets the report currently active profile identifier.
*
* @param string $profile_id The profile identifier to set.
*
* @return void
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function setActiveProfile(string $profile_id)
{
VBOFactory::getConfig()->set('report_active_profile_' . $this->getFileName(), $profile_id);
}
/**
* Returns an associative list for the report setting profiles available.
*
* @return array
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function getSettingProfiles()
{
return (array) VBOFactory::getConfig()->getArray('report_profile_list_' . $this->getFileName(), []);
}
/**
* Sets a new report setting profile identifier.
*
* @param string $profile_name The profile name.
*
* @return array List of profile identifier and name.
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function setSettingProfile(string $profile_name)
{
// get all report profiles
$profiles = $this->getSettingProfiles();
// build profile identifier
$profile_id = preg_replace('/[^A-Z0-9]/i', '', strtolower($profile_name));
$profile_id = $profile_id ?: uniqid();
// set report profile
$profiles[$profile_id] = $profile_name;
VBOFactory::getConfig()->set('report_profile_list_' . $this->getFileName(), $profiles);
return [$profile_id, $profile_name];
}
/**
* Clears all profile settings and related data.
*
* @return void
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public function clearProfiles()
{
// unset active profile
VBOFactory::getConfig()->set('report_active_profile_' . $this->getFileName(), '');
// unset profiles list
VBOFactory::getConfig()->set('report_profile_list_' . $this->getFileName(), []);
// clear profile settings
VBOFactory::getConfig()->set('report_profile_settings_' . $this->getFileName(), []);
}
/**
* Returns the current report custom settings, optionally loaded from a
* given profile identifier in case the report supports multiple settings.
*
* @param string $profile Optional settings profile identifier.
*
* @return array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function loadSettings(string $profile = '')
{
// access report current settings
$current_settings = (array) VBOFactory::getConfig()->getArray('report_settings_' . $this->getFileName(), []);
// default settings
$default_settings = [];
if (!$profile && $this->allowsProfileSettings()) {
// load current profile settings
$profile = $this->getActiveProfile();
$default_settings = $current_settings;
}
if ($profile) {
// check if the requested profile settings are available
$profile_settings = (array) VBOFactory::getConfig()->getArray('report_profile_settings_' . $this->getFileName(), []);
// overwrite report current settings
$current_settings = $profile_settings[$profile] ?? $default_settings;
}
return $current_settings;
}
/**
* Saves the report custom settings defined.
* The visibility should be public.
*
* @param array $data The associative list of settings to save.
* @param bool $merge If true, the previous settings will be merged.
* @param string $profile Optional settings profile identifier.
*
* @return void
*
* @since 1.17.1 (J) - 1.7.1 (WP)
* @since 1.17.7 (J) - 1.7.7 (WP) added 3rd argument $profile and related support.
*/
public function saveSettings(array $data, $merge = true, string $profile = '')
{
if ($merge) {
// build report global settings
$data = array_merge($this->loadSettings($profile), $data);
}
// save report global settings
VBOFactory::getConfig()->set('report_settings_' . $this->getFileName(), $data);
if ($profile) {
// get report current profile settings
$profile_settings = (array) VBOFactory::getConfig()->getArray('report_profile_settings_' . $this->getFileName(), []);
// set new profile settings
$profile_settings[$profile] = $data;
// save report profile settings
VBOFactory::getConfig()->set('report_profile_settings_' . $this->getFileName(), $profile_settings);
}
}
/**
* Returns a numeric list of scoped extra actions.
*
* @param string $scope Optional scope identifier (cron, web, etc..).
* @param bool $visible If true, the hidden actions will not be returned.
*
* @return array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getScopedActions($scope = null, $visible = true)
{
return [];
}
/**
* Executes a custom scoped action within the report.
*
* @param string $action The custom action to invoke.
* @param string $scope Optional scope identifier (cron, web, etc..).
* @param array $data Optional data for the action to execute.
*
* @return mixed
*
* @throws Exception
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function executeAction($action, $scope = null, array $data = [])
{
if (is_null($scope)) {
$scope = $this->getScope();
}
if (!$data) {
$data = $this->getActionData($registry = false);
}
$callable = [$this, $action];
if (!is_callable($callable)) {
throw new Exception('Could not call the requested report action.', 500);
}
return call_user_func_array($callable, [$scope, $data]);
}
/**
* Proxy for executing a custom scoped action and returning a property.
*
* @param string $action The custom action to invoke.
* @param string $return The action result property to return.
* @param string $scope Optional scope identifier (cron, web, etc..).
* @param array $data Optional data for the action to execute.
*
* @return mixed
*
* @throws Exception
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function _callActionReturn($action, $return, $scope = null, array $data = [])
{
if (is_null($scope)) {
$scope = $this->getScope();
}
if (!$data) {
$data = $this->getActionData($registry = false);
}
$result = (array) $this->executeAction($action, $scope, $data);
if (!isset($result[$return])) {
throw new Exception('Could not return the requested action result property.', 500);
}
return $result[$return];
}
/**
* Defines a new resource file generated through a custom action.
*
* @param array $data Resource file data to bind.
*
* @return self
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
protected function defineResourceFile(array $data)
{
$element = new VBOReportResourceElement($data);
if ($element->getUrl()) {
// ensure the resource is available
$this->resourceFiles[] = $element;
}
return $this;
}
/**
* Returns the resource files generated through custom actions.
*
* @return array
*
* @since 1.17.1 (J) - 1.7.1 (WP)
*/
public function getResourceFiles()
{
return $this->resourceFiles;
}
/**
* Sets warning messages by concatenating the existing ones.
* Method used only by sub-classes.
*
* @param string $str
*
* @return self
*/
protected function setWarning($str)
{
$this->warning .= $str."\n";
return $this;
}
/**
* Gets the current warning string.
*
* @return string
*/
public function getWarning()
{
return rtrim($this->warning, "\n");
}
/**
* Sets errors by concatenating the existing ones.
* Method used only by sub-classes.
*
* @param string $str
*
* @return self
*/
protected function setError($str)
{
$this->error .= $str."\n";
return $this;
}
/**
* Gets the current error string.
*
* @return string
*/
public function getError()
{
return rtrim($this->error, "\n");
}
}