diff --git a/composer.json b/composer.json index 1d74b1d..ac29a4c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require": { "php": ">=8.0", "reportico/adodb-php": "^8.1", - "twig/twig": "^3.0", + "twig/twig": "^3.19", "tecnickcom/tcpdf": "^6.2", "szymach/c-pchart": "^3.0", "reportico/assetter": "^8.1", diff --git a/dyngraph.php b/dyngraph.php index 4eb2bf7..f82805a 100755 --- a/dyngraph.php +++ b/dyngraph.php @@ -12,7 +12,7 @@ */ - ini_set("memory_limit","100M"); + ini_set("memory_limit", getenv("REPORTICO_MEMORY_LIMIT") ?: "512M"); error_reporting(E_ALL); date_default_timezone_set(@date_default_timezone_get()); @@ -258,7 +258,7 @@ function convertSpecialChars($intext) $graph->Add($lplot[$lplotct]); break; case "LINE": - default; + default: if ( count($v["data"]) == 1 ) $v["data"][] = 0; $lplot[$lplotct]=new LinePlot($v["data"]); diff --git a/partial.php b/partial.php index c664912..5efcb65 100755 --- a/partial.php +++ b/partial.php @@ -18,7 +18,7 @@ error_reporting(E_ALL); date_default_timezone_set(@date_default_timezone_get()); - ini_set("memory_limit","100M"); + ini_set("memory_limit", getenv("REPORTICO_MEMORY_LIMIT") ?: "512M"); //ob_start(); require_once('reportico.php'); diff --git a/run.php b/run.php index 2d0923b..9df6565 100755 --- a/run.php +++ b/run.php @@ -25,10 +25,10 @@ date_default_timezone_set(@date_default_timezone_get()); // Reserver 100Mb for running -ini_set("memory_limit","100M"); +ini_set("memory_limit", getenv("REPORTICO_MEMORY_LIMIT") ?: "512M"); // Allow a good time for long reports to run. Set to 0 to allow unlimited time -ini_set("max_execution_time","90"); +ini_set("max_execution_time", getenv("REPORTICO_MAX_EXECUTION_TIME") ?: "300"); // Instantiate Reportico $q = new Reportico\Engine\Reportico(); diff --git a/src/ChartJpgraph.php b/src/ChartJpgraph.php index a058f28..5ca74c8 100755 --- a/src/ChartJpgraph.php +++ b/src/ChartJpgraph.php @@ -575,7 +575,7 @@ function generateGraphImage () $graph->Add($lplot[$lplotct]); break; case "LINE": - default; + default: if ( count($v["data"]) == 1 ) $v["data"][] = 0; $lplot[$lplotct]=new LinePlot($v["data"]); diff --git a/src/ChartPchart.php b/src/ChartPchart.php index 7e3129a..e9300fe 100755 --- a/src/ChartPchart.php +++ b/src/ChartPchart.php @@ -828,7 +828,7 @@ public function generateGraphImage($outputfile) $image->writeValues($data->GetData(), $data->GetDataDescription(), $series2); break; case "LINE": - default; + default: if ($linedrawn) { break; } diff --git a/src/ChartPchart3.php b/src/ChartPchart3.php index 72e8583..ca72784 100644 --- a/src/ChartPchart3.php +++ b/src/ChartPchart3.php @@ -778,7 +778,7 @@ public function generateGraphImage($outputfile) break; case "LINE": - default; + default: if (count($v["data"]) == 1) { $v["data"][] = 0; diff --git a/src/QueryColumn.php b/src/QueryColumn.php index 89fc540..11d8c38 100644 --- a/src/QueryColumn.php +++ b/src/QueryColumn.php @@ -280,6 +280,22 @@ public function __call($method, $args) public function getValueDelimiter() { if (strtoupper($this->column_type) == "CHAR") { + // PostgreSQL (and the SQL standard) use single quotes for string literals. + // Double quotes delimit identifiers, so a date like "2026-05-13" is parsed as a + // column name and fails with "column does not exist" — uncaught PDOException → WSOD. + $driver = ""; + if ($this->datasource && !empty($this->datasource->_conn_driver)) { + $driver = strtolower((string) $this->datasource->_conn_driver); + } + if ( + $driver === "pdo_pgsql" + || $driver === "postgres" + || $driver === "pgsql" + || str_contains($driver, "pgsql") + ) { + return "'"; + } + return ("\""); } diff --git a/src/ReportHtml.php b/src/ReportHtml.php index 3e2a140..4747792 100644 --- a/src/ReportHtml.php +++ b/src/ReportHtml.php @@ -343,6 +343,13 @@ public function openGroup() { */ public function closeGroup() { + // PHP 8.1+: writing to $this->currentGroup[...] when it is `false` triggers + // an "Automatic conversion of false to array" deprecation. Treat closeGroup() + // with no open group as a no-op, matching the behaviour of openGroup() init paths. + if (!is_array($this->currentGroup)) { + return; + } + $x= $this->line_count; $this->currentGroup["endrow"] = $this->line_count - 1; $this->jar["pages"][$this->page_count]["rows"][$this->line_count]["closerowsection"] = true; diff --git a/src/ReportHtml2pdf.php b/src/ReportHtml2pdf.php index 8ad6291..015f8d5 100644 --- a/src/ReportHtml2pdf.php +++ b/src/ReportHtml2pdf.php @@ -342,6 +342,13 @@ public function openGroup() { */ public function closeGroup() { + // PHP 8.1+: writing to $this->currentGroup[...] when it is `false` triggers + // an "Automatic conversion of false to array" deprecation. Treat closeGroup() + // with no open group as a no-op. + if (!is_array($this->currentGroup)) { + return; + } + $x= $this->line_count; $this->currentGroup["endrow"] = $this->line_count - 1; $this->jar["pages"][$this->page_count]["rows"][$this->line_count]["closerowsection"] = true; diff --git a/src/ReportTCPDF.php b/src/ReportTCPDF.php index 6fbbc9b..f0b1dde 100644 --- a/src/ReportTCPDF.php +++ b/src/ReportTCPDF.php @@ -25,6 +25,12 @@ class ReportTCPDF extends Report public $abs_col_left_margin; public $abs_left_margin; public $abs_right_margin; + // PHP 8.2+: declare these explicitly to avoid Creation-of-dynamic-property deprecations. + public $abs_row_right_margin; + public $abs_col_right_margin; + public $abs_row_width; + public $abs_columns_width; + public $column_spacing = 0; public $abs_page_width = 0; public $abs_page_height = 0; public $abs_print_width = 0; @@ -3244,7 +3250,9 @@ public function newReportPageLine($txt = "") $this->newReportPageLineByStyle("LINEPAGE$txt", $this->mid_page_page_styles, false); } - public function newReportPageLineByStyle($txt = "", &$styles, $blankline = false) + // PHP 8.0+: optional parameters cannot precede required ones; $styles is by-ref/required, + // so $txt must also be required (callers always pass it explicitly). + public function newReportPageLineByStyle($txt, &$styles, $blankline = false) { // Line page wrapper $this->applyStyleTags("$txt", $styles); diff --git a/src/Reportico.php b/src/Reportico.php index bef0fc8..0f6a2f5 100755 --- a/src/Reportico.php +++ b/src/Reportico.php @@ -4963,7 +4963,7 @@ public function generateDropdownMenu(&$menu) } $filename = $proj_parent . "/" . $project . "/" . $menuitem["reportfile"]; - if (!preg_match("/\.xml/", $filename)) { + if (!preg_match("/\.xml$/i", $filename)) { $filename .= ".xml"; } diff --git a/src/ReporticoApp.php b/src/ReporticoApp.php index 094c04d..f28fd07 100644 --- a/src/ReporticoApp.php +++ b/src/ReporticoApp.php @@ -352,6 +352,12 @@ static function ErrorLogger($errno, $errstr, $errfile = false, $errline = false) // error handler function static function ErrorHandler($errno, $errstr, $errfile, $errline) { + // PHP 8.x: deprecations must not be stored as blocking "system errors" — Reportico treats + // anything in that list like a fatal in several execute paths (blank / broken pages). + if ($errno === E_DEPRECATED || $errno === E_USER_DEPRECATED) { + return true; + } + switch ($errno) { case E_ERROR: $errtype = ReporticoLang::translate("Error"); diff --git a/src/ReporticoSession.php b/src/ReporticoSession.php index 8c40c1c..1c1801c 100644 --- a/src/ReporticoSession.php +++ b/src/ReporticoSession.php @@ -195,7 +195,13 @@ static function setUpReporticoSession($namespace) */ static function existsReporticoSession() { - if (isset($_SESSION[ReporticoApp::get("session_namespace_key")])) { + $key = ReporticoApp::get("session_namespace_key"); + // PHP 8.1+: $_SESSION[null] triggers "Using null as an array offset is deprecated". + // When session_namespace_key has not been set yet, the namespace by definition does not exist. + if ($key === null || $key === "") { + return false; + } + if (isset($_SESSION[$key])) { return true; } else { return false; @@ -269,7 +275,12 @@ static function issetReporticoSessionParam($param, $session_name = false) { if (!$session_name) $session_name = ReporticoApp::get("session_namespace_key"); - + + // PHP 8.1+: $_SESSION[null] triggers "Using null as an array offset is deprecated". + if ($session_name === null || $session_name === "") { + return false; + } + return isset($_SESSION[$session_name][$param]); } @@ -288,6 +299,12 @@ static function setReporticoSessionParam($param, $value, $namespace = false, $ar if (!$namespace) $namespace = ReporticoApp::get("session_namespace_key"); + // PHP 8.1+: $_SESSION[null] triggers "Using null as an array offset is deprecated". + // If the namespace key is not yet set there is no session bucket to write to. + if ($namespace === null || $namespace === "") { + return; + } + //echo "Set $namespace:$param
"; if (!$array) { $_SESSION[$namespace][$param] = $value; @@ -321,8 +338,13 @@ static function getReporticoSessionParam($param) */ static function unsetReporticoSessionParam($param) { - if (isset($_SESSION[ReporticoApp::get("session_namespace_key")][$param])) { - unset($_SESSION[ReporticoApp::get("session_namespace_key")][$param]); + $key = ReporticoApp::get("session_namespace_key"); + // PHP 8.1+: $_SESSION[null] triggers "Using null as an array offset is deprecated". + if ($key === null || $key === "") { + return; + } + if (isset($_SESSION[$key][$param])) { + unset($_SESSION[$key][$param]); } } @@ -370,7 +392,9 @@ static function reporticoNamespace() static function initializeReporticoNamespace($namespace = "reportico") { $namespace = ReporticoApp::get("session_namespace_key"); - if (isset($_SESSION[$namespace])) { + // PHP 8.1+: $_SESSION[null] triggers "Using null as an array offset is deprecated". + // Nothing to clear if the namespace key has not been set. + if ($namespace !== null && $namespace !== "" && isset($_SESSION[$namespace])) { unset($_SESSION[$namespace]); } diff --git a/src/ReporticoUtility.php b/src/ReporticoUtility.php index 054cefd..9a02b9f 100644 --- a/src/ReporticoUtility.php +++ b/src/ReporticoUtility.php @@ -159,6 +159,11 @@ static function backtrace() // Look for a file in the include path, or the path of the current source file static function findFileToInclude($file_path, &$new_file_path, &$rel_to_include = "") { + if (!$file_path || !is_string($file_path)) { + $new_file_path = $file_path; + return false; + } + // First look in path of current file static $_path_array = null; if (__DIR__) { diff --git a/src/XmlReader.php b/src/XmlReader.php index 86dc161..7cc09c4 100644 --- a/src/XmlReader.php +++ b/src/XmlReader.php @@ -397,9 +397,10 @@ public function __construct(&$query, $filename, $xmlstring = false, $search_tag $this->field_display["YTickLabelInterval"]["Type"] = "HIDE"; } - xml_set_object($this->parser, $this); - xml_set_element_handler($this->parser, 'startElement', 'endElement'); - xml_set_character_data_handler($this->parser, 'cdata'); + // PHP 8.4 deprecated xml_set_object() and string-name callbacks for xml_set_*_handler(). + // Pass [object, methodName] callables instead, which are also valid in PHP 7.4+. + xml_set_element_handler($this->parser, [$this, 'startElement'], [$this, 'endElement']); + xml_set_character_data_handler($this->parser, [$this, 'cdata']); xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, false); // 1 = single field, 2 = array field, 3 = record container @@ -467,7 +468,11 @@ public function __construct(&$query, $filename, $xmlstring = false, $search_tag echo "XML $filename
"; } if ($this->query) { - $readfile = $this->query->projects_folder . "/" . ReporticoApp::getConfig("project") . "/" . $filename; + if (!empty($this->query->reports_path)) { + $readfile = $this->query->reports_path . "/" . $filename; + } else { + $readfile = $this->query->projects_folder . "/" . ReporticoApp::getConfig("project") . "/" . $filename; + } $adminfile = $this->query->admin_projects_folder . "/admin/" . $filename; } else { $readfile = $filename; @@ -492,27 +497,27 @@ public function __construct(&$query, $filename, $xmlstring = false, $search_tag if ($readfile && is_file($readfile)) { $readfile = $readfile; } else { - if (!is_file($adminfile)) { + if ($adminfile && !is_file($adminfile)) { ReporticoUtility::findFileToInclude($adminfile, $readfile); - if (is_file($readfile)) { - $readfile = $readfile; - } - - } else { + } elseif ($adminfile && is_file($adminfile)) { $use_admin_xml = true; $readfile = $adminfile; } } - if ($readfile) { + if ($readfile && is_file($readfile)) { //if ( $use_admin_xml ) //Authenticator::flag("admin-report-selected"); - if ( !file_exists($readfile) ) { - ReporticoApp::backtrace(); - } $x = join("", file($readfile)); + } elseif ($this->search_tag) { + // Menu title lookup for a non-report path; skip without fatal error + $this->search_response = ""; + } elseif ($readfile) { + $report_path = ($this->query && $this->query->reports_path) ? $this->query->reports_path : $readfile; + trigger_error("Report Definition File " . $report_path . "/" . $filename . " Not Found", E_USER_ERROR); } else { - trigger_error("Report Definition File " . $this->query->reports_path . "/" . $filename . " Not Found", E_USER_ERROR); + $report_path = ($this->query && $this->query->reports_path) ? $this->query->reports_path : ""; + trigger_error("Report Definition File " . $report_path . "/" . $filename . " Not Found", E_USER_ERROR); } } @@ -520,7 +525,8 @@ public function __construct(&$query, $filename, $xmlstring = false, $search_tag if ($x) { xml_parse($this->parser, $x); - xml_parser_free($this->parser); + // xml_parser_free() was deprecated in PHP 8.5 (no-op since 8.0). + // Parser is freed automatically when $this->parser goes out of scope. } //var_dump($this->data); @@ -2306,7 +2312,7 @@ public function &draw_add_button($in_tag, $in_value = false) break; case "mainqueroutppgft":$importtype = "IMPORT"; break; - default; + default: $importtype = false; } diff --git a/src/widgets/AdminMenu.php b/src/widgets/AdminMenu.php index f93acbf..e040b2e 100644 --- a/src/widgets/AdminMenu.php +++ b/src/widgets/AdminMenu.php @@ -131,6 +131,9 @@ public function generateMenuList () if (is_dir(ReporticoApp::get("projpath"))) { if ($dh = opendir(ReporticoApp::get("projpath"))) { while (($file = readdir($dh)) !== false) { + if (!preg_match('/\.xml$/i', $file)) { + continue; + } $mtch = "/" . $menuitem["report"] . "/"; if (preg_match($mtch, $file)) { $repxml = new XmlReader($this->engine, $file, false, "ReportTitle"); diff --git a/src/widgets/Criteria.php b/src/widgets/Criteria.php index df913d6..6d4c2dc 100644 --- a/src/widgets/Criteria.php +++ b/src/widgets/Criteria.php @@ -30,6 +30,10 @@ class Criteria extends Widget public $buttonTypes = array(); public $formTypes = array(); + // PHP 8.2+: declared explicitly to avoid Creation-of-dynamic-property deprecation. + // Populated by Widget::handleUrlParameters() when criteria_type is "DATE" (passed by reference into ReporticoLocale::convertDateRangeDefaultsToDates()). + public $range_start = false; + public function __construct($engine, $load = false, $engineCriteria = false ) { diff --git a/src/widgets/CriteriaForm.php b/src/widgets/CriteriaForm.php index ae4ced7..00fcc15 100644 --- a/src/widgets/CriteriaForm.php +++ b/src/widgets/CriteriaForm.php @@ -98,9 +98,14 @@ public function render() } + $project = htmlspecialchars(ReporticoApp::getConfig("project", ""), ENT_QUOTES); + $xmlin = htmlspecialchars($this->engine->xmlinput ? $this->engine->xmlinput : "", ENT_QUOTES); + $sections["begin"] = "
+ + "; $sections["end"] = "
"; diff --git a/src/widgets/DatePicker.php b/src/widgets/DatePicker.php index 94ebad1..a7f2c13 100644 --- a/src/widgets/DatePicker.php +++ b/src/widgets/DatePicker.php @@ -26,6 +26,11 @@ class DatePicker extends Widget public $range_raw = false; public $range_start = false; public $range_end = false; + // PHP 8.2+: declared explicitly to avoid Creation-of-dynamic-property deprecations + // (assigned in deriveValue()). Matches DateRangePicker / TimeRangePicker. + public $range_name = false; + public $range_start_raw = false; + public $range_end_raw = false; public $options = [ "Today" => [ @@ -199,7 +204,6 @@ public function getCriteriaClause($lhs = true, $operand = true, $rhs = true, $rh } } - echo $cls; $del = ""; if ($add_del) { diff --git a/src/widgets/ProjectMenu.php b/src/widgets/ProjectMenu.php index 3c24fad..f7750b9 100644 --- a/src/widgets/ProjectMenu.php +++ b/src/widgets/ProjectMenu.php @@ -116,6 +116,10 @@ public function generateMenuList () if (is_dir(ReporticoApp::get("projpath"))) { if ($dh = opendir(ReporticoApp::get("projpath"))) { while (($file = readdir($dh)) !== false) { + // Only real report files (e.g. Report.xml), not *.xml.bak.* backups + if (!preg_match('/\.xml$/i', $file)) { + continue; + } $mtch = "/" . $menuitem["report"] . "/"; if (preg_match($mtch, $file)) { $repxml = new XmlReader($this->engine, $file, false, "ReportTitle"); diff --git a/src/widgets/TimeRangePicker.php b/src/widgets/TimeRangePicker.php index 991f83c..23786ea 100644 --- a/src/widgets/TimeRangePicker.php +++ b/src/widgets/TimeRangePicker.php @@ -29,6 +29,7 @@ class TimeRangePicker extends Widget public $range_end = false; public $range_raw = false; public $derived = false; + public $range_name = false; public $options = [ "Today" => [ diff --git a/start.php b/start.php index 142bfcc..4f38f93 100755 --- a/start.php +++ b/start.php @@ -22,10 +22,11 @@ date_default_timezone_set(@date_default_timezone_get()); // Reserver 100Mb for running -ini_set("memory_limit","100M"); +// Large reports (many rows × assignments / PDF / grids) can exceed 100M; allow more headroom. +ini_set("memory_limit", getenv("REPORTICO_MEMORY_LIMIT") ?: "512M"); // Allow a good time for long reports to run. Set to 0 to allow unlimited time -ini_set("max_execution_time","90"); +ini_set("max_execution_time", getenv("REPORTICO_MAX_EXECUTION_TIME") ?: "300"); // Only turn on output buffering if necessary, normally leave this commented //ob_start();