You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

455 lines
17 KiB

<?php
/**
* Document Template Download Module.
*
* This module downloads a specified document template to the browser after
* substituting relevant patient data into its variables.
*
* @package OpenEMR
* @link http://www.open-emr.org
* @author Rod Roark <rod@sunsetsystems.com>
* @author Brady Miller <brady.g.miller@gmail.com>
* @author Ruth Moulton
* @copyright Copyright (c) 2013-2014 Rod Roark <rod@sunsetsystems.com>
* @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/
/* 3-feb-21 RM - addition of {CurrentDate} and {CurrentTime} */
require_once('../globals.php');
require_once($GLOBALS['srcdir'] . '/appointments.inc.php');
require_once($GLOBALS['srcdir'] . '/options.inc.php');
use OpenEMR\Common\Crypto\CryptoGen;
use OpenEMR\Common\Csrf\CsrfUtils;
if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"])) {
CsrfUtils::csrfNotVerified();
}
$nextLocation = 0; // offset to resume scanning
$keyLocation = false; // offset of a potential {string} to replace
$keyLength = 0; // length of {string} to replace
$groupLevel = 0; // 0 if not in a {GRP} section
$groupCount = 0; // 0 if no items in the group yet
$itemSeparator = '; '; // separator between group items
// Check if the current location has the specified {string}.
function keySearch(&$s, $key)
{
global $keyLocation, $keyLength;
$keyLength = strlen($key);
if ($keyLength == 0) {
return false;
}
return $key == substr($s, $keyLocation, $keyLength);
}
// Replace the {string} at the current location with the specified data.
// Also update the location to resume scanning accordingly.
function keyReplace(&$s, $data)
{
global $keyLocation, $keyLength, $nextLocation;
$nextLocation = $keyLocation + strlen($data);
return substr($s, 0, $keyLocation) . $data . substr($s, $keyLocation + $keyLength);
}
// Do some final processing of field data before it's put into the document.
function dataFixup($data, $title = '')
{
global $groupLevel, $groupCount, $itemSeparator;
if ($data !== '') {
// Replace some characters that can mess up XML without assuming XML content type.
$data = str_replace('&', '[and]', $data);
$data = str_replace('<', '[less]', $data);
$data = str_replace('>', '[greater]', $data);
// If in a group, include labels and separators.
if ($groupLevel) {
if ($title !== '') {
$data = $title . ': ' . $data;
}
if ($groupCount) {
$data = $itemSeparator . $data;
}
++$groupCount;
}
}
return $data;
}
// Return a string naming all issues for the specified patient and issue type.
function getIssues($type)
{
$tmp = '';
$lres = sqlStatement("SELECT title, comments FROM lists WHERE " .
"pid = ? AND type = ? AND enddate IS NULL " .
"ORDER BY begdate", array($GLOBALS['pid'], $type));
while ($lrow = sqlFetchArray($lres)) {
if ($tmp) {
$tmp .= '; ';
}
$tmp .= $lrow['title'];
if ($lrow['comments']) {
$tmp .= ' (' . $lrow['comments'] . ')';
}
}
return $tmp;
}
// Top level function for scanning and replacement of a file's contents.
function doSubs($s)
{
global $ptrow, $hisrow, $enrow, $nextLocation, $keyLocation, $keyLength;
global $groupLevel, $groupCount, $itemSeparator, $pid, $encounter;
$nextLocation = 0;
$groupLevel = 0;
$groupCount = 0;
while (($keyLocation = strpos($s, '{', $nextLocation)) !== false) {
$nextLocation = $keyLocation + 1;
if (keySearch($s, '{PatientName}')) {
$tmp = $ptrow['fname'];
if ($ptrow['mname']) {
if ($tmp) {
$tmp .= ' ';
}
$tmp .= $ptrow['mname'];
}
if ($ptrow['lname']) {
if ($tmp) {
$tmp .= ' ';
}
$tmp .= $ptrow['lname'];
}
$s = keyReplace($s, dataFixup($tmp, xl('Name')));
} elseif (keySearch($s, '{PatientID}')) {
$s = keyReplace($s, dataFixup($ptrow['pubpid'], xl('Chart ID')));
} elseif (keySearch($s, '{Address}')) {
$s = keyReplace($s, dataFixup($ptrow['street'], xl('Street')));
} elseif (keySearch($s, '{City}')) {
$s = keyReplace($s, dataFixup($ptrow['city'], xl('City')));
} elseif (keySearch($s, '{State}')) {
$s = keyReplace($s, dataFixup(getListItemTitle('state', $ptrow['state']), xl('State')));
} elseif (keySearch($s, '{Zip}')) {
$s = keyReplace($s, dataFixup($ptrow['postal_code'], xl('Postal Code')));
} elseif (keySearch($s, '{PatientPhone}')) {
$ptphone = $ptrow['phone_contact'];
if (empty($ptphone)) {
$ptphone = $ptrow['phone_home'];
}
if (empty($ptphone)) {
$ptphone = $ptrow['phone_cell'];
}
if (empty($ptphone)) {
$ptphone = $ptrow['phone_biz'];
}
if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)/", $ptphone, $tmp)) {
$ptphone = '(' . $tmp[1] . ')' . $tmp[2] . '-' . $tmp[3];
}
$s = keyReplace($s, dataFixup($ptphone, xl('Phone')));
} elseif (keySearch($s, '{PatientDOB}')) {
$s = keyReplace($s, dataFixup(oeFormatShortDate($ptrow['DOB']), xl('Birth Date')));
} elseif (keySearch($s, '{PatientSex}')) {
$s = keyReplace($s, dataFixup(getListItemTitle('sex', $ptrow['sex']), xl('Sex')));
} elseif (keySearch($s, '{DOS}')) {
$s = keyReplace($s, dataFixup(oeFormatShortDate(substr($enrow['date'], 0, 10)), xl('Service Date')));
} elseif (keySearch($s, '{ChiefComplaint}')) {
$cc = $enrow['reason'];
$patientid = $ptrow['pid'];
$DOS = substr($enrow['date'], 0, 10);
// Prefer appointment comment if one is present.
$evlist = fetchEvents($DOS, $DOS, " AND pc_pid = ? ", null, false, 0, array($patientid));
foreach ($evlist as $tmp) {
if ($tmp['pc_pid'] == $pid && !empty($tmp['pc_hometext'])) {
$cc = $tmp['pc_hometext'];
}
}
$s = keyReplace($s, dataFixup($cc, xl('Chief Complaint')));
} elseif (keySearch($s, '{ReferringDOC}')) {
$tmp = empty($ptrow['ur_fname']) ? '' : $ptrow['ur_fname'];
if (!empty($ptrow['ur_mname'])) {
if ($tmp) {
$tmp .= ' ';
}
$tmp .= $ptrow['ur_mname'];
}
if (!empty($ptrow['ur_lname'])) {
if ($tmp) {
$tmp .= ' ';
}
$tmp .= $ptrow['ur_lname'];
}
$s = keyReplace($s, dataFixup($tmp, xl('Referer')));
} elseif (keySearch($s, '{Allergies}')) {
$tmp = generate_plaintext_field(array('data_type' => '24','list_id' => ''), '');
$s = keyReplace($s, dataFixup($tmp, xl('Allergies')));
} elseif (keySearch($s, '{Medications}')) {
$s = keyReplace($s, dataFixup(getIssues('medication'), xl('Medications')));
} elseif (keySearch($s, '{ProblemList}')) {
$s = keyReplace($s, dataFixup(getIssues('medical_problem'), xl('Problem List')));
} elseif (preg_match('/^{CurrentDate:?.*}/', substr($s, $keyLocation), $matches)) {
/* defaults to ISO standard date format yyyy-mm-dd
* modified by string following ':' as follows
* 'global' will use the global date format setting
* 'YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY' overide the global setting
* anything else is ignored
*
* oeFormatShortDate($date = 'today', $showYear = true) - OpenEMR function to format
* date using global setting, defaults to ISO standard yyyy-mm-dd
*/
$keyLength = strlen($matches[0]);
$matched = $matches[0];
$format = 'Y-m-d'; /* default yyyy-mm-dd */
$currentdate = '';
if (preg_match('/GLOBAL/i', $matched, $matches)) {
/* use global setting */
$currentdate = oeFormatShortDate(date('Y-m-d'), true);
} elseif (
/* there's an overiding format */
preg_match('/YYYY-MM-DD/i', $matched, $matches)
) {
/* nothing to do here as this is the default format */
} elseif (preg_match('[MM/DD/YYYY]i', $matched, $matches)) {
$format = 'm/d/Y';
} elseif (preg_match('[DD/MM/YYYY]i', $matched, $matches)) {
$format = 'd/m/Y';
}
if (!$currentdate) {
$currentdate = date($format); /* get the current date in specified format */
}
$s = keyReplace($s, dataFixup($currentdate, xl('Date')));
} elseif (keySearch($s, '{CurrentTime}')) {
$format = 'H:i'; /* 24 hour clock with leading zeros */
$currenttime = date($format); /* format to hh:mm for local time zone */
$s = keyReplace($s, dataFixup($currenttime, xl('Time')));
} elseif (keySearch($s, '{GRP}')) { // This tag indicates the fields from here until {/GRP} are a group
// of fields separated by semicolons. Fields with no data are omitted, and fields with
// data are prepended with their field label from the form layout.
++$groupLevel;
$groupCount = 0;
$s = keyReplace($s, '');
} elseif (keySearch($s, '{/GRP}')) {
if ($groupLevel > 0) {
--$groupLevel;
}
$s = keyReplace($s, '');
} elseif (preg_match('/^\{ITEMSEP\}(.*?)\{\/ITEMSEP\}/', substr($s, $keyLocation), $matches)) {
// This is how we specify the separator between group items in a way that
// is independent of the document format. Whatever is between {ITEMSEP} and
// {/ITEMSEP} is the separator string. Default is "; ".
$itemSeparator = $matches[1];
$keyLength = strlen($matches[0]);
$s = keyReplace($s, '');
} elseif (preg_match('/^\{(LBF\w+):(\w+)\}/', substr($s, $keyLocation), $matches)) {
// This handles keys like {LBFxxx:fieldid} for layout-based encounter forms.
$formname = $matches[1];
$fieldid = $matches[2];
$keyLength = 3 + strlen($formname) + strlen($fieldid);
$data = '';
$currvalue = '';
$title = '';
$frow = sqlQuery(
"SELECT * FROM layout_options " .
"WHERE form_id = ? AND field_id = ? LIMIT 1",
array($formname, $fieldid)
);
if (!empty($frow)) {
$ldrow = sqlQuery(
"SELECT ld.field_value " .
"FROM lbf_data AS ld, forms AS f WHERE " .
"f.pid = ? AND f.encounter = ? AND f.formdir = ? AND f.deleted = 0 AND " .
"ld.form_id = f.form_id AND ld.field_id = ? " .
"ORDER BY f.form_id DESC LIMIT 1",
array($pid, $encounter, $formname, $fieldid)
);
if (!empty($ldrow)) {
$currvalue = $ldrow['field_value'];
$title = $frow['title'];
}
if ($currvalue !== '') {
$data = generate_plaintext_field($frow, $currvalue);
}
}
$s = keyReplace($s, dataFixup($data, $title));
} elseif (preg_match('/^\{(DEM|HIS):(\w+)\}/', substr($s, $keyLocation), $matches)) {
// This handles keys like {DEM:fieldid} and {HIS:fieldid}.
$formname = $matches[1];
$fieldid = $matches[2];
$keyLength = 3 + strlen($formname) + strlen($fieldid);
$data = '';
$currvalue = '';
$title = '';
$frow = sqlQuery(
"SELECT * FROM layout_options " .
"WHERE form_id = ? AND field_id = ? LIMIT 1",
array($formname, $fieldid)
);
if (!empty($frow)) {
$tmprow = $formname == 'DEM' ? $ptrow : $hisrow;
if (isset($tmprow[$fieldid])) {
$currvalue = $tmprow[$fieldid];
$title = $frow['title'];
}
if ($currvalue !== '') {
$data = generate_plaintext_field($frow, $currvalue);
}
}
$s = keyReplace($s, dataFixup($data, $title));
}
} // End if { character found.
return $s;
}
// Get patient demographic info.
$ptrow = sqlQuery("SELECT pd.*, " .
"ur.fname AS ur_fname, ur.mname AS ur_mname, ur.lname AS ur_lname " .
"FROM patient_data AS pd " .
"LEFT JOIN users AS ur ON ur.id = pd.ref_providerID " .
"WHERE pd.pid = ?", array($pid));
$hisrow = sqlQuery("SELECT * FROM history_data WHERE pid = ? " .
"ORDER BY date DESC LIMIT 1", array($pid));
$enrow = array();
// Get some info for the currently selected encounter.
if ($encounter) {
$enrow = sqlQuery("SELECT * FROM form_encounter WHERE pid = ? AND " .
"encounter = ?", array($pid, $encounter));
}
$form_filename = $_REQUEST['form_filename'];
$templatedir = "$OE_SITE_DIR/documents/doctemplates";
$templatepath = "$templatedir/" . check_file_dir_name($form_filename);
// Create a temporary file to hold the output.
$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';
$ext = strtolower(array_pop(explode('.', $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' ;
}
}
// Place file in variable.
$fileData = file_get_contents($templatepath);
// Decrypt file, if applicable.
$cryptoGen = new CryptoGen();
if ($cryptoGen->cryptCheckStandard($fileData)) {
$fileData = $cryptoGen->decryptStandard($fileData, null, 'database');
}
// Create a temporary file to hold the template.
$dname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
file_put_contents($dname, $fileData);
$zipin = new ZipArchive();
if ($zipin->open($dname) === true) {
// Must be a zip archive.
$zipout = new ZipArchive();
$zipout->open($fname, ZipArchive::OVERWRITE);
for ($i = 0; $i < $zipin->numFiles; ++$i) {
$ename = $zipin->getNameIndex($i);
$edata = $zipin->getFromIndex($i);
$edata = doSubs($edata);
$zipout->addFromString($ename, $edata);
}
$zipout->close();
$zipin->close();
} else {
// Not a zip archive.
$edata = file_get_contents($dname);
$edata = doSubs($edata);
file_put_contents($fname, $edata);
}
// Remove the temporary template file.
unlink($dname);
// Compute a download name like "filename_lastname_pid.odt".
$pi = pathinfo($form_filename);
$dlname = $pi['filename'] . '_' . $ptrow['lname'] . '_' . $pid;
if ($pi['extension'] !== '') {
$dlname .= '.' . $pi['extension'];
}
header('Content-Description: File Transfer');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header("Cache-control: private");
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header("Content-Type: $mimetype; charset=utf-8");
header("Content-Length: " . filesize($fname));
header('Content-Disposition: attachment; filename="' . $dlname . '"');
ob_end_clean();
@readfile($fname) or error_log("Template temp file not found: " . $fname);
unlink($fname);
exit;