Portal credential enhancements
[openemr.git] / interface / patient_file / download_template.php
blob9626a91a683c7ff45da231c533e4f5c5adcb84de
1 <?php
2 /**
3 * Document Template Download Module.
5 * This module downloads a specified document template to the browser after
6 * substituting relevant patient data into its variables.
8 * @package OpenEMR
9 * @link http://www.open-emr.org
10 * @author Rod Roark <rod@sunsetsystems.com>
11 * @author Brady Miller <brady.g.miller@gmail.com>
12 * @copyright Copyright (c) 2013-2014 Rod Roark <rod@sunsetsystems.com>
13 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
18 require_once('../globals.php');
19 require_once($GLOBALS['srcdir'] . '/acl.inc');
20 require_once($GLOBALS['srcdir'] . '/appointments.inc.php');
21 require_once($GLOBALS['srcdir'] . '/options.inc.php');
23 use OpenEMR\Common\Crypto\CryptoGen;
24 use OpenEMR\Common\Csrf\CsrfUtils;
26 if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"])) {
27 CsrfUtils::csrfNotVerified();
30 $nextLocation = 0; // offset to resume scanning
31 $keyLocation = false; // offset of a potential {string} to replace
32 $keyLength = 0; // length of {string} to replace
33 $groupLevel = 0; // 0 if not in a {GRP} section
34 $groupCount = 0; // 0 if no items in the group yet
35 $itemSeparator = '; '; // separator between group items
37 // Check if the current location has the specified {string}.
38 function keySearch(&$s, $key)
40 global $keyLocation, $keyLength;
41 $keyLength = strlen($key);
42 if ($keyLength == 0) {
43 return false;
46 return $key == substr($s, $keyLocation, $keyLength);
49 // Replace the {string} at the current location with the specified data.
50 // Also update the location to resume scanning accordingly.
51 function keyReplace(&$s, $data)
53 global $keyLocation, $keyLength, $nextLocation;
54 $nextLocation = $keyLocation + strlen($data);
55 return substr($s, 0, $keyLocation) . $data . substr($s, $keyLocation + $keyLength);
58 // Do some final processing of field data before it's put into the document.
59 function dataFixup($data, $title = '')
61 global $groupLevel, $groupCount, $itemSeparator;
62 if ($data !== '') {
63 // Replace some characters that can mess up XML without assuming XML content type.
64 $data = str_replace('&', '[and]', $data);
65 $data = str_replace('<', '[less]', $data);
66 $data = str_replace('>', '[greater]', $data);
67 // If in a group, include labels and separators.
68 if ($groupLevel) {
69 if ($title !== '') {
70 $data = $title . ': ' . $data;
73 if ($groupCount) {
74 $data = $itemSeparator . $data;
77 ++$groupCount;
81 return $data;
84 // Return a string naming all issues for the specified patient and issue type.
85 function getIssues($type)
87 $tmp = '';
88 $lres = sqlStatement("SELECT title, comments FROM lists WHERE " .
89 "pid = ? AND type = ? AND enddate IS NULL " .
90 "ORDER BY begdate", array($GLOBALS['pid'], $type));
91 while ($lrow = sqlFetchArray($lres)) {
92 if ($tmp) {
93 $tmp .= '; ';
96 $tmp .= $lrow['title'];
97 if ($lrow['comments']) {
98 $tmp .= ' (' . $lrow['comments'] . ')';
102 return $tmp;
105 // Top level function for scanning and replacement of a file's contents.
106 function doSubs($s)
108 global $ptrow, $hisrow, $enrow, $nextLocation, $keyLocation, $keyLength;
109 global $groupLevel, $groupCount, $itemSeparator, $pid, $encounter;
111 $nextLocation = 0;
112 $groupLevel = 0;
113 $groupCount = 0;
115 while (($keyLocation = strpos($s, '{', $nextLocation)) !== false) {
116 $nextLocation = $keyLocation + 1;
118 if (keySearch($s, '{PatientName}')) {
119 $tmp = $ptrow['fname'];
120 if ($ptrow['mname']) {
121 if ($tmp) {
122 $tmp .= ' ';
125 $tmp .= $ptrow['mname'];
128 if ($ptrow['lname']) {
129 if ($tmp) {
130 $tmp .= ' ';
133 $tmp .= $ptrow['lname'];
136 $s = keyReplace($s, dataFixup($tmp, xl('Name')));
137 } else if (keySearch($s, '{PatientID}')) {
138 $s = keyReplace($s, dataFixup($ptrow['pubpid'], xl('Chart ID')));
139 } else if (keySearch($s, '{Address}')) {
140 $s = keyReplace($s, dataFixup($ptrow['street'], xl('Street')));
141 } else if (keySearch($s, '{City}')) {
142 $s = keyReplace($s, dataFixup($ptrow['city'], xl('City')));
143 } else if (keySearch($s, '{State}')) {
144 $s = keyReplace($s, dataFixup(getListItemTitle('state', $ptrow['state']), xl('State')));
145 } else if (keySearch($s, '{Zip}')) {
146 $s = keyReplace($s, dataFixup($ptrow['postal_code'], xl('Postal Code')));
147 } else if (keySearch($s, '{PatientPhone}')) {
148 $ptphone = $ptrow['phone_contact'];
149 if (empty($ptphone)) {
150 $ptphone = $ptrow['phone_home'];
153 if (empty($ptphone)) {
154 $ptphone = $ptrow['phone_cell'];
157 if (empty($ptphone)) {
158 $ptphone = $ptrow['phone_biz'];
161 if (preg_match("/([2-9]\d\d)\D*(\d\d\d)\D*(\d\d\d\d)/", $ptphone, $tmp)) {
162 $ptphone = '(' . $tmp[1] . ')' . $tmp[2] . '-' . $tmp[3];
165 $s = keyReplace($s, dataFixup($ptphone, xl('Phone')));
166 } else if (keySearch($s, '{PatientDOB}')) {
167 $s = keyReplace($s, dataFixup(oeFormatShortDate($ptrow['DOB']), xl('Birth Date')));
168 } else if (keySearch($s, '{PatientSex}')) {
169 $s = keyReplace($s, dataFixup(getListItemTitle('sex', $ptrow['sex']), xl('Sex')));
170 } else if (keySearch($s, '{DOS}')) {
171 $s = keyReplace($s, dataFixup(oeFormatShortDate(substr($enrow['date'], 0, 10)), xl('Service Date')));
172 } else if (keySearch($s, '{ChiefComplaint}')) {
173 $cc = $enrow['reason'];
174 $patientid = $ptrow['pid'];
175 $DOS = substr($enrow['date'], 0, 10);
176 // Prefer appointment comment if one is present.
177 $evlist = fetchEvents($DOS, $DOS, " AND pc_pid = ? ", null, false, 0, array($patientid));
178 foreach ($evlist as $tmp) {
179 if ($tmp['pc_pid'] == $pid && !empty($tmp['pc_hometext'])) {
180 $cc = $tmp['pc_hometext'];
184 $s = keyReplace($s, dataFixup($cc, xl('Chief Complaint')));
185 } else if (keySearch($s, '{ReferringDOC}')) {
186 $tmp = empty($ptrow['ur_fname']) ? '' : $ptrow['ur_fname'];
187 if (!empty($ptrow['ur_mname'])) {
188 if ($tmp) {
189 $tmp .= ' ';
192 $tmp .= $ptrow['ur_mname'];
195 if (!empty($ptrow['ur_lname'])) {
196 if ($tmp) {
197 $tmp .= ' ';
200 $tmp .= $ptrow['ur_lname'];
203 $s = keyReplace($s, dataFixup($tmp, xl('Referer')));
204 } else if (keySearch($s, '{Allergies}')) {
205 $tmp = generate_plaintext_field(array('data_type'=>'24','list_id'=>''), '');
206 $s = keyReplace($s, dataFixup($tmp, xl('Allergies')));
207 } else if (keySearch($s, '{Medications}')) {
208 $s = keyReplace($s, dataFixup(getIssues('medication'), xl('Medications')));
209 } else if (keySearch($s, '{ProblemList}')) {
210 $s = keyReplace($s, dataFixup(getIssues('medical_problem'), xl('Problem List')));
211 } else if (keySearch($s, '{GRP}')) { // This tag indicates the fields from here until {/GRP} are a group
212 // of fields separated by semicolons. Fields with no data are omitted, and fields with
213 // data are prepended with their field label from the form layout.
214 ++$groupLevel;
215 $groupCount = 0;
216 $s = keyReplace($s, '');
217 } else if (keySearch($s, '{/GRP}')) {
218 if ($groupLevel > 0) {
219 --$groupLevel;
222 $s = keyReplace($s, '');
223 } else if (preg_match('/^\{ITEMSEP\}(.*?)\{\/ITEMSEP\}/', substr($s, $keyLocation), $matches)) {
224 // This is how we specify the separator between group items in a way that
225 // is independent of the document format. Whatever is between {ITEMSEP} and
226 // {/ITEMSEP} is the separator string. Default is "; ".
227 $itemSeparator = $matches[1];
228 $keyLength = strlen($matches[0]);
229 $s = keyReplace($s, '');
230 } else if (preg_match('/^\{(LBF\w+):(\w+)\}/', substr($s, $keyLocation), $matches)) {
231 // This handles keys like {LBFxxx:fieldid} for layout-based encounter forms.
232 $formname = $matches[1];
233 $fieldid = $matches[2];
234 $keyLength = 3 + strlen($formname) + strlen($fieldid);
235 $data = '';
236 $currvalue = '';
237 $title = '';
238 $frow = sqlQuery(
239 "SELECT * FROM layout_options " .
240 "WHERE form_id = ? AND field_id = ? LIMIT 1",
241 array($formname, $fieldid)
243 if (!empty($frow)) {
244 $ldrow = sqlQuery(
245 "SELECT ld.field_value " .
246 "FROM lbf_data AS ld, forms AS f WHERE " .
247 "f.pid = ? AND f.encounter = ? AND f.formdir = ? AND f.deleted = 0 AND " .
248 "ld.form_id = f.form_id AND ld.field_id = ? " .
249 "ORDER BY f.form_id DESC LIMIT 1",
250 array($pid, $encounter, $formname, $fieldid)
252 if (!empty($ldrow)) {
253 $currvalue = $ldrow['field_value'];
254 $title = $frow['title'];
257 if ($currvalue !== '') {
258 $data = generate_plaintext_field($frow, $currvalue);
262 $s = keyReplace($s, dataFixup($data, $title));
263 } else if (preg_match('/^\{(DEM|HIS):(\w+)\}/', substr($s, $keyLocation), $matches)) {
264 // This handles keys like {DEM:fieldid} and {HIS:fieldid}.
265 $formname = $matches[1];
266 $fieldid = $matches[2];
267 $keyLength = 3 + strlen($formname) + strlen($fieldid);
268 $data = '';
269 $currvalue = '';
270 $title = '';
271 $frow = sqlQuery(
272 "SELECT * FROM layout_options " .
273 "WHERE form_id = ? AND field_id = ? LIMIT 1",
274 array($formname, $fieldid)
276 if (!empty($frow)) {
277 $tmprow = $formname == 'DEM' ? $ptrow : $hisrow;
278 if (isset($tmprow[$fieldid])) {
279 $currvalue = $tmprow[$fieldid];
280 $title = $frow['title'];
283 if ($currvalue !== '') {
284 $data = generate_plaintext_field($frow, $currvalue);
288 $s = keyReplace($s, dataFixup($data, $title));
290 } // End if { character found.
292 return $s;
295 // if (!acl_check('admin', 'super')) die(htmlspecialchars(xl('Not authorized')));
297 // Get patient demographic info.
298 $ptrow = sqlQuery("SELECT pd.*, " .
299 "ur.fname AS ur_fname, ur.mname AS ur_mname, ur.lname AS ur_lname " .
300 "FROM patient_data AS pd " .
301 "LEFT JOIN users AS ur ON ur.id = pd.ref_providerID " .
302 "WHERE pd.pid = ?", array($pid));
304 $hisrow = sqlQuery("SELECT * FROM history_data WHERE pid = ? " .
305 "ORDER BY date DESC LIMIT 1", array($pid));
307 $enrow = array();
309 // Get some info for the currently selected encounter.
310 if ($encounter) {
311 $enrow = sqlQuery("SELECT * FROM form_encounter WHERE pid = ? AND " .
312 "encounter = ?", array($pid, $encounter));
315 $form_filename = $_REQUEST['form_filename'];
316 $templatedir = "$OE_SITE_DIR/documents/doctemplates";
317 $templatepath = "$templatedir/" . check_file_dir_name($form_filename);
319 // Create a temporary file to hold the output.
320 $fname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
322 // Get mime type in a way that works with old and new PHP releases.
323 $mimetype = 'application/octet-stream';
324 $ext = strtolower(array_pop(explode('.', $filename)));
325 if ('dotx' == $ext) {
326 // PHP does not seem to recognize this type.
327 $mimetype = 'application/msword';
328 } else if (function_exists('finfo_open')) {
329 $finfo = finfo_open(FILEINFO_MIME_TYPE);
330 $mimetype = finfo_file($finfo, $templatepath);
331 finfo_close($finfo);
332 } else if (function_exists('mime_content_type')) {
333 $mimetype = mime_content_type($templatepath);
334 } else {
335 if ('doc' == $ext) {
336 $mimetype = 'application/msword' ;
337 } else if ('dot' == $ext) {
338 $mimetype = 'application/msword' ;
339 } else if ('htm' == $ext) {
340 $mimetype = 'text/html' ;
341 } else if ('html' == $ext) {
342 $mimetype = 'text/html' ;
343 } else if ('odt' == $ext) {
344 $mimetype = 'application/vnd.oasis.opendocument.text' ;
345 } else if ('ods' == $ext) {
346 $mimetype = 'application/vnd.oasis.opendocument.spreadsheet' ;
347 } else if ('ott' == $ext) {
348 $mimetype = 'application/vnd.oasis.opendocument.text' ;
349 } else if ('pdf' == $ext) {
350 $mimetype = 'application/pdf' ;
351 } else if ('ppt' == $ext) {
352 $mimetype = 'application/vnd.ms-powerpoint' ;
353 } else if ('ps' == $ext) {
354 $mimetype = 'application/postscript' ;
355 } else if ('rtf' == $ext) {
356 $mimetype = 'application/rtf' ;
357 } else if ('txt' == $ext) {
358 $mimetype = 'text/plain' ;
359 } else if ('xls' == $ext) {
360 $mimetype = 'application/vnd.ms-excel' ;
364 // Place file in variable.
365 $fileData = file_get_contents($templatepath);
367 // Decrypt file, if applicable.
368 $cryptoGen = new CryptoGen();
369 if ($cryptoGen->cryptCheckStandard($fileData)) {
370 $fileData = $cryptoGen->decryptStandard($fileData, null, 'database');
373 // Create a temporary file to hold the template.
374 $dname = tempnam($GLOBALS['temporary_files_dir'], 'OED');
375 file_put_contents($dname, $fileData);
377 $zipin = new ZipArchive;
378 if ($zipin->open($dname) === true) {
379 // Must be a zip archive.
380 $zipout = new ZipArchive;
381 $zipout->open($fname, ZipArchive::OVERWRITE);
382 for ($i = 0; $i < $zipin->numFiles; ++$i) {
383 $ename = $zipin->getNameIndex($i);
384 $edata = $zipin->getFromIndex($i);
385 $edata = doSubs($edata);
386 $zipout->addFromString($ename, $edata);
389 $zipout->close();
390 $zipin->close();
391 } else {
392 // Not a zip archive.
393 $edata = file_get_contents($dname);
394 $edata = doSubs($edata);
395 file_put_contents($fname, $edata);
398 // Remove the temporary template file.
399 unlink($dname);
401 // Compute a download name like "filename_lastname_pid.odt".
402 $pi = pathinfo($form_filename);
403 $dlname = $pi['filename'] . '_' . $ptrow['lname'] . '_' . $pid;
404 if ($pi['extension'] !== '') {
405 $dlname .= '.' . $pi['extension'];
408 header('Content-Description: File Transfer');
409 header('Content-Transfer-Encoding: binary');
410 header('Expires: 0');
411 header("Cache-control: private");
412 header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
413 header("Content-Type: $mimetype; charset=utf-8");
414 header("Content-Length: " . filesize($fname));
415 header('Content-Disposition: attachment; filename="'. $dlname .'"');
417 ob_end_clean();
418 @readfile($fname) or error_log("Template temp file not found: " . $fname);
420 unlink($fname);
421 exit;