Various changes and fixes (#7424)
[openemr.git] / interface / patient_file / download_template.php
blobf4b8626d2069e04b02b1d9cbde1765d2256c9629
1 <?php
3 /**
4 * Document Template Download Module.
6 * This module downloads a specified document template to the browser after
7 * substituting relevant patient data into its variables.
9 * @package OpenEMR
10 * @link http://www.open-emr.org
11 * @author Rod Roark <rod@sunsetsystems.com>
12 * @author Brady Miller <brady.g.miller@gmail.com>
13 * @author Ruth Moulton
14 * @copyright Copyright (c) 2013-2014 Rod Roark <rod@sunsetsystems.com>
15 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
16 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
19 /* 3-feb-21 RM - addition of {CurrentDate} and {CurrentTime} */
20 require_once('../globals.php');
21 require_once($GLOBALS['srcdir'] . '/appointments.inc.php');
22 require_once($GLOBALS['srcdir'] . '/options.inc.php');
24 use OpenEMR\Common\Crypto\CryptoGen;
25 use OpenEMR\Common\Csrf\CsrfUtils;
27 if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"])) {
28 CsrfUtils::csrfNotVerified();
31 $nextLocation = 0; // offset to resume scanning
32 $keyLocation = false; // offset of a potential {string} to replace
33 $keyLength = 0; // length of {string} to replace
34 $groupLevel = 0; // 0 if not in a {GRP} section
35 $groupCount = 0; // 0 if no items in the group yet
36 $itemSeparator = '; '; // separator between group items
38 // Check if the current location has the specified {string}.
39 function keySearch(&$s, $key)
41 global $keyLocation, $keyLength;
42 $keyLength = strlen($key);
43 if ($keyLength == 0) {
44 return false;
47 return $key == substr($s, $keyLocation, $keyLength);
50 // Replace the {string} at the current location with the specified data.
51 // Also update the location to resume scanning accordingly.
52 function keyReplace(&$s, $data)
54 global $keyLocation, $keyLength, $nextLocation;
55 $nextLocation = $keyLocation + strlen($data);
56 return substr($s, 0, $keyLocation) . $data . substr($s, $keyLocation + $keyLength);
59 // Do some final processing of field data before it's put into the document.
60 function dataFixup($data, $title = '')
62 global $groupLevel, $groupCount, $itemSeparator, $ext;
63 if ($data !== '') {
64 // Replace some characters that can mess up XML without assuming XML content type.
65 $data = str_replace('&', '[and]', $data);
66 $data = str_replace('<', '[less]', $data);
67 $data = str_replace('>', '[greater]', $data);
68 if ($ext == 'odt') {
69 $data = str_replace("\r\n", "<text:line-break />", $data);
71 // If in a group, include labels and separators.
72 if ($groupLevel) {
73 if ($title !== '') {
74 $data = $title . ': ' . $data;
77 if ($groupCount) {
78 $data = $itemSeparator . $data;
81 ++$groupCount;
85 return $data;
88 // Return a string naming all issues for the specified patient and issue type.
89 function getIssues($type)
91 $tmp = '';
92 $lres = sqlStatement("SELECT title, comments FROM lists WHERE " .
93 "pid = ? AND type = ? AND enddate IS NULL " .
94 "ORDER BY begdate", array($GLOBALS['pid'], $type));
95 while ($lrow = sqlFetchArray($lres)) {
96 if ($tmp) {
97 $tmp .= '; ';
100 $tmp .= $lrow['title'];
101 if ($lrow['comments']) {
102 $tmp .= ' (' . $lrow['comments'] . ')';
106 return $tmp;
109 // Top level function for scanning and replacement of a file's contents.
110 function doSubs($s)
112 global $ptrow, $hisrow, $enrow, $nextLocation, $keyLocation, $keyLength;
113 global $groupLevel, $groupCount, $itemSeparator, $pid, $encounter, $ext;
115 $nextLocation = 0;
116 $groupLevel = 0;
117 $groupCount = 0;
119 while (($keyLocation = strpos($s, '{', $nextLocation)) !== false) {
120 $nextLocation = $keyLocation + 1;
122 if (keySearch($s, '{PatientName}')) {
123 $tmp = $ptrow['fname'];
124 if ($ptrow['mname']) {
125 if ($tmp) {
126 $tmp .= ' ';
129 $tmp .= $ptrow['mname'];
132 if ($ptrow['lname']) {
133 if ($tmp) {
134 $tmp .= ' ';
137 $tmp .= $ptrow['lname'];
140 $s = keyReplace($s, dataFixup($tmp, xl('Name')));
141 } elseif (keySearch($s, '{PatientID}')) {
142 $s = keyReplace($s, dataFixup($ptrow['pubpid'], xl('Chart ID')));
143 } elseif (keySearch($s, '{Address}')) {
144 $s = keyReplace($s, dataFixup($ptrow['street'], xl('Street')));
145 } elseif (keySearch($s, '{City}')) {
146 $s = keyReplace($s, dataFixup($ptrow['city'], xl('City')));
147 } elseif (keySearch($s, '{State}')) {
148 $s = keyReplace($s, dataFixup(getListItemTitle('state', $ptrow['state']), xl('State')));
149 } elseif (keySearch($s, '{Zip}')) {
150 $s = keyReplace($s, dataFixup($ptrow['postal_code'], xl('Postal Code')));
151 } elseif (keySearch($s, '{PatientPhone}')) {
152 $ptphone = $ptrow['phone_contact'];
153 if (empty($ptphone)) {
154 $ptphone = $ptrow['phone_home'];
157 if (empty($ptphone)) {
158 $ptphone = $ptrow['phone_cell'];
161 if (empty($ptphone)) {
162 $ptphone = $ptrow['phone_biz'];
165 if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)/", $ptphone, $tmp)) {
166 $ptphone = '(' . $tmp[1] . ')' . $tmp[2] . '-' . $tmp[3];
169 $s = keyReplace($s, dataFixup($ptphone, xl('Phone')));
170 } elseif (keySearch($s, '{PatientDOB}')) {
171 $s = keyReplace($s, dataFixup(oeFormatShortDate($ptrow['DOB']), xl('Birth Date')));
172 } elseif (keySearch($s, '{PatientSex}')) {
173 $s = keyReplace($s, dataFixup(getListItemTitle('sex', $ptrow['sex']), xl('Sex')));
174 } elseif (keySearch($s, '{DOS}')) {
175 $s = keyReplace($s, dataFixup(oeFormatShortDate(substr($enrow['date'], 0, 10)), xl('Service Date')));
176 } elseif (keySearch($s, '{ChiefComplaint}')) {
177 $cc = $enrow['reason'];
178 $patientid = $ptrow['pid'];
179 $DOS = substr($enrow['date'], 0, 10);
180 // Prefer appointment comment if one is present.
181 $evlist = fetchEvents($DOS, $DOS, " AND pc_pid = ? ", null, false, 0, array($patientid));
182 foreach ($evlist as $tmp) {
183 if ($tmp['pc_pid'] == $pid && !empty($tmp['pc_hometext'])) {
184 $cc = $tmp['pc_hometext'];
188 $s = keyReplace($s, dataFixup($cc, xl('Chief Complaint')));
189 } elseif (keySearch($s, '{ReferringDOC}')) {
190 $tmp = empty($ptrow['ur_fname']) ? '' : $ptrow['ur_fname'];
191 if (!empty($ptrow['ur_mname'])) {
192 if ($tmp) {
193 $tmp .= ' ';
196 $tmp .= $ptrow['ur_mname'];
199 if (!empty($ptrow['ur_lname'])) {
200 if ($tmp) {
201 $tmp .= ' ';
204 $tmp .= $ptrow['ur_lname'];
207 $s = keyReplace($s, dataFixup($tmp, xl('Referer')));
208 } elseif (keySearch($s, '{Allergies}')) {
209 $tmp = generate_plaintext_field(array('data_type' => '24','list_id' => ''), '');
210 $s = keyReplace($s, dataFixup($tmp, xl('Allergies')));
211 } elseif (keySearch($s, '{Medications}')) {
212 $s = keyReplace($s, dataFixup(getIssues('medication'), xl('Medications')));
213 } elseif (keySearch($s, '{ProblemList}')) {
214 $s = keyReplace($s, dataFixup(getIssues('medical_problem'), xl('Problem List')));
215 } elseif (preg_match('/^{CurrentDate:?.*}/', substr($s, $keyLocation), $matches)) {
216 /* defaults to ISO standard date format yyyy-mm-dd
217 * modified by string following ':' as follows
218 * 'global' will use the global date format setting
219 * 'YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY' overide the global setting
220 * anything else is ignored
222 * oeFormatShortDate($date = 'today', $showYear = true) - OpenEMR function to format
223 * date using global setting, defaults to ISO standard yyyy-mm-dd
225 $keyLength = strlen($matches[0]);
226 $matched = $matches[0];
227 $format = 'Y-m-d'; /* default yyyy-mm-dd */
228 $currentdate = '';
229 if (preg_match('/GLOBAL/i', $matched, $matches)) {
230 /* use global setting */
231 $currentdate = oeFormatShortDate(date('Y-m-d'), true);
232 } elseif (
233 /* there's an overiding format */
234 preg_match('/YYYY-MM-DD/i', $matched, $matches)
236 /* nothing to do here as this is the default format */
237 } elseif (preg_match('[MM/DD/YYYY]i', $matched, $matches)) {
238 $format = 'm/d/Y';
239 } elseif (preg_match('[DD/MM/YYYY]i', $matched, $matches)) {
240 $format = 'd/m/Y';
243 if (!$currentdate) {
244 $currentdate = date($format); /* get the current date in specified format */
246 $s = keyReplace($s, dataFixup($currentdate, xl('Date')));
247 } elseif (keySearch($s, '{CurrentTime}')) {
248 $format = 'H:i'; /* 24 hour clock with leading zeros */
249 $currenttime = date($format); /* format to hh:mm for local time zone */
250 $s = keyReplace($s, dataFixup($currenttime, xl('Time')));
251 } elseif (keySearch($s, '{GRP}')) { // This tag indicates the fields from here until {/GRP} are a group
252 // of fields separated by semicolons. Fields with no data are omitted, and fields with
253 // data are prepended with their field label from the form layout.
254 ++$groupLevel;
255 $groupCount = 0;
256 $s = keyReplace($s, '');
257 } elseif (keySearch($s, '{/GRP}')) {
258 if ($groupLevel > 0) {
259 --$groupLevel;
261 $s = keyReplace($s, '');
262 } elseif (preg_match('/^\{ITEMSEP\}(.*?)\{\/ITEMSEP\}/', substr($s, $keyLocation), $matches)) {
263 // This is how we specify the separator between group items in a way that
264 // is independent of the document format. Whatever is between {ITEMSEP} and
265 // {/ITEMSEP} is the separator string. Default is "; ".
266 $itemSeparator = $matches[1];
267 $keyLength = strlen($matches[0]);
268 $s = keyReplace($s, '');
269 } elseif (preg_match('/^\{(LBF\w+):(\w+)\}/', substr($s, $keyLocation), $matches)) {
270 // This handles keys like {LBFxxx:fieldid} for layout-based encounter forms.
271 $formname = $matches[1];
272 $fieldid = $matches[2];
273 $keyLength = 3 + strlen($formname) + strlen($fieldid);
274 $data = '';
275 $currvalue = '';
276 $title = '';
277 $frow = sqlQuery(
278 "SELECT * FROM layout_options " .
279 "WHERE form_id = ? AND field_id = ? LIMIT 1",
280 array($formname, $fieldid)
282 if (!empty($frow)) {
283 $ldrow = sqlQuery(
284 "SELECT ld.field_value " .
285 "FROM lbf_data AS ld, forms AS f WHERE " .
286 "f.pid = ? AND f.encounter = ? AND f.formdir = ? AND f.deleted = 0 AND " .
287 "ld.form_id = f.form_id AND ld.field_id = ? " .
288 "ORDER BY f.form_id DESC LIMIT 1",
289 array($pid, $encounter, $formname, $fieldid)
291 if (!empty($ldrow)) {
292 $currvalue = $ldrow['field_value'];
293 $title = $frow['title'];
296 if ($currvalue !== '') {
297 $data = generate_plaintext_field($frow, $currvalue);
301 $s = keyReplace($s, dataFixup($data, $title));
302 } elseif (preg_match('/^\{(DEM|HIS):(\w+)\}/', substr($s, $keyLocation), $matches)) {
303 // This handles keys like {DEM:fieldid} and {HIS:fieldid}.
304 $formname = $matches[1];
305 $fieldid = $matches[2];
306 $keyLength = 3 + strlen($formname) + strlen($fieldid);
307 $data = '';
308 $currvalue = '';
309 $title = '';
310 $frow = sqlQuery(
311 "SELECT * FROM layout_options " .
312 "WHERE form_id = ? AND field_id = ? LIMIT 1",
313 array($formname, $fieldid)
315 if (!empty($frow)) {
316 $tmprow = $formname == 'DEM' ? $ptrow : $hisrow;
317 if (isset($tmprow[$fieldid])) {
318 $currvalue = $tmprow[$fieldid];
319 $title = $frow['title'];
322 if ($currvalue !== '') {
323 $data = generate_plaintext_field($frow, $currvalue);
327 $s = keyReplace($s, dataFixup($data, $title));
329 } // End if { character found.
331 return $s;
334 // Get patient demographic info.
335 $ptrow = sqlQuery("SELECT pd.*, " .
336 "ur.fname AS ur_fname, ur.mname AS ur_mname, ur.lname AS ur_lname " .
337 "FROM patient_data AS pd " .
338 "LEFT JOIN users AS ur ON ur.id = pd.ref_providerID " .
339 "WHERE pd.pid = ?", array($pid));
341 $hisrow = sqlQuery("SELECT * FROM history_data WHERE pid = ? " .
342 "ORDER BY date DESC LIMIT 1", array($pid));
344 $enrow = array();
346 // Get some info for the currently selected encounter.
347 if ($encounter) {
348 $enrow = sqlQuery("SELECT * FROM form_encounter WHERE pid = ? AND " .
349 "encounter = ?", array($pid, $encounter));
352 $form_filename = $_REQUEST['form_filename'];
353 $templatedir = "$OE_SITE_DIR/documents/doctemplates";
354 $templatepath = "$templatedir/" . check_file_dir_name($form_filename);
356 // Create a temporary file to hold the output.
357 $fname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
359 // Get mime type in a way that works with old and new PHP releases.
360 $mimetype = 'application/octet-stream';
361 $ext = strtolower(array_pop(explode('.', $filename)));
362 if ('dotx' == $ext) {
363 // PHP does not seem to recognize this type.
364 $mimetype = 'application/msword';
365 } elseif (function_exists('finfo_open')) {
366 $finfo = finfo_open(FILEINFO_MIME_TYPE);
367 $mimetype = finfo_file($finfo, $templatepath);
368 finfo_close($finfo);
369 } elseif (function_exists('mime_content_type')) {
370 $mimetype = mime_content_type($templatepath);
371 } else {
372 if ('doc' == $ext) {
373 $mimetype = 'application/msword' ;
374 } elseif ('dot' == $ext) {
375 $mimetype = 'application/msword' ;
376 } elseif ('htm' == $ext) {
377 $mimetype = 'text/html' ;
378 } elseif ('html' == $ext) {
379 $mimetype = 'text/html' ;
380 } elseif ('odt' == $ext) {
381 $mimetype = 'application/vnd.oasis.opendocument.text' ;
382 } elseif ('ods' == $ext) {
383 $mimetype = 'application/vnd.oasis.opendocument.spreadsheet' ;
384 } elseif ('ott' == $ext) {
385 $mimetype = 'application/vnd.oasis.opendocument.text' ;
386 } elseif ('pdf' == $ext) {
387 $mimetype = 'application/pdf' ;
388 } elseif ('ppt' == $ext) {
389 $mimetype = 'application/vnd.ms-powerpoint' ;
390 } elseif ('ps' == $ext) {
391 $mimetype = 'application/postscript' ;
392 } elseif ('rtf' == $ext) {
393 $mimetype = 'application/rtf' ;
394 } elseif ('txt' == $ext) {
395 $mimetype = 'text/plain' ;
396 } elseif ('xls' == $ext) {
397 $mimetype = 'application/vnd.ms-excel' ;
401 // Place file in variable.
402 $fileData = file_get_contents($templatepath);
404 // Decrypt file, if applicable.
405 $cryptoGen = new CryptoGen();
406 if ($cryptoGen->cryptCheckStandard($fileData)) {
407 $fileData = $cryptoGen->decryptStandard($fileData, null, 'database');
410 // Create a temporary file to hold the template.
411 $dname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
412 file_put_contents($dname, $fileData);
414 $zipin = new ZipArchive();
415 if ($zipin->open($dname) === true) {
416 // Must be a zip archive.
417 $zipout = new ZipArchive();
418 $zipout->open($fname, ZipArchive::OVERWRITE);
419 for ($i = 0; $i < $zipin->numFiles; ++$i) {
420 $ename = $zipin->getNameIndex($i);
421 $edata = $zipin->getFromIndex($i);
422 $edata = doSubs($edata);
423 $zipout->addFromString($ename, $edata);
426 $zipout->close();
427 $zipin->close();
428 } else {
429 // Not a zip archive.
430 $edata = file_get_contents($dname);
431 $edata = doSubs($edata);
432 file_put_contents($fname, $edata);
435 // Remove the temporary template file.
436 unlink($dname);
438 // Compute a download name like "filename_lastname_pid.odt".
439 $pi = pathinfo($form_filename);
440 $dlname = $pi['filename'] . '_' . $ptrow['lname'] . '_' . $pid;
441 if ($pi['extension'] !== '') {
442 $dlname .= '.' . $pi['extension'];
445 header('Content-Description: File Transfer');
446 header('Content-Transfer-Encoding: binary');
447 header('Expires: 0');
448 header("Cache-control: private");
449 header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
450 header("Content-Type: $mimetype; charset=utf-8");
451 header("Content-Length: " . filesize($fname));
452 header('Content-Disposition: attachment; filename="' . $dlname . '"');
454 ob_end_clean();
455 @readfile($fname) or error_log("Template temp file not found: " . $fname);
457 unlink($fname);
458 exit;