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"] = "