minor documentation fix
[openemr.git] / interface / orders / receive_hl7_results.inc.php
blob6b30e4d3f07b36ddac1ddcd35fe80bda5dd2f4e4
1 <?php
2 /**
3 * Functions to support parsing and saving hl7 results.
5 * Copyright (C) 2013-2016 Rod Roark <rod@sunsetsystems.com>
7 * LICENSE: This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>.
18 * @package OpenEMR
19 * @author Rod Roark <rod@sunsetsystems.com>
21 * 07-2015: Ensoftek: Edited for MU2 170.314(b)(5)(A)
24 require_once("$srcdir/forms.inc");
26 $rhl7_return = array();
28 function rhl7LogMsg($msg, $fatal = true)
30 global $rhl7_return;
31 if ($fatal) {
32 $rhl7_return['mssgs'][] = '*' . $msg;
33 $rhl7_return['fatal'] = true;
34 newEvent("lab-results-error", $_SESSION['authUser'], $_SESSION['authProvider'], 0, $msg);
35 } else {
36 $rhl7_return['mssgs'][] = '>' . $msg;
39 return $rhl7_return;
42 function rhl7InsertRow(&$arr, $tablename)
44 if (empty($arr)) {
45 return;
48 // echo "<!-- "; // debugging
49 // print_r($arr);
50 // echo " -->\n";
52 $query = "INSERT INTO $tablename SET";
53 $binds = array();
54 $sep = '';
55 foreach ($arr as $key => $value) {
56 $query .= "$sep `$key` = ?";
57 $sep = ',';
58 $binds[] = $value;
61 $arr = array();
62 return sqlInsert($query, $binds);
65 // Write all of the accumulated reports and their results.
66 function rhl7FlushMain(&$amain, $commentdelim = "\n")
68 foreach ($amain as $arr) {
69 $procedure_report_id = rhl7InsertRow($arr['rep'], 'procedure_report');
70 foreach ($arr['res'] as $ares) {
71 $ares['procedure_report_id'] = $procedure_report_id;
72 // obxkey was used to identify parent results but is not stored.
73 unset($ares['obxkey']);
74 // If TX result is not over 10 characters, move it from comments to result field.
75 if ($ares['result'] === '' && $ares['result_data_type'] == 'L') {
76 $i = strpos($ares['comments'], $commentdelim);
77 if ($i && $i <= 10) {
78 $ares['result' ] = substr($ares['comments'], 0, $i);
79 $ares['comments'] = substr($ares['comments'], $i);
83 rhl7InsertRow($ares, 'procedure_result');
88 // Write the MDM document if appropriate.
90 function rhl7FlushMDM($patient_id, $mdm_docname, $mdm_datetime, $mdm_text, $mdm_category_id, $provider)
92 if ($patient_id) {
93 if (!empty($mdm_docname)) {
94 $mdm_docname .= '_';
97 $mdm_docname .= preg_replace('/[^0-9]/', '', $mdm_datetime);
98 $filename = $mdm_docname . '.txt';
99 $d = new Document();
100 $rc = $d->createDocument($patient_id, $mdm_category_id, $filename, 'text/plain', $mdm_text);
101 if (!$rc) {
102 rhl7LogMsg(xl('Document created') . ": $filename", false);
103 if ($provider) {
104 $d->postPatientNote($provider, $mdm_category_id, xl('Electronic document received'));
105 rhl7LogMsg(xl('Notification sent to') . ": $provider", false);
106 } else {
107 rhl7LogMsg(xl('No provider was matched'), false);
111 return $rc;
114 return '';
117 function rhl7Text($s, $allow_newlines = false)
119 $s = str_replace('\\S\\', '^', $s);
120 $s = str_replace('\\F\\', '|', $s);
121 $s = str_replace('\\R\\', '~', $s);
122 $s = str_replace('\\T\\', '&', $s);
123 $s = str_replace('\\X0d\\', "\r", $s);
124 $s = str_replace('\\E\\', '\\', $s);
125 if ($allow_newlines) {
126 $s = str_replace('\\.br\\', "\n", $s);
127 } else {
128 $s = str_replace('\\.br\\', '~', $s);
131 return $s;
134 function rhl7DateTime($s)
136 // Remove UTC offset if present.
137 if (preg_match('/^([0-9.]+)[+-]/', $s, $tmp)) {
138 $s = $tmp[1];
141 $s = preg_replace('/[^0-9]/', '', $s);
142 if (empty($s)) {
143 return '0000-00-00 00:00:00';
146 $ret = substr($s, 0, 4) . '-' . substr($s, 4, 2) . '-' . substr($s, 6, 2);
147 if (strlen($s) > 8) {
148 $ret .= ' ' . substr($s, 8, 2) . ':' . substr($s, 10, 2) . ':';
149 if (strlen($s) > 12) {
150 $ret .= substr($s, 12, 2);
151 } else {
152 $ret .= '00';
156 return $ret;
159 function rhl7DateTimeZone($s)
161 // UTC offset if present always begins with "+" or "-".
162 if (preg_match('/^[0-9.]+([+-].*)$/', $s, $tmp)) {
163 return trim($tmp[1]);
166 return '';
169 function rhl7Date($s)
171 return substr(rhl7DateTime($s), 0, 10);
174 function rhl7Abnormal($s)
176 if ($s == '') {
177 return 'no';
180 if ($s == 'N') {
181 return 'no';
184 if ($s == 'A') {
185 return 'yes';
188 if ($s == 'H') {
189 return 'high';
192 if ($s == 'L') {
193 return 'low';
196 if ($s == 'HH') {
197 return 'vhigh';
200 if ($s == 'LL') {
201 return 'vlow';
204 return rhl7Text($s);
207 function rhl7ReportStatus($s)
209 if ($s == 'F') {
210 return 'final';
213 if ($s == 'P') {
214 return 'prelim';
217 if ($s == 'C') {
218 return 'correct';
221 if ($s == 'X') {
222 return 'error';
225 return rhl7Text($s);
229 * Convert a lower case file extension to a MIME type.
230 * The extension comes from OBX[5][0] which is itself a huge assumption that
231 * the HL7 2.3 standard does not help with. Don't be surprised when we have to
232 * adapt to conventions of various other labs.
234 * @param string $fileext The lower case extension.
235 * @return string MIME type.
237 function rhl7MimeType($fileext)
239 if ($fileext == 'pdf') {
240 return 'application/pdf';
243 if ($fileext == 'doc') {
244 return 'application/msword';
247 if ($fileext == 'rtf') {
248 return 'application/rtf';
251 if ($fileext == 'txt') {
252 return 'text/plain';
255 if ($fileext == 'zip') {
256 return 'application/zip';
259 return 'application/octet-stream';
263 * Extract encapsulated document data according to its encoding type.
265 * @param string $enctype Encoding type from OBX[5][3].
266 * @param string &$src Encoded data from OBX[5][4].
267 * @return string Decoded data, or FALSE if error.
269 function rhl7DecodeData($enctype, &$src)
271 if ($enctype == 'Base64') {
272 return base64_decode($src);
275 if ($enctype == 'A') {
276 return rhl7Text($src);
279 if ($enctype == 'Hex') {
280 $data = '';
281 for ($i = 0; $i < strlen($src) - 1; $i += 2) {
282 $data .= chr(hexdec($src[$i] . $src[$i+1]));
285 return $data;
288 return false;
291 function rhl7CWE($s, $componentdelimiter)
293 $out = '';
294 if ($s === '') {
295 return $out;
298 $arr = explode($componentdelimiter, $s);
299 if (!empty($arr[8])) {
300 $out = $arr[8];
301 } else {
302 $out = $arr[0];
303 if (isset($arr[1])) {
304 $out .= " (" . $arr[1] . ")";
308 return $out;
312 * Parse the SPM segment and get the specimen display name and update the table.
314 * @param string $specimen Encoding type from SPM.
316 function rhl7UpdateReportWithSpecimen(&$amain, $specimen, $d2)
318 $specimen_display = '';
320 // SPM4: Specimen Type: Example: 119297000^BLD^SCT^BldSpc^Blood^99USA^^^Blood Specimen
321 $specimen_display = rhl7CWE($specimen[4], $d2);
323 $tmpnotes = xl('Specimen type') . ': ' . $specimen_display;
324 $tmp = rhl7CWE($specimen[21], $d2);
325 if ($tmp) {
326 $tmpnotes .= '; ' . xl('Rejected') . ': ' . $tmp;
329 $tmp = rhl7CWE($specimen[24], $d2);
330 if ($tmp) {
331 $tmpnotes .= '; ' . xl('Condition') . ': ' . $tmp;
334 $alast = count($amain) - 1;
335 $amain[$alast]['rep']['specimen_num'] = $specimen_display;
336 $amain[$alast]['rep']['report_notes'] .= rhl7Text($tmpnotes) . "\n";
340 * Get the Performing Lab Details from the OBX segment. Mandatory for MU2.
342 * @param string $obx23 Encoding type from OBX23.
343 * @param string $obx23 Encoding type from OBX24.
344 * @param string $obx23 Encoding type from OBX25.
345 * @param string $obx23 New line character.
347 function getPerformingOrganizationDetails($obx23, $obx24, $obx25, $componentdelimiter, $commentdelim)
349 $s = null;
351 if (!empty($obx23) || !empty($obx24) || !empty($obx25)) {
352 // Organization Name
353 // OBX23 Example: "Century Hospital^^^^^NIST-AA-1&2.16.840.1.113883.3.72.5.30.1&ISO^XX^^^987"
354 $obx23_segs = explode($componentdelimiter, $obx23);
355 if (!empty($obx23_segs[0])) {
356 $s .= $obx23_segs[0] . $commentdelim;
359 // Medical Director
360 // OBX25 Example: "2343242^Knowsalot^Phil^J.^III^Dr.^^^NIST-AA-1&2.16.840.1.113883.3.72.5.30.1&ISO^L^^^DNSPM"
361 // Dr. Phil Knowsalot J. III
362 if (!empty($obx25)) {
363 $obx25_segs = explode($componentdelimiter, $obx25);
364 $s .= "$obx25_segs[5] $obx25_segs[2] $obx25_segs[1] $obx25_segs[3] $obx25_segs[4]" . $commentdelim;
367 // Organization Address
368 // OBX24 Example: "2070 Test Park^^Los Angeles^CA^90067^USA^B^^06037"
369 if (!empty($obx24)) {
370 $obx24_segs = explode($componentdelimiter, $obx24);
371 //$s .= "$obx24_segs[0] $obx24_segs[1], $obx24_segs[2], $obx24_segs[3], $obx24_segs[4], $obx24_segs[5]" . $commentdelim;
372 $s .= "$obx24_segs[0]$commentdelim$obx24_segs[2], $obx24_segs[3] $obx24_segs[4]$commentdelim$obx24_segs[5]$commentdelim";
373 if (!empty($obx24_segs[8])) {
374 $s .= "County/Parish Code: $obx24_segs[8]$commentdelim";
379 return $s;
383 * Look for a patient matching the given data.
384 * Return values are:
385 * >0 Definite match, this is the pid.
386 * 0 No patient is close to a match.
387 * -1 It's not clear if there is a match.
389 function match_patient($ptarr)
391 $in_ss = str_replace('-', '', $ptarr['ss']);
392 $in_fname = $ptarr['fname'];
393 $in_lname = $ptarr['lname'];
394 $in_dob = $ptarr['DOB'];
395 $patient_id = 0;
396 $res = sqlStatement(
397 "SELECT pid FROM patient_data WHERE " .
398 "((ss IS NULL OR ss = '' OR '' = ?) AND " .
399 "fname IS NOT NULL AND fname != '' AND fname = ? AND " .
400 "lname IS NOT NULL AND lname != '' AND lname = ? AND " .
401 "DOB IS NOT NULL AND DOB = ?) OR " .
402 "(ss IS NOT NULL AND ss != '' AND REPLACE(ss, '-', '') = ? AND (" .
403 "fname IS NOT NULL AND fname != '' AND fname = ? OR " .
404 "lname IS NOT NULL AND lname != '' AND lname = ? OR " .
405 "DOB IS NOT NULL AND DOB = ?)) " .
406 "ORDER BY ss DESC, pid DESC LIMIT 2",
407 array($in_ss, $in_fname, $in_lname, $in_dob, $in_ss, $in_fname, $in_lname, $in_dob)
409 if (sqlNumRows($res) > 1) {
410 // Multiple matches, so ambiguous.
411 $patient_id = -1;
412 } else if (sqlNumRows($res) == 1) {
413 // Got exactly one match, so use it.
414 $tmp = sqlFetchArray($res);
415 $patient_id = intval($tmp['pid']);
416 } else {
417 // No match good enough, figure out if there's enough ambiguity to ask the user.
418 $tmp = sqlQuery(
419 "SELECT pid FROM patient_data WHERE " .
420 "(ss IS NOT NULL AND ss != '' AND REPLACE(ss, '-', '') = ?) OR " .
421 "(fname IS NOT NULL AND fname != '' AND fname = ? AND " .
422 "lname IS NOT NULL AND lname != '' AND lname = ?) OR " .
423 "(DOB IS NOT NULL AND DOB = ?) " .
424 "LIMIT 1",
425 array($in_ss, $in_fname, $in_lname, $in_dob)
427 if (!empty($tmp['pid'])) {
428 $patient_id = -1;
432 return $patient_id;
436 * Look for a local provider matching the given XCN field from some segment.
438 * @param array $arr array(NPI, lastname, firstname) identifying a provider.
439 * @return mixed Array(id, username), or FALSE if no match.
441 function match_provider($arr)
443 if (empty($arr)) {
444 return false;
447 $op_lname = $op_fname = '';
448 $op_npi = preg_replace('/[^0-9]/', '', $arr[0]);
449 if (!empty($arr[1])) {
450 $op_lname = $arr[1];
453 if (!empty($arr[2])) {
454 $op_fname = $arr[2];
457 if ($op_npi || ($op_fname && $op_lname)) {
458 if ($op_npi) {
459 if ($op_fname && $op_lname) {
460 $where = "((npi IS NOT NULL AND npi = ?) OR ((npi IS NULL OR npi = ?) AND lname = ? AND fname = ?))";
461 $qarr = array($op_npi, '', $op_lname, $op_fname);
462 } else {
463 $where = "npi IS NOT NULL AND npi = ?";
464 $qarr = array($op_npi);
466 } else {
467 $where = "lname = ? AND fname = ?";
468 $qarr = array($op_lname, $op_fname);
471 $oprow = sqlQuery(
472 "SELECT id, username FROM users WHERE " .
473 "username IS NOT NULL AND username != '' AND $where " .
474 "ORDER BY active DESC, authorized DESC, username, id LIMIT 1",
475 $qarr
477 if (!empty($oprow)) {
478 return $oprow;
482 return false;
486 * Create a patient using whatever patient_data attributes are provided.
488 function create_skeleton_patient($patient_data)
490 $employer_data = array();
491 $tmp = sqlQuery("SELECT MAX(pid)+1 AS pid FROM patient_data");
492 $ptid = empty($tmp['pid']) ? 1 : intval($tmp['pid']);
493 if (!isset($patient_data['pubpid'])) {
494 $patient_data['pubpid'] = $ptid;
497 updatePatientData($ptid, $patient_data, true);
498 updateEmployerData($ptid, $employer_data, true);
499 newHistoryData($ptid);
500 return $ptid;
504 * Parse and save.
506 * @param string &$hl7 The input HL7 text
507 * @param string &$matchreq Array of shared patient matching requests
508 * @param int $lab_id Lab ID
509 * @param char $direction B=Bidirectional, R=Results-only
510 * @param bool $dryrun True = do not update anything, just report errors
511 * @param array $matchresp Array of responses to match requests; key is relative segment number,
512 * value is an existing pid or 0 to specify creating a patient
513 * @return array Array of errors and match requests, if any
515 function receive_hl7_results(&$hl7, &$matchreq, $lab_id = 0, $direction = 'B', $dryrun = false, $matchresp = null)
517 global $rhl7_return;
519 // This will hold returned error messages and related variables.
520 $rhl7_return = array();
521 $rhl7_return['mssgs'] = array();
522 $rhl7_return['needmatch'] = false; // indicates if this file is pending a match request
524 $rhl7_segnum = 0;
526 if (substr($hl7, 0, 3) != 'MSH') {
527 return rhl7LogMsg(xl('Input does not begin with a MSH segment'), true);
530 // This array holds everything to be written to the database.
531 // We save and postpone these writes in case of errors while processing the message,
532 // so we can look up data from parent results when child results are encountered,
533 // and for other logic simplification.
534 // Each element of this array is another array containing the following possible keys:
535 // 'rep' - row of data to write to procedure_report
536 // 'res' - array of rows to write to procedure_result for this procedure_report
537 // 'fid' - unique lab-provided identifier for this report
539 $amain = array();
541 // End-of-line delimiter for text in procedure_result.comments and other multi-line notes.
542 $commentdelim = "\n";
544 // Ensoftek: Different labs seem to send different EOLs. Edit HL7 input to a character we know.
545 $hl7 = (string)str_replace(array("\r\n", "\r", "\n"), "\r", $hl7);
547 $today = time();
549 $in_message_id = '';
550 $in_ssn = '';
551 $in_dob = '';
552 $in_lname = '';
553 $in_fname = '';
554 $in_orderid = 0;
555 $in_procedure_code = '';
556 $in_report_status = '';
557 $in_encounter = 0;
558 $patient_id = 0; // for results-only patient matching logic
559 $porow = false;
560 $pcrow = false;
561 $oprow = false;
563 $code_seq_array = array(); // tracks sequence numbers of order codes
564 $results_category_id = 0; // document category ID for lab results
566 // This is so we know where we are if a segment like NTE that can appear in
567 // different places is encountered.
568 $context = '';
570 // This will be "ORU" or "MDM".
571 $msgtype = '';
573 // Stuff collected for MDM documents.
574 $mdm_datetime = '';
575 $mdm_docname = '';
576 $mdm_text = '';
578 // Delimiters
579 $d0 = "\r";
580 $d1 = substr($hl7, 3, 1); // typically |
581 $d2 = substr($hl7, 4, 1); // typically ^
582 $d3 = substr($hl7, 5, 1); // typically ~
583 $d4 = substr($hl7, 6, 1); // typically \
584 $d5 = substr($hl7, 7, 1); // typically &
586 // We'll need the document category IDs for any embedded documents.
587 $catrow = sqlQuery(
588 "SELECT id FROM categories WHERE name = ?",
589 array($GLOBALS['lab_results_category_name'])
591 if (empty($catrow['id'])) {
592 return rhl7LogMsg(xl('Document category for lab results does not exist') .
593 ': ' . $GLOBALS['lab_results_category_name'], true);
594 } else {
595 $results_category_id = $catrow['id'];
596 $mdm_category_id = $results_category_id;
597 $catrow = sqlQuery(
598 "SELECT id FROM categories WHERE name = ?",
599 array($GLOBALS['gbl_mdm_category_name'])
601 if (!empty($catrow['id'])) {
602 $mdm_category_id = $catrow['id'];
606 $segs = explode($d0, $hl7);
608 foreach ($segs as $seg) {
609 if (empty($seg)) {
610 continue;
613 // echo "<!-- $dryrun $seg -->\n"; // debugging
615 ++$rhl7_segnum;
616 $a = explode($d1, $seg);
618 if ($a[0] == 'MSH') {
619 if (!$dryrun) {
620 rhl7FlushMain($amain, $commentdelim);
623 $amain = array();
625 if ('MDM' == $msgtype && !$dryrun) {
626 $rc = rhl7FlushMDM(
627 $patient_id,
628 $mdm_docname,
629 $mdm_datetime,
630 $mdm_text,
631 $mdm_category_id,
632 $oprow ? $oprow['username'] : 0
634 if ($rc) {
635 return rhl7LogMsg($rc);
638 $patient_id = 0;
641 $context = $a[0];
642 // Ensoftek: Could come is as 'ORU^R01^ORU_R01'. Handle all cases when 'ORU^R01' is seen.
643 if (strstr($a[8], "ORU^R01")) {
644 $msgtype = 'ORU';
645 } else if ($a[8] == 'MDM^T02' || $a[8] == 'MDM^T04' || $a[8] == 'MDM^T08') {
646 $msgtype = 'MDM';
647 $mdm_datetime = '';
648 $mdm_docname = '';
649 $mdm_text = '';
650 } else {
651 return rhl7LogMsg(xl('MSH.8 message type is not supported') . ": '" . $a[8] . "'", true);
654 $in_message_id = $a[9];
655 } else if ($a[0] == 'PID') {
656 $context = $a[0];
658 if ('MDM' == $msgtype && !$dryrun) {
659 $rc = rhl7FlushMDM(
660 $patient_id,
661 $mdm_docname,
662 $mdm_datetime,
663 $mdm_text,
664 $mdm_category_id,
665 $oprow ? $oprow['username'] : 0
667 if ($rc) {
668 return rhl7LogMsg($rc);
672 $porow = false;
673 $pcrow = false;
674 $oprow = false;
675 $in_orderid = 0;
676 $in_ssn = preg_replace('/[^0-9]/', '', $a[4]);
677 $in_dob = rhl7Date($a[7]);
678 $tmp = explode($d2, $a[5]);
679 $in_lname = rhl7Text($tmp[0]);
680 $in_fname = rhl7Text($tmp[1]);
681 $in_mname = rhl7Text($tmp[2]);
682 $patient_id = 0;
683 // Patient matching is needed for a results-only interface or MDM message type.
684 if ('R' == $direction || 'MDM' == $msgtype) {
685 $ptarr = array('ss' => strtoupper($in_ss), 'fname' => strtoupper($in_fname),
686 'lname' => strtoupper($in_lname), 'mname' => strtoupper($in_mname),
687 'DOB' => strtoupper($in_dob));
688 $patient_id = match_patient($ptarr);
689 if ($patient_id == -1) {
690 // Result is indeterminate.
691 // Make a stringified form of $ptarr to use as a key.
692 $ptstring = serialize($ptarr);
693 // Check if the user has specified the patient.
694 if (isset($matchresp[$ptstring])) {
695 // This will be an existing pid, or 0 to specify creating a patient.
696 $patient_id = intval($matchresp[$ptstring]);
697 } else {
698 if ($dryrun) {
699 // Nope, ask the user to match.
700 $matchreq[$ptstring] = true;
701 $rhl7_return['needmatch'] = true;
702 } else {
703 // Should not happen, but it would be bad to abort now. Create the patient.
704 $patient_id = 0;
705 rhl7LogMsg(xl('Unexpected non-match, creating new patient for segment') .
706 ' ' . $rhl7_segnum, false);
711 if ($patient_id == 0 && !$dryrun) {
712 // We must create the patient.
713 $patient_id = create_skeleton_patient($ptarr);
716 if ($patient_id == -1) {
717 $patient_id = 0;
719 } // end results-only/MDM logic
720 } else if ('PD1' == $a[0]) {
721 // TBD: Save primary care provider name ($a[4]) somewhere?
722 } else if ('PV1' == $a[0]) {
723 if ('ORU' == $msgtype) {
724 // Save placer encounter number if present.
725 if ($direction != 'R' && !empty($a[19])) {
726 $tmp = explode($d2, $a[19]);
727 $in_encounter = intval($tmp[0]);
729 } else if ('MDM' == $msgtype) {
730 // For documents we want the ordering provider.
731 // Try Referring Provider first.
732 $oprow = match_provider(explode($d2, $a[8]));
733 // If no match, try Other Provider.
734 if (empty($oprow)) {
735 $oprow = match_provider(explode($d2, $a[52]));
738 } else if ('ORC' == $a[0] && 'ORU' == $msgtype) {
739 $context = $a[0];
740 $arep = array();
741 $porow = false;
742 $pcrow = false;
743 if ($direction != 'R' && $a[2]) {
744 $in_orderid = intval($a[2]);
746 } else if ('TXA' == $a[0] && 'MDM' == $msgtype) {
747 $context = $a[0];
748 $mdm_datetime = rhl7DateTime($a[4]);
749 $mdm_docname = rhl7Text($a[12]);
750 } else if ($a[0] == 'NTE' && ($context == 'ORC' || $context == 'TXA')) {
751 // Is this ever used?
752 } else if ('OBR' == $a[0] && 'ORU' == $msgtype) {
753 $context = $a[0];
754 $arep = array();
755 if ($direction != 'R' && $a[2]) {
756 $in_orderid = intval($a[2]);
757 $porow = false;
758 $pcrow = false;
761 $tmp = explode($d2, $a[4]);
762 $in_procedure_code = $tmp[0];
763 $in_procedure_name = $tmp[1];
764 $in_report_status = rhl7ReportStatus($a[25]);
766 // Filler identifier is supposed to be unique for each incoming report.
767 $in_filler_id = $a[3];
768 // Child results will have these pointers to their parent.
769 $in_parent_obrkey = '';
770 $in_parent_obxkey = '';
771 $parent_arep = false; // parent report, if any
772 $parent_ares = false; // parent result, if any
773 if (!empty($a[29])) {
774 // This is a child so there should be a parent.
775 $tmp = explode($d2, $a[29]);
776 $in_parent_obrkey = str_replace($d5, $d2, $tmp[1]);
777 $tmp = explode($d2, $a[26]);
778 $in_parent_obxkey = str_replace($d5, $d2, $tmp[0]) . $d1 . $tmp[1];
779 // Look for the parent report.
780 foreach ($amain as $arr) {
781 if (isset($arr['fid']) && $arr['fid'] == $in_parent_obrkey) {
782 $parent_arep = $arr['rep'];
783 // Now look for the parent result within that report.
784 foreach ($arr['res'] as $tmpres) {
785 if (isset($tmpres['obxkey']) && $tmpres['obxkey'] == $in_parent_obxkey) {
786 $parent_ares = $tmpres;
787 break;
791 break;
796 if ($parent_arep) {
797 $in_orderid = $parent_arep['procedure_order_id'];
800 if ($direction == 'R') {
801 // Save their order ID to procedure_order.control_id.
802 // Look for an existing order using that plus lab_id.
803 // Ordering provider is OBR.16 (NPI^Last^First).
804 // Might not need to create a dummy encounter.
805 // Need also provider_id (probably), patient_id, date_ordered, lab_id.
806 // We have observation date/time in OBR.7.
807 // We have report date/time in OBR.22.
808 // We do not have an order date.
810 $external_order_id = empty($a[2]) ? $a[3] : $a[2];
811 $porow = false;
813 if (!$in_orderid && $external_order_id) {
814 $porow = sqlQuery(
815 "SELECT * FROM procedure_order " .
816 "WHERE lab_id = ? AND control_id = ? " .
817 "ORDER BY procedure_order_id DESC LIMIT 1",
818 array($lab_id, $external_order_id)
822 if (!empty($porow)) {
823 $in_orderid = intval($porow['procedure_order_id']);
826 if (!$in_orderid) {
827 // Create order.
828 // Need to identify the ordering provider and, if possible, a recent encounter.
829 $datetime_report = rhl7DateTime($a[22]);
830 $date_report = substr($datetime_report, 0, 10) . ' 00:00:00';
831 $encounter_id = 0;
832 $provider_id = 0;
833 // Look for the most recent encounter within 30 days of the report date.
834 $encrow = sqlQuery(
835 "SELECT encounter FROM form_encounter WHERE " .
836 "pid = ? AND date <= ? AND DATE_ADD(date, INTERVAL 30 DAY) > ? " .
837 "ORDER BY date DESC, encounter DESC LIMIT 1",
838 array($patient_id, $date_report, $date_report)
840 if (!empty($encrow)) {
841 $encounter_id = intval($encrow['encounter']);
842 $provider_id = intval($encrow['provider_id']);
845 if (!$provider_id) {
846 // Attempt ordering provider matching by name or NPI.
847 $oprow = match_provider(explode($d2, $a[16]));
848 if (!empty($oprow)) {
849 $provider_id = intval($oprow['id']);
853 if (!$dryrun) {
854 // Now create the procedure order.
855 $in_orderid = sqlInsert(
856 "INSERT INTO procedure_order SET " .
857 "date_ordered = ?, " .
858 "provider_id = ?, " .
859 "lab_id = ?, " .
860 "date_collected = ?, " .
861 "date_transmitted = ?, " .
862 "patient_id = ?, " .
863 "encounter_id = ?, " .
864 "control_id = ?",
865 array($datetime_report, $provider_id, $lab_id, rhl7DateTime($a[22]),
866 rhl7DateTime($a[7]),
867 $patient_id,
868 $encounter_id,
869 $external_order_id)
871 // If an encounter was identified then link the order to it.
872 if ($encounter_id && $in_orderid) {
873 addForm($encounter_id, "Procedure Order", $in_orderid, "procedure_order", $patient_id);
876 } // end no $porow
877 } // end results-only
878 if (empty($porow)) {
879 $porow = sqlQuery("SELECT * FROM procedure_order WHERE " .
880 "procedure_order_id = ?", array($in_orderid));
881 // The order must already exist. Currently we do not handle electronic
882 // results returned for manual orders.
883 if (empty($porow) && !($dryrun && $direction == 'R')) {
884 return rhl7LogMsg(xl('Procedure order not found') . ": $in_orderid", true);
887 if ($in_encounter) {
888 if ($direction != 'R' && $porow['encounter_id'] != $in_encounter) {
889 return rhl7LogMsg(xl('Encounter ID') .
890 " '" . $porow['encounter_id'] . "' " .
891 xl('for OBR placer order number') .
892 " '$in_orderid' " .
893 xl('does not match the PV1 encounter number') .
894 " '$in_encounter'");
896 } else {
897 // They did not return an encounter number to verify, so more checking
898 // might be done here to make sure the patient seems to match.
901 // Save the lab's control ID if there is one.
902 $tmp = explode($d2, $a[3]);
903 $control_id = $tmp[0];
904 if ($control_id && empty($porow['control_id'])) {
905 sqlStatement("UPDATE procedure_order SET control_id = ? WHERE " .
906 "procedure_order_id = ?", array($control_id, $in_orderid));
909 $code_seq_array = array();
912 // Find the order line item (procedure code) that matches this result.
913 // If there is more than one, then we select the one whose sequence number
914 // is next after the last sequence number encountered for this procedure
915 // code; this assumes that result OBRs are returned in the same sequence
916 // as the corresponding OBRs in the order.
917 if (!isset($code_seq_array[$in_procedure_code])) {
918 $code_seq_array[$in_procedure_code] = 0;
921 $pcquery = "SELECT pc.* FROM procedure_order_code AS pc " .
922 "WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " .
923 "ORDER BY (procedure_order_seq <= ?), procedure_order_seq LIMIT 1";
924 $pcqueryargs = array($in_orderid, $in_procedure_code, $code_seq_array[$in_procedure_code]);
925 $pcrow = sqlQuery($pcquery, $pcqueryargs);
926 if (empty($pcrow)) {
927 // There is no matching procedure in the order, so it must have been
928 // added after the original order was sent, either as a manual request
929 // from the physician or as a "reflex" from the lab.
930 // procedure_source = '2' indicates this.
931 if (!$dryrun) {
932 sqlBeginTrans();
933 $procedure_order_seq = sqlQuery("SELECT IFNULL(MAX(procedure_order_seq),0) + 1 AS increment FROM procedure_order_code WHERE procedure_order_id = ? ", array($in_orderid));
934 sqlInsert(
935 "INSERT INTO procedure_order_code SET " .
936 "procedure_order_id = ?, " .
937 "procedure_order_seq = ?, " .
938 "procedure_code = ?, " .
939 "procedure_name = ?, " .
940 "procedure_source = '2'",
941 array($in_orderid, $procedure_order_seq['increment'], $in_procedure_code, $in_procedure_name)
943 $pcrow = sqlQuery($pcquery, $pcqueryargs);
944 sqlCommitTrans();
945 } else {
946 // Dry run, make a dummy procedure_order_code row.
947 $pcrow = array(
948 'procedure_order_id' => $in_orderid,
949 'procedure_order_seq' => 0, // TBD?
954 $code_seq_array[$in_procedure_code] = 0 + $pcrow['procedure_order_seq'];
955 $arep = array();
956 $arep['procedure_order_id'] = $in_orderid;
957 $arep['procedure_order_seq'] = $pcrow['procedure_order_seq'];
958 $arep['date_collected'] = rhl7DateTime($a[7]);
959 $arep['date_collected_tz'] = rhl7DateTimeZone($a[7]);
960 $arep['date_report'] = rhl7DateTime($a[22]);
961 $arep['date_report_tz'] = rhl7DateTimeZone($a[22]);
962 $arep['report_status'] = $in_report_status;
963 $arep['report_notes'] = '';
964 $arep['specimen_num'] = '';
966 // If this is a child report, add some info from the parent.
967 if (!empty($parent_ares)) {
968 $arep['report_notes'] .= xl('This is a child of result') . ' ' .
969 $parent_ares['result_code'] . ' ' . xl('with value') . ' "' .
970 $parent_ares['result'] . '".' . "\n";
973 if (!empty($parent_arep)) {
974 $arep['report_notes'] .= $parent_arep['report_notes'];
975 $arep['specimen_num'] = $parent_arep['specimen_num'];
978 // Create the main array entry for this report and its results.
979 $i = count($amain);
980 $amain[$i] = array();
981 $amain[$i]['rep'] = $arep;
982 $amain[$i]['fid'] = $in_filler_id;
983 $amain[$i]['res'] = array();
984 } else if ($a[0] == 'NTE' && $context == 'OBR') {
985 // Append this note to those for the most recent report.
986 $amain[count($amain)-1]['rep']['report_notes'] .= rhl7Text($a[3], true) . "\n";
987 } else if ('OBX' == $a[0] && 'ORU' == $msgtype) {
988 $tmp = explode($d2, $a[3]);
989 $result_code = rhl7Text($tmp[0]);
990 $result_text = rhl7Text($tmp[1]);
991 // If this is a text result that duplicates the previous result except
992 // for its value, then treat it as an extension of that result's value.
993 $i = count($amain) - 1;
994 $j = count($amain[$i]['res']) - 1;
995 if ($j >= 0 && $context == 'OBX' && $a[2] == 'TX'
996 && $amain[$i]['res'][$j]['result_data_type'] == 'L'
997 && $amain[$i]['res'][$j]['result_code' ] == $result_code
998 && $amain[$i]['res'][$j]['date' ] == rhl7DateTime($a[14])
999 && $amain[$i]['res'][$j]['facility' ] == rhl7Text($a[15])
1000 && $amain[$i]['res'][$j]['abnormal' ] == rhl7Abnormal($a[8])
1001 && $amain[$i]['res'][$j]['result_status' ] == rhl7ReportStatus($a[11])
1003 $amain[$i]['res'][$j]['comments'] =
1004 substr($amain[$i]['res'][$j]['comments'], 0, strlen($amain[$i]['res'][$j]['comments']) - 1) .
1005 '~' . rhl7Text($a[5]) . $commentdelim;
1006 continue;
1009 $context = $a[0];
1010 $ares = array();
1011 $ares['result_data_type'] = substr($a[2], 0, 1); // N, S, F or E
1012 $ares['comments'] = $commentdelim;
1013 if ($a[2] == 'ED') {
1014 // This is the case of results as an embedded document. We will create
1015 // a normal patient document in the assigned category for lab results.
1016 $tmp = explode($d2, $a[5]);
1017 $fileext = strtolower($tmp[0]);
1018 $filename = date("Ymd_His") . '.' . $fileext;
1019 $data = rhl7DecodeData($tmp[3], $tmp[4]);
1020 if ($data === false) {
1021 return rhl7LogMsg(xl('Invalid encapsulated data encoding type') . ': ' . $tmp[3]);
1024 if (!$dryrun) {
1025 $d = new Document();
1026 $rc = $d->createDocument(
1027 $porow['patient_id'],
1028 $results_category_id, // TBD: Make sure not 0
1029 $filename,
1030 rhl7MimeType($fileext),
1031 $data
1033 if ($rc) {
1034 return rhl7LogMsg($rc);
1037 $ares['document_id'] = $d->get_id();
1039 } else if ($a[2] == 'CWE') {
1040 $ares['result'] = rhl7CWE($a[5], $d2);
1041 } else if ($a[2] == 'SN') {
1042 $ares['result'] = trim(str_replace($d2, ' ', $a[5]));
1043 } else if ($a[2] == 'TX' || strlen($a[5]) > 200) {
1044 // OBX-5 can be a very long string of text with "~" as line separators.
1045 // The first line of comments is reserved for such things.
1046 $ares['result_data_type'] = 'L';
1047 $ares['result'] = '';
1048 $ares['comments'] = rhl7Text($a[5]) . $commentdelim;
1049 } else {
1050 $ares['result'] = rhl7Text($a[5]);
1053 $ares['result_code' ] = $result_code;
1054 $ares['result_text' ] = $result_text;
1055 $ares['date' ] = rhl7DateTime($a[14]);
1056 $ares['facility' ] = rhl7Text($a[15]);
1057 // Ensoftek: Units may have mutiple segments(as seen in MU2 samples), parse and take just first segment.
1058 $tmp = explode($d2, $a[6]);
1059 $ares['units'] = rhl7Text($tmp[0]);
1060 $ares['range' ] = rhl7Text($a[7]);
1061 $ares['abnormal' ] = rhl7Abnormal($a[8]); // values are lab dependent
1062 $ares['result_status'] = rhl7ReportStatus($a[11]);
1064 // Ensoftek: Performing Organization Details. Goes into "Pending Review/Patient Results--->Notes--->Facility" section.
1065 $performingOrganization = getPerformingOrganizationDetails($a[23], $a[24], $a[25], $d2, $commentdelim);
1066 if (!empty($performingOrganization)) {
1067 $ares['facility'] .= $performingOrganization . $commentdelim;
1070 /****
1071 // Probably need a better way to report this, if it matters.
1072 if (!empty($a[19])) {
1073 $ares['comments'] .= xl('Analyzed') . ' ' . rhl7DateTime($a[19]) . '.' . $commentdelim;
1075 ****/
1077 // obxkey is to allow matching this as a parent result.
1078 $ares['obxkey'] = $a[3] . $d1 . $a[4];
1080 // Append this result to those for the most recent report.
1081 // Note the 'procedure_report_id' item is not yet present.
1082 $amain[count($amain)-1]['res'][] = $ares;
1083 } else if ('OBX' == $a[0] && 'MDM' == $msgtype) {
1084 $context = $a[0];
1085 if ($a[2] == 'TX') {
1086 if ($mdm_text !== '') {
1087 $mdm_text .= "\r\n";
1090 $mdm_text .= rhl7Text($a[5]);
1091 } else {
1092 return rhl7LogMsg(xl('Unsupported MDM OBX result type') . ': ' . $a[2]);
1094 } else if ('ZEF' == $a[0] && 'ORU' == $msgtype) {
1095 // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF.
1096 $context = 'OBX';
1097 $ares = array();
1098 $ares['result_data_type'] = 'E';
1099 $ares['comments'] = $commentdelim;
1101 $fileext = 'pdf';
1102 $filename = date("Ymd_His") . '.' . $fileext;
1103 $data = rhl7DecodeData('Base64', $a[2]);
1104 if ($data === false) {
1105 return rhl7LogMsg(xl('ZEF segment internal error'));
1108 if (!$dryrun) {
1109 $d = new Document();
1110 $rc = $d->createDocument(
1111 $porow['patient_id'],
1112 $results_category_id, // TBD: Make sure not 0
1113 $filename,
1114 rhl7MimeType($fileext),
1115 $data
1117 if ($rc) {
1118 return rhl7LogMsg($rc);
1121 $ares['document_id'] = $d->get_id();
1124 $ares['date'] = $arep['date_report']; // $arep is left over from the OBR logic.
1125 // Append this result to those for the most recent report.
1126 // Note the 'procedure_report_id' item is not yet present.
1127 $amain[count($amain)-1]['res'][] = $ares;
1128 } else if ('NTE' == $a[0] && 'OBX' == $context && 'ORU' == $msgtype) {
1129 // Append this note to the most recent result item's comments.
1130 $alast = count($amain) - 1;
1131 $rlast = count($amain[$alast]['res']) - 1;
1132 $amain[$alast]['res'][$rlast]['comments'] .= rhl7Text($a[3], true) . $commentdelim;
1133 } // Ensoftek: Get data from SPM segment for specimen.
1134 // SPM segment always occurs after the OBX segment.
1135 else if ('SPM' == $a[0] && 'ORU' == $msgtype) {
1136 rhl7UpdateReportWithSpecimen($amain, $a, $d2);
1137 } // Add code here for any other segment types that may be present.
1139 // Ensoftek: Get data from SPM segment for specimen. Comes in with MU2 samples, but can be ignored.
1140 else if ('TQ1' == $a[0] && 'ORU' == $msgtype) {
1141 // Ignore and do nothing.
1142 } else {
1143 return rhl7LogMsg(xl('Segment name') . " '${a[0]}' " . xl('is misplaced or unknown'));
1147 // Write all reports and their results to the database.
1148 // This will do nothing if a dry run or MDM message type.
1149 if ('ORU' == $msgtype && !$dryrun) {
1150 rhl7FlushMain($amain, $commentdelim);
1153 if ('MDM' == $msgtype && !$dryrun) {
1154 // Write documents.
1155 $rc = rhl7FlushMDM(
1156 $patient_id,
1157 $mdm_docname,
1158 $mdm_datetime,
1159 $mdm_text,
1160 $mdm_category_id,
1161 $oprow ? $oprow['username'] : 0
1163 if ($rc) {
1164 return rhl7LogMsg($rc);
1168 return $rhl7_return;
1172 * Poll all eligible labs for new results and store them in the database.
1174 * @param array &$info Conveys information to and from the caller:
1175 * FROM THE CALLER:
1176 * $info["$ppid/$filename"]['delete'] = a non-empty value if file deletion is requested.
1177 * $info['select'] = array of patient matching responses where key is serialized patient
1178 * attributes and value is selected pid for this patient, or 0 to create the patient.
1179 * TO THE CALLER:
1180 * $info["$ppid/$filename"]['mssgs'] = array of messages from this function.
1181 * $info['match'] = array of patient matching requests where key is serialized patient
1182 * attributes (ss, fname, lname, DOB) and value is TRUE (irrelevant).
1184 * @return string Error text, or empty if no errors.
1186 function poll_hl7_results(&$info)
1188 global $srcdir;
1190 // echo "<!-- post: "; print_r($_POST); echo " -->\n"; // debugging
1191 // echo "<!-- in: "; print_r($info); echo " -->\n"; // debugging
1193 $filecount = 0;
1194 $badcount = 0;
1196 if (!isset($info['match' ])) {
1197 $info['match' ] = array(); // match requests
1200 if (!isset($info['select'])) {
1201 $info['select'] = array(); // match request responses
1204 $ppres = sqlStatement("SELECT * FROM procedure_providers ORDER BY name");
1206 while ($pprow = sqlFetchArray($ppres)) {
1207 $ppid = $pprow['ppid'];
1208 $protocol = $pprow['protocol'];
1209 $remote_host = $pprow['remote_host'];
1210 $hl7 = '';
1212 if ($protocol == 'SFTP') {
1213 $remote_port = 22;
1214 // Hostname may have ":port" appended to specify a nonstandard port number.
1215 if ($i = strrpos($remote_host, ':')) {
1216 $remote_port = 0 + substr($remote_host, $i + 1);
1217 $remote_host = substr($remote_host, 0, $i);
1220 // Compute the target path name.
1221 $pathname = '.';
1222 if ($pprow['results_path']) {
1223 $pathname = $pprow['results_path'] . '/' . $pathname;
1226 // Connect to the server and enumerate files to process.
1227 $sftp = new \phpseclib\Net\SFTP($remote_host, $remote_port);
1228 if (!$sftp->login($pprow['login'], $pprow['password'])) {
1229 return xl('Login to remote host') . " '$remote_host' " . xl('failed');
1232 $files = $sftp->nlist($pathname);
1233 foreach ($files as $file) {
1234 if (substr($file, 0, 1) == '.') {
1235 continue;
1238 ++$filecount;
1239 if (!isset($info["$ppid/$file"])) {
1240 $info["$ppid/$file"] = array();
1243 // Ensure that archive directory exists.
1244 $prpath = $GLOBALS['OE_SITE_DIR'] . "/procedure_results";
1245 if (!file_exists($prpath)) {
1246 mkdir($prpath);
1249 $prpath .= '/' . $pprow['ppid'];
1250 if (!file_exists($prpath)) {
1251 mkdir($prpath);
1254 // Get file contents.
1255 $hl7 = $sftp->get("$pathname/$file");
1256 // If user requested reject and delete, do that.
1257 if (!empty($info["$ppid/$file"]['delete'])) {
1258 $fh = fopen("$prpath/$file.rejected", 'w');
1259 if ($fh) {
1260 fwrite($fh, $hl7);
1261 fclose($fh);
1262 } else {
1263 return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"';
1266 if (!$sftp->delete("$pathname/$file")) {
1267 return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"';
1270 continue;
1273 // Do a dry run of its contents and check for errors and match requests.
1274 $tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], true, $info['select']);
1275 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
1276 // $info["$ppid/$file"]['match'] = $tmp['match'];
1277 if (!empty($tmp['fatal']) || !empty($tmp['needmatch'])) {
1278 // There are errors or matching requests so skip this file.
1279 continue;
1282 // Now the money shot - not a dry run.
1283 $tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], false, $info['select']);
1284 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
1285 // $info["$ppid/$file"]['match'] = $tmp['match'];
1286 if (empty($tmp['fatal']) && empty($tmp['needmatch'])) {
1287 // It worked, archive and delete the file.
1288 $fh = fopen("$prpath/$file", 'w');
1289 if ($fh) {
1290 fwrite($fh, $hl7);
1291 fclose($fh);
1292 } else {
1293 return xl('Cannot create file') . ' "' . "$prpath/$file" . '"';
1296 if (!$sftp->delete("$pathname/$file")) {
1297 return xl('Cannot delete (from SFTP server) file') . ' "' . "$pathname/$file" . '"';
1300 } // end of this file
1301 } // end SFTP
1303 else if ($protocol == 'FS') {
1304 // Filesystem directory containing results files.
1305 $pathname = $pprow['results_path'];
1306 if (!($dh = opendir($pathname))) {
1307 return xl('Unable to access directory') . " '$pathname'";
1310 // Sort by filename just because.
1311 $files = array();
1312 while (false !== ($file = readdir($dh))) {
1313 if (substr($file, 0, 1) == '.') {
1314 continue;
1317 $files[$file] = $file;
1320 closedir($dh);
1321 ksort($files);
1322 // For each file...
1323 foreach ($files as $file) {
1324 ++$filecount;
1325 if (!isset($info["$ppid/$file"])) {
1326 $info["$ppid/$file"] = array();
1329 // Ensure that archive directory exists.
1330 $prpath = $GLOBALS['OE_SITE_DIR'] . "/procedure_results";
1331 if (!file_exists($prpath)) {
1332 mkdir($prpath);
1335 $prpath .= '/' . $pprow['ppid'];
1336 if (!file_exists($prpath)) {
1337 mkdir($prpath);
1340 // Get file contents.
1341 $hl7 = file_get_contents("$pathname/$file");
1342 // If user requested reject and delete, do that.
1343 if (!empty($info["$ppid/$file"]['delete'])) {
1344 $fh = fopen("$prpath/$file.rejected", 'w');
1345 if ($fh) {
1346 fwrite($fh, $hl7);
1347 fclose($fh);
1348 } else {
1349 return xl('Cannot create file') . ' "' . "$prpath/$file.rejected" . '"';
1352 if (!unlink("$pathname/$file")) {
1353 return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"';
1356 continue;
1359 // Do a dry run of its contents and check for errors and match requests.
1360 $tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], true, $info['select']);
1361 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
1362 // $info["$ppid/$file"]['match'] = $tmp['match'];
1363 if (!empty($tmp['fatal']) || !empty($tmp['needmatch'])) {
1364 // There are errors or matching requests so skip this file.
1365 continue;
1368 // Now the money shot - not a dry run.
1369 $tmp = receive_hl7_results($hl7, $info['match'], $ppid, $pprow['direction'], false, $info['select']);
1370 $info["$ppid/$file"]['mssgs'] = $tmp['mssgs'];
1371 // $info["$ppid/$file"]['match'] = $tmp['match'];
1372 if (empty($tmp['fatal']) && empty($tmp['needmatch'])) {
1373 // It worked, archive and delete the file.
1374 $fh = fopen("$prpath/$file", 'w');
1375 if ($fh) {
1376 fwrite($fh, $hl7);
1377 fclose($fh);
1378 } else {
1379 return xl('Cannot create file') . ' "' . "$prpath/$file" . '"';
1382 if (!unlink("$pathname/$file")) {
1383 return xl('Cannot delete file') . ' "' . "$pathname/$file" . '"';
1386 } // end of this file
1387 } // end FS protocol
1389 // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols.
1390 } // end procedure provider
1392 // echo "<!-- out: "; print_r($info); echo " -->\n"; // debugging
1394 return '';
1396 // PHP end tag omitted intentionally.