From cce6e7139a25b6508f992ba727dc2fba5872019d Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 22 Dec 2025 18:16:13 -0500
Subject: [PATCH 001/139] refactor: remove deprecated mode, prepend, append
parameters from xl() (#9960)
* Initial plan
* Update all xl() callsites to remove deprecated mode, prepend, append parameters
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Remove deprecated mode, prepend, append parameters from xl() and wrapper functions
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Use sprintf instead of string concatenation with escaped quotes
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Fix sprintf formatting in dispense_drug.php for better clarity
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Combine sprintf calls in dispense_drug.php for better performance
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Add PHPDoc and type declarations to xl() and wrapper functions
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Refactor xl_* wrapper functions to use ternaries and add PHPDoc blocks
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Move PHPDoc block inside if block for proper parsing
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* Refactor to use ternary operators for cleaner code
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---
controllers/C_Document.class.php | 12 +-
.../de_identification_screen1.php | 2 +-
interface/drugs/dispense_drug.php | 3 +-
interface/forms/CAMOS/notegen.php | 2 +-
interface/forms/vitals/growthchart/chart.php | 20 +-
interface/logview/logview.php | 2 +-
.../patient_file/summary/shot_record.php | 2 +-
interface/reports/audit_log_tamper_report.php | 2 +-
interface/reports/front_receipts_report.php | 2 +-
interface/reports/inventory_transactions.php | 2 +-
library/translation.inc.php | 215 +++++++-----------
11 files changed, 102 insertions(+), 162 deletions(-)
diff --git a/controllers/C_Document.class.php b/controllers/C_Document.class.php
index ef2ef2de8c92..6930cb3d7ca9 100644
--- a/controllers/C_Document.class.php
+++ b/controllers/C_Document.class.php
@@ -416,7 +416,7 @@ public function note_action_process($patient_id)
$temp_url = $GLOBALS['OE_SITE_DIR'] . '/documents/' . $from_pathname . '/' . $from_filename;
}
if (!file_exists($temp_url)) {
- echo xl('The requested document is not present at the expected location on the filesystem or there are not sufficient permissions to access it.', '', '', ' ') . $temp_url;
+ echo xl('The requested document is not present at the expected location on the filesystem or there are not sufficient permissions to access it.') . ' ' . $temp_url;
}
$url = $temp_url;
$pdetails = getPatientData($patient_id);
@@ -947,7 +947,7 @@ public function move_action_process(?string $patient_id, $document_id)
//move to new category
if (is_numeric($new_category_id) && is_numeric($document_id)) {
$sql = "UPDATE categories_to_documents set category_id = ? where document_id = ?";
- $messages .= xl('Document moved to new category', '', '', ' \'') . $this->tree->_id_name[$new_category_id]['name'] . xl('successfully.', '', '\' ') . "\n";
+ $messages .= sprintf("%s '%s' %s\n", xl('Document moved to new category'), $this->tree->_id_name[$new_category_id]['name'], xl('successfully.'));
//echo $sql;
$this->tree->_db->Execute($sql, [$new_category_id, $document_id]);
}
@@ -960,16 +960,12 @@ public function move_action_process(?string $patient_id, $document_id)
if (!$result || $result->EOF) {
//patient id does not exist
- $messages .= xl('Document could not be moved to patient id', '', '', ' \'') . $new_patient_id . xl('because that id does not exist.', '', '\' ') . "\n";
+ $messages .= sprintf("%s '%s' %s\n", xl('Document could not be moved to patient id'), $new_patient_id, xl('because that id does not exist.'));
} else {
$changefailed = !$d->change_patient($new_patient_id);
$this->_state = false;
- if (!$changefailed) {
- $messages .= xl('Document moved to patient id', '', '', ' \'') . $new_patient_id . xl('successfully.', '', '\' ') . "\n";
- } else {
- $messages .= xl('Document moved to patient id', '', '', ' \'') . $new_patient_id . xl('Failed.', '', '\' ') . "\n";
- }
+ $messages .= sprintf("%s '%s' %s\n", xl('Document moved to patient id'), $new_patient_id, xl($changefailed ? 'Failed.' : 'successfully.'));
$this->assign("messages", $messages);
return $this->list_action($patient_id);
}
diff --git a/interface/de_identification_forms/de_identification_screen1.php b/interface/de_identification_forms/de_identification_screen1.php
index 6245ff3f4025..fa90702ac1de 100644
--- a/interface/de_identification_forms/de_identification_screen1.php
+++ b/interface/de_identification_forms/de_identification_screen1.php
@@ -442,7 +442,7 @@ function download_file()
diff --git a/interface/forms/vitals/growthchart/chart.php b/interface/forms/vitals/growthchart/chart.php
index f2b9e5a10fbd..7b5830fa66b1 100644
--- a/interface/forms/vitals/growthchart/chart.php
+++ b/interface/forms/vitals/growthchart/chart.php
@@ -106,26 +106,18 @@
function unitsWt($wt)
{
global $isMetric;
- if ($isMetric) {
- //convert to metric
- return (number_format(($wt * 0.45359237), 2, '.', '') . xl('kg', '', ' '));
- } else {
- //keep US
- return number_format($wt, 2) . xl('lb', '', ' ');
- }
+ return $isMetric
+ ? sprintf('%s %s', number_format(($wt * 0.45359237), 2, '.', ''), xl('kg'))
+ : sprintf('%s %s', number_format($wt, 2), xl('lb'));
}
// convert to applicable length units from Config Locale
function unitsDist($dist)
{
global $isMetric;
- if ($isMetric) {
- //convert to metric
- return (number_format(($dist * 2.54), 2, '.', '') . xl('cm', '', ' '));
- } else {
- //keep US
- return number_format($dist, 2) . xl('in', '', ' ');
- }
+ return $isMetric
+ ? sprintf('%s %s', number_format(($dist * 2.54), 2, '.', ''), xl('cm'))
+ : sprintf('%s %s', number_format($dist, 2), xl('in'));
}
// convert vitals service data to US values for graphing
diff --git a/interface/logview/logview.php b/interface/logview/logview.php
index 8b955a904972..ef0a3f2529ce 100644
--- a/interface/logview/logview.php
+++ b/interface/logview/logview.php
@@ -316,7 +316,7 @@ function setpatient(pid, lname, fname, dob) {
//translate comments
$patterns = ['/^success/', '/^failure/', '/ encounter/'];
- $replace = [xl('success'), xl('failure'), xl('encounter', '', ' ')];
+ $replace = [xl('success'), xl('failure'), sprintf(' %s', xl('encounter'))];
$commentEncrStatus = !empty($iter['encrypt']) ? $iter['encrypt'] : "No";
$encryptVersion = !empty($iter['version']) ? $iter['version'] : 0;
diff --git a/interface/patient_file/summary/shot_record.php b/interface/patient_file/summary/shot_record.php
index 0c2eb9bb0d89..66581a955afb 100644
--- a/interface/patient_file/summary/shot_record.php
+++ b/interface/patient_file/summary/shot_record.php
@@ -204,7 +204,7 @@ function printHTML($res, $res2, $data): void
width: 100%;
}
-
+
diff --git a/interface/reports/audit_log_tamper_report.php b/interface/reports/audit_log_tamper_report.php
index e2d072d252a1..39527dfd8284 100644
--- a/interface/reports/audit_log_tamper_report.php
+++ b/interface/reports/audit_log_tamper_report.php
@@ -232,7 +232,7 @@ function setpatient(pid, lname, fname, dob) {
//translate comments
$patterns = ['/^success/','/^failure/','/ encounter/'];
- $replace = [ xl('success'), xl('failure'), xl('encounter', '', ' ')];
+ $replace = [ xl('success'), xl('failure'), sprintf(' %s', xl('encounter'))];
$checkSumOld = $iter['checksum'];
if (empty($checkSumOld)) {
diff --git a/interface/reports/front_receipts_report.php b/interface/reports/front_receipts_report.php
index 6332ca29fbc7..b18f26d4d14f 100755
--- a/interface/reports/front_receipts_report.php
+++ b/interface/reports/front_receipts_report.php
@@ -169,7 +169,7 @@ function show_receipt(pid,timestamp) {
- :
+ :
diff --git a/interface/reports/inventory_transactions.php b/interface/reports/inventory_transactions.php
index e5d059ce204a..a6541e6cb06a 100644
--- a/interface/reports/inventory_transactions.php
+++ b/interface/reports/inventory_transactions.php
@@ -294,7 +294,7 @@ function mysubmit(action) {
value=''>
- :
+ :
diff --git a/library/translation.inc.php b/library/translation.inc.php
index 25a3685b3f40..c07f6bf085fb 100644
--- a/library/translation.inc.php
+++ b/library/translation.inc.php
@@ -1,16 +1,17 @@
Date: Mon, 22 Dec 2025 23:32:05 -0500
Subject: [PATCH 002/139] refactor(library/date_functions.php): replace switch
with match and arrays, add type declarations (#9966)
* Initial plan
* refactor(library/date_functions.php): replace switch with match, add type declarations and standard docblock
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* refactor: address code review feedback - remove unnecessary cast and simplify dow formatting
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* refactor: use arrays instead of match for days and months lookups
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* refactor: simplify array approach - call xl() on lookup result instead of each element
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* perf: use static arrays to avoid repeated array creation on each function call
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* refactor: combine match arms for languages with same format and extract date variables
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
* style: improve readability by placing each language on separate line in match
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---
library/date_functions.php | 191 ++++++++++++-------------------------
1 file changed, 59 insertions(+), 132 deletions(-)
diff --git a/library/date_functions.php b/library/date_functions.php
index 7b7ce61cb26a..28e7aa7a1481 100755
--- a/library/date_functions.php
+++ b/library/date_functions.php
@@ -1,149 +1,76 @@
6 saturday
+ // name the day of the week for different languages
+ $day = (int) date("w", $strtime); // 0 sunday -> 6 saturday
- switch ($day) {
- case 0:
- $dow = xl('Sunday');
- break;
- case 1:
- $dow = xl('Monday');
- break;
- case 2:
- $dow = xl('Tuesday');
- break;
- case 3:
- $dow = xl('Wednesday');
- break;
- case 4:
- $dow = xl('Thursday');
- break;
- case 5:
- $dow = xl('Friday');
- break;
- case 6:
- $dow = xl('Saturday');
- break;
- }
+ static $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ $dow = xl($days[$day]);
-// name of the month in different languages
+ // name of the month in different languages
$month = (int) date('m', $strtime);
- switch ($month) {
- case 1:
- $nom = xl('January');
- break;
- case 2:
- $nom = xl('February');
- break;
- case 3:
- $nom = xl('March');
- break;
- case 4:
- $nom = xl('April');
- break;
- case 5:
- $nom = xl('May');
- break;
- case 6:
- $nom = xl('June');
- break;
- case 7:
- $nom = xl('July');
- break;
- case 8:
- $nom = xl('August');
- break;
- case 9:
- $nom = xl('September');
- break;
- case 10:
- $nom = xl('October');
- break;
- case 11:
- $nom = xl('November');
- break;
- case 12:
- $nom = xl('December');
- break;
- }
+ static $months = [
+ 1 => 'January',
+ 2 => 'February',
+ 3 => 'March',
+ 4 => 'April',
+ 5 => 'May',
+ 6 => 'June',
+ 7 => 'July',
+ 8 => 'August',
+ 9 => 'September',
+ 10 => 'October',
+ 11 => 'November',
+ 12 => 'December',
+ ];
+ $nom = xl($months[$month]);
-// Date string format
-// First, get current language title
+ // Date string format
+ // First, get current language title
$languageTitle = getLanguageTitle($_SESSION['language_choice']);
- switch ($languageTitle) {
+ $day_num = date("d", $strtime);
+ $year = date("Y", $strtime);
+ $dt = match ($languageTitle) {
// standard english first
- case getLanguageTitle(1):
- $dt = date("F j, Y", $strtime);
- if ($with_dow) {
- $dt = "$dow, $dt";
- }
- break;
- case "Swedish":
- $dt = date("Y", $strtime) . " $nom " . date("d", $strtime);
- if ($with_dow) {
- $dt = "$dow $dt";
- }
- break;
- case "Spanish":
- case "Spanish (Spain)":
- case "Spanish (Latin American)":
- $dt = date("d", $strtime) . " $nom " . date("Y", $strtime);
- if ($with_dow) {
- $dt = "$dow $dt";
- }
- break;
- case "German":
- $dt = date("d", $strtime) . " $nom " . date("Y", $strtime);
- if ($with_dow) {
- $dt = "$dow $dt";
- }
- break;
- case "Dutch":
- $dt = date("d", $strtime) . " $nom " . date("Y", $strtime);
- if ($with_dow) {
- $dt = "$dow $dt";
- }
- break;
- // hebrew (israel) , display english NOT jewish calendar
- case "Hebrew":
- $dt = date("d", $strtime) . " $nom " . date("Y", $strtime);
- if ($with_dow) {
- $dt = "$dow, $dt";
- }
- break;
- // default case
- default:
- $dt = "$nom " . date("d", $strtime) . ", " . date("Y", $strtime);
- if ($with_dow) {
- $dt = "$dow, $dt";
- }
+ getLanguageTitle(1) => date("F j, Y", $strtime),
+ "Swedish" => "$year $nom $day_num",
+ "Dutch",
+ "German",
+ "Hebrew",
+ "Spanish",
+ "Spanish (Latin American)",
+ "Spanish (Spain)" => "$day_num $nom $year",
+ default => "$nom $day_num, $year",
+ };
+
+ if ($with_dow) {
+ $separator = match ($languageTitle) {
+ getLanguageTitle(1), "Hebrew" => ", ",
+ default => " ",
+ };
+ $dt = "$dow$separator$dt";
}
return $dt;
From 866b839a69e4412d5ba093cbd3664075972eb7a3 Mon Sep 17 00:00:00 2001
From: Jerry Padgett
Date: Tue, 23 Dec 2025 00:50:58 -0500
Subject: [PATCH 003/139] Fix deprecated PHP prpperty_exist error in portal
(#9964)
trying to test for property oblect on an array is a no no for PHP 8.0+
---
portal/patient/fwk/libs/verysimple/Phreeze/Reporter.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/Reporter.php b/portal/patient/fwk/libs/verysimple/Phreeze/Reporter.php
index 25ed1c75908e..c1ce0bc2154a 100644
--- a/portal/patient/fwk/libs/verysimple/Phreeze/Reporter.php
+++ b/portal/patient/fwk/libs/verysimple/Phreeze/Reporter.php
@@ -30,7 +30,7 @@ abstract class Reporter implements Serializable
];
/** @var cache of public properties for each type for improved performance when enumerating */
- private static $PublicPropCache = [];
+ private static array $PublicPropCache = [];
/**
* Returns true if the current object has been loaded
@@ -176,7 +176,7 @@ public function GetPublicProperties()
{
$className = static::class;
- if (! property_exists(self::$PublicPropCache, $className)) {
+ if (! array_key_exists($className, self::$PublicPropCache)) {
$props = [];
$ro = new ReflectionObject($this);
From 951aa4bc3e8fb4e9a6077af5e549ebdf683a6ee7 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 23 Dec 2025 17:10:32 -0500
Subject: [PATCH 004/139] fix: undefined $filename breaks extension detection
in download_template.php (#9968)
* Initial plan
* fix(template): correct undefined $filename variable to $form_filename in download_template.php
Changed line 361 from using undefined $filename to $form_filename.
This fixes the bug where $ext was always empty, preventing ODT-specific
line break processing and potentially incorrect MIME type detection.
Fixes issue where LBF textarea line breaks were lost in ODT generation.
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---
interface/patient_file/download_template.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/interface/patient_file/download_template.php b/interface/patient_file/download_template.php
index 627f1595377c..7aa17e50bec7 100644
--- a/interface/patient_file/download_template.php
+++ b/interface/patient_file/download_template.php
@@ -358,7 +358,7 @@ function doSubs($s)
// Get mime type in a way that works with old and new PHP releases.
$mimetype = 'application/octet-stream';
-$ext = strtolower(array_pop(explode('.', (string) $filename)));
+$ext = strtolower(array_pop(explode('.', (string) $form_filename)));
if ('dotx' == $ext) {
// PHP does not seem to recognize this type.
$mimetype = 'application/msword';
From 2fd01e8534779ce5b80f35717925b8d6feff5548 Mon Sep 17 00:00:00 2001
From: "Michael A. Smith"
Date: Tue, 23 Dec 2025 18:21:56 -0500
Subject: [PATCH 005/139] refactor(patient_file): file type lookup in
download_template (#9970)
---
interface/patient_file/download_template.php | 65 +++++++++-----------
1 file changed, 28 insertions(+), 37 deletions(-)
diff --git a/interface/patient_file/download_template.php b/interface/patient_file/download_template.php
index 7aa17e50bec7..937bae8dc7e0 100644
--- a/interface/patient_file/download_template.php
+++ b/interface/patient_file/download_template.php
@@ -357,44 +357,35 @@ function doSubs($s)
$fname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
// Get mime type in a way that works with old and new PHP releases.
-$mimetype = 'application/octet-stream';
+$default_mimetype = 'application/octet-stream';
$ext = strtolower(array_pop(explode('.', (string) $form_filename)));
-if ('dotx' == $ext) {
- // PHP does not seem to recognize this type.
- $mimetype = 'application/msword';
-} elseif (function_exists('finfo_open')) {
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
- $mimetype = finfo_file($finfo, $templatepath);
- finfo_close($finfo);
-} elseif (function_exists('mime_content_type')) {
- $mimetype = mime_content_type($templatepath);
-} else {
- if ('doc' == $ext) {
- $mimetype = 'application/msword' ;
- } elseif ('dot' == $ext) {
- $mimetype = 'application/msword' ;
- } elseif ('htm' == $ext) {
- $mimetype = 'text/html' ;
- } elseif ('html' == $ext) {
- $mimetype = 'text/html' ;
- } elseif ('odt' == $ext) {
- $mimetype = 'application/vnd.oasis.opendocument.text' ;
- } elseif ('ods' == $ext) {
- $mimetype = 'application/vnd.oasis.opendocument.spreadsheet' ;
- } elseif ('ott' == $ext) {
- $mimetype = 'application/vnd.oasis.opendocument.text' ;
- } elseif ('pdf' == $ext) {
- $mimetype = 'application/pdf' ;
- } elseif ('ppt' == $ext) {
- $mimetype = 'application/vnd.ms-powerpoint' ;
- } elseif ('ps' == $ext) {
- $mimetype = 'application/postscript' ;
- } elseif ('rtf' == $ext) {
- $mimetype = 'application/rtf' ;
- } elseif ('txt' == $ext) {
- $mimetype = 'text/plain' ;
- } elseif ('xls' == $ext) {
- $mimetype = 'application/vnd.ms-excel' ;
+$doc_to_mimetype = [
+ 'doc' => 'application/msword',
+ 'dot' => 'application/msword',
+ 'dotx' => 'application/msword',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'ott' => 'application/vnd.oasis.opendocument.text',
+ 'pdf' => 'application/pdf',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'ps' => 'application/postscript',
+ 'rtf' => 'application/rtf',
+ 'txt' => 'text/plain',
+ 'xls' => 'application/vnd.ms-excel',
+];
+$mimetype = $doc_to_mimetype[$ext] ?? $default_mimetype;
+
+// PHP does not seem to recognize 'dotx'
+// so we don't let it override that type.
+if ($ext != 'dotx') {
+ if (function_exists('finfo_open')) {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mimetype = finfo_file($finfo, $templatepath);
+ finfo_close($finfo);
+ } elseif (function_exists('mime_content_type')) {
+ $mimetype = mime_content_type($templatepath);
}
}
From 5e1886e88359da32f769636117e7781c0f105718 Mon Sep 17 00:00:00 2001
From: "Michael A. Smith"
Date: Tue, 23 Dec 2025 20:17:18 -0500
Subject: [PATCH 006/139] perf: add composite index on lang_definitions for
translation lookups (#9974)
---
sql/7_0_4-to-7_0_5_upgrade.sql | 4 ++++
sql/database.sql | 3 ++-
version.php | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/sql/7_0_4-to-7_0_5_upgrade.sql b/sql/7_0_4-to-7_0_5_upgrade.sql
index bac282c7cded..e169980004e4 100644
--- a/sql/7_0_4-to-7_0_5_upgrade.sql
+++ b/sql/7_0_4-to-7_0_5_upgrade.sql
@@ -112,3 +112,7 @@
-- #IfMBOEncounterNeeded
-- desc: Add encounter to the form_misc_billing_options table
-- arguments: none
+
+#IfNotIndex lang_definitions lang_cons
+CREATE INDEX `lang_cons` ON `lang_definitions` (`lang_id`, `cons_id`);
+#EndIf
diff --git a/sql/database.sql b/sql/database.sql
index 7034656061ff..a13464bcbb1f 100644
--- a/sql/database.sql
+++ b/sql/database.sql
@@ -3537,7 +3537,8 @@ CREATE TABLE `lang_definitions` (
`lang_id` int(11) NOT NULL default '0',
`definition` mediumtext,
UNIQUE KEY `def_id` (`def_id`),
- KEY `cons_id` (`cons_id`)
+ KEY `cons_id` (`cons_id`),
+ KEY `lang_cons` (`lang_id`, `cons_id`)
) ENGINE=InnoDB;
-- --------------------------------------------------------
diff --git a/version.php b/version.php
index eb24b675aaaa..3d81ac654bca 100644
--- a/version.php
+++ b/version.php
@@ -28,7 +28,7 @@
// is a database change in the course of development. It is used
// internally to determine when a database upgrade is needed.
//
-$v_database = 529;
+$v_database = 530;
// Access control version identifier, this is to be incremented whenever there
// is a access control change in the course of development. It is used
From 972a9123b829c33658abb78da15d928e11bb5341 Mon Sep 17 00:00:00 2001
From: "Michael A. Smith"
Date: Tue, 23 Dec 2025 20:23:55 -0500
Subject: [PATCH 007/139] feat: add configuration option to disable translation
engine (#9976)
---
.phpstan/phpstan-globals-baseline.neon | 2 +-
library/globals.inc.php | 7 +++++++
library/translation.inc.php | 2 +-
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/.phpstan/phpstan-globals-baseline.neon b/.phpstan/phpstan-globals-baseline.neon
index 98a455bdf779..7e7255a21788 100644
--- a/.phpstan/phpstan-globals-baseline.neon
+++ b/.phpstan/phpstan-globals-baseline.neon
@@ -2486,7 +2486,7 @@ parameters:
path: ../library/templates/telecom_form.php
- message: '#^Direct access to \$GLOBALS is forbidden\. Use OEGlobalsBag\:\:getInstance\(\)\-\>get\(\) instead\.$#'
identifier: openemr.forbiddenGlobalsAccess
- count: 8
+ count: 9
path: ../library/translation.inc.php
- message: '#^Direct access to \$GLOBALS is forbidden\. Use OEGlobalsBag\:\:getInstance\(\)\-\>get\(\) instead\.$#'
identifier: openemr.forbiddenGlobalsAccess
diff --git a/library/globals.inc.php b/library/globals.inc.php
index af68ff2157d9..28372c34c63c 100644
--- a/library/globals.inc.php
+++ b/library/globals.inc.php
@@ -666,6 +666,13 @@ function gblTimeZones()
xl('This will turn off use of safe apostrophe, which is done by converting \' and " to `.(it is highly recommended that this setting is turned off and that safe apostrophe\'s are used)')
],
+ 'disable_translation' => [
+ xl('Disable Translation Engine'),
+ 'bool', // data type
+ '0', // default = false
+ xl('Completely disable the translation engine. When enabled, xl() returns the input string unchanged. Use this for English-only deployments to eliminate translation overhead.')
+ ],
+
'translate_layout' => [
xl('Translate Layouts'),
'bool', // data type
diff --git a/library/translation.inc.php b/library/translation.inc.php
index c07f6bf085fb..0497a35fa76b 100644
--- a/library/translation.inc.php
+++ b/library/translation.inc.php
@@ -13,7 +13,7 @@
*/
function xl(string $constant): string
{
- if (!empty($GLOBALS['temp_skip_translations'])) {
+ if (!empty($GLOBALS['disable_translation']) || !empty($GLOBALS['temp_skip_translations'])) {
return $constant;
}
From 4a9c83515d2d1a77f4ebcf53e8cea8dcaf37da7b Mon Sep 17 00:00:00 2001
From: Brady Miller
Date: Tue, 23 Dec 2025 21:37:35 -0800
Subject: [PATCH 008/139] chore: update changelog for 7.0.4 (#9973)
* chore: update changelog for 7.0.4
* context manager, search and add providers with npi
---------
Co-authored-by: stephen waite
---
CHANGELOG.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 153 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed6503d99ac1..d38b6a2aad1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,158 @@
# CHANGELOG.md
+## [7.0.4](https://github.com/openemr/openemr/milestone/17?closed=1) - 2025-12-23
+
+### Added
+- New Dashboard Context Manager ([#9644](https://github.com/openemr/openemr/issues/9644))
+- EHI Export export with new tables and fields ([#9509](https://github.com/openemr/openemr/issues/9509))
+- SMART on FHIR V2.2.0 - support new scope syntax for granular permissions ([#8639](https://github.com/openemr/openemr/issues/8639))
+- Upgrade USPS address verification to latest API ([#7815](https://github.com/openemr/openemr/issues/7815))
+- change selenium chrome to chromium for arm64 support and incorporate dependabot for updates ([#8737](https://github.com/openemr/openemr/issues/8737))
+- migrate old care team data ([#9551](https://github.com/openemr/openemr/issues/9551))
+- openemr api documentation ([#9827](https://github.com/openemr/openemr/issues/9827))
+- pt pay invoice via onetime token ([#8079](https://github.com/openemr/openemr/issues/8079))
+- support for redis sentinel via predis, step 1 of 2 ([#8410](https://github.com/openemr/openemr/pull/8410))
+- search and add external providers with nppes npi api ([#9276](https://github.com/openemr/openemr/issues/9276))
+
+
+### Fixed
+- "Retrieve" is sometimes misspelled "retrive" ([#8595](https://github.com/openemr/openemr/issues/8595))
+- Add Care Team Management for ONC 2025 Compliance ([#8693](https://github.com/openemr/openemr/issues/8693))
+- Add Sent Fax Tracking to etherFAX Module ([#9850](https://github.com/openemr/openemr/issues/9850))
+- Add USCDI v5 Average Blood Pressure Observation to CCDA Vitals Organizer ([#9571](https://github.com/openemr/openemr/issues/9571))
+- Add reason code to our R4 FHIRCarePlan generated domain ([#9262](https://github.com/openemr/openemr/issues/9262))
+- Advance Directives USCDI v5 FHIR Observation ([#9224](https://github.com/openemr/openemr/issues/9224))
+- Argument passed to class with no constructor ([#8814](https://github.com/openemr/openemr/issues/8814))
+- Attempt to migrate USPS Address Verification to API v3 Tried to switc… ([#9132](https://github.com/openemr/openemr/pull/9132))
+- CCDA ONC 2025 Updates ([#8647](https://github.com/openemr/openemr/issues/8647))
+- Care plan is missing an end date and status. ([#8909](https://github.com/openemr/openemr/issues/8909))
+- Consistently initialize $string in toString implementations ([#8449](https://github.com/openemr/openemr/issues/8449))
+- Cover EventAuditLogger with tests ([#8678](https://github.com/openemr/openemr/issues/8678))
+- CryptoGen needs an interface ([#8744](https://github.com/openemr/openemr/issues/8744))
+- Current Bugs Demographic Related Person ([#9414](https://github.com/openemr/openemr/issues/9414))
+- Dockerfile healthchecks for openemr ([#8432](https://github.com/openemr/openemr/issues/8432))
+- Enable Dependabot Version Upgrades for Github Actions ([#8438](https://github.com/openemr/openemr/issues/8438))
+- Enhance FHIR Coverage to US Core 8.0 / USCDI v5 Compliance ([#9264](https://github.com/openemr/openemr/issues/9264))
+- EventAuditLogger unnecessarily instantiates CryptoGen when disabled ([#8652](https://github.com/openemr/openemr/issues/8652))
+- Feat openemr 9564 api documentation ([#9569](https://github.com/openemr/openemr/pull/9569))
+- Finish ONC USCDI v5 US Core 8.0 Service Request ([#9107](https://github.com/openemr/openemr/issues/9107))
+- Fix PHP 8.0+ undefined array key 'ins' warning in ins_search.php ([#9518](https://github.com/openemr/openemr/pull/9518))
+- Fix PHP 8.0+ warnings for undefined Smarty variables in prescription lookup ([#9517](https://github.com/openemr/openemr/pull/9517))
+- Fix SQL error when sorting by Allergies/Medical Problems without diagnosis filter ([#9519](https://github.com/openemr/openemr/pull/9519))
+- Fix sporadic test failure in HMAC validation tests ([#9536](https://github.com/openemr/openemr/pull/9536))
+- Fix undefined array key 'site_id' in authCloseSession() ([#9522](https://github.com/openemr/openemr/pull/9522))
+- Fix undefined array key warnings in CAMOS form POST handling ([#9523](https://github.com/openemr/openemr/pull/9523))
+- Fix undefined array key warnings in transfer_summary form ([#9521](https://github.com/openemr/openemr/pull/9521))
+- Fix undefined variable $odrstmt in clinical_reports.php ([#9520](https://github.com/openemr/openemr/pull/9520))
+- Fix undefined variables in find_code_dynamic_ajax.php when $what == 'codes' ([#9524](https://github.com/openemr/openemr/pull/9524))
+- Fixes #9552 remove debugger statement ([#9553](https://github.com/openemr/openemr/pull/9553))
+- Fixes #9617 prescription encounter display. ([#9618](https://github.com/openemr/openemr/pull/9618))
+- Fixes #9702 swagger bugs ([#9703](https://github.com/openemr/openemr/pull/9703))
+- Fixes #9756 related person endpoint ([#9758](https://github.com/openemr/openemr/pull/9758))
+- Fixes to CI Caching ([#8672](https://github.com/openemr/openemr/issues/8672))
+- Flow Board Colors are unset when you set them ([#9401](https://github.com/openemr/openemr/issues/9401))
+- Healthchecks for docker-compose used in ci/tests ([#8399](https://github.com/openemr/openemr/issues/8399))
+- Implement health checks in docker ([#8368](https://github.com/openemr/openemr/issues/8368))
+- Implement stand-alone SDOH (USCDI v3) history form ([#8849](https://github.com/openemr/openemr/issues/8849))
+- Incorporate multi-file composition with ci docker test environments ([#8426](https://github.com/openemr/openemr/issues/8426))
+- Introduce tests that can run without the database ([#8725](https://github.com/openemr/openemr/issues/8725))
+- Inventory lot add/edit JS errors ([#9912](https://github.com/openemr/openemr/issues/9912))
+- Missing couchdb volume causes rsync error during openemr startup in development-easy-light setup ([#8593](https://github.com/openemr/openemr/issues/8593))
+- Missing support for tests Specimen collection USCDI v5 ([#9000](https://github.com/openemr/openemr/issues/9000))
+- Multiple FHIR services missing or not passing $puuidBind parameter for patient compartment security ([#9160](https://github.com/openemr/openemr/issues/9160))
+- Persisted Data Not Saving Integer 0/False Values in ORDataObject ([#9629](https://github.com/openemr/openemr/issues/9629))
+- Portal History form Family diagnosis code session error ([#9621](https://github.com/openemr/openemr/issues/9621))
+- Portal Phreezable controller causing errors ([#9727](https://github.com/openemr/openemr/issues/9727))
+- Provide video artifacts from e2e tests for easy debugging ([#8581](https://github.com/openemr/openemr/issues/8581))
+- Publish coverage reports for tests ([#8403](https://github.com/openemr/openemr/issues/8403))
+- Related Persons ([#8851](https://github.com/openemr/openemr/issues/8851))
+- Rename `sqlQuery` in `admin.php` to avoid confusion ([#8464](https://github.com/openemr/openemr/issues/8464))
+- Result of void function is used ([#8805](https://github.com/openemr/openemr/issues/8805))
+- Set Rector to Level 1 and PHP 8.1 ([#8600](https://github.com/openemr/openemr/issues/8600))
+- Simplify CI Test Runner using Matrix ([#8402](https://github.com/openemr/openemr/issues/8402))
+- Status Badges report no status ([#8413](https://github.com/openemr/openemr/issues/8413))
+- Swagger docs normalization ([#9196](https://github.com/openemr/openemr/issues/9196))
+- Tidy up CryptoGen docblocks and types ([#8742](https://github.com/openemr/openemr/issues/8742))
+- US Core 8.0 & USCDI v5 Goal Profile Migration Guide ([#9139](https://github.com/openemr/openemr/issues/9139))
+- US Core 8.0 & USCDI v5 Medication Implementation Gap Analysis ([#9159](https://github.com/openemr/openemr/issues/9159))
+- US Core 8.0 USCDI v3-5 FHIR Observation Treatment Intervention Preference - Observation Care Experience Preference ([#9295](https://github.com/openemr/openemr/issues/9295))
+- USCDI v5 and US Core 8.0 Compliance Gap Analysis Procedure ([#8968](https://github.com/openemr/openemr/issues/8968))
+- Unable to set Compatibility mode flag OEGlobalsBag ([#9681](https://github.com/openemr/openemr/issues/9681))
+- Update CarePlan FHIR to U.S Core 8.0 and USCDI v5 ([#9195](https://github.com/openemr/openemr/issues/9195))
+- Update NewCrop/Ensora eRx Site Address ([#9745](https://github.com/openemr/openemr/issues/9745))
+- Update add_edit_event.php to allow repeat every 7th, 8th, 9th ([#7968](https://github.com/openemr/openemr/pull/7968))
+- Update and refactor CareTeam Services. Support U.S Core 8.0 and USCDI v5 ([#9189](https://github.com/openemr/openemr/issues/9189))
+- Update eCQM measures from 2022 to 2023 reporting period ([#8749](https://github.com/openemr/openemr/issues/8749))
+- Upgrade FHIR Laboratory Resources to USCDI v5 / US Core 8.0 ([#9105](https://github.com/openemr/openemr/issues/9105))
+- [CI][API][Docs] Added API docs freshness check ([#9232](https://github.com/openemr/openemr/pull/9232))
+- allow appointment repeat every 7th, 8th, 9th occurrences ([#7967](https://github.com/openemr/openemr/issues/7967))
+- bug openemr 9619 fix gender unk code ([#9622](https://github.com/openemr/openemr/pull/9622))
+- Clinical Notes Add Item for existing record clones previous author in author field instead of using current user. ([#9530](https://github.com/openemr/openemr/issues/9530))
+- Fix openemr:generate-access-token when no offline_access scope is present ([#9620](https://github.com/openemr/openemr/issues/9620))
+- Patient Gender should come from valueset https://hl7.org/fhir/R4/valueset-administrative-gender.html ([#9619](https://github.com/openemr/openemr/issues/9619))
+- RelatedPerson not respecting puuidBind and missing from swagger ([#9756](https://github.com/openemr/openemr/issues/9756))
+- Save & Continue Procedure Order with Specimen did not save specimen for new order, had to enter and save data again on existing order for save to work ([#9528](https://github.com/openemr/openemr/issues/9528))
+- Swagger tool v2 scopes missing, invalid $docref operation, trusted user save bug ([#9702](https://github.com/openemr/openemr/issues/9702))
+- add_edit_issue_medication_fragment.php has a javascript debugger statement left in the file ([#9552](https://github.com/openemr/openemr/issues/9552))
+- billing related causes codes missing ([#9668](https://github.com/openemr/openemr/issues/9668))
+- delete glasses prescription ([#9614](https://github.com/openemr/openemr/issues/9614))
+- encounter select list on prescription edit is showing as a non-editable number instead of a select dropdown ([#9617](https://github.com/openemr/openemr/issues/9617))
+- unit testing has a few deprecations ([#8421](https://github.com/openemr/openemr/issues/8421))
+- use custom temp directory for htmlpurify for OnsiteDocumentController.php ([#9583](https://github.com/openemr/openemr/issues/9583))
+- bump setup-node action from v3 to v4 ([#8441](https://github.com/openemr/openemr/issues/8441))
+- ccdaservice doesn't build on node 24 ([#8418](https://github.com/openemr/openemr/issues/8418))
+- etherFAX display renders faxes in wrong tab ([#9725](https://github.com/openemr/openemr/issues/9725))
+- feat openemr 9551 migrate careteam ([#9563](https://github.com/openemr/openemr/pull/9563))
+- isSmsEnabled is defined and not nullable ([#8816](https://github.com/openemr/openemr/issues/8816))
+- phpstan whitespace after ?> ([#8800](https://github.com/openemr/openemr/issues/8800))
+- reduce repetition in ci docker-compose files ([#8416](https://github.com/openemr/openemr/issues/8416))
+- remove ubuntu package scripts ([#8519](https://github.com/openemr/openemr/issues/8519))
+- replace tiff.js with utif2 for fax ([#8210](https://github.com/openemr/openemr/pull/8210))
+- unpinned, unmaintained dependency on summernote-plugins repo ([#8383](https://github.com/openemr/openemr/issues/8383))
+
+
+### Changed
+- Related Person Save ([#9562](https://github.com/openemr/openemr/issues/9562))
+- Product Email Registration Error, GenericProductRegistrationException ([#9680](https://github.com/openemr/openemr/issues/9680))
+- :parseToTokens() to use preg_split and fix PHP 8.x warnings ([#9525](https://github.com/openemr/openemr/pull/9525))
+- bump express from 4.22.0 to 4.22.1 in /ccdaservice ([#9861](https://github.com/openemr/openemr/pull/9861))
+- bump jspdf from 3.0.3 to 3.0.4 ([#9582](https://github.com/openemr/openemr/pull/9582))
+- bump stylelint from 16.25.0 to 16.26.0 in the build-tools group ([#9581](https://github.com/openemr/openemr/pull/9581))
+- Backport to rel-704: Merge commit from fork ([#9722](https://github.com/openemr/openemr/issues/9722))
+- Backport to rel-704: Merge commit from fork ([#9697](https://github.com/openemr/openemr/issues/9697))
+- Need to keep robthree/twofactorauth at 2.1.0 since higher requires php > 8.1 ([#8256](https://github.com/openemr/openemr/issues/8256))
+- add mariadb 11.8 LTS and mysql 9.3 to ci ([#8440](https://github.com/openemr/openemr/issues/8440))
+- add mariadb 12.0 to ci ([#9193](https://github.com/openemr/openemr/issues/9193))
+- add php 8.6-dev to pertinent ci ([#9227](https://github.com/openemr/openemr/issues/9227))
+- add php dependencies to composer json ([#8708](https://github.com/openemr/openemr/issues/8708))
+- add php extensions dom and imagick to dev-php-fpm ci dockers ([#9335](https://github.com/openemr/openemr/issues/9335))
+- also support mailpit in insane dev environment ([#9267](https://github.com/openemr/openemr/issues/9267))
+- api docs freshness test failing ([#9586](https://github.com/openemr/openemr/issues/9586))
+- increment crypto to version 7 and pertinent testing changes ([#8820](https://github.com/openemr/openemr/issues/8820))
+- migrate core ci alpine stuff to php 8.4 (openemr/openemr:flex-3.21) ([#8406](https://github.com/openemr/openemr/issues/8406))
+- migrated onc-certification-g10-test-kit repo to openemr org ([#8607](https://github.com/openemr/openemr/issues/8607))
+- php 8.5-rc for ci and 8.6-dev for ci and insane dev environment ([#8935](https://github.com/openemr/openemr/issues/8935))
+- prep docker for auto upgrade for future 7.0.4 release ([#9577](https://github.com/openemr/openemr/issues/9577))
+- update acknowledgments for planned 7.0.4 release ([#9885](https://github.com/openemr/openemr/issues/9885))
+- update api docs ([#9670](https://github.com/openemr/openemr/pull/9670))
+- update readme and ci to nodejs 22 ([#8654](https://github.com/openemr/openemr/issues/8654))
+- upgrade phpunit/phpunit at 10 before 2026 since higher requires php > 8.1 #8254 ([#8276](https://github.com/openemr/openemr/issues/8276))
+- drop php 8.1 support, update league/csv, robthree/twofactorauth, phpunit/phpunit ([#8274](https://github.com/openemr/openemr/issues/8274))
+- bump actions/checkout from 5 to 6 ([#9579](https://github.com/openemr/openemr/pull/9579))
+- fix older php-fpm dockers (update to newer debian version) ([#8605](https://github.com/openemr/openemr/issues/8605))
+- fix openemr/dev-php-fpm dockers ([#8578](https://github.com/openemr/openemr/issues/8578))
+- fix openemr/dev-php-fpm:pre-build-dev-85 docker ([#8574](https://github.com/openemr/openemr/issues/8574))
+- fix edge case in functional testing when adding user and fix inferno skipping logic ([#8604](https://github.com/openemr/openemr/issues/8604))
+- asset loading errors for header ([#9492](https://github.com/openemr/openemr/pull/9492))
+- couple function void fixes ([#8609](https://github.com/openemr/openemr/issues/8609))
+- crypto encrypt fix to maintain behavior of encryption of empty string when empty string or null ([#8902](https://github.com/openemr/openemr/issues/8902))
+- dependabot fix for selenium ([#8765](https://github.com/openemr/openemr/issues/8765))
+- ignore submodules for composer.json dependabot ([#8780](https://github.com/openemr/openemr/issues/8780))
+- minor escaping fix in add_edit_issue.php ([#9246](https://github.com/openemr/openemr/issues/9246))
+- remove requirement of opcache php extension from composer ([#9343](https://github.com/openemr/openemr/issues/9343))
+- unit testing for Patient Controller Security ([#9731](https://github.com/openemr/openemr/issues/9731))
+
+
## [7.0.3.4](https://github.com/openemr/openemr/milestone/21?closed=1) - 2025-05-18
### Added
From c4d18356eb0e0440cfb93da9cc6e7bb6abd73e54 Mon Sep 17 00:00:00 2001
From: Jerry Padgett
Date: Wed, 24 Dec 2025 22:28:22 -0500
Subject: [PATCH 009/139] Refactor Portal CCDA actions (#9950)
* Refactor Portal CCDA actions
Remove API request for CCDA view to use CCM services.
* refactor for new portal CCD actions supplanting API request for same actions.
had goober help me ensure exceptions, translations and formatting.
* woops
* rector starting to get on my nerves. local want string cast but not get. local comes from current master
* I refactored Laminas Zip to use Ziparchive similar to CdaDocument class. I could do some consolation and may do yet.
I had claude run through and satirize to make our security gods happy.
* satisfy new composer checker test. Trying to figure out where this is going.
Shouldn't this be included in composer for the entire namespace?
---
.composer-require-checker.json | 2 +
ccdaservice/ccda_gateway.php | 123 +++---
.../Controller/EncountermanagerController.php | 127 +++++-
src/Services/CDADocumentService.php | 415 ++++++++++--------
4 files changed, 427 insertions(+), 240 deletions(-)
diff --git a/.composer-require-checker.json b/.composer-require-checker.json
index 3a7819199313..180af3932784 100644
--- a/.composer-require-checker.json
+++ b/.composer-require-checker.json
@@ -5,6 +5,8 @@
"BillingExport",
"C_Document",
"Carecoordination\\Model\\CarecoordinationTable",
+ "Carecoordination\\Model\\CcdaGenerator",
+ "Carecoordination\\Model\\EncounterccdadispatchTable",
"FPDF_FONTPATH",
"GenericProductRegistrationException",
"IS_WINDOWS",
diff --git a/ccdaservice/ccda_gateway.php b/ccdaservice/ccda_gateway.php
index 1ff14cec1c29..fcbb35148f62 100644
--- a/ccdaservice/ccda_gateway.php
+++ b/ccdaservice/ccda_gateway.php
@@ -7,101 +7,120 @@
* @link https://www.open-emr.org
* @author Jerry Padgett
* @author Brady Miller
- * @copyright Copyright (c) 2016-2022 Jerry Padgett
+ * @copyright Copyright (c) 2016-2025 Jerry Padgett
* @copyright Copyright (c) 2019 Brady Miller
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/
use OpenEMR\Common\Csrf\CsrfUtils;
+use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Common\Session\SessionUtil;
+use OpenEMR\Core\OEGlobalsBag;
use OpenEMR\Services\CDADocumentService;
-// authenticate for portal or main- never know where it gets used
// Will start the (patient) portal OpenEMR session/cookie.
// Need access to classes, so run autoloader now instead of in globals.php.
$GLOBALS['already_autoloaded'] = true;
-require_once(__DIR__ . "/../vendor/autoload.php");
+require_once __DIR__ . "/../vendor/autoload.php";
SessionUtil::portalSessionStart();
$sessionAllowWrite = true;
-if (isset($_SESSION['pid']) && isset($_SESSION['patient_portal_onsite_two'])) {
+if (isset($_SESSION['pid'], $_SESSION['patient_portal_onsite_two'])) {
$pid = $_SESSION['pid'];
$ignoreAuth = true;
- require_once(__DIR__ . "/../interface/globals.php");
+ require_once __DIR__ . "/../interface/globals.php";
define('IS_DASHBOARD', false);
define('IS_PORTAL', $_SESSION['pid']);
} else {
SessionUtil::portalSessionCookieDestroy();
$ignoreAuth = false;
- require_once(__DIR__ . "/../interface/globals.php");
- if (!isset($_SESSION['authUserID'])) {
- $landingpage = "index.php";
- header('Location: ' . $landingpage);
+ require_once __DIR__ . "/../interface/globals.php";
+ if (empty($_SESSION['authUserID'])) {
+ header('Location: index.php');
exit;
}
define('IS_DASHBOARD', $_SESSION['authUserID']);
define('IS_PORTAL', false);
}
-if (!CsrfUtils::verifyCsrfToken($_GET["csrf_token_form"])) {
+if (!CsrfUtils::verifyCsrfToken($_GET["csrf_token_form"] ?? '')) {
CsrfUtils::csrfNotVerified();
}
-if (empty($GLOBALS['ccda_alt_service_enable'])) {
- die("Cda generation service turned off: Verify in Administration->Globals! Click back to return home."); // Die an honorable death!!
-}
-if (IS_PORTAL && $GLOBALS['ccda_alt_service_enable'] < 2) {
- die("Cda generation service turned off: Verify in Administration->Globals! Click back to return home."); // Die an honorable death!!
-}
-if (IS_DASHBOARD && ($GLOBALS['ccda_alt_service_enable'] != 1 && $GLOBALS['ccda_alt_service_enable'] != 3)) {
- die("Cda generation service turned off: Verify in Administration->Globals! Click back to return home."); // Die an honorable death!!
-}
-
-if (!isset($_SESSION['site_id'])) {
- $_SESSION ['site_id'] = 'default';
+if (!isServiceEnabled()) {
+ die(xlt("CDA generation service is disabled. Verify in Administration->Globals."));
}
+$_SESSION['site_id'] ??= 'default';
session_write_close();
-$cdaService = new CDADocumentService();
+$action = $_REQUEST['action'] ?? '';
+$pid ??= 0;
-if ($_REQUEST['action'] === 'dl') {
- $ccda_xml = $cdaService->portalGenerateCCDZip($pid);
- // download zip containing CCDA.xml, CCDA.html and cda.xsl files
- header("Cache-Control: public");
- header("Content-Description: File Transfer");
- header("Content-Disposition: attachment; filename=SummaryofCare.zip");
- header("Content-Type: application/zip");
- header("Content-Transfer-Encoding: binary");
- echo $ccda_xml;
- exit;
-}
-if ($_REQUEST['action'] === 'view') {
- $ccda_xml = $cdaService->portalGenerateCCD($pid);
- // CCM returns viewable CCD html file
- // that displays to new tab opened from home
- echo $ccda_xml;
- exit;
+try {
+ $cdaService = new CDADocumentService();
+
+ switch ($action) {
+ case 'dl':
+ case 'report_ccd_download':
+ sendZipDownload($cdaService->generateCCDZip($pid));
+ break;
+
+ case 'view':
+ echo $cdaService->generateCCDHtml($pid);
+ break;
+
+ case 'report_ccd_view':
+ $html = $cdaService->generateCCDHtml($pid);
+ if (stripos($html, '/interface/login_screen.php') !== false) {
+ http_response_code(401);
+ echo xlt("Error: Not Authorized");
+ exit;
+ }
+ echo $html;
+ break;
+
+ default:
+ http_response_code(400);
+ die(xlt("Error: Invalid action requested."));
+ }
+} catch (Exception $e) {
+ (new SystemLogger())->errorLogCaller($e->getMessage(), ['action' => $action, 'pid' => $pid]);
+ http_response_code(500);
+ die(xlt("Error generating CDA document. Please contact support."));
}
-if ($_REQUEST['action'] === 'report_ccd_view') {
- $ccda_xml = $cdaService->generateCCDHtml($pid);
- if (stripos($ccda_xml, '/interface/login_screen.php') !== false) {
- echo(xlt("Error. Not Authorized."));
- exit;
+
+/**
+ * Check if CDA service is enabled for current context.
+ */
+function isServiceEnabled(): bool
+{
+ $setting = OEGlobalsBag::getInstance()->getInt('ccda_alt_service_enable', 0);
+
+ if (empty($setting)) {
+ return false;
+ }
+ if (IS_PORTAL && $setting < 2) {
+ return false;
+ }
+ if (IS_DASHBOARD && $setting != 1 && $setting != 3) {
+ return false;
}
- echo $ccda_xml;
- exit;
+ return true;
}
-if ($_REQUEST['action'] === 'report_ccd_download') {
- $ccda_xml = $cdaService->generateCCDZip($pid);
- // download zip containing CCDA.xml, CCDA.html and cda.xsl files
+
+/**
+ * Send ZIP file as download response.
+ */
+function sendZipDownload(string $content): void
+{
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=SummaryofCare.zip");
header("Content-Type: application/zip");
header("Content-Transfer-Encoding: binary");
- echo $ccda_xml;
+ header("Content-Length: " . strlen($content));
+ echo $content;
exit;
}
-die(xlt("Error. Nothing to do."));
diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php
index cba3ec89335c..45d318637abb 100644
--- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php
+++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncountermanagerController.php
@@ -21,7 +21,6 @@
use Carecoordination\Model\CcdaUserPreferencesTransformer;
use Carecoordination\Model\EncountermanagerTable;
use DOMDocument;
-use Laminas\Filter\Compress\Zip;
use Laminas\Hydrator\Exception\RuntimeException;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\JsonModel;
@@ -304,6 +303,12 @@ public function buildCCDAHtml($content)
public function downloadAction()
{
$id = $this->getRequest()->getQuery('id');
+ // Validate that id is numeric to prevent path traversal
+ if (!is_numeric($id)) {
+ throw new \InvalidArgumentException('Invalid document ID');
+ }
+ $id = (int)$id;
+
$dir = sys_get_temp_dir() . "/CCDA_$id/";
$filename = "CCDA_$id.xml";
$filename_html = "CCDA_$id.html";
@@ -331,21 +336,20 @@ public function downloadAction()
copy(__DIR__ . "/../../../../../public/xsl/cda.xsl", $dir . "CDA.xsl");
- $zip = new Zip();
- $zip->setArchive($zip_dir . $zip_name);
- $zip->compress($dir);
+ $this->zipDirectory($dir, $zip_dir . $zip_name);
ob_clean();
header("Cache-Control: public");
header("Content-Description: File Transfer");
- header("Content-Disposition: attachment; filename=$zip_name");
+ // Sanitize filename for Content-Disposition header
+ header("Content-Disposition: attachment; filename=\"" . $this->sanitizeFilename($zip_name) . "\"");
header("Content-Type: application/download");
header("Content-Transfer-Encoding: binary");
readfile($zip_dir . $zip_name);
- // we need to unlink both the directory and the zip file once are done... as its a security hazard
- // to have these files just hanging around in a tmp folder
+ // Clean up temp files - both the zip and source directory
unlink($zip_dir . $zip_name);
+ $this->deleteDirectory($dir);
$view = new ViewModel();
$view->setTerminal(true);
@@ -360,7 +364,6 @@ public function downloadallAction()
$pids = $this->params('pids');
$document_type = $this->params('document_type') ?? '';
if ($pids != '') {
- $zip = new Zip();
$parent_dir = sys_get_temp_dir() . "/CCDA_Patient_Documents_" . time();
if (!is_dir($parent_dir)) {
if (!mkdir($parent_dir, true) && !is_dir($parent_dir)) {
@@ -370,7 +373,7 @@ public function downloadallAction()
}
$dir = $parent_dir . "/";
- $arr = explode('|', (string) $pids);
+ $arr = explode('|', (string)$pids);
foreach ($arr as $row) {
$pid = $row;
$ids = $this->getEncountermanagerTable()->getFileID($pid, 2);
@@ -388,7 +391,10 @@ public function downloadallAction()
}
/* let's not have a dir per patient for now! though we'll keep for awhile.
* $dir = $parent_dir . "/CCDA_{$row_inner['lname']}_{$row_inner['fname']}/";*/
- $filename = "CCDA_{$row_inner['lname']}_{$row_inner['fname']}";
+ // Sanitize patient names for safe filesystem use
+ $safeLname = $this->sanitizeFilename($row_inner['lname'] ?? '');
+ $safeFname = $this->sanitizeFilename($row_inner['fname'] ?? '');
+ $filename = "CCDA_{$safeLname}_{$safeFname}";
if (!empty($doc_type) && in_array($doc_type, self::VALID_CCDA_DOCUMENT_TYPES)) {
$filename .= "_" . $doc_type;
}
@@ -423,20 +429,20 @@ public function downloadallAction()
$zip_name .= "_All";
}
$zip_name .= "_" . date("Y_m_d_His") . ".zip";
- $zip->setArchive($zip_dir . $zip_name);
- $zip->compress($parent_dir);
+ $this->zipDirectory($parent_dir, $zip_dir . $zip_name);
ob_clean();
header("Cache-Control: public");
header("Content-Description: File Transfer");
- header("Content-Disposition: attachment; filename=$zip_name");
+ // Sanitize filename for Content-Disposition header
+ header("Content-Disposition: attachment; filename=\"" . $this->sanitizeFilename($zip_name) . "\"");
header("Content-Type: application/download");
header("Content-Transfer-Encoding: binary");
readfile($zip_dir . $zip_name);
- // we need to unlink both the directory and the zip file once are done... as its a security hazard
- // to have these files just hanging around in a tmp folder
+ // Clean up temp files - both the zip and source directory
unlink($zip_dir . $zip_name);
+ $this->deleteDirectory($parent_dir);
$view = new ViewModel();
$view->setTerminal(true);
@@ -474,4 +480,95 @@ public function getEncountermanagerTable()
{
return $this->encountermanagerTable;
}
+
+ /**
+ * Sanitize a string for safe use in filenames and HTTP Content-Disposition headers.
+ * Preserves patient name readability while removing dangerous characters.
+ *
+ * @param string $name The string to sanitize
+ * @return string Sanitized string safe for filesystem and header use
+ */
+ private function sanitizeFilename(string $name): string
+ {
+ // Remove null bytes and path traversal sequences
+ $name = str_replace(["\0", '..'], '', $name);
+ // Remove directory separators
+ $name = str_replace(['/', '\\'], '', $name);
+ // Remove characters that break Content-Disposition headers (newlines, quotes)
+ $name = preg_replace('/[\r\n"\']/', '', $name);
+ // Replace any remaining non-safe characters with underscore
+ // Allow: alphanumeric, space, underscore, hyphen, period, and common intl chars
+ $name = preg_replace('/[^\p{L}\p{N} _.\-]/u', '_', (string) $name);
+ // Collapse multiple underscores/spaces
+ $name = preg_replace('/[_ ]+/', '_', (string) $name);
+ // Trim underscores from ends
+ return trim((string) $name, '_');
+ }
+
+ /**
+ * Recursively delete a directory and all its contents.
+ *
+ * @param string $dir Directory path to delete
+ * @return void
+ */
+ private function deleteDirectory(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+ foreach ($iterator as $file) {
+ /** @var \SplFileInfo $file */
+ if ($file->isDir()) {
+ rmdir($file->getPathname());
+ } else {
+ unlink($file->getPathname());
+ }
+ }
+ rmdir($dir);
+ }
+
+ /**
+ * Replacement for Laminas\Filter\Compress\Zip::compress() using PHP's ZipArchive.
+ *
+ * @param string $sourceDir Directory to zip (contents included recursively)
+ * @param string $zipFilePath Full path to resulting .zip file
+ * @return void
+ */
+ private function zipDirectory(string $sourceDir, string $zipFilePath): void
+ {
+ $zip = new \ZipArchive();
+ if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
+ throw new \RuntimeException(sprintf('Unable to create ZIP archive "%s"', $zipFilePath));
+ }
+
+ $sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator(
+ $sourceDir,
+ \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS
+ ),
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $fileInfo) {
+ /** @var \SplFileInfo $fileInfo */
+ $filePath = $fileInfo->getPathname();
+ $localName = substr($filePath, strlen($sourceDir));
+
+ if ($fileInfo->isDir()) {
+ if ($localName !== '') {
+ $zip->addEmptyDir($localName);
+ }
+ } else {
+ $zip->addFile($filePath, $localName);
+ }
+ }
+
+ $zip->close();
+ }
}
diff --git a/src/Services/CDADocumentService.php b/src/Services/CDADocumentService.php
index dc4075d7ee79..5f162df54ba2 100644
--- a/src/Services/CDADocumentService.php
+++ b/src/Services/CDADocumentService.php
@@ -6,250 +6,319 @@
* @package OpenEMR
* @link https://www.open-emr.org
* @author Jerry Padgett
- * @copyright Copyright (c) 2021 Jerry Padgett
+ * @copyright Copyright (c) 2021-2026 Jerry Padgett
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/
namespace OpenEMR\Services;
+use Application\Model\ApplicationTable;
+use Carecoordination\Model\CcdaGenerator;
+use Carecoordination\Model\EncounterccdadispatchTable;
use CouchDB;
+use DOMDocument;
+use Exception;
use OpenEMR\Common\Crypto\CryptoGen;
-use OpenEMR\Common\Utils\NetworkUtils;
+use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Common\Uuid\UuidRegistry;
-use Symfony\Component\HttpClient\HttpClient;
+use OpenEMR\Core\OEGlobalsBag;
+use RuntimeException;
+use XSLTProcessor;
+use ZipArchive;
/**
* Class CDADocumentService
*
* @package OpenEMR\Services
- *
- * See interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/EncounterccdadispatchController.php
- * indexAction() and interface/modules/zend_modules/public/index.php
*/
class CDADocumentService extends BaseService
{
- const TABLE_NAME = "ccda";
- protected string $serverUrl;
- protected bool $verifySsl;
- protected string|bool $caCert;
+ private const TABLE_NAME = "ccda";
+ private const XSL_PATH = '/interface/modules/zend_modules/public/xsl/cda.xsl';
public function __construct()
{
parent::__construct(self::TABLE_NAME);
UuidRegistry::createMissingUuidsForTables([self::TABLE_NAME]);
-
- // Determine the server URL for internal CCD generation
- // Use the configured site address and apply appropriate SSL verification
-
- $this->serverUrl = $GLOBALS['qualified_site_addr'];
-
- $networkUtils = new NetworkUtils();
- if ($networkUtils->isLoopbackAddress($this->serverUrl)) {
- // Loopback address - traffic never leaves the local machine
- // SSL verification is always disabled for loopback (no security benefit, often fails)
- $this->verifySsl = false;
- $this->caCert = false;
- } else {
- // Non-loopback address (e.g., nginx sidecar, docker compose, kubernetes)
- $this->verifySsl = (bool) ($GLOBALS['http_verify_ssl'] ?? true);
- $this->caCert = $GLOBALS['http_ca_cert'] ?? false; // Use custom CA cert for self-signed certificates
- }
}
- protected function createHttpClient()
+ /**
+ * Get the path to the CDA stylesheet.
+ */
+ private function getXslPath(): string
{
- $config = [
- 'verify_host' => $this->verifySsl,
- 'verify_peer' => $this->verifySsl,
- ];
-
- // If SSL verification is enabled and a custom CA cert is provided, use it
- if ($this->verifySsl && $this->caCert && file_exists($this->caCert)) {
- $config['cafile'] = $this->caCert;
- }
-
- return HttpClient::create($config);
+ return OEGlobalsBag::getInstance()->get('fileroot') . self::XSL_PATH;
}
/**
- * @param $pid
+ * @param int|string $pid
* @return array|false|null
*/
- public function getLastCdaMeta($pid)
+ public function getLastCdaMeta($pid): false|array|null
{
$query = "SELECT cc.uuid, cc.date, pd.fname, pd.lname, pd.pid FROM ccda AS cc
- LEFT JOIN patient_data AS pd ON pd.pid=cc.pid
- WHERE cc.pid = ?
- ORDER BY cc.id DESC LIMIT 1";
+ LEFT JOIN patient_data AS pd ON pd.pid=cc.pid
+ WHERE cc.pid = ?
+ ORDER BY cc.id DESC LIMIT 1";
return sqlQuery($query, [$pid]);
}
/**
- * @param $id
+ * @param string $id UUID
* @return false|string
*/
- public function getFile($id)
+ public function getFile(string $id): false|string
{
- $query = "select couch_docid, couch_revid, ccda_data, encrypted from ccda where uuid=?";
+ $query = "SELECT couch_docid, couch_revid, ccda_data, encrypted FROM ccda WHERE uuid = ?";
$row = sqlQuery($query, [$id]);
$content = '';
- if (!empty($row)) {
- if (!empty($row['couch_docid'])) {
- $couch = new CouchDB();
- $resp = $couch->retrieve_doc($row['couch_docid']);
- if ($row['encrypted']) {
- $cryptoGen = new CryptoGen();
- $content = $cryptoGen->decryptStandard($resp->data, null, 'database');
- } else {
- $content = base64_decode((string) $resp->data);
- }
- } elseif (!empty($row['ccda_data'])) {
- $fccda = fopen($row['ccda_data'], "r");
- if ($row['encrypted']) {
- $cryptoGen = new CryptoGen();
- $content = $cryptoGen->decryptStandard(fread($fccda, filesize($row['ccda_data'])), null, 'database');
- } else {
- $content = fread($fccda, filesize($row['ccda_data']));
- }
- fclose($fccda);
+
+ if (empty($row)) {
+ return $content;
+ }
+
+ if (!empty($row['couch_docid'])) {
+ $couch = new CouchDB();
+ $resp = $couch->retrieve_doc($row['couch_docid']);
+ if ($row['encrypted']) {
+ $cryptoGen = new CryptoGen();
+ $content = $cryptoGen->decryptStandard($resp->data, null, 'database');
+ } else {
+ $content = base64_decode((string)$resp->data);
+ }
+ } elseif (!empty($row['ccda_data'])) {
+ $fileData = file_get_contents($row['ccda_data']);
+ if ($fileData === false) {
+ return '';
}
+ if ($row['encrypted']) {
+ $cryptoGen = new CryptoGen();
+ $content = $cryptoGen->decryptStandard($fileData, null, 'database');
+ } else {
+ $content = $fileData;
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Generate CCDA XML using all documented components.
+ *
+ * @param int|string $pid Patient ID
+ * @return string CCDA XML content
+ * @throws Exception
+ */
+ public function generateCCDXml($pid): string
+ {
+ $dispatchTable = new EncounterccdadispatchTable(new ApplicationTable());
+ $ccdaGenerator = new CcdaGenerator($dispatchTable);
+ $result = $ccdaGenerator->generate(
+ $pid,
+ null,
+ null,
+ '0',
+ '1',
+ '0',
+ null,
+ null,
+ 'patient',
+ '',
+ 'ccd',
+ null,
+ []
+ );
+ $content = $result->getContent();
+ unset($result);
+
+ if (str_starts_with($content, 'ERROR:')) {
+ (new SystemLogger())->errorLogCaller("Error generating CCDA", ['message' => $content]);
+ throw new Exception(xlt("Error generating CCDA") . ": " . $content);
}
return $content;
}
/**
- * @param $pid
- * @return string
+ * Generate CCDA as HTML.
+ *
+ * @param int|string $pid Patient ID
+ * @return string HTML content
+ * @throws Exception
*/
public function generateCCDHtml($pid): string
{
- $url = $this->serverUrl . "/interface/modules/zend_modules/public/encounterccdadispatch";
- $httpClient = $this->createHttpClient();
- $response = $httpClient->request('GET', $url, [
- 'query' => [
- 'combination' => $pid,
- 'recipient' => 'self',
- 'view' => '1',
- 'site' => $_SESSION ['site_id'],
- 'sent_by_app' => 'core_api',
- 'me' => session_id()
- ]
- ]);
-
- $status = $response->getStatusCode(); // @todo validate
-
- return $response->getContent();
+ $content = $this->generateCCDXml($pid);
+ return $this->xmlToHtmlContent($content);
}
/**
- * @param $pid
- * @return string
+ * Generate CCDA as a ZIP bundle containing XML, HTML, and XSL.
+ *
+ * @param int|string $pid Patient ID
+ * @return string ZIP file contents
+ * @throws Exception
*/
- public function generateCCDXml($pid): string
+ public function generateCCDZip($pid): string
{
- $url = $this->serverUrl . "/interface/modules/zend_modules/public/encounterccdadispatch";
- $httpClient = $this->createHttpClient();
- $response = $httpClient->request('GET', $url, [
- 'query' => [
- 'combination' => $pid,
- 'recipient' => 'patient',
- 'view' => '0',
- 'hiehook' => '1',
- 'sent_by_app' => 'core_api',
- 'me' => session_id()
- ]
- ]);
-
- $status = $response->getStatusCode(); // @todo validate
-
- return $response->getContent();
+ $content = $this->generateCCDXml($pid);
+ return $this->generateCCDAZipBundle($content);
}
/**
- * @param $pid
- * @return string
+ * Create a ZIP bundle from CCDA XML content.
+ *
+ * @param string $content CCDA XML content
+ * @return string ZIP file contents
+ * @throws Exception
*/
- public function portalGenerateCCD($pid): string
+ public function generateCCDAZipBundle(string $content): string
{
- $url = $this->serverUrl . "/interface/modules/zend_modules/public/encounterccdadispatch";
- $httpClient = $this->createHttpClient();
- $response = $httpClient->request('GET', $url, [
- 'query' => [
- 'combination' => $pid,
- 'recipient' => 'patient',
- 'view' => '1',
- 'me' => session_id(),// to authenticate in CCM. Portal only.
- 'site' => $_SESSION ['site_id']
- ]
- ]);
-
- $status = $response->getStatusCode(); // @todo validate
-
- return $response->getContent();
+ $xslSource = $this->getXslPath();
+ if (!file_exists($xslSource)) {
+ throw new RuntimeException(xlt("CDA stylesheet not found"));
+ }
+
+ $uniqueId = bin2hex(random_bytes(16));
+ $tempDir = sys_get_temp_dir();
+ $parentDir = $tempDir . "/CCDA_" . $uniqueId;
+ $zipPath = $tempDir . "/CCDA_" . $uniqueId . ".zip";
+
+ try {
+ if (!mkdir($parentDir, 0700, true) && !is_dir($parentDir)) {
+ throw new RuntimeException(xlt("Failed to create temporary directory"));
+ }
+
+ $filename = "CCDA_ccd_" . date("Y_m_d_His");
+ $filenameXml = $filename . ".xml";
+ $filenameHtml = $filename . ".html";
+
+ if (file_put_contents($parentDir . "/" . $filenameXml, $content) === false) {
+ throw new RuntimeException(xlt("Failed to write XML file"));
+ }
+
+ $htmlContent = $this->xmlToHtmlContent($content);
+ if (file_put_contents($parentDir . "/" . $filenameHtml, $htmlContent) === false) {
+ throw new RuntimeException(xlt("Failed to write HTML file"));
+ }
+
+ if (!copy($xslSource, $parentDir . "/CDA.xsl")) {
+ throw new RuntimeException(xlt("Failed to copy stylesheet"));
+ }
+
+ $zip = new ZipArchive();
+ $zipResult = $zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ if ($zipResult !== true) {
+ throw new RuntimeException(xlt("Failed to create ZIP file") . " (code: $zipResult)");
+ }
+
+ $zip->addFile($parentDir . "/" . $filenameXml, $filenameXml);
+ $zip->addFile($parentDir . "/" . $filenameHtml, $filenameHtml);
+ $zip->addFile($parentDir . "/CDA.xsl", "CDA.xsl");
+
+ if (!$zip->close()) {
+ throw new RuntimeException(xlt("Failed to finalize ZIP file"));
+ }
+
+ $zipContent = file_get_contents($zipPath);
+ if ($zipContent === false) {
+ throw new RuntimeException(xlt("Failed to read ZIP file"));
+ }
+
+ return $zipContent;
+
+ } finally {
+ if (file_exists($zipPath)) {
+ unlink($zipPath);
+ }
+ $this->removeDirectory($parentDir);
+ }
}
/**
- * @param $pid
- * @return string
+ * Recursively remove a directory and its contents.
+ *
+ * @param string $dir Directory path
*/
- public function portalGenerateCCDZip($pid): string
+ private function removeDirectory(string $dir): void
{
- $parameterArray = [
- 'combination' => $pid,
- 'components' => 'allergies|medications|problems|immunizations|procedures|results|plan_of_care|vitals|social_history|encounters|functional_status|referral|instructions|medical_devices|goals',
- 'downloadccda' => 'download_ccda',
- 'latestccda' => '0',
- 'send_to' => 'download_all',
- 'sent_by_app' => 'portal',
- 'ccda_pid' => [0 => $pid],
- 'view' => 0,
- 'recipient' => 'patient',
- 'site' => $_SESSION ['site_id'],
- ];
- $url = $this->serverUrl . "/interface/modules/zend_modules/public/encounterccdadispatch";
- $httpClient = $this->createHttpClient();
- $response = $httpClient->request('POST', $url, [
- 'query' => ['me' => session_id()], // to authenticate in CCM. Portal only.
- 'body' => $parameterArray
- ]);
-
- $status = $response->getStatusCode(); // @todo validate
-
- return $response->getContent();
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $items = scandir($dir);
+ foreach ($items as $item) {
+ if ($item === '.' || $item === '..') {
+ continue;
+ }
+ $path = $dir . DIRECTORY_SEPARATOR . $item;
+ if (is_dir($path)) {
+ $this->removeDirectory($path);
+ } else {
+ unlink($path);
+ }
+ }
+ rmdir($dir);
}
/**
- * Complete zip of xml, html version
- * when called within an openemr authorized session.
+ * Transform CCDA XML to HTML using XSL stylesheet.
*
- * @param $pid
- * @return string
+ * @param string $content CCDA XML content
+ * @return string HTML content
+ * @throws Exception
*/
- public function generateCCDZip($pid): string
+ private function xmlToHtmlContent(string $content): string
{
- $parameterArray = [
- 'combination' => $pid,
- 'components' => 'allergies|medications|problems|immunizations|procedures|results|plan_of_care|vitals|social_history|encounters|functional_status|referral|instructions|medical_devices|goals',
- 'downloadccda' => 'download_ccda',
- 'latestccda' => '0',
- 'send_to' => 'download_all',
- 'sent_by_app' => 'core_api',
- 'ccda_pid' => [0 => $pid],
- 'view' => 0,
- 'recipient' => 'self',
- 'site' => $_SESSION['site_id'],
- ];
- $url = $this->serverUrl . "/interface/modules/zend_modules/public/encounterccdadispatch"; // add for debug ?XDEBUG_SESSION=PHPSTORM
- $httpClient = $this->createHttpClient();
- $response = $httpClient->request('POST', $url, [
- 'query' => ['me' => session_id()],
- 'body' => $parameterArray
- ]);
-
- $status = $response->getStatusCode(); // @todo validate
-
- return $response->getContent();
+ $sheet = $this->getXslPath();
+ if (!file_exists($sheet)) {
+ throw new RuntimeException(xlt("CDA stylesheet not found"));
+ }
+
+ $xml = simplexml_load_string($content);
+ if ($xml === false) {
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+ (new SystemLogger())->errorLogCaller("Failed to parse CCDA XML", ['errors' => $errors]);
+ throw new RuntimeException(xlt("Failed to parse CCDA XML"));
+ }
+
+ $xsl = new DOMDocument();
+ if (!$xsl->load($sheet)) {
+ throw new RuntimeException(xlt("Failed to load CDA stylesheet"));
+ }
+
+ $proc = new XSLTProcessor();
+ if (!$proc->importStyleSheet($xsl)) {
+ throw new RuntimeException(xlt("Failed to import CDA stylesheet"));
+ }
+
+ $uniqueId = bin2hex(random_bytes(16));
+ $outputFile = sys_get_temp_dir() . '/cda_html_' . $uniqueId . '.html';
+
+ try {
+ $result = $proc->transformToURI($xml, $outputFile);
+ if ($result === false) {
+ throw new RuntimeException(xlt("Failed to transform CCDA to HTML"));
+ }
+
+ $htmlContent = file_get_contents($outputFile);
+ if ($htmlContent === false) {
+ throw new RuntimeException(xlt("Failed to read transformed HTML"));
+ }
+
+ return $htmlContent;
+
+ } finally {
+ if (file_exists($outputFile)) {
+ if (!unlink($outputFile)) {
+ (new SystemLogger())->errorLogCaller(
+ "Failed to unlink temporary CDA output. This could expose PHI.",
+ ['filename' => $outputFile]
+ );
+ }
+ }
+ }
}
}
From d3b253d7c6b74f9ace589a401ea005a70571e162 Mon Sep 17 00:00:00 2001
From: steve waite
Date: Fri, 26 Dec 2025 10:35:31 -0500
Subject: [PATCH 010/139] fix: current user dropdown hover state persisting
(#9989)
---
.../main/tabs/user_data_template.html.twig | 54 ++++++++++++-------
1 file changed, 36 insertions(+), 18 deletions(-)
diff --git a/templates/interface/main/tabs/user_data_template.html.twig b/templates/interface/main/tabs/user_data_template.html.twig
index 48611b64416e..392b6019b134 100644
--- a/templates/interface/main/tabs/user_data_template.html.twig
+++ b/templates/interface/main/tabs/user_data_template.html.twig
@@ -14,25 +14,43 @@
From 8ef279398f1f55dc205cf2f9c33b3bf8e8877a4d Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 27 Dec 2025 22:00:41 -0500
Subject: [PATCH 011/139] Remove dead code from verysimple library - 22 unused
files (#9771)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kojiromike <1566303+kojiromike@users.noreply.github.com>
---
.../libs/verysimple/Authentication/Bcrypt.php | 169 --------
.../verysimple/Authentication/OAuthUtil.php | 62 ---
.../Authentication/SimpleAccount.php | 50 ---
.../verysimple/DB/Reflection/DBColumn.php | 239 -----------
.../verysimple/DB/Reflection/DBConnection.php | 197 ---------
.../DB/Reflection/DBConnectionString.php | 29 --
.../verysimple/DB/Reflection/DBConstraint.php | 57 ---
.../DB/Reflection/DBEventHandler.php | 88 ----
.../libs/verysimple/DB/Reflection/DBKey.php | 39 --
.../verysimple/DB/Reflection/DBSchema.php | 78 ----
.../verysimple/DB/Reflection/DBServer.php | 53 ---
.../libs/verysimple/DB/Reflection/DBSet.php | 65 ---
.../libs/verysimple/DB/Reflection/DBTable.php | 303 --------------
.../fwk/libs/verysimple/HTTP/HttpRequest.php | 381 ------------------
.../fwk/libs/verysimple/IO/FileHelper.php | 41 --
.../fwk/libs/verysimple/IO/FolderHelper.php | 56 ---
.../verysimple/Phreeze/BladeRenderEngine.php | 135 -------
.../verysimple/Phreeze/SmartyRenderEngine.php | 99 -----
.../verysimple/Phreeze/TwigRenderEngine.php | 105 -----
.../fwk/libs/verysimple/String/NameValue.php | 84 ----
.../libs/verysimple/String/SimpleTemplate.php | 188 ---------
.../libs/verysimple/Util/TextImageWriter.php | 104 -----
22 files changed, 2622 deletions(-)
delete mode 100644 portal/patient/fwk/libs/verysimple/Authentication/Bcrypt.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Authentication/OAuthUtil.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Authentication/SimpleAccount.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBColumn.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnection.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnectionString.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBConstraint.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBEventHandler.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBKey.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBSchema.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBServer.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBSet.php
delete mode 100644 portal/patient/fwk/libs/verysimple/DB/Reflection/DBTable.php
delete mode 100644 portal/patient/fwk/libs/verysimple/HTTP/HttpRequest.php
delete mode 100644 portal/patient/fwk/libs/verysimple/IO/FileHelper.php
delete mode 100644 portal/patient/fwk/libs/verysimple/IO/FolderHelper.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Phreeze/BladeRenderEngine.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Phreeze/SmartyRenderEngine.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Phreeze/TwigRenderEngine.php
delete mode 100644 portal/patient/fwk/libs/verysimple/String/NameValue.php
delete mode 100644 portal/patient/fwk/libs/verysimple/String/SimpleTemplate.php
delete mode 100644 portal/patient/fwk/libs/verysimple/Util/TextImageWriter.php
diff --git a/portal/patient/fwk/libs/verysimple/Authentication/Bcrypt.php b/portal/patient/fwk/libs/verysimple/Authentication/Bcrypt.php
deleted file mode 100644
index 6909839fab39..000000000000
--- a/portal/patient/fwk/libs/verysimple/Authentication/Bcrypt.php
+++ /dev/null
@@ -1,169 +0,0 @@
-
- * $bcrypt = new Bcrypt(15);
- * $hash = $bcrypt->hash('password');
- * $isGood = $bcrypt->verify('password', $hash);
- *
- */
-class Bcrypt
-{
- private $randomState;
-
- /**
- * Constructor
- *
- * @param int $rounds number of crypt rounds
- * @throws Exception if bcrypt is not supported
- */
- public function __construct(private $rounds = 12)
- {
- if (CRYPT_BLOWFISH != 1) {
- throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
- }
- }
-
- /**
- * Return true if the given hash is crypted with the blowfish algorithm
- *
- * @param string $hash
- */
- static function isBlowfish($hash)
- {
- return str_starts_with($hash, '$2a$');
- }
-
- /**
- * generate a hash
- *
- * @param
- * string plain text input
- */
- public function hash($input)
- {
- $hash = crypt((string) $input, (string) $this->getSalt());
-
- if (strlen($hash) > 13) {
- return $hash;
- }
-
- return false;
- }
-
- /**
- * Verify if the input is equal to the hashed value
- *
- * @param
- * string plain text input
- * @param
- * string hashed input
- */
- public function verify($input, $existingHash)
- {
- $hash = crypt((string) $input, (string) $existingHash);
-
- return $hash === $existingHash;
- }
-
- /**
- * return an ascii-encoded 16 char salt
- */
- private function getSalt()
- {
- $salt = sprintf('$2a$%02d$', $this->rounds);
-
- $bytes = $this->getRandomBytes(16);
-
- $salt .= $this->encodeBytes($bytes);
-
- return $salt;
- }
-
- /**
- * get random bytes to be used in random salts
- *
- * @param int $count
- */
- private function getRandomBytes($count)
- {
- $bytes = '';
-
- if (function_exists('openssl_random_pseudo_bytes') && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
- $bytes = openssl_random_pseudo_bytes($count);
- }
-
- if ($bytes === '' && is_readable('/dev/urandom') && ($hRand = @fopen('/dev/urandom', 'rb')) !== false) {
- $bytes = fread($hRand, $count);
- fclose($hRand);
- }
-
- if (strlen($bytes) < $count) {
- $bytes = '';
-
- if ($this->randomState === null) {
- $this->randomState = microtime();
- if (function_exists('getmypid')) {
- $this->randomState .= getmypid();
- }
- }
-
- for ($i = 0; $i < $count; $i += 16) {
- $this->randomState = md5(microtime() . $this->randomState);
-
- if (PHP_VERSION >= '5') {
- $bytes .= md5($this->randomState, true);
- } else {
- $bytes .= pack('H*', md5($this->randomState));
- }
- }
-
- $bytes = substr($bytes, 0, $count);
- }
-
- return $bytes;
- }
-
- /**
- * ascii-encode used for converting random salt into legit ascii value
- *
- * @param string $input
- */
- private function encodeBytes($input)
- {
- // The following is code from the PHP Password Hashing Framework
- $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
-
- $output = '';
- $i = 0;
- do {
- $c1 = ord($input [$i++]);
- $output .= $itoa64 [$c1 >> 2];
- $c1 = ($c1 & 0x03) << 4;
- if ($i >= 16) {
- $output .= $itoa64 [$c1];
- break;
- }
-
- $c2 = ord($input [$i++]);
- $c1 |= $c2 >> 4;
- $output .= $itoa64 [$c1];
- $c1 = ($c2 & 0x0f) << 2;
-
- $c2 = ord($input [$i++]);
- $c1 |= $c2 >> 6;
- $output .= $itoa64 [$c1];
- $output .= $itoa64 [$c2 & 0x3f];
- } while (1);
-
- return $output;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Authentication/OAuthUtil.php b/portal/patient/fwk/libs/verysimple/Authentication/OAuthUtil.php
deleted file mode 100644
index 175d59f5f0c9..000000000000
--- a/portal/patient/fwk/libs/verysimple/Authentication/OAuthUtil.php
+++ /dev/null
@@ -1,62 +0,0 @@
- $key,
- 'consumer_secret' => $secret
- ];
- $params = $params ?: [];
-
- OAuthStore::instance("2Leg", $options);
-
- // Obtain a request object for the request we want to make
- $request = new OAuthRequester($url, $method, $params, $body);
-
- $sig = $request->sign($key, null, '');
-
- $data = $request->signatureBaseString();
-
- $url = substr(urldecode($data . '&oauth_signature=' . $request->calculateDataSignature($data, $secret, '', $signature_method)), strlen($method) + 1);
-
- $url = VerySimpleStringUtil::ReplaceFirst('&', '?', $url);
-
- return $url;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Authentication/SimpleAccount.php b/portal/patient/fwk/libs/verysimple/Authentication/SimpleAccount.php
deleted file mode 100644
index 598efeb584e4..000000000000
--- a/portal/patient/fwk/libs/verysimple/Authentication/SimpleAccount.php
+++ /dev/null
@@ -1,50 +0,0 @@
-_authenticated);
- }
- public function IsAuthorized($permission)
- {
- return $this->_authenticated;
- }
- public function Login($username, $password)
- {
- if ($this->_username == $username && $this->_password == $password) {
- $this->_authenticated = true;
- return true;
- }
-
- return false;
- }
-
- /**
- * This is implemented only for Phreeze but is not utilized
- *
- * @param
- * object
- */
- public function Refresh($obj)
- {
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBColumn.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBColumn.php
deleted file mode 100644
index 423bf36e7465..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBColumn.php
+++ /dev/null
@@ -1,239 +0,0 @@
-Table = & $table;
- $this->Name = $row ["Field"];
- $this->NameWithoutPrefix = $row ["Field"];
- $this->Type = $typesize [0];
- $this->Unsigned = isset($sizesign [1]);
- $this->Null = $row ["Null"];
- $this->Key = $row ["Key"];
- $this->Default = $row ["Default"];
- $this->Extra = $row ["Extra"];
-
- // enums are a little different because they contain a list of legal values instead of a size limit
- if ($this->IsEnum()) {
- // enum size is in the format 'val1','val2',...
- $this->Size = explode("','", substr($sizesign [0], 1, - 1));
- $this->MaxSize = 0;
- } else {
- $this->Size = $sizesign [0];
- // size may be saved for decimals as "n,n" so we need to convert that to an int
- $tmp = explode(",", $this->Size);
- $this->MaxSize = count($tmp) > 1 ? ($tmp [0] + $tmp [1]) : $this->Size;
- }
-
- // if ($this->Key == "MUL") print " ########################## " . print_r($row,1) . " ########################## ";
- }
-
- /**
- * Return true if this column is an enum type
- *
- * @return boolean
- */
- function IsEnum()
- {
- return $this->Type == 'enum';
- }
-
- /**
- * Return the enum values if this column type is an enum
- *
- * @return array
- */
- function GetEnumValues()
- {
- return $this->IsEnum() ? $this->Size : [];
- }
-
- /**
- * Return the Phreeze column constant that most closely matches this column type
- *
- * @return string
- */
- function GetPhreezeType()
- {
- return FieldMap::GetConstantFromType($this->Type);
- }
-
- /**
- * Return the PHP variable type that most closely matches this column type
- *
- * @return string
- */
- function GetPhpType()
- {
- $rt = $this->Type;
- switch ($this->Type) {
- case "smallint":
- case "bigint":
- case "tinyint":
- case "mediumint":
- $rt = "int";
- break;
- case "varchar":
- case "text":
- case "tinytext":
- $rt = "string";
- break;
- case "date":
- case "datetime":
- $rt = "date";
- break;
- case "decimal":
- case "float":
- $rt = "float";
- break;
- default:
- break;
- }
-
- return $rt;
- }
-
- /**
- * Get the SQLite type that most closely matches this column type
- *
- * @return string
- */
- function GetSqliteType()
- {
- $rt = $this->Type;
- switch ($this->Type) {
- case "int":
- $rt = "integer";
- break;
- case "smallint":
- $rt = "integer";
- break;
- case "tinyint":
- $rt = "integer";
- break;
- case "varchar":
- $rt = "text";
- break;
- case "text":
- $rt = "text";
- break;
- case "tinytext":
- $rt = "text";
- break;
- case "date":
- $rt = "datetime";
- break;
- case "datetime":
- $rt = "datetime";
- break;
- case "mediumint":
- $rt = "integer";
- break;
- case "bigint":
- $rt = "integer";
- break;
- case "decimal":
- $rt = "real";
- break;
- case "float":
- $rt = "real";
- break;
- default:
- break;
- }
-
- return $rt;
- }
-
- /**
- * Return the AS3/Flex type that most closely matches this column type
- *
- * @return string
- */
- function GetFlexType()
- {
- $rt = $this->Type;
- switch ($this->Type) {
- case "int":
- $rt = "int";
- break;
- case "smallint":
- $rt = "int";
- break;
- case "tinyint":
- $rt = $this->MaxSize > 1 ? "int" : "Boolean";
- break;
- case "varchar":
- $rt = "String";
- break;
- case "text":
- $rt = "String";
- break;
- case "tinytext":
- $rt = "String";
- break;
- case "datetime":
- $rt = "Date";
- break;
- case "mediumint":
- $rt = "int";
- break;
- case "bigint":
- $rt = "int";
- break;
- case "decimal":
- $rt = "Number";
- break;
- case "float":
- $rt = "Number";
- break;
- default:
- break;
- }
-
- return $rt;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnection.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnection.php
deleted file mode 100644
index 36e3b1910d53..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnection.php
+++ /dev/null
@@ -1,197 +0,0 @@
-dbopen = false;
-
- $this->Host = $dbconnstring->Host;
- $this->Port = $dbconnstring->Port;
- $this->Username = $dbconnstring->Username;
- $this->Password = $dbconnstring->Password;
- $this->DBName = $dbconnstring->DBName;
-
- // TODO: this is redundant after switching to the DataAdapter
- $this->csetting = new ConnectionSetting();
- $this->csetting->ConnectionString = $dbconnstring->Host . ($dbconnstring->Port ? ':' . $dbconnstring->Port : '');
- $this->csetting->DBName = $dbconnstring->DBName;
- $this->csetting->Username = $dbconnstring->Username;
- $this->csetting->Password = $dbconnstring->Password;
- $this->csetting->Type = $dbconnstring->Type;
-
- if ($handler) {
- $this->handler = & $handler;
- } else {
- $this->handler = new DBEventHandler();
- }
-
- $this->handler->Log(DBH_LOG_INFO, "Connection Initialized");
- }
-
- /**
- * Destructor closes the db connection.
- *
- * @access public
- */
- function __destruct()
- {
- $this->Disconnect();
- }
-
- /**
- * Opens a connection to the MySQL Server and selects the specified database
- *
- * @access public
- * @param string $dbname
- */
- function Connect()
- {
- $this->handler->Log(DBH_LOG_INFO, "Opening Connection...");
- if ($this->dbopen) {
- $this->handler->Log(DBH_LOG_WARNING, "Connection Already Open");
- } else {
- $this->adapter = new DataAdapter($this->csetting);
-
- try {
- $this->adapter->Open();
- } catch (Exception $ex) {
- $this->handler->Crash(DatabaseException::$CONNECTION_ERROR, $ex->getMessage());
- }
-
- $this->handler->Log(DBH_LOG_INFO, "Connection Open");
- $this->dbopen = true;
- }
- }
-
- /**
- * Checks that the connection is open and if not, crashes
- *
- * @access public
- * @param bool $auto
- * Automatically try to connect if connection isn't already open
- */
- private function RequireConnection($auto = false)
- {
- if (! $this->dbopen) {
- if ($auto) {
- $this->Connect();
- } else {
- $this->handler->Crash(DatabaseException::$CONNECTION_ERROR, "DB is not connected. Please call DBConnection->Connect() first.");
- }
- }
- }
-
- /**
- * Closing the connection to the MySQL Server
- *
- * @access public
- */
- function Disconnect()
- {
- $this->handler->Log(DBH_LOG_INFO, "Closing Connection...");
-
- if ($this->dbopen) {
- $this->adapter->Close();
- $this->dbopen = false;
- $this->handler->Log(DBH_LOG_INFO, "Connection closed");
- } else {
- $this->handler->Log(DBH_LOG_WARNING, "Connection Already Closed");
- }
- }
-
- /**
- * Executes a SQL select statement and returns a MySQL resultset
- *
- * @access public
- * @param string $sql
- * @return mysql_query
- */
- function Select($sql)
- {
- $this->RequireConnection(true);
-
- $this->handler->Log(DBH_LOG_QUERY, "Executing Query", $sql);
-
- return $this->adapter->Select($sql);
- }
-
- /**
- * Executes a SQL query that does not return a resultset
- *
- * @access public
- * @param string $sql
- */
- function Update($sql)
- {
- $this->RequireConnection(true);
-
- return $this->adapter->Escape($sql);
- }
-
- /**
- * Moves the database curser forward and returns the current row as an associative array
- *
- * @access public
- * @param mysql_query $rs
- * @return Array
- */
- function Next($rs)
- {
- $this->RequireConnection();
-
- $this->handler->Log(DBH_LOG_DEBUG, "Fetching next result as array");
- return $this->adapter->Fetch($rs);
- }
-
- /**
- * Releases the resources for the given resultset
- *
- * @access public
- * @param mysql_query $rs
- */
- function Release($rs)
- {
- $this->RequireConnection();
-
- $this->handler->Log(DBH_LOG_DEBUG, "Releasing result resources");
- return $this->adapter->Release($rs);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnectionString.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnectionString.php
deleted file mode 100644
index b87c665b4c0e..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBConnectionString.php
+++ /dev/null
@@ -1,29 +0,0 @@
-Table = & $table;
-
- $this->Name = $row [0];
- $this->KeyColumn = $row [1];
- $this->ReferenceTableName = $row [2];
- $this->ReferenceKeyColumn = $row [3];
-
- $this->ReferenceTable = $this->Table->Schema->Tables [$this->ReferenceTableName];
- // print "" . $this->Table->Name . " constraint references " . $reftable->Name . "
";
-
- $this->NameNoPrefix = $this->Table->RemovePrefix($this->Name);
- $this->KeyColumnNoPrefix = $this->Table->RemovePrefix($this->KeyColumn);
- $this->ReferenceKeyColumnNoPrefix = $this->ReferenceTable->RemovePrefix($this->ReferenceKeyColumn);
-
- // intelligently decide what a good name for this constraint might be
- $tmp1 = str_replace("__", "_", str_replace($this->ReferenceTableName, "", str_replace("_id", "", $this->KeyColumnNoPrefix)) . "_");
- $tmp2 = $this->ReferenceTableName;
- $this->GetterName = ($tmp1 == "_") ? $tmp2 : ($tmp1 . $tmp2);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBEventHandler.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBEventHandler.php
deleted file mode 100644
index 779f00c34877..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBEventHandler.php
+++ /dev/null
@@ -1,88 +0,0 @@
-LogLevel & DBH_LOG_DEBUG) {
- print "$message \r\n";
- }
- break;
- case DBH_LOG_INFO:
- if ($this->LogLevel & DBH_LOG_INFO) {
- print "$message $data \r\n";
- }
- break;
- case DBH_LOG_QUERY:
- if ($this->LogLevel & DBH_LOG_QUERY) {
- print "$message $data \r\n";
- }
- break;
- case DBH_LOG_WARNING:
- if ($this->LogLevel & DBH_LOG_WARNING) {
- print "$message $data \r\n";
- }
- break;
- case DBH_LOG_ERROR:
- if ($this->LogLevel & DBH_LOG_ERROR) {
- print "$message $data \r\n";
- }
- break;
- }
- }
-
- /**
- * Called by DB objects when a critical error occurs
- *
- * @access public
- * @param int $code
- * unique numerical identifier for error
- * @param string $message
- * human-readable error
- * @param string $data
- * any additional information that may help with debugging
- */
- function Crash($code, $message = "", $data = ""): never
- {
- throw new DatabaseException($message, $code, $data);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBKey.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBKey.php
deleted file mode 100644
index 397a75f4c8a9..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBKey.php
+++ /dev/null
@@ -1,39 +0,0 @@
-Table = & $table;
- $this->KeyColumn = str_replace("`", "", $columnname);
- $this->KeyComment = $this->Table->Columns [$this->KeyColumn]->Comment;
-
- $this->NameNoPrefix = $this->Table->RemovePrefix($this->Name);
- $this->GetterName = $this->NameNoPrefix;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSchema.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSchema.php
deleted file mode 100644
index 2a841aefa249..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSchema.php
+++ /dev/null
@@ -1,78 +0,0 @@
-Server = & $server;
- $this->Name = $server->Connection->DBName;
- $this->Tables = [];
-
- $this->Load();
-
- // print ""; print_r($this->Tables["ticket"]); die();
- }
-
- /**
- * Inspects the current schema and loads all tables, keys, etc.
- *
- * @access public
- */
- private function Load()
- {
- $sql = "show tables";
- $rs = $this->Server->Connection->Select($sql);
-
- // first pass load all the tables. this will initialize each object. we have to
- // do this first so that we can correctly determine and store "Set" information
- while ($row = $this->Server->Connection->Next($rs)) {
- $this->Tables [$row ["Tables_in_" . $this->Name]] = new DBTable($this, $row);
- }
-
- // now load all the keys and constraints for each table
- foreach ($this->Tables as $table) {
- $table->LoadKeys();
- }
-
- $this->Server->Connection->Release($rs);
-
- $sql = "show table status from `" . $this->Name . "`";
- $rs2 = $this->Server->Connection->Select($sql);
-
- // load the extra data
- while ($row = $this->Server->Connection->Next($rs2)) {
- $this->Tables [$row ["Name"]]->Engine = $row ["Engine"];
- $this->Tables [$row ["Name"]]->Comment = $row ["Comment"];
- }
-
- $this->Server->Connection->Release($rs2);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBServer.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBServer.php
deleted file mode 100644
index 5219881bd063..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBServer.php
+++ /dev/null
@@ -1,53 +0,0 @@
-Connection = & $connection;
- }
-
- /**
- * Return the schema with the given name from this server
- *
- * @access public
- * @param string $name
- * @return DBSchema
- */
- function GetSchema()
- {
- $this->Connection->Connect();
-
- $schema = new DBSchema($this);
-
- $this->Connection->Disconnect();
-
- return $schema;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSet.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSet.php
deleted file mode 100644
index ba6bae6bb538..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBSet.php
+++ /dev/null
@@ -1,65 +0,0 @@
-Table = & $table->Schema->Tables [$row [2]];
-
- $this->Name = $row [0];
- $this->KeyColumn = $row [3];
- $this->KeyComment = $this->Table->Columns [$this->KeyColumn]->Comment;
- $this->SetTableName = $table->Name;
- $this->SetKeyColumn = $row [1];
- $this->SetKeyComment = $table->Columns [$this->SetKeyColumn]->Comment;
-
- $reftable = $this->Table->Schema->Tables [$this->SetTableName];
- // print "" . $this->Table->Name . " set references " . $reftable->Name . "
";
-
- $this->SetPrimaryKey = $reftable->GetPrimaryKeyName(false);
-
- $this->NameNoPrefix = $this->Table->RemovePrefix($this->Name);
- $this->KeyColumnNoPrefix = $this->Table->RemovePrefix($this->KeyColumn);
- $this->SetKeyColumnNoPrefix = $reftable->RemovePrefix($this->SetKeyColumn);
- $this->SetPrimaryKeyNoPrefix = $reftable->RemovePrefix($this->SetPrimaryKey);
-
- // intelligently decide what a good name for this set would be
- $tmp1 = str_replace("__", "_", str_replace($this->Table->Name, "", str_replace("_id", "", $this->SetKeyColumnNoPrefix)) . "_");
- $tmp2 = $this->SetTableName . "s";
- $this->GetterName = ($tmp1 == "_") ? $tmp2 : ($tmp1 . $tmp2);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBTable.php b/portal/patient/fwk/libs/verysimple/DB/Reflection/DBTable.php
deleted file mode 100644
index 7ad825be1cc0..000000000000
--- a/portal/patient/fwk/libs/verysimple/DB/Reflection/DBTable.php
+++ /dev/null
@@ -1,303 +0,0 @@
-Name = $row ["Tables_in_" . $this->Schema->Name];
- $this->Columns = [];
- $this->PrimaryKeys = [];
- $this->ForeignKeys = [];
- $this->Constraints = [];
- $this->Sets = [];
-
- $this->LoadColumns();
- $this->DiscoverColumnPrefix();
- }
-
- /**
- * Returns the number of columns involved in the primary key
- *
- * @return number
- */
- function NumberOfPrimaryKeyColumns()
- {
- return count($this->PrimaryKeys);
- }
-
- /**
- * Returns name of the primary key
- * TODO: If there are multiple keys, this is no accurate.
- * Only returns the first key found
- *
- * @access public
- * @param bool $remove_prefix
- * @return string
- */
- function GetPrimaryKeyName($remove_prefix = true)
- {
- foreach ($this->PrimaryKeys as $key) {
- return ($remove_prefix) ? $this->RemovePrefix($key->KeyColumn) : $key->KeyColumn;
- }
-
- // views don't technically have a primary key but we will return the first column if anybody asks
- if ($this->IsView) {
- return $this->GetColumnNameByIndex(0, $remove_prefix);
- }
- }
-
- /**
- * Returns the name of the column at the given index
- *
- * @access public
- * @param int $index
- * (zero based)
- * @param bool $remove_prefix
- * @return string
- */
- function GetColumnNameByIndex($index, $remove_prefix = true)
- {
- $count = 0;
- foreach ($this->Columns as $column) {
- if ($count == $index) {
- return ($remove_prefix) ? $column->NameWithoutPrefix : $column->Name;
- }
- }
-
- throw new Exception('Index out of bounds');
- }
-
- /**
- * Returns true if the primary key for this table is an auto_increment
- * TODO: Only checks the first key if there are multiple primary keys
- *
- * @access public
- * @return bool
- */
- function PrimaryKeyIsAutoIncrement()
- {
- $pk = $this->GetPrimaryKeyName(false);
- return $pk && $this->Columns [$pk]->Extra == "auto_increment";
- }
-
- /**
- * Returns name of the first varchar field which could be used as a "label"
- *
- * @access public
- * @return string
- */
- function GetDescriptorName($remove_prefix = true)
- {
- foreach ($this->Columns as $column) {
- if ($column->Type == "varchar") {
- return ($remove_prefix) ? $this->RemovePrefix($column->Name) : $column->Name;
- }
- }
-
- // give up because there are no varchars in this table
- return $this->GetPrimaryKeyName($remove_prefix);
- }
-
- /**
- * Inspects all columns to see if there is a common prefix in the format: XXX_
- *
- * @access private
- */
- private function DiscoverColumnPrefix()
- {
- $prev_prefix = "";
- $has_prefix = true;
- foreach ($this->Columns as $column) {
- $curr_prefix = substr((string) $column->Name, 0, strpos((string) $column->Name, "_") + 1);
-
- if ($prev_prefix == "") {
- // first time through the loop
- $prev_prefix = $curr_prefix ?: "#NONE#";
- } elseif ($prev_prefix != $curr_prefix) {
- $has_prefix = false;
- }
- }
-
- if ($has_prefix) {
- // set the table column prefix property
- $this->ColumnPrefix = $curr_prefix;
-
- // update the columns to reflect the prefix as well
- foreach ($this->Columns as $column) {
- $column->NameWithoutPrefix = substr((string) $column->Name, strlen($curr_prefix));
- }
- }
-
- // if a column begins with a numeric character then prepend a string to prevent generated code errors
- if (self::$NUMERIC_COLUMN_PREFIX) {
- foreach ($this->Columns as $column) {
- if (is_numeric(substr((string) $column->NameWithoutPrefix, 0, 1))) {
- $column->NameWithoutPrefix = self::$NUMERIC_COLUMN_PREFIX . $column->NameWithoutPrefix;
- }
- }
- }
- }
-
- /**
- * Returns a name that is acceptable to be used as the "object" name in generated code
- */
- public function GetObjectName()
- {
- if (is_numeric(substr((string) $this->Name, 0, 1))) {
- return self::$NUMERIC_TABLE_PREFIX . $this->Name;
- }
-
- return $this->Name;
- }
-
- /**
- * Given a column name, removes the prefix
- *
- * @access private
- */
- public function RemovePrefix($name)
- {
- // print "remove prefix $name: " . $this->ColumnPrefix . " ";
- return substr((string) $name, strlen((string) $this->ColumnPrefix));
- }
-
- /**
- * Inspects the current table and loads all Columns
- *
- * @access private
- */
- private function LoadColumns()
- {
- // get the colums
- $sql = "describe `" . $this->Name . "`";
-
- $rs = $this->Schema->Server->Connection->Select($sql);
-
- while ($row = $this->Schema->Server->Connection->Next($rs)) {
- $this->Columns [$row ["Field"]] = new DBColumn($this, $row);
- }
-
- $this->Schema->Server->Connection->Release($rs);
- }
-
- /**
- * Load the keys and constraints for this table and populate the sets for
- * all tables on which this table is dependents
- *
- * @access public
- */
- public function LoadKeys()
- {
-
- // get the keys and constraints
- $sql = "show create table `" . $this->Name . "`";
-
- $create_table = "";
-
- $rs = $this->Schema->Server->Connection->Select($sql);
-
- if ($row = $this->Schema->Server->Connection->Next($rs)) {
- if (isset($row ["Create Table"])) {
- $create_table = $row ["Create Table"];
- } elseif (isset($row ["Create View"])) {
- $this->IsView = true;
- $create_table = $row ["Create View"];
-
- // treat the 1st column in a view as the primary key
- $this->Columns [$this->GetColumnNameByIndex(0, false)]->Key = 'PRI';
- } else {
- throw new Exception("Unknown Table Type");
- }
- }
-
- $this->Schema->Server->Connection->Release($rs);
-
- $lines = explode("\n", $create_table);
-
- foreach ($lines as $line) {
- $line = trim($line);
- if (str_starts_with($line, "PRIMARY KEY")) {
- preg_match_all("/`(\w+)`/", $line, $matches, PREG_PATTERN_ORDER);
- // print ""; print_r($matches); die(); // DEBUG
- $this->PrimaryKeys [$matches [1] [0]] = new DBKey($this, "PRIMARY KEY", $matches [0] [0]);
- } elseif (str_starts_with($line, "KEY")) {
- preg_match_all("/`(\w+)`/", $line, $matches, PREG_PATTERN_ORDER);
- // print ""; print_r($matches); die(); // DEBUG
- $this->ForeignKeys [$matches [1] [0]] = new DBKey($this, $matches [1] [0], $matches [1] [1]);
-
- // Add keys to the column for convenience
- $this->Columns [$matches [1] [1]]->Keys [] = $matches [1] [0];
- } elseif (str_starts_with($line, "CONSTRAINT")) {
- preg_match_all("/`(\w+)`/", $line, $matches, PREG_PATTERN_ORDER);
- // print ""; print_r($matches); die(); // DEBUG
- $this->Constraints [$matches [1] [0]] = new DBConstraint($this, $matches [1]);
-
- // the set is basically the reverse of the constraint, but we want to add it to the
- // constraining table so we don't have to do reverse-lookup looking for child relationships
- $this->Schema->Tables [$matches [1] [2]]->Sets [$matches [1] [0]] = new DBSet($this, $matches [1]);
-
- // print "##########################\r\n" . print_r($matches,1) . "\r\n##########################\r\n";
-
- // Add constraints to the column for convenience
- $this->Columns [$matches [1] [1]]->Constraints [] = $matches [1] [0];
- } elseif (strstr($line, "COMMENT ")) {
- // TODO: this is pretty fragile... ?
- // table comments and column comments are seemingly differentiated by "COMMENT=" vs "COMMENT "
- $parts = explode("`", $line);
- $column = $parts [1];
- $comment = strstr($line, "COMMENT ");
- $comment = substr($comment, 9, strlen($comment) - 11);
- $comment = str_replace("''", "'", $comment);
- $this->Columns [$column]->Comment = $comment;
-
- if ($this->Columns [$column]->Default == "" && str_starts_with($comment, "default=")) {
- $this->Columns [$column]->Default = substr($comment, 9, strlen($comment) - 10);
- }
-
- // print "" . $column . "=" . htmlspecialchars( $this->Columns[$column]->Default );
- }
-
- // TODO: look for COMMENT
- }
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/HTTP/HttpRequest.php b/portal/patient/fwk/libs/verysimple/HTTP/HttpRequest.php
deleted file mode 100644
index e490b8ddc788..000000000000
--- a/portal/patient/fwk/libs/verysimple/HTTP/HttpRequest.php
+++ /dev/null
@@ -1,381 +0,0 @@
-Path = str_replace("\\", "/", $path); // normalize any directory paths
-
- $this->Name = substr($this->Path, strrpos($this->Path, "/") + 1);
- $this->Extention = substr($this->Path, strrpos($this->Path, ".") + 1);
- $this->Prefix = substr($this->Name, 0, strpos($this->Name, "."));
- $this->MiddleBit = substr($this->Name, strpos($this->Name, ".") + 1, strrpos($this->Name, ".") - strpos($this->Name, ".") - 1);
- $this->FolderPath = substr($this->Path, 0, strrpos($this->Path, "/") + 1);
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/IO/FolderHelper.php b/portal/patient/fwk/libs/verysimple/IO/FolderHelper.php
deleted file mode 100644
index 13f6c1149e80..000000000000
--- a/portal/patient/fwk/libs/verysimple/IO/FolderHelper.php
+++ /dev/null
@@ -1,56 +0,0 @@
-Path);
-
- while ($fname = readdir($dh)) {
- if (is_file($this->Path . $fname)) {
- if ($pattern == "" || preg_match($pattern, $fname) > 0) {
- $files [] = new FileHelper($this->Path . $fname);
- }
- }
- }
-
- closedir($dh);
-
- return $files;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/BladeRenderEngine.php b/portal/patient/fwk/libs/verysimple/Phreeze/BladeRenderEngine.php
deleted file mode 100644
index 6e1c74404059..000000000000
--- a/portal/patient/fwk/libs/verysimple/Phreeze/BladeRenderEngine.php
+++ /dev/null
@@ -1,135 +0,0 @@
- BladeRenderEngine::$TEMPLATE_PATH . $view . '.blade.php');
- }
-
- /**
- * @inheritdoc
- */
- public function assign($key, $value)
- {
- $this->model [$key] = $value;
- }
-
- /**
- * @inheritdoc
- */
- public function display($template)
- {
- // die('template = ' . $template);
- $template = str_replace('.tpl', '', $template); // normalize any old smarty template paths
- echo $this->fetch($template);
- }
-
- /**
- * Returns the specified model value
- */
- public function get($key)
- {
- return $this->model [$key];
- }
-
- /**
- * @inheritdoc
- */
- public function fetch($template)
- {
- $view = Laravel\View::make($template, $this->model);
-
- Laravel\Blade::sharpen();
-
- $responses = Laravel\Event::fire(Laravel\View::engine, [
- $view
- ]);
-
- return $responses [0];
- }
-
- /**
- *
- * @see IRenderEngine::clear()
- */
- function clear($key)
- {
- if (array_key_exists($key, $this->model)) {
- unset($this->model [$key]);
- }
- }
-
- /**
- *
- * @see IRenderEngine::clearAll()
- */
- function clearAll()
- {
- $this->model == [];
- }
-
- /**
- *
- * @see IRenderEngine::getAll()
- */
- function getAll()
- {
- return $this->model;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/SmartyRenderEngine.php b/portal/patient/fwk/libs/verysimple/Phreeze/SmartyRenderEngine.php
deleted file mode 100644
index d256dacc17dd..000000000000
--- a/portal/patient/fwk/libs/verysimple/Phreeze/SmartyRenderEngine.php
+++ /dev/null
@@ -1,99 +0,0 @@
-smarty = new Smarty();
-
- if ($templatePath) {
- $this->smarty->template_dir = $templatePath;
- }
-
- if ($compilePath) {
- $this->smarty->compile_dir = $compilePath;
- $this->smarty->config_dir = $compilePath;
- $this->smarty->cache_dir = $compilePath;
- }
- }
-
- /**
- *
- * @see IRenderEngine::assign()
- */
- function assign($key, $value)
- {
- return $this->smarty->assign($key, $value);
- }
-
- /**
- *
- * @see IRenderEngine::display()
- */
- function display($template)
- {
- return $this->smarty->display($template);
- }
-
- /**
- *
- * @see IRenderEngine::fetch()
- */
- function fetch($template)
- {
- return $this->smarty->fetch($template);
- }
-
- /**
- *
- * @see IRenderEngine::clear()
- */
- function clear($key)
- {
- $this->smarty->clearAssign($key);
- }
-
- /**
- *
- * @see IRenderEngine::clearAll()
- */
- function clearAll()
- {
- $this->smarty->clearAllAssign();
- }
-
- /**
- *
- * @see IRenderEngine::getAll()
- */
- function getAll()
- {
- return $this->smarty->getTemplateVars();
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/TwigRenderEngine.php b/portal/patient/fwk/libs/verysimple/Phreeze/TwigRenderEngine.php
deleted file mode 100644
index 900aa6c517d8..000000000000
--- a/portal/patient/fwk/libs/verysimple/Phreeze/TwigRenderEngine.php
+++ /dev/null
@@ -1,105 +0,0 @@
-loader = new Twig_Loader_Filesystem($templatePath);
- $this->twig = new Twig_Environment($this->loader, [
- 'cache' => $compilePath
- ]);
- }
-
- /**
- *
- * @see IRenderEngine::assign()
- */
- function assign($key, $value)
- {
- return $this->assignments [$key] = $value;
- }
-
- /**
- *
- * @see IRenderEngine::display()
- */
- function display($template)
- {
- if (!str_contains('.', $template)) {
- $template .= '.html';
- }
-
- return $this->twig->display($template, $this->assignments);
- }
-
- /**
- *
- * @see IRenderEngine::fetch()
- */
- function fetch($template)
- {
- if (!str_contains('.', $template)) {
- $template .= '.html';
- }
-
- return $this->twig->render($template, $this->assignments);
- }
-
- /**
- *
- * @see IRenderEngine::clear()
- */
- function clear($key)
- {
- unset($this->assignments [$key]);
- }
-
- /**
- *
- * @see IRenderEngine::clearAll()
- */
- function clearAll()
- {
- $this->assignments = [];
- }
-
- /**
- *
- * @see IRenderEngine::getAll()
- */
- function getAll()
- {
- return $this->assignments;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/String/NameValue.php b/portal/patient/fwk/libs/verysimple/String/NameValue.php
deleted file mode 100644
index 949d395a5a92..000000000000
--- a/portal/patient/fwk/libs/verysimple/String/NameValue.php
+++ /dev/null
@@ -1,84 +0,0 @@
-Name = $keyval [0];
- $this->Value = $nameonly == false && isset($keyval [1]) ? $keyval [1] : $keyval [0];
- }
-
- /**
- * Parses a string into an array of NameValue objects.
- *
- * @access public
- * @param $lines string
- * in the format name1=val1\nname2=val2 etc...
- * @param $delim the
- * delimiter between name and value (default "=")
- * @param $nameonly returns
- * only the name in the name/value pair
- * @return Array of NameValue objects
- */
- static function Parse($lines, $delim = "=", $nameonly = false)
- {
- $return = [];
-
- $lines = str_replace("\r\n", "\n", $lines);
- $lines = str_replace("\r", "\n", $lines);
- $arr = explode("\n", $lines);
-
- if ($lines == "") {
- return $return;
- }
-
- foreach ($arr as $line) {
- $return [] = new NameValue($line, $delim, $nameonly);
- }
-
- return $return;
- }
-
- /**
- * Converts an array of NameValue objects into a simple 1 dimensional array.
- * WARNING: if there are duplicate Names in your array, they will be overwritten
- *
- * @access public
- * @param $nvArray Array
- * of NameValue objects (as returned from Parse)
- * @return array
- */
- static function ToSimpleArray($nvArray)
- {
- $sa = [];
- foreach ($nvArray as $nv) {
- $sa [$nv->Name] = $nv->Value;
- }
-
- return $sa;
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/String/SimpleTemplate.php b/portal/patient/fwk/libs/verysimple/String/SimpleTemplate.php
deleted file mode 100644
index d85e91594d0e..000000000000
--- a/portal/patient/fwk/libs/verysimple/String/SimpleTemplate.php
+++ /dev/null
@@ -1,188 +0,0 @@
-', '>', $txt);
- $txt = str_replace('< ', '<', $txt);
-
- // Transforms accents in html entities.
- $txt = htmlentities($txt);
-
- // We need some HTML entities back!
- $txt = str_replace('"', '"', $txt);
- $txt = str_replace('<', '<', $txt);
- $txt = str_replace('>', '>', $txt);
- $txt = str_replace('&', '&', $txt);
-
- // Ajdusts links - anything starting with HTTP opens in a new window
- // $txt = str_ireplace("' . str_replace("$eol$eol", "", $txt) . '
';
- $html = str_replace("$eol", " \n", $html);
- $html = str_replace("", "\n\n", $html);
- $html = str_replace("
", "
", $html);
-
- // Wipes after block tags (for when the user includes some html in the text).
- $wipebr = [
- "table",
- "tr",
- "td",
- "blockquote",
- "ul",
- "ol",
- "li"
- ];
-
- for ($x = 0; $x < count($wipebr); $x++) {
- $tag = $wipebr [$x];
- $html = str_ireplace("<$tag> ", "<$tag>", $html);
- $html = str_ireplace("$tag> ", "$tag>", $html);
- }
-
- return $html;
- }
-
- /**
- * Merges data into a template with placeholder variables
- * (for example "Hello {{NAME}}").
- * Useful for simple templating
- * needs such as email, form letters, etc.
- *
- * If a placeholder is in the template but there is no matching value,
- * then the placeholder will be left alone and will appear in the output.
- *
- * Note that there is no escape character so ensure the right and
- * left delimiters do not appear as normal text within the template
- *
- * @param string $template
- * string with placeholder variables
- * @param
- * mixed (array or object) $values an associative array or object with key/value pairs
- * @param
- * bool true to strip out placeholders with missing value, false to leave them as-is in the output (default true)
- * @param
- * string the left (opening) delimiter for placeholders. default = {{
- * @param
- * string the right (closing) delimiter for placeholders. default = }}
- * @return string merged template
- */
- static function Merge($template, $values, $stripMissingValues = true, $ldelim = "{{", $rdelim = "}}")
- {
- return $stripMissingValues ? self::MergeRegEx($template, $values, $ldelim, $rdelim) : self::MergeSimple($template, $values, $ldelim, $rdelim);
- }
-
- /**
- * Used internally by Merge, or may be called directly.
- * If a placeholder is in the template but there is no matching value,
- * it will be left alone and appear in the template, for example: {{PLACEHOLDER}}.
- *
- * @param string $template
- * string with placeholder variables
- * @param
- * mixed (array or object) $values an associative array or object with key/value pairs
- * @param
- * string the left (opening) delimiter for placeholders. default = {{
- * @param
- * string the right (closing) delimiter for placeholders. default = }}
- * @return string merged template
- */
- static function MergeSimple($template, $values, $ldelim = "{{", $rdelim = "}}")
- {
- $replacements = [];
-
- foreach ($values as $key => $val) {
- $replacements [$ldelim . $key . $rdelim] = $val;
- }
-
- return strtr($template, $replacements);
- }
-
- /**
- * Used internally by Merge, or may be called directly.
- * If a placeholder is in the template but there is no matching value,
- * it will be replaced with empty string and will NOT appear in the output.
- *
- * @param string $template
- * string with placeholder variables
- * @param
- * mixed (array or object) $values an associative array or object with key/value pairs
- * @param
- * string the left (opening) delimiter for placeholders. default = {{
- * @param
- * string the right (closing) delimiter for placeholders. default = }}
- * @return string merged template
- */
- static function MergeRegEx($template, $values, $ldelim = "{{", $rdelim = "}}")
- {
- self::$_MERGE_TEMPLATE_VALUES = $values;
-
- if ($ldelim != "{{" || $rdelim != "}}") {
- throw new Exception("Custom delimiters are not yet implemented. Sorry!");
- }
-
- $results = preg_replace_callback('!\{\{(\w+)\}\}!', SimpleTemplate::_MergeRegExCallback(...), $template);
-
- self::$_MERGE_TEMPLATE_VALUES = null;
-
- return $results;
- }
-
- /**
- * called internally by preg_replace_callback
- *
- * @param array $matches
- */
- static function _MergeRegExCallback($matches)
- {
- if (isset(self::$_MERGE_TEMPLATE_VALUES [$matches [1]])) {
- return self::$_MERGE_TEMPLATE_VALUES [$matches [1]];
- } else {
- return "";
- }
- }
-}
diff --git a/portal/patient/fwk/libs/verysimple/Util/TextImageWriter.php b/portal/patient/fwk/libs/verysimple/Util/TextImageWriter.php
deleted file mode 100644
index 134bb455691b..000000000000
--- a/portal/patient/fwk/libs/verysimple/Util/TextImageWriter.php
+++ /dev/null
@@ -1,104 +0,0 @@
-
Date: Sun, 28 Dec 2025 13:49:28 -0500
Subject: [PATCH 012/139] refactor(php): no call_user_func* (#9767)
* chore(rector): CallUserFuncArrayToVariadicRector
* refactor(php): avoid call_user_func_array
* chore(phpstan): rule to block call_user_func*
* refactor(php): no call_user_func*
* refactor(php): rector updates
---
.phpstan/ForbiddenFunctionsRule.php | 20 +++-
.phpstan/README.md | 43 +++++++-
ccr/createCCR.php | 2 +-
controllers/C_Document.class.php | 2 +-
custom/zutil.cli.doc_import.php | 28 +++--
interface/code_systems/list_staged.php | 2 +-
.../code_systems/standard_tables_manage.php | 14 +--
interface/drugs/add_edit_drug.php | 2 +-
interface/drugs/add_edit_lot.php | 2 +-
interface/fax/fax_dispatch.php | 2 +-
interface/forms/LBF/new.php | 10 +-
interface/forms/eye_mag/a_issue.php | 4 +-
interface/forms/eye_mag/save.php | 2 +-
interface/main/calendar/add_edit_event.php | 2 +-
interface/main/finder/document_select.php | 22 ++--
.../src/Controller/AppDispatch.php | 4 +-
.../Model/EncounterccdadispatchTable.php | 2 +-
.../Controller/InstallerController.php | 2 +-
interface/patient_file/history/encounters.php | 6 +-
.../patient_file/history/history_sdoh.php | 2 +-
interface/patient_file/summary/stats.php | 2 +-
.../transaction/add_transaction.php | 4 +-
interface/reports/clinical_reports.php | 2 +-
.../templates/field_html_display_section.php | 3 +-
library/classes/Controller.class.php | 6 +-
library/custom_template/add_context.php | 24 ++---
library/custom_template/add_custombutton.php | 14 +--
library/custom_template/add_template.php | 10 +-
library/custom_template/custom_template.php | 16 +--
library/custom_template/delete_category.php | 12 +--
library/custom_template/quest_popup.php | 4 +-
library/custom_template/save_as.php | 2 +-
library/encounter_events.inc.php | 4 +-
library/options.inc.php | 102 +++++++++---------
library/payment_jav.inc.php | 10 +-
library/report.inc.php | 2 +-
.../smarty/Smarty_Compiler_Legacy.class.php | 8 +-
.../smarty/Smarty_Legacy.class.php | 18 ++--
.../internals/core.get_php_resource.php | 4 +-
.../smarty/internals/core.is_secure.php | 5 +-
.../smarty/internals/core.is_trusted.php | 4 +-
.../smarty/internals/core.read_cache_file.php | 4 +-
.../internals/core.write_cache_file.php | 4 +-
library/standard_tables_capture.inc.php | 6 +-
portal/patient/fwk/libs/savant/Savant3.php | 20 ++--
.../fwk/libs/verysimple/Phreeze/Criteria.php | 10 +-
.../libs/verysimple/Phreeze/Dispatcher.php | 5 +-
.../fwk/libs/verysimple/Phreeze/Phreezer.php | 20 +---
rector.php | 3 +-
.../BillingProcessor/BillingLogger.php | 2 +-
src/Common/Forms/FormReportRenderer.php | 2 +-
src/Common/Forms/FormVitals.php | 2 +-
src/Common/Forms/Types/BillingCodeType.php | 2 +-
.../Forms/Types/LocalProviderListType.php | 2 +-
src/Common/Forms/Types/SmokingStatusType.php | 14 +--
src/Common/ORDataObject/ORDataObject.php | 8 +-
.../AuthorizationController.php | 8 +-
.../Isolated/Billing/BillingLoggerTest.php | 2 +-
58 files changed, 278 insertions(+), 265 deletions(-)
diff --git a/.phpstan/ForbiddenFunctionsRule.php b/.phpstan/ForbiddenFunctionsRule.php
index e74f6bce1d34..8c0a3731df7d 100644
--- a/.phpstan/ForbiddenFunctionsRule.php
+++ b/.phpstan/ForbiddenFunctionsRule.php
@@ -1,10 +1,11 @@
@@ -42,6 +43,8 @@ class ForbiddenFunctionsRule implements Rule
'sqlStatementNoLog' => 'Use QueryUtils::fetchRecordsNoLog() instead of sqlStatementNoLog().',
'sqlStatementThrowException' => 'Use QueryUtils::sqlStatementThrowException() instead of global sqlStatementThrowException().',
'sqlQueryNoLog' => 'Use QueryUtils::querySingleRow() instead of sqlQueryNoLog().',
+ 'call_user_func' => 'Use uniform variable syntax $callable(...$args) or the argument unpacking operator instead of call_user_func().',
+ 'call_user_func_array' => 'Use uniform variable syntax $callable(...$args) or the argument unpacking operator instead of call_user_func_array().',
];
public function getNodeType(): string
@@ -68,6 +71,17 @@ public function processNode(Node $node, Scope $scope): array
$message = self::FORBIDDEN_FUNCTIONS[$functionName];
+ // Determine error identifier and tip based on function type
+ if (in_array($functionName, ['call_user_func', 'call_user_func_array'])) {
+ return [
+ RuleErrorBuilder::message($message)
+ ->identifier('openemr.legacyCallUserFunc')
+ ->tip('Example: $myFunction(...$args) or [$object, \'method\'](...$args)')
+ ->build()
+ ];
+ }
+
+ // Default for SQL functions
return [
RuleErrorBuilder::message($message)
->identifier('openemr.deprecatedSqlFunction')
diff --git a/.phpstan/README.md b/.phpstan/README.md
index c093f7123d4c..81d87ffb3acd 100644
--- a/.phpstan/README.md
+++ b/.phpstan/README.md
@@ -46,9 +46,48 @@ $apiKey = $cryptoGen->decryptStandard($globals->get('gateway_api_key'));
### ForbiddenFunctionsRule
-**Purpose:** Prevents use of legacy `sql.inc.php` functions in the `src/` directory.
+**Purpose:** Prevents use of legacy functions:
+- Legacy `sql.inc.php` functions in the `src/` directory
+- `call_user_func()` and `call_user_func_array()` functions (use modern PHP syntax instead)
-**Rationale:** Contributors should use `QueryUtils` or `DatabaseQueryTrait` instead for modern database patterns.
+**Rationale for SQL functions:** Contributors should use `QueryUtils` or `DatabaseQueryTrait` instead for modern database patterns.
+
+**Rationale for call_user_func:**
+- Modern PHP supports **uniform variable syntax** for dynamic function calls
+- The **argument unpacking operator** (`...`) provides cleaner syntax
+- Variadic functions with `...$args` are more readable than array-based arguments
+- Better static analysis and IDE support with modern syntax
+
+**Before (❌ Forbidden):**
+```php
+// Legacy dynamic function calls
+$result = call_user_func('myFunction', $arg1, $arg2);
+$result = call_user_func_array('myFunction', [$arg1, $arg2]);
+$result = call_user_func([$object, 'method'], $arg1);
+$result = call_user_func_array([$object, 'method'], $args);
+```
+
+**After (âś… Recommended):**
+```php
+// Modern PHP 7+ syntax
+$result = myFunction($arg1, $arg2);
+
+// Dynamic function name
+$functionName = 'myFunction';
+$result = $functionName($arg1, $arg2);
+
+// With argument unpacking
+$args = [$arg1, $arg2];
+$result = $functionName(...$args);
+
+// Object method calls
+$result = $object->method($arg1);
+// or with callable syntax
+$callable = [$object, 'method'];
+$result = $callable($arg1, $arg2);
+// or with argument unpacking
+$result = $callable(...$args);
+```
### ForbiddenClassesRule
diff --git a/ccr/createCCR.php b/ccr/createCCR.php
index c709e3d3b9df..db5f73fa52fd 100644
--- a/ccr/createCCR.php
+++ b/ccr/createCCR.php
@@ -391,7 +391,7 @@ function createHybridXML($ccr): void
if (str_starts_with((string) $raw, "send")) {
$send_to = trim(stripslashes(substr((string) $raw, 5)));
if (!PHPMailer::ValidateAddress($send_to)) {
- echo(htmlspecialchars((string) xl('Invalid recipient address. Please try again.'), ENT_QUOTES));
+ echo(htmlspecialchars(xl('Invalid recipient address. Please try again.'), ENT_QUOTES));
return;
}
diff --git a/controllers/C_Document.class.php b/controllers/C_Document.class.php
index 6930cb3d7ca9..74b8bd33268e 100644
--- a/controllers/C_Document.class.php
+++ b/controllers/C_Document.class.php
@@ -335,7 +335,7 @@ public function upload_action_process()
}
$upload_plugin_pp = 'documentUploadPostProcess';
if (function_exists($upload_plugin_pp)) {
- $tmp = call_user_func($upload_plugin_pp, $value, $d);
+ $tmp = $upload_plugin_pp($value, $d);
if ($tmp) {
$error = $tmp;
}
diff --git a/custom/zutil.cli.doc_import.php b/custom/zutil.cli.doc_import.php
index 0f9228a01e28..487cf3dae4fc 100644
--- a/custom/zutil.cli.doc_import.php
+++ b/custom/zutil.cli.doc_import.php
@@ -139,21 +139,19 @@
}
printf('%s - %s%s', text($doc_pathname), (is_numeric($objDoc->get_id()) ? text($objDoc->get_url()) : xlt('Documents setup error')), "\n");
} else {
- // Too many parameters for the function make the following setup necessary for readability.
- $doc_params = [
- 'name' => $doc->getFilename(),
- 'mime_type' => $str_mime,
- 'full_path' => $doc_pathname,
- 'upload_error' => '',
- 'size' => $doc->getSize(),
- 'owner' => $arg['owner'],
- 'patient_id' => $arg['pid'],
- 'category_id' => '1',
- 'higher_level_path' => '',
- 'path_depth' => '1',
- 'skip_acl_check' => true
- ];
- $new_doc = call_user_func_array(addNewDocument(...), $doc_params);
+ $new_doc = addNewDocument(
+ name: $doc->getFilename(),
+ type: $str_mime,
+ tmp_name: $doc_pathname,
+ error: '',
+ size: $doc->getSize(),
+ owner: $arg['owner'],
+ patient_id_or_simple_directory: $arg['pid'],
+ category_id: '1',
+ higher_level_path: '',
+ path_depth: '1',
+ skip_acl_check: true,
+ );
printf('%s - %s%s', text($doc_pathname), (isset($new_doc) ? text($new_doc->get_url()) : xlt('Documents setup error')), "\n");
if (!$new_doc) {
die();
diff --git a/interface/code_systems/list_staged.php b/interface/code_systems/list_staged.php
index b948fe1deeb9..a41f43ada1bd 100644
--- a/interface/code_systems/list_staged.php
+++ b/interface/code_systems/list_staged.php
@@ -444,7 +444,7 @@
}
}
- if (strlen((string) $action) > 0) {
+ if (strlen($action) > 0) {
$rf = "rf1";
if (!empty($rf2)) {
$rf = "rf2";
diff --git a/interface/code_systems/standard_tables_manage.php b/interface/code_systems/standard_tables_manage.php
index 2a7e629cb5f5..d7f3073db9b3 100644
--- a/interface/code_systems/standard_tables_manage.php
+++ b/interface/code_systems/standard_tables_manage.php
@@ -56,14 +56,14 @@
// load the database
if ($db == 'RXNORM') {
if (!rxnorm_import(IS_WINDOWS)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
}
} elseif ($db == 'SNOMED') {
if ($rf == "rf2") {
if (!snomedRF2_import()) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
} else {
@@ -72,7 +72,7 @@
}
} elseif ($version == "US Extension") {
if (!snomed_import(true)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
} else {
@@ -81,7 +81,7 @@
}
} else {
if (!snomed_import(false)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
} else {
@@ -91,13 +91,13 @@
}
} elseif ($db == 'CQM_VALUESET') {
if (!valueset_import($db)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
}
} else { //$db == 'ICD'
if (!icd_import($db)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to load the file into the database.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
}
@@ -105,7 +105,7 @@
// set the revision version in the database
if (!update_tracker_table($db, $file_revision_date, $version, $file_checksum)) {
- echo htmlspecialchars((string) xl('ERROR: Unable to set the version number.'), ENT_NOQUOTES) . " ";
+ echo htmlspecialchars(xl('ERROR: Unable to set the version number.'), ENT_NOQUOTES) . " ";
temp_dir_cleanup($db);
exit;
}
diff --git a/interface/drugs/add_edit_drug.php b/interface/drugs/add_edit_drug.php
index e41e3e57b7d8..11cc1cdea321 100644
--- a/interface/drugs/add_edit_drug.php
+++ b/interface/drugs/add_edit_drug.php
@@ -701,7 +701,7 @@ function validate(f) {
diff --git a/interface/drugs/add_edit_lot.php b/interface/drugs/add_edit_lot.php
index b38d55a07940..0d74bb5f54e8 100644
--- a/interface/drugs/add_edit_lot.php
+++ b/interface/drugs/add_edit_lot.php
@@ -782,7 +782,7 @@ function trans_type_changed() {
@@ -646,7 +646,7 @@ function submitme() {
// titleChanged();
diff --git a/interface/reports/clinical_reports.php b/interface/reports/clinical_reports.php
index 00feb01e12c8..60c1a5859967 100644
--- a/interface/reports/clinical_reports.php
+++ b/interface/reports/clinical_reports.php
@@ -219,7 +219,7 @@ function submitForm() {
-
+