Implement direct message receive and background services manager, take 1.
authorLuis Maas <lcmaas@emrdirect.com>
Mon, 11 Feb 2013 05:13:34 +0000 (10 21:13 -0800)
committerbradymiller <bradymiller@users.sourceforge.net>
Sun, 17 Feb 2013 06:32:44 +0000 (16 22:32 -0800)
 1. Background Service Manager Framework
    --Documentation provided in the main library/ajax/execute_background_services.php script.
    --Report at Administration->Reports->Services->Background Services
 2. Direct Message Receive
    --Service built on top of the Background Service Manager Framework
    --Can be set/modified in Administration->Globals->Connectors (bottom section)
    --Log can be viewed at Administration->Reports->Services->Direct Message Log
    --Added support for importing of documents with no patient mapping. GUI for
      this is at Miscellaneous->New Documents.
    --Added support for sending pnotes with no patient mapping.
    --Added support for service bots to enable storage of documents,
      sending of messages and accurate logging by services.

 Miscellaneous mods: Changed the Administration->Services to Administration->Codes in left_nav menu.
                     (to avoid confusion with the Services reports)

18 files changed:
ccr/transmitCCD.php
controllers/C_Document.class.php
interface/main/left_nav.php
interface/main/messages/messages.php
interface/reports/background_services.php [new file with mode: 0644]
interface/reports/direct_message_log.php [new file with mode: 0644]
interface/super/edit_globals.php
library/ajax/execute_background_services.php [new file with mode: 0644]
library/classes/Controller.class.php
library/classes/Installer.class.php
library/direct_message_check.inc [new file with mode: 0644]
library/globals.inc.php
library/pnotes.inc
sql/4_1_1-to-4_1_2_upgrade.sql
sql/database.sql
sql/official_additional_users.sql [new file with mode: 0644]
templates/documents/general_upload.html
version.php

index f9b5d39..4d42b8f 100644 (file)
@@ -25,6 +25,7 @@
  */
 
 require_once(dirname(__FILE__) . "/../library/log.inc");
+require_once(dirname(__FILE__) . "/../library/sql.inc");
 
 /*
  * Connect to a phiMail Direct Messaging server and transmit
@@ -54,22 +55,22 @@ function transmitCCD($ccd,$recipient,$requested_by) {
    $fp=@fsockopen($server,$phimail_server['port']);
    if ($fp===false) return("$config_err 3");
    @fwrite($fp,"AUTH $phimail_username $phimail_password\n");
-   @fflush($fp);
-   $ret=@fgets($fp,256);
+   fflush($fp);
+   $ret=fgets($fp,256);
    if($ret!="OK\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return("$config_err 4");
    }
-   @fwrite($fp,"TO $recipient\n");
-   @fflush($fp);
-   $ret=@fgets($fp,256);
+   fwrite($fp,"TO $recipient\n");
+   fflush($fp);
+   $ret=fgets($fp,256);
    if($ret!="OK\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return( xl("Delivery is not currently permitted to the specified Direct Address.") );
    }
-   $ret=@fgets($fp,1024); //ignore extra server data
+   $ret=fgets($fp,1024); //ignore extra server data
 
    if($requested_by=="patient")
        $text_out = xl("Delivery of the attached clinical document was requested by the patient.");
@@ -77,61 +78,86 @@ function transmitCCD($ccd,$recipient,$requested_by) {
        $text_out = xl("A clinical document is attached.");
 
    $text_len=strlen($text_out);
-   @fwrite($fp,"TEXT $text_len\n");
-   @fflush($fp);
+   fwrite($fp,"TEXT $text_len\n");
+   fflush($fp);
    $ret=@fgets($fp,256);
    if($ret!="BEGIN\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return("$config_err 5");
    }
-   @fwrite($fp,$text_out);
-   @fflush($fp);
+   fwrite($fp,$text_out);
+   fflush($fp);
    $ret=@fgets($fp,256);
    if($ret!="OK\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return("$config_err 6");
    }
 
    $ccd_out=$ccd->saveXml();
    $ccd_len=strlen($ccd_out);
 
-   @fwrite($fp,"CDA $ccd_len\n");
-   @fflush($fp);
-   $ret=@fgets($fp,256);
+   fwrite($fp,"CDA $ccd_len\n");
+   fflush($fp);
+   $ret=fgets($fp,256);
    if($ret!="BEGIN\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return("$config_err 7");
    }
-   @fwrite($fp,$ccd_out);
-   @fflush($fp);
-   $ret=@fgets($fp,256);
+   fwrite($fp,$ccd_out);
+   fflush($fp);
+   $ret=fgets($fp,256);
    if($ret!="OK\n") {
-       @fwrite($fp,"BYE\n");
-       @fclose($fp);
+       fwrite($fp,"BYE\n");
+       fclose($fp);
        return("$config_err 8");
    }
-   @fwrite($fp,"SEND\n");
-   @fflush($fp);
-   $ret=@fgets($fp,256);
-   @fwrite($fp,"BYE\n");
-   @fclose($fp);
+   fwrite($fp,"SEND\n");
+   fflush($fp);
+   $ret=fgets($fp,256);
+   fwrite($fp,"BYE\n");
+   fclose($fp);
+
+   if($requested_by=="patient")  {
+       $reqBy="portal-user";
+       $sql = "SELECT id FROM users WHERE username='portal-user'";
+       if (($r = sqlStatementNoLog($sql)) === FALSE ||
+           ($u = sqlFetchArray($r)) === FALSE) {
+           $reqID = 1; //default if we don't have a service user
+       } else {
+           $reqID = $u['id'];
+       }
 
-   if(substr($ret,5)=="ERROR")
+   } else {
+       $reqBy=$_SESSION['authUser'];
+        $reqID=$_SESSION['authUserID'];
+   }
+
+   if(substr($ret,5)=="ERROR") {
+       //log the failure
+       newEvent("transmit-ccd",$reqBy,$_SESSION['authProvider'],0,$ret,$pid);
        return( xl("The message could not be sent at this time."));
+   }
 
    /**
     * If we get here, the message was successfully sent and the return
     * value $ret is of the form "QUEUED recipient message-id" which
     * is suitable for logging. 
-    * 
     */
-   if($requested_by=="patient")
-     newEvent("transmit-ccd","portal-user",$_SESSION['authProvider'],1,$ret,$pid);
-   else
-     newEvent("transmit-ccd",$_SESSION['authUser'],$_SESSION['authProvider'],1,$ret,$pid);
+   $msg_id=explode(" ",trim($ret),4);
+   if($msg_id[0]!="QUEUED" || !isset($msg_id[2])) { //unexpected response
+       $ret = "UNEXPECTED RESPONSE: " . $ret;
+       newEvent("transmit-ccd",$reqBy,$_SESSION['authProvider'],0,$ret,$pid);
+       return( xl("There was a problem sending the message."));
+   }
+   newEvent("transmit-ccd",$reqBy,$_SESSION['authProvider'],1,$ret,$pid);
+   $adodb=$GLOBALS['adodb']['db'];
+   $sql="INSERT INTO direct_message_log (msg_type,msg_id,sender,recipient,status,status_ts,patient_id,user_id) " .
+       "VALUES ('S', ?, ?, ?, 'S', NOW(), ?, ?)";
+   $res=@sqlStatementNoLog($sql,array($msg_id[2],$phimail_username,$recipient,$pid,$reqID));
+
    return("SUCCESS");
 }
 
index b5d4049..993c034 100644 (file)
@@ -56,7 +56,9 @@ class C_Document extends Controller {
        }
        
        //Upload multiple files on single click
-       function upload_action_process() {
+       //2013-02-10 EMR Direct: added $non_HTTP_owner to allow storage of Direct Message attachments
+       //through this mechanism, and is set to the user_id for the background process adding the document
+       function upload_action_process($non_HTTP_owner=false) {
                $couchDB = false;
                $harddisk = false;
                if($GLOBALS['document_storage_method']==0){
@@ -178,7 +180,8 @@ class C_Document extends Controller {
                                        
                                        if($harddisk == true){
                                                $uploadSuccess = false;
-                                               if(move_uploaded_file($_FILES['file']['tmp_name'][$key],$this->file_path.$fname)){
+                                               $move_cmd = ($non_HTTP_owner ? "rename" : "move_uploaded_file");
+                                               if($move_cmd($_FILES['file']['tmp_name'][$key],$this->file_path.$fname)){
                                                        $uploadSuccess = true;
                                                }
                                                else{
@@ -204,7 +207,7 @@ class C_Document extends Controller {
                                                $d->mimetype = $_FILES['file']['type'][$key];
                                        }                                 
                                        $d->size = $_FILES['file']['size'][$key];
-                                       $d->owner = $_SESSION['authUserID'];                    
+                                       $d->owner = $non_HTTP_owner ? $non_HTTP_owner : $_SESSION['authUserID'];
                                        $sha1Hash = sha1_file( $this->file_path.$fname );
                                        if($couchDB == true){
                                                //Removing the temporary file which is used to create the hash
index 3f82148..99b2a60 100644 (file)
@@ -372,6 +372,10 @@ function genFindBlock() {
     // run updater every 60 seconds 
      var repeater = setTimeout("getReminderCount()", 60000); 
    });
+   //piggy-back on this repeater to run other background-services
+   //this is a silent task manager that returns no output
+   $.post("<?php echo $GLOBALS['webroot']; ?>/library/ajax/execute_background_services.php",
+      { skip_timeout_reset: "1", ajax: "1" });
  }   
  
  $(document).ready(function (){
@@ -1094,7 +1098,7 @@ if ($GLOBALS['athletic_team']) {
       <?php if (acl_check('admin', 'users'    )) genMiscLink('RTop','adm','0',xl('Users'),'usergroup/usergroup_admin.php'); ?>
       <?php genTreeLink('RTop','pwd','Users Password Change'); ?>
       <?php if (acl_check('admin', 'practice' )) genMiscLink('RTop','adm','0',xl('Practice'),'../controller.php?practice_settings'); ?>
-      <?php if (acl_check('admin', 'superbill')) genTreeLink('RTop','sup',xl('Services')); ?>
+      <?php if (acl_check('admin', 'superbill')) genTreeLink('RTop','sup',xl('Codes')); ?>
       <?php if (acl_check('admin', 'super'    )) genMiscLink('RTop','adm','0',xl('Layouts'),'super/edit_layout.php'); ?>
       <?php if (acl_check('admin', 'super'    )) genMiscLink('RTop','adm','0',xl('Lists'),'super/edit_list.php'); ?>
       <?php if (acl_check('admin', 'acl'      )) genMiscLink('RTop','adm','0',xl('ACL'),'usergroup/adminacl.php'); ?>
@@ -1122,6 +1126,7 @@ if ($GLOBALS['athletic_team']) {
       <?php genTreeLink('RTop','ono',xl('Ofc Notes')); ?>
       <?php genMiscLink('RTop','adm','0',xl('BatchCom'),'batchcom/batchcom.php'); ?>
       <?php genMiscLink('RTop','prf','0',xl('Preferences'),'super/edit_globals.php?mode=user'); ?>
+      <?php if(acl_check('patients','docs')) genMiscLink('RTop','adm','0',xl('New Documents'),'../controller.php?document&list&patient_id=0'); ?>
     </ul>
   </li>
 
@@ -1247,7 +1252,7 @@ if (!empty($reg)) {
       <?php
          // Changed the target URL from practice settings -> Practice Settings - Pharmacy... Dec 09,09 .. Visolve ... This replaces empty frame with Pharmacy window
          if (acl_check('admin', 'practice' )) genMiscLink('RTop','adm','0',xl('Practice'),'../controller.php?practice_settings&pharmacy&action=list'); ?>
-      <?php if (acl_check('admin', 'superbill')) genTreeLink('RTop','sup',xl('Services')); ?>
+      <?php if (acl_check('admin', 'superbill')) genTreeLink('RTop','sup',xl('Codes')); ?>
       <?php if (acl_check('admin', 'super'    )) genMiscLink('RTop','adm','0',xl('Layouts'),'super/edit_layout.php'); ?>
       <?php if (acl_check('admin', 'super'    )) genMiscLink('RTop','adm','0',xl('Lists'),'super/edit_list.php'); ?>
       <?php if (acl_check('admin', 'acl'      )) genMiscLink('RTop','adm','0',xl('ACL'),'usergroup/adminacl.php'); ?>
@@ -1380,6 +1385,15 @@ if (!empty($reg)) {
 ?>
         </ul>
       </li>
+    <?php if (acl_check('admin','super')) { ?>
+      <li><a class="collapsed_lv2"><span><?php echo xlt('Services') ?></span></a>
+        <ul>
+          <?php genMiscLink('RTop','rep','0',xl('Background Services'),'reports/background_services.php'); ?>
+          <?php genMiscLink('RTop','rep','0',xl('Direct Message Log'),'reports/direct_message_log.php'); ?>
+        </ul>
+      </li>
+    <?php } ?>
+
       <?php // genTreeLink('RTop','rep','Other'); ?>
     </ul>
   </li>
@@ -1395,6 +1409,7 @@ if (!empty($reg)) {
       <?php genMiscLink('RTop','adm','0',xl('BatchCom'),'batchcom/batchcom.php'); ?>
       <?php genTreeLink('RTop','pwd',xl('Password')); ?>
       <?php genMiscLink('RTop','prf','0',xl('Preferences'),'super/edit_globals.php?mode=user'); ?>
+      <?php if(acl_check('patients','docs')) genMiscLink('RTop','adm','0',xl('New Documents'),'../controller.php?document&list&patient_id=00'); ?>
     </ul>
   </li>
 
index c714220..335b5b1 100644 (file)
@@ -5,14 +5,17 @@
  * modify it under the terms of the GNU General Public License
  * as published by the Free Software Foundation; either version 2
  * of the License, or (at your option) any later version.
+ *
+ * 2013/02/08 Minor tweaks by EMR Direct to allow integration with Direct messaging
  */
+
 //SANITIZE ALL ESCAPES
 $sanitize_all_escapes=true;
 
 //STOP FAKE REGISTER GLOBALS
 $fake_register_globals=false;
 
-require_once('../../globals.php');
+require_once("../../globals.php");
 require_once("$srcdir/pnotes.inc");
 require_once("$srcdir/patient.inc");
 require_once("$srcdir/acl.inc");
@@ -141,16 +144,20 @@ switch($task) {
           }
         }
     } break;
+    case "savePatient":
     case "save" : {
         // Update alert.
         $noteid = $_POST['noteid'];
         $form_message_status = $_POST['form_message_status'];
-        updatePnoteMessageStatus($noteid,$form_message_status);
+        $reply_to = $_POST['reply_to'];
+        if ($task=="save")
+            updatePnoteMessageStatus($noteid,$form_message_status);
+        else
+            updatePnotePatient($noteid,$reply_to);
         $task = "edit";
         $note = $_POST['note'];
         $title = $_POST['form_note_type'];
         $assigned_to = $_POST['assigned_to'];
-        $reply_to = $_POST['reply_to'];
     }
     case "edit" : {
         if ($noteid == "") {
@@ -232,7 +239,7 @@ $ures = sqlStatement("SELECT username, fname, lname FROM users " .
 </tr>
 <tr>
   <td class='text' align='center'>
-   <?php if ($task != "addnew") { ?>
+   <?php if ($task != "addnew" && $result['pid']!=0) { ?>
      <a class="patLink" onclick="goPid('<?php echo attr($result['pid']);?>')"><?php echo htmlspecialchars( xl('Patient'), ENT_NOQUOTES); ?>:</a>
    <?php } else { ?>
      <b class='<?php echo ($task=="addnew"?"required":"") ?>'><?php echo htmlspecialchars( xl('Patient'), ENT_NOQUOTES); ?>:</b>
@@ -246,7 +253,11 @@ $ures = sqlStatement("SELECT username, fname, lname FROM users " .
    if ($patientname == '') {
        $patientname = xl('Click to select');
    } ?>
-   <input type='text' size='10' name='form_patient' style='width:150px;<?php echo ($task=="addnew"?"cursor:pointer;cursor:hand;":"") ?>' value='<?php echo htmlspecialchars($patientname, ENT_QUOTES); ?>' <?php echo ($task=="addnew"?"onclick='sel_patient()' readonly":"disabled") ?> title='<?php echo ($task=="addnew"?(htmlspecialchars( xl('Click to select patient'), ENT_QUOTES)):"") ?>'  />
+   <input type='text' size='10' name='form_patient' style='width:150px;<?php 
+      echo ($task=="addnew"?"cursor:pointer;cursor:hand;":"") ?>' value='<?php 
+      echo htmlspecialchars($patientname, ENT_QUOTES); ?>' <?php 
+      echo (($task=="addnew" || $result['pid']==0) ? "onclick='sel_patient()' readonly":"disabled") ?> title='<?php 
+      echo ($task=="addnew"?(htmlspecialchars( xl('Click to select patient'), ENT_QUOTES)):"") ?>'  />
    <input type='hidden' name='reply_to' id='reply_to' value='<?php echo htmlspecialchars( $reply_to, ENT_QUOTES) ?>' />
    &nbsp; &nbsp;
    <b><?php echo htmlspecialchars( xl('Status'), ENT_NOQUOTES); ?>:</b>
@@ -302,7 +313,7 @@ $(document).ready(function(){
 
     var NewNote = function () {
         top.restoreSession();
-      if (document.forms[0].reply_to.value.length == 0) {
+      if (document.forms[0].reply_to.value.length == 0 || document.forms[0].reply_to.value == '0') {
        alert('<?php echo htmlspecialchars( xl('Please choose a patient'), ENT_QUOTES); ?>');
       }
       else if (document.forms[0].assigned_to.value.length == 0) {
@@ -338,6 +349,13 @@ $(document).ready(function(){
   var f = document.forms[0];
   f.form_patient.value = lname + ', ' + fname;
   f.reply_to.value = pid;
+<?php if ($noteid) { ?>
+  //used when direct messaging service inserts a pnote with indeterminate patient
+  //to allow the user to assign the message to a patient.
+  top.restoreSession();
+  $("#task").val("savePatient");
+  $("#new_note").submit();
+<?php } ?>
  }
 
  // This invokes the find-patient popup.
@@ -439,9 +457,13 @@ else {
                 $name .= ", " . $myrow['users_fname'];
             }
             $patient = $myrow['pid'];
-            $patient = $myrow['patient_data_lname'];
-            if ($myrow['patient_data_fname']) {
-                $patient .= ", " . $myrow['patient_data_fname'];
+            if ($patient>0) {
+                $patient = $myrow['patient_data_lname'];
+                if ($myrow['patient_data_fname']) {
+                    $patient .= ", " . $myrow['patient_data_fname'];
+                }
+            } else {
+                $patient = "* Patient must be set manually *";
             }
             $count++;
             echo "
diff --git a/interface/reports/background_services.php b/interface/reports/background_services.php
new file mode 100644 (file)
index 0000000..dc5d5bc
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+/**
+ * Report to view the background services.
+ *
+ * Copyright (C) 2013 Brady Miller <brady@sparmy.com>
+ *
+ * LICENSE: This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
+ *
+ * @package OpenEMR
+ * @author  Brady Miller <brady@sparmy.com>
+ * @link    http://www.open-emr.org
+ */
+
+//SANITIZE ALL ESCAPES
+$sanitize_all_escapes=true;
+//
+
+//STOP FAKE REGISTER GLOBALS
+$fake_register_globals=false;
+//
+
+require_once("../globals.php");
+?>
+
+<html>
+
+<head>
+<?php html_header_show();?>
+
+<link rel="stylesheet" href="<?php echo $css_header;?>" type="text/css">
+
+<title><?php echo xlt('Background Services'); ?></title>
+
+<script type="text/javascript" src="../../library/js/jquery-1.7.2.min.js"></script>
+
+<style type="text/css">
+
+/* specifically include & exclude from printing */
+@media print {
+    #report_parameters {
+        visibility: hidden;
+        display: none;
+    }
+    #report_parameters_daterange {
+        visibility: visible;
+        display: inline;
+    }
+    #report_results table {
+       margin-top: 0px;
+    }
+}
+
+/* specifically exclude some from the screen */
+@media screen {
+    #report_parameters_daterange {
+        visibility: hidden;
+        display: none;
+    }
+}
+
+</style>
+</head>
+
+<body class="body_top">
+
+<span class='title'><?php echo xlt('Background Services'); ?></span>
+
+<form method='post' name='theform' id='theform' action='background_services.php' onsubmit='return top.restoreSession()'>
+
+<div id="report_parameters">
+<table>
+ <tr>
+  <td width='470px'>
+       <div style='float:left'>
+
+       <table class='text'>
+             <div style='margin-left:15px'>
+               <a id='refresh_button' href='#' class='css_button' onclick='top.restoreSession(); $("#theform").submit()'>
+               <span>
+               <?php echo xlt('Refresh'); ?>
+               </span>
+               </a>
+             </div>
+        </table>
+  </td>
+ </tr>
+</table>
+</div>  <!-- end of search parameters -->
+
+<br>
+
+
+
+<div id="report_results">
+<table>
+
+ <thead>
+  <th align='center'>
+   <?php echo xlt('Service Name'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Active'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Automatic'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Interval (minutes)'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Currently Running'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Last Run Started At'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Next Scheduled Run'); ?>
+  </th>
+
+  <th align='center'>
+   &nbsp;
+  </th>
+
+ </thead>
+ <tbody>  <!-- added for better print-ability -->
+<?php
+
+ $res = sqlStatement("SELECT *, (`next_run` - INTERVAL `execute_interval` MINUTE) as `last_run_start`" .
+       " FROM `background_services` ORDER BY `sort_order`");
+ while ($row = sqlFetchArray($res)) {
+?>
+ <tr>
+      <td align='center'><?php echo xlt($row['title']); ?></td>
+
+      <td align='center'><?php echo ($row['active']) ? xlt("Yes") : xlt("No"); ?></td>
+
+      <?php if ($row['active']) { ?>
+          <td align='center'><?php echo ($row['execute_interval'] > 0) ? xlt("Yes") : xlt("No"); ?></td>
+      <?php } else { ?>
+          <td align='center'><?php echo xlt('Not Applicable'); ?></td>
+      <?php } ?>
+
+      <?php if ($row['active'] && ($row['execute_interval'] > 0)) { ?>
+          <td align='center'><?php echo text($row['execute_interval']); ?></td>
+      <?php } else { ?>
+          <td align='center'><?php echo xlt('Not Applicable'); ?></td>
+      <?php } ?>
+
+          <td align='center'><?php echo ($row['running']>0) ? xlt("Yes") : xlt("No"); ?></td>
+
+      <?php if ( $row['running'] > -1) { ?>
+          <td align='center'><?php echo text($row['last_run_start']); ?></td>
+      <?php } else { ?>
+          <td align='center'><?php echo xlt('Never'); ?></td>
+      <?php } ?>
+
+      <?php if ( $row['active'] && ($row['execute_interval'] > 0) ) { ?>
+          <td align='center'><?php echo text($row['next_run']); ?></td>
+      <?php } else { ?>
+          <td align='center'><?php echo xlt('Not Applicable'); ?></td>
+      <?php } ?>
+
+      <?php if ($row['name'] == "phimail") { ?>
+         <td align='center'><a href='direct_message_log.php' onclick='top.restoreSession()'><?php echo xlt("View Log"); ?></a></td>
+      <?php } else { ?>
+         <td align='center'>&nbsp;</td>
+      <?php } ?>
+
+ </tr>
+<?php
+ } // $row = sqlFetchArray($res) while
+?>
+</tbody>
+</table>
+</div>  <!-- end of search results -->
+
+</form>
+
+</body>
+</html>
+
diff --git a/interface/reports/direct_message_log.php b/interface/reports/direct_message_log.php
new file mode 100644 (file)
index 0000000..c4df0f1
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+/**
+ * Report to view the Direct Message log.
+ *
+ * Copyright (C) 2013 Brady Miller <brady@sparmy.com>
+ *
+ * LICENSE: This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
+ *
+ * @package OpenEMR
+ * @author  Brady Miller <brady@sparmy.com>
+ * @link    http://www.open-emr.org
+ */
+
+//SANITIZE ALL ESCAPES
+$sanitize_all_escapes=true;
+//
+
+//STOP FAKE REGISTER GLOBALS
+$fake_register_globals=false;
+//
+
+require_once("../globals.php");
+?>
+
+<html>
+
+<head>
+<?php html_header_show();?>
+
+<link rel="stylesheet" href="<?php echo $css_header;?>" type="text/css">
+
+<title><?php echo xlt('Direct Message Log'); ?></title>
+
+<script type="text/javascript" src="../../library/js/jquery-1.7.2.min.js"></script>
+
+<style type="text/css">
+
+/* specifically include & exclude from printing */
+@media print {
+    #report_parameters {
+        visibility: hidden;
+        display: none;
+    }
+    #report_parameters_daterange {
+        visibility: visible;
+        display: inline;
+    }
+    #report_results table {
+       margin-top: 0px;
+    }
+}
+
+/* specifically exclude some from the screen */
+@media screen {
+    #report_parameters_daterange {
+        visibility: hidden;
+        display: none;
+    }
+}
+
+</style>
+</head>
+
+<body class="body_top">
+
+<span class='title'><?php echo xlt('Direct Message Log'); ?></span>
+
+<form method='post' name='theform' id='theform' action='direct_message_log.php' onsubmit='return top.restoreSession()'>
+
+<div id="report_parameters">
+<table>
+ <tr>
+  <td width='470px'>
+       <div style='float:left'>
+
+       <table class='text'>
+             <div style='margin-left:15px'>
+               <a id='refresh_button' href='#' class='css_button' onclick='top.restoreSession(); $("#theform").submit()'>
+               <span>
+               <?php echo xlt('Refresh'); ?>
+               </span>
+               </a>
+             </div>
+        </table>
+  </td>
+ </tr>
+</table>
+</div>  <!-- end of search parameters -->
+
+<br>
+
+
+
+<div id="report_results">
+<table>
+
+ <thead>
+
+  <th align='center'>
+   <?php echo xlt('ID'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Type'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Date Created'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Sender'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Recipient'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Status'); ?>
+  </th>
+
+  <th align='center'>
+   <?php echo xlt('Date of Status Change'); ?>
+  </th>
+
+ </thead>
+ <tbody>  <!-- added for better print-ability -->
+<?php
+
+ $res = sqlStatement("SELECT * FROM `direct_message_log` ORDER BY `create_ts` DESC");
+ while ($row = sqlFetchArray($res)) {
+?>
+ <tr>
+      <td align='center'><?php echo text($row['id']); ?></td>
+
+      <?php if ($row['msg_type'] == "R") { ?>
+          <td align='center'><?php echo xlt("Received") ?></td>
+      <?php } else if ($row['msg_type'] == "S") { ?>
+          <td align='center'><?php echo xlt("Sent") ?></td>
+      <?php } else {?>
+          <td align='center'>&nbsp;</td>
+      <?php } ?>
+
+      <td align='center'><?php echo text($row['create_ts']); ?></td>
+      <td align='center'><?php echo text($row['sender']); ?></td>
+      <td align='center'><?php echo text($row['recipient']); ?></td>
+
+      <?php if ($row['status'] == "Q") { ?>
+          <td align='center'><?php echo xlt("Queued") ?></td>
+      <?php } else if ($row['status'] == "D") { ?>
+          <td align='center'><?php echo xlt("Dispatched") ?></td>
+      <?php } else if ($row['status'] == "R") { ?>
+          <td align='center'><?php echo xlt("Received") ?></td>
+      <?php } else if ($row['status'] == "F") { ?>
+          <td align='center'><?php echo xlt("Failed") ?></td>
+      <?php } else {?>
+          <td align='center'>&nbsp;</td>
+      <?php } ?>
+
+      <td align='center'><?php echo text($row['status_ts']); ?></td>
+
+ </tr>
+<?php
+ } // $row = sqlFetchArray($res) while
+?>
+</tbody>
+</table>
+</div>  <!-- end of search results -->
+
+</form>
+
+</body>
+</html>
+
index 62aa5f2..1957913 100644 (file)
@@ -47,6 +47,44 @@ function checkCreateCDB(){
   }
   return true;
 }
+
+/**
+ * Update background_services table for a specific service following globals save.
+ * @author EMR Direct
+ */
+function updateBackgroundService($name,$active,$interval) {
+   //order important here: next_run change dependent on _old_ value of execute_interval so it comes first
+   $sql = 'UPDATE background_services SET active=?, '
+       . 'next_run = next_run + INTERVAL (? - execute_interval) MINUTE, execute_interval=? WHERE name=?';
+   return sqlStatement($sql,array($active,$interval,$interval,$name));
+}
+
+/**
+ * Make any necessary changes to background_services table when globals are saved.
+ * To prevent an unexpected service call during startup or shutdown, follow these rules:
+ * 1. Any "startup" operations should occur _before_ the updateBackgroundService() call.
+ * 2. Any "shutdown" operations should occur _after_ the updateBackgroundService() call. If these operations
+ * would cause errors in a running service call, it would be best to make the shutdown function itself
+ * a background service that is activated here, does nothing if active=1 or running=1 for the
+ * parent service, then deactivates itself by setting active=0 when it is done shutting the parent service
+ * down. This will prevent nonresponsiveness to the user by waiting for a service to finish.
+ * 3. If any "previous" values for globals are required for startup/shutdown logic, they need to be
+ * copied to a temp variable before the while($globalsrow...) loop.
+ * @author EMR Direct
+ */
+function checkBackgroundServices(){
+  //load up any necessary globals
+  $bgservices = sqlStatement("SELECT gl_name, gl_index, gl_value FROM globals WHERE gl_name IN
+  ('phimail_enable','phimail_interval')");
+    while($globalsrow = sqlFetchArray($bgservices)){
+      $GLOBALS[$globalsrow['gl_name']] = $globalsrow['gl_value'];
+    }
+
+   //Set up phimail service
+   $phimail_active = $GLOBALS['phimail_enable'] ? '1' : '0';
+   $phimail_interval = max(0,(int)$GLOBALS['phimail_interval']);
+   updateBackgroundService('phimail',$phimail_active,$phimail_interval);
+}
 ?>
 
 <html>
@@ -132,6 +170,7 @@ if ($_POST['form_save'] && $_GET['mode'] != "user") {
     }
   }
   checkCreateCDB();
+  checkBackgroundServices();
   echo "<script type='text/javascript'>";
   echo "parent.left_nav.location.reload();";
   echo "parent.Title.location.reload();";
diff --git a/library/ajax/execute_background_services.php b/library/ajax/execute_background_services.php
new file mode 100644 (file)
index 0000000..fa37298
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+/**
+ * Manage background operations that should be executed at intervals.
+ *
+ * This script may be executed by a suitable Ajax request, by a cron job, or both.
+ *
+ * When called from cron, optinal args are [site] [service] [force]
+ * @param site to specify a specific site, 'default' used if omitted
+ * @param service to specify a specific service, 'all' used if omitted
+ * @param force '1' to ignore specified wait interval, '0' to honor wait interval
+ *
+ * The same parameters can be accessed via Ajax using the $_POST variables
+ * 'site', 'background_service', and 'background_force', respectively.
+ *
+ * For both calling methods, this script guarantees that each active
+ * background service function: (1) will not be called again before it has completed,
+ * and (2) will not be called any more frequently than at the specified interval
+ * (unless the force execution flag is used).  A service function that is already running 
+ * will not be called a second time even if the force execution flag is used.
+ *
+ * Notes for the default background behavior:
+ * 1. If the Ajax method is used, services will only be checked while
+ * Ajax requests are being received, which is currently only when users are
+ * logged in. 
+ * 2. All services are checked and called sequentially in the order specified
+ * by the sort_order field in the background_services table. Service calls that are "slow" 
+ * should be given a higher sort_order value.
+ * 3. The actual interval between two calls to a given background service may be
+ * as long as the time to complete that service plus the interval between
+ * n+1 calls to this script where n is the number of other services preceding it
+ * in the array, even if the specified minimum interval is shorter, so plan
+ * accordingly. Example: with a 5 min cron interval, the 4th service on the list
+ * may not be started again for up to 20 minutes after it has completed if 
+ * services 1, 2, and 3 take more than 15, 10, and 5 minutes to complete,
+ * respectively.
+ *
+ * Copyright (C) 2013 EMR Direct <http://www.emrdirect.com/>
+ *
+ * LICENSE: This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
+ *
+ * @package OpenEMR
+ * @author  EMR Direct <http://www.emrdirect.com/>
+ * @link    http://www.open-emr.org
+ */
+
+//SANITIZE ALL ESCAPES
+$sanitize_all_escapes=true;
+
+//STOP FAKE REGISTER GLOBALS
+$fake_register_globals=false;
+
+//ajax param should be set by calling ajax scripts
+$isAjaxCall = isset($_POST['ajax']);
+
+//if false, we may assume this is a cron job and set up accordingly
+if (!$isAjaxCall) {
+   $ignoreAuth = 1; 
+   //process optional arguments when called from cron
+   $_GET['site'] = (isset($argv[1])) ? $argv[1] : 'default';
+   if (isset($argv[2]) && $argv[2]!='all') $_GET['background_service'] = $argv[2];
+   if (isset($argv[3]) && $argv[3]=='1') $_GET['background_force'] = 1;
+}
+
+//an additional require file can be specified for each service in the background_services table
+require_once(dirname(__FILE__) . "/../../interface/globals.php");
+require_once(dirname(__FILE__) . "/../sql.inc");
+
+//Remove time limit so script doesn't time out
+set_time_limit(0);
+
+//Release session lock to prevent freezing of other scripts
+session_write_close();
+
+//Safety in case one of the background functions tries to output data
+ignore_user_abort(1);
+
+/**
+ * Execute background services
+ * This function reads a list of available services from the background_services table
+ * For each service that is not already running and is due for execution, the associated
+ * background function is run.
+ * 
+ * Note: Each service must do its own logging, as appropriate, and should disable itself
+ * to prevent continued service calls if an error condition occurs which requires 
+ * administrator intervention. Any service function return values and output are ignored.
+ */
+
+function execute_background_service_calls() {
+  /**
+   * Note: The global $service_name below is set to the name of the service currently being 
+   * processed before the actual service function call, and is unset after normal
+   * completion of the loop. If the script exits abnormally, the shutdown_function
+   * uses the value of $service_name to do any required clean up.
+   */
+  global $service_name;
+
+  $single_service = (isset($_GET['background_service']) ? $_GET['background_service'] : 
+       (isset($_POST['background_service']) ? $_POST['background_service'] : ''));
+  $force = ($_GET['background_force'] || $_POST['background_force']);
+
+  $sql = 'SELECT * FROM background_services WHERE ' . ($force ? '1' : 'execute_interval > 0');
+  if ($single_service!="")
+    $services = sqlStatementNoLog($sql.' AND name=?',array($single_service));
+  else
+    $services = sqlStatementNoLog($sql.' ORDER BY sort_order');
+
+  while($service = sqlFetchArray($services)){
+    $service_name = $service['name'];
+    if(!$service['active'] || $service['running'] == 1) continue;
+    $interval=(int)$service['execute_interval'];
+
+    //leverage locking built-in to UPDATE to prevent race conditions
+    //will need to assess performance in high concurrency setting at some point
+    $sql='UPDATE background_services SET running = 1, next_run = NOW()+ INTERVAL ?'
+       . ' MINUTE WHERE running < 1 ' . ($force ? '' : 'AND NOW() > next_run ') . 'AND name = ?';
+    if(sqlStatementNoLog($sql,array($interval,$service_name))===FALSE) continue;
+    $acquiredLock =  mysql_affected_rows($GLOBALS['dbh']);
+    if($acquiredLock<1) continue; //service is already running or not due yet
+
+    if ($service['require_once'])
+       require_once($GLOBALS['fileroot'] . $service['require_once']);
+    if (!function_exists($service['function'])) continue;
+
+    //use try/catch in case service functions throw an unexpected Exception
+    try {
+       $service['function']();
+    } catch (Exception $e) {
+       //do nothing
+    }
+
+    $sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
+    $res = sqlStatementNoLog($sql, array($service_name));
+  }
+
+}
+
+/**
+ * Catch unexpected failures.
+ * 
+ * if the global $service_name is still set, then a die() or exit() occurred during the execution
+ * of that service's function call, and we did not complete the foreach loop properly,
+ * so we need to reset the is_running flag for that service before quitting
+ */
+
+function background_shutdown() {
+  global $service_name;
+  if (isset($service_name)) {
+    
+    $sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
+    $res = sqlStatementNoLog($sql, array($service_name));
+
+  }
+}
+
+register_shutdown_function(background_shutdown);
+execute_background_service_calls();
+unset($service_name);
+
+?>
index 983e8ae..27cddac 100644 (file)
@@ -114,7 +114,8 @@ class Controller extends Smarty {
                foreach ($args as $arg) {
                        $arg = preg_replace("/[^A-Za-z0-9_]/","",$arg);
                        //this is a workaround because call user func does funny things with passing args if they have no assigned value
-                       if (empty($qarray[$arg])) {
+                       //2013-02-10 EMR Direct: workaround modified since "0" is also considered empty;
+                       if (empty($qarray[$arg]) && $qarray[$arg]!="0") {
                                //if argument is empty pass null as value and arg as assoc array key
                                $args_array[$arg] = null;
                        }
index 89e176c..cbb0716 100644 (file)
@@ -43,6 +43,7 @@ class Installer
     $this->ippf_sql = dirname(__FILE__) . "/../../sql/ippf_layout.sql";
     $this->icd9 = dirname(__FILE__) . "/../../sql/icd9.sql";
     $this->cvx = dirname(__FILE__) . "/../../sql/cvx_codes.sql";
+    $this->additional_users = dirname(__FILE__) . "/../../sql/official_additional_users.sql";
 
     // Record name of php-gacl installation files
     $this->gaclSetupScript1 = dirname(__FILE__) . "/../../gacl/setup.php";
@@ -158,37 +159,44 @@ class Installer
   public function load_dumpfiles() {
     $sql_results = ''; // information string which is returned
     foreach ($this->dumpfiles as $filename => $title) {
-        $sql_results .= "Creating $title tables...\n";
-        $fd = fopen($filename, 'r');
-        if ($fd == FALSE) {
-          $this->error_message = "ERROR.  Could not open dumpfile '$filename'.\n";
-          return FALSE;
-        }
-        $query = "";
-        $line = "";
-        while (!feof ($fd)){
-                $line = fgets($fd,1024);
-                $line = rtrim($line);
-                if (substr($line,0,2) == "--") // Kill comments
-                        continue;
-                if (substr($line,0,1) == "#") // Kill comments
-                        continue;
-                if ($line == "")
-                        continue;
-                $query = $query.$line;          // Check for full query
-                $chr = substr($query,strlen($query)-1,1);
-                if ($chr == ";") { // valid query, execute
-                        $query = rtrim($query,";");
-                        $this->execute_sql( $query );
-                        $query = "";
-                }
-        }
-        $sql_results .= "OK<br>\n";
-        fclose($fd);
+        $sql_results .= $this->load_file($filename,$title);
+        if ($sql_results == FALSE) return FALSE;
     }
     return $sql_results;
   }
 
+  public function load_file($filename,$title) {
+    $sql_results = ''; // information string which is returned
+    $sql_results .= "Creating $title tables...\n";
+    $fd = fopen($filename, 'r');
+    if ($fd == FALSE) {
+      $this->error_message = "ERROR.  Could not open dumpfile '$filename'.\n";
+      return FALSE;
+    }
+    $query = "";
+    $line = "";
+    while (!feof ($fd)){
+            $line = fgets($fd,1024);
+            $line = rtrim($line);
+            if (substr($line,0,2) == "--") // Kill comments
+                    continue;
+            if (substr($line,0,1) == "#") // Kill comments
+                    continue;
+            if ($line == "")
+                    continue;
+            $query = $query.$line;          // Check for full query
+            $chr = substr($query,strlen($query)-1,1);
+            if ($chr == ";") { // valid query, execute
+                    $query = rtrim($query,";");
+                    $this->execute_sql( $query );
+                    $query = "";
+            }
+    }
+    $sql_results .= "OK<br>\n";
+    fclose($fd);
+    return $sql_results;
+  }
+
   public function add_version_info() {
     include dirname(__FILE__) . "/../../version.php";
     if ($this->execute_sql("UPDATE version SET v_major = '$v_major', v_minor = '$v_minor', v_patch = '$v_patch', v_realpatch = '$v_realpatch', v_tag = '$v_tag', v_database = '$v_database', v_acl = '$v_acl'") == FALSE) {
@@ -211,6 +219,9 @@ class Installer
         "<p>".mysql_error()." (#".mysql_errno().")\n";
       return FALSE;
     }
+    // Add the official openemr users (services)
+    if ($this->load_file($this->additional_users,"Additional Official Users") == FALSE) return FALSE;
+
     return TRUE;
   }
 
diff --git a/library/direct_message_check.inc b/library/direct_message_check.inc
new file mode 100644 (file)
index 0000000..4f5e3a2
--- /dev/null
@@ -0,0 +1,425 @@
+<?php
+/**
+ * Background receive function for phiMail Direct Messaging service.
+ *
+ * This script is called by the background service manager
+ * at /library/ajax/execute_background_services.php
+ *
+ * Copyright (C) 2013 EMR Direct <http://www.emrdirect.com/>
+ *
+ * LICENSE: This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
+ *
+ * @package OpenEMR
+ * @author  EMR Direct <http://www.emrdirect.com/>
+ * @link    http://www.open-emr.org
+ */
+
+require_once(dirname(__FILE__) . "/log.inc");
+require_once(dirname(__FILE__) . "/sql.inc");
+require_once(dirname(__FILE__) . "/pnotes.inc");
+require_once(dirname(__FILE__) . "/../controllers/C_Document.class.php");
+
+/**
+ * Connect to a phiMail Direct Messaging server and check for any incoming status
+ * messages related to previously transmitted messages or any new messages received.
+ */
+
+function phimail_check() {
+   if ($GLOBALS['phimail_enable']==false) return; //for safety
+
+   $phimail_server=@parse_url($GLOBALS['phimail_server_address']);
+   $phimail_username=$GLOBALS['phimail_username'];
+   $phimail_password=$GLOBALS['phimail_password'];
+   switch ($phimail_server['scheme']) {
+       case "http": $server="tcp://".$phimail_server['host'];
+               break;
+       case "https": $server="ssl://".$phimail_server['host'];
+               break;
+       default: return;
+   }
+   $fp=@fsockopen($server,$phimail_server['port']);
+   if ($fp===false) {
+      phimail_logit(0,"could not connect to server");
+      return;
+   }
+
+   $ret = phimail_write_expect_OK($fp,"AUTH $phimail_username $phimail_password\n");
+   if($ret!==TRUE) return; //authentication error
+
+   while (1) {
+      phimail_write($fp,"CHECK\n");
+      $ret=fgets($fp,512);
+
+      if($ret=="NONE\n") { //nothing to process
+          phimail_close($fp);
+         phimail_logit(1,"message check completed");
+          return;
+      }
+      else if(substr($ret,0,6)=="STATUS") {
+         //Format STATUS message-id status-code [additional-information]
+          $val=explode(" ",trim($ret),4);
+         $sql='SELECT * from direct_message_log WHERE msg_id = ?';
+         $res = sqlStatementNoLog($sql,array($val[1]));
+         if ($res===FALSE) { //database problem
+            phimail_close($fp);
+            phimail_logit(0,"database problem");
+            return;
+         }
+         if (($msg=sqlFetchArray($res))===FALSE) {
+            //no match, so log it and move on (should never happen)
+            phimail_logit(0,"NO MATCH: ".$ret);
+            $ret = phimail_write_expect_OK($fp,"OK\n"); 
+            if($ret!==TRUE) return; else continue; 
+         }
+
+          //if we get here, $msg contains the matching outgoing message record
+         if($val[2]=='failed') {
+            $success=0;
+            $status='F';
+         } else if ($val[2]=='dispatched') {
+            $success=1;
+            $status='D';
+          } else {
+            //unrecognized status, log it and move on (should never happen)
+            $ret = "UNKNOWN STATUS: ".$ret;
+            $success=0;
+            $status='U';
+          }
+
+         phimail_logit($success,$ret,$msg['patient_id']);
+
+         if (!isset($val[3])) $val[3]="";
+         $sql = "UPDATE direct_message_log SET status=?, status_ts=NOW(), status_info=? WHERE msg_type='S' AND msg_id=?";
+         $res = sqlStatementNoLog($sql,array($status,$val[3],$val[1]));
+         if ($res===FALSE) { //database problem
+            phimail_close($fp);
+            phimail_logit(0,"database problem updating: ".$val[1]);
+            return;
+         }
+
+         if(!$success) {
+            phimail_notify( xl('Direct Messaging Send Failure.'), $ret);
+          }
+
+         //done with this status message
+         $ret = phimail_write_expect_OK($fp,"OK\n");
+         if($ret!==TRUE) {
+             phimail_close($fp);
+            return;
+          } 
+
+      }
+
+      else if(substr($ret,0,4)=="MAIL") {
+
+         $val = explode(" ",trim($ret),5); // MAIL recipient sender #attachments msg-id
+         $recipient=$val[1];
+        $sender=$val[2];
+        $att=(int)$val[3];
+        $msg_id=$val[4];
+
+         //request main message
+        $ret2 = phimail_write_expect_OK($fp,"SHOW 0\n");
+         if($ret2!==TRUE) {
+            phimail_close($fp);
+            return;
+         }
+
+        //get message headers
+         $hdrs="";
+         while (($next_hdr = fgets($fp,1024)) != "\n") 
+            $hdrs .= $next_hdr;
+
+        $mime_type=fgets($fp,512);
+        $mime_info=explode(";",$mime_type);
+         $mime_type_main=strtolower($mime_info[0]);
+
+        //get main message body
+         $body_len=fgets($fp,256);
+         $body=phimail_read_blob($fp,$body_len);
+        if ($body===FALSE) {
+          phimail_close($fp);
+          return;
+         }
+
+         $att2=fgets($fp,256);
+        if($att2!=$att) { //safety for mismatch on attachments
+           phimail_close($fp);
+            return;
+         }
+
+        //get attachment info
+        if($att>0) {
+           for ($attnum=0;$attnum<$att;$attnum++) {
+               if(  ($attinfo[$attnum]['name']=fgets($fp,1024)) === FALSE
+                 || ($attinfo[$attnum]['mime']=fgets($fp,1024)) === FALSE
+                 || ($attinfo[$attnum]['desc']=fgets($fp,1024)) === FALSE) {
+                    phimail_close($fp);
+                    return;
+               }
+            }
+         }
+
+         //main part gets stored as document if not plain text content
+         //(if plain text it will be the body of the final pnote)
+         $doc_id=0;
+          $att_detail="";
+         if ($mime_type_main != "text/plain") {
+            $name = uniqid("dm-message-") . phimail_extension($mime_type_main);
+             $doc_id = phimail_store($name,$mime_type_main,$body);
+             if (!$doc_id) { 
+               phimail_close($fp);
+               return;
+            }
+            $idnum=$doc_id['doc_id']; 
+            $url=$doc_id['url']; 
+            $url=substr($url,strrpos($url,"/")+1);
+             $att_detail = "\n" . xl ("Document") . " $idnum (\"$url\"; $mime_type_main; " . 
+               filesize($body) . " bytes) Main message body";
+         }
+
+         //download and store attachments
+          for($attnum=0;$attnum<$att;$attnum++) {
+            $ret2 = phimail_write_expect_OK($fp,"SHOW " . ($attnum+1) . "\n");
+            if ($ret2!==TRUE) {
+               phimail_close($fp);
+               return;
+            }
+
+            //we can ignore next two lines (repeat of name and mime-type)
+            if ( ($a1=fgets($fp,512))===FALSE || ($a2=fgets($fp,512))===FALSE ) {
+               phimail_close($fp);
+               return;
+            }
+
+            $att_len = fgets($fp,256); //length of file
+            $attdata = phimail_read_blob($fp,$att_len);
+            if ($attdata===FALSE) {
+               phimail_close($fp);
+               return;
+            }
+             $attinfo[$attnum]['file']=$attdata;
+
+            $req_name = trim($attinfo[$attnum]['name']);
+            $req_name = (empty($req_name) ? $attdata : "dm-") . $req_name;
+            $attinfo[$attnum]['mime'] = explode(";",trim($attinfo[$attnum]['mime']));
+            $attmime = strtolower($attinfo[$attnum]['mime'][0]);
+            $att_doc_id = phimail_store($req_name, $attmime, $attdata);
+             if (!$att_doc_id) { 
+               phimail_close($fp);
+               return;
+            }
+            $attinfo[$attnum]['doc_id']=$att_doc_id;
+            $idnum=$att_doc_id['doc_id']; 
+            $url=$att_doc_id['url']; 
+            $url=substr($url,strrpos($url,"/")+1);
+             $att_detail = $att_detail . "\n" . xl ("Document") . " $idnum (\"$url\"; $attmime; " . 
+                filesize($attdata) . " bytes) " . trim($attinfo[$attnum]['desc']);
+        }
+
+        if ($att_detail != "") 
+          $att_detail = "\n\n" . xl("The following documents were attached to this Direct message:") . $att_detail;
+
+        $ret2 = phimail_write_expect_OK($fp,"DONE\n"); //we'll check for failure after logging.
+
+        //logging only after succesful download, storage, and acknowledgement of message
+         $sql = "INSERT INTO direct_message_log (msg_type,msg_id,sender,recipient,status,status_ts,user_id) " .
+            "VALUES ('R', ?, ?, ?, 'R', NOW(), ?)";
+         $res = sqlStatementNoLog($sql,array($msg_id,$sender,$recipient,phimail_service_userID()));
+
+        phimail_logit(1,$ret);
+
+        if(!($notifyUsername = $GLOBALS['phimail_notify'])) $notifyUsername='admin'; //fallback
+        
+        //alert appointed user about new message
+        switch($mime_type_main) {
+
+          case "text/plain":
+            $body_text = @file_get_contents($body); //this was not uploaded as a document
+            unlink($body);
+             $pnote_id = addPnote(0, xl("Direct Message Received.") . "\n$hdrs\n$body_text$att_detail",
+               0, 1, "Unassigned", $notifyUsername, "", "New", "phimail-service");
+            break;
+
+           default:
+            $note = xl("Direct Message Received.") . "\n$hdrs\n"
+               . xl("Message content is not plain text so it has been stored as a document.") . $att_detail;
+            $pnote_id = addPnote(0, $note, 0, 1, "Unassigned", $notifyUsername, "", "New", "phimail-service");
+            break;
+
+         }
+
+        if ($ret2!==TRUE) { 
+           phimail_close();
+           return; 
+         }
+
+      }
+      else { //unrecognized or FAIL response
+          phimail_close($fp);
+          return;
+      } 
+   }
+}
+
+/**
+ * Helper functions
+ */
+function phimail_write($fp,$text) {
+   fwrite($fp,$text);
+   fflush($fp);
+}
+
+function phimail_write_expect_OK($fp,$text) {
+   phimail_write($fp,$text);
+   $ret = fgets($fp,256);
+   if($ret!="OK\n") { //unexpected error
+      phimail_close($fp);
+      return $ret;
+   }
+   return TRUE;
+}
+
+function phimail_close($fp) {
+   fwrite($fp,"BYE\n");
+   fflush($fp);
+   fclose($fp);
+}
+
+function phimail_logit($success,$text,$pid=0,$event="direct-message-check") {
+   newEvent($event,"phimail-service",0,$success,$text,$pid);
+}
+
+/**
+ * Read a blob of data into a local temporary file
+ * @param $len number of bytes to read
+ * @return the temp filename, or FALSE if failure
+ */
+function phimail_read_blob($fp,$len) {
+
+   $fpath=dirname(__FILE__) . "/../sites/default/documents/tmp_direct";
+   if(!@file_exists($fpath)) {
+      if(!@mkdir($fpath,0700)) {
+         return FALSE;
+      }
+   }
+   $name = uniqid("direct-");
+   $fn = $fpath . "/" . $name . ".dat";
+   $dup = 1;
+   while(file_exists($fn)) {
+     $fn = $fpath . "/" . $name . "." . $dup++ . ".dat";
+   }
+
+   $ff = @fopen ($fn, "w");
+   if(!$ff) return FALSE;
+
+   $bytes_left=$len;
+   $chunk_size=1024;
+   while (!feof($fp) && $bytes_left>0) {
+      if ($bytes_left < $chunk_size ) $chunk_size = $bytes_left;
+      $chunk = fread($fp,$chunk_size);
+      if($chunk===FALSE || @fwrite($ff,$chunk)===FALSE) {
+        @fclose($ff);
+        @unlink($fn);
+         return FALSE;
+      }
+      $bytes_left -= strlen($chunk);
+   }
+   @fclose($ff);
+   return($fn);
+}
+
+/**
+ * Return a suitable filename extension based on MIME-type
+ * (very limited, default is .dat)
+ */
+function phimail_extension($mime) {
+  $m=explode("/",$mime);
+  switch($mime) {
+       case 'text/plain': 
+               return (".txt");
+       default:
+  }
+  switch($m[1]) {
+       case 'html':
+       case 'xml':
+       case 'pdf':
+               return (".".$m[1]);
+       default:
+               return (".dat");
+  }
+}
+
+function phimail_service_userID($name='phimail-service') {
+   $sql = "SELECT id FROM users WHERE username=?";
+   if (($r = sqlStatementNoLog($sql,array($name))) === FALSE ||
+       ($u = sqlFetchArray($r)) === FALSE) {
+      $user=1; //default if we don't have a service user
+   } else {
+      $user = $u['id'];
+   }
+   return ($user);
+}
+
+
+/**
+ * Registers an attachment or non-text message file using the existing Document structure
+ * @return Array(doc_id,URL) of the file as stored in documents table, false = failure
+ */
+function phimail_store($name,$mime_type,$fn) {
+
+   //upload using existing controller framework
+   $_FILES['file']['name'][0]=$name;
+   $_FILES['file']['type'][0]=$mime_type;
+   $_FILES['file']['tmp_name'][0]=$fn;
+   $_FILES['file']['error'][0]=0;
+   $_FILES['file']['size'][0]=filesize($fn);
+   $_GET['patient_id']='direct'; //new documents get put into in [fileroot]/sites/[site]/documents/direct/
+   $_POST['destination']='';
+   $_POST['submit']='Upload';
+   $_POST['patient_id']='00'; //workaround because '0' becomes a Null and not a zero in documents.foreign_id
+   $_POST['category_id']='1'; //since we don't know what kind of document; these show up in root Documents folder
+   $_POST['process']='true';
+
+   $user = phimail_service_userID();
+   
+   $cd = new C_Document();
+   $cd->upload_action_process($user); 
+   @unlink($fn);
+   $v = $cd->get_template_vars("file");
+   if (!isset($v) || !$v) return false;
+   return array ("doc_id" => $v[0]->id, "url" => $v[0]->url);
+}
+
+/**
+ * Send an error notification or other alert to the notification address specified in globals.
+ * (notification email function modified from interface/drugs/dispense_drug.php)
+ * @return true if notificaiton successfully sent, false otherwise
+ */
+function phimail_notify($subj,$body) {
+  $recipient = $GLOBALS['practice_return_email_path'];
+  if (empty($recipient)) return false;
+  $mail = new PHPMailer();
+  $mail->SetLanguage("en", $GLOBALS['fileroot'] . "/library/" );
+  $mail->From = $recipient;
+  $mail->FromName = 'phiMail Gateway';
+  $mail->isMail();
+  $mail->Host = "localhost";
+  $mail->Mailer = "mail";
+  $mail->Body = $body;
+  $mail->Subject = $subject;
+  $mail->AddAddress($recipient);
+  return ($mail->Send());
+}
+
+
+?>
index ac76d2e..ebe5c02 100644 (file)
@@ -1544,7 +1544,22 @@ $GLOBALS_METADATA = array(
       'pass',                           // data type
       '',
       xl('Contact EMR Direct to subscribe to the phiMail Direct messaging service')
+    ),
+
+    'phimail_notify' => array(
+      xl('phiMail notification user'),
+      'text',                           // data type
+      'admin',
+      xl('This user will receive notification of new incoming Direct messages')
+    ),
+
+    'phimail_interval' => array(
+      xl('phiMail Message Check Interval (minutes)'),
+      'num',                           // data type
+      '5',
+      xl('Interval between message checks (set to zero for manual checks only)')
     )
+
   ),
   
   'Rx' => array(
index 0de44a5..6fa6fb8 100644 (file)
@@ -6,6 +6,8 @@
  * modify it under the terms of the GNU General Public License
  * as published by the Free Software Foundation; either version 2
  * of the License, or (at your option) any later version.
+ *
+ * 2013-02-08 EMR Direct: changes to allow notes added by background-services with pid=0
  */
 
 /* for sqlQuery(), sqlStatement(), sqlNumRows(), sqlFetchArray(). */
@@ -63,13 +65,14 @@ function getPnotesByUser($activity="1",$show_all="no",$user='',$count=false,$sor
     $usrvar=$user;
   } 
 
-  // run the query
+  // run the query 
+  // 2013-02-08 EMR Direct: minor changes to query so notes with pid=0 don't disappear
   $sql = "SELECT pnotes.id, pnotes.user, pnotes.pid, pnotes.title, pnotes.date, pnotes.message_status,
-          IF(pnotes.user != pnotes.pid,users.fname,patient_data.fname) as users_fname,
-          IF(pnotes.user != pnotes.pid,users.lname,patient_data.lname) as users_lname,
+          IF(pnotes.pid = 0 OR pnotes.user != pnotes.pid,users.fname,patient_data.fname) as users_fname,
+          IF(pnotes.pid = 0 OR pnotes.user != pnotes.pid,users.lname,patient_data.lname) as users_lname,
           patient_data.fname as patient_data_fname, patient_data.lname as patient_data_lname
           FROM ((pnotes LEFT JOIN users ON pnotes.user = users.username)
-          JOIN patient_data ON pnotes.pid = patient_data.pid) WHERE $activity_query
+          LEFT JOIN patient_data ON pnotes.pid = patient_data.pid) WHERE $activity_query
           pnotes.deleted != '1' AND pnotes.assigned_to LIKE ?";
   if (!empty($sortby) || !empty($sortorder)  || !empty($begin) || !empty($listnumber)) {
     $sql .= " order by ".add_escape_custom($sortby)." ".add_escape_custom($sortorder).
@@ -331,25 +334,27 @@ function getPnotesByPid ($pid, $activity = "1", $cols = "*", $limit=10, $start=0
  * @param string $assigned_to
  * @param string $datetime
  * @param string $message_status
+ * @param string $background_user if set then the pnote is created by a background-service rather than a user
  * @return int the ID of the added note.
  */
 function addPnote($pid, $newtext, $authorized = '0', $activity = '1',
   $title= 'Unassigned', $assigned_to = '', $datetime = '',
-  $message_status = 'New')
+  $message_status = 'New', $background_user="")
 {
   if (empty($datetime)) $datetime = date('Y-m-d H:i:s');
 
   // make inactive if set as Done
   if ($message_status == 'Done') $activity = 0;
 
-  $body = date('Y-m-d H:i') . ' (' . $_SESSION['authUser'];
+  $user = ($background_user!="" ? $background_user : $_SESSION['authUser']);
+  $body = date('Y-m-d H:i') . ' (' . $user;
   if ($assigned_to) $body .= " to $assigned_to";
   $body = $body . ') ' . $newtext;
 
   return sqlInsert('INSERT INTO pnotes (date, body, pid, user, groupname, ' .
     'authorized, activity, title, assigned_to, message_status) VALUES ' .
     '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
-    array($datetime, $body, $pid, $_SESSION['authUser'], $_SESSION['authProvider'], $authorized, $activity, $title, $assigned_to, $message_status) );
+    array($datetime, $body, $pid, $user, $_SESSION['authProvider'], $authorized, $activity, $title, $assigned_to, $message_status) );
 }
 
 function addMailboxPnote($pid, $newtext, $authorized = '0', $activity = '1',
@@ -407,6 +412,28 @@ function updatePnoteMessageStatus($id, $message_status)
   }
 }
 
+/**
+ * Set the patient id in an existing message where pid=0
+ * @param $id the id of the existing note
+ * @param $patient_id the patient id to associate with the note
+ * @author EMR Direct <http://www.emrdirect.com/>
+ */
+function updatePnotePatient($id, $patient_id)
+{
+  $row = getPnoteById($id);
+  if (! $row) die("updatePnotePatient() did not find id '$id'");
+  $activity = $assigned_to ? '1' : '0';
+
+  $pid = $row['pid'];
+  if($pid != 0 || (int)$patient_id < 1) die("updatePnotePatient invalid operation");
+
+  $pid = (int) $patient_id;
+  $newtext = "\n" . date('Y-m-d H:i') . " (patient set by " . $_SESSION['authUser'] .")";
+  $body = $row['body'] . $newtext;
+
+  sqlStatement("UPDATE pnotes SET pid = ?, body = ? WHERE id = ?", array($pid, $body, $id) );
+}
+
 function authorizePnote($id, $authorized = "1")
 {
   sqlQuery("UPDATE pnotes SET authorized = ? WHERE id = ?", array ($authorized,$id) );
index ac61248..e08764e 100644 (file)
@@ -281,3 +281,51 @@ INSERT INTO code_types (ct_key, ct_id, ct_seq, ct_mod, ct_just, ct_fee, ct_rel,
 DROP TABLE `temp_table_one`;
 #EndIf
 
+#IfNotTable background_services 
+CREATE TABLE IF NOT EXISTS `background_services` (
+  `name` varchar(31) NOT NULL,
+  `title` varchar(127) NOT NULL COMMENT 'name for reports',
+  `active` tinyint(1) NOT NULL default '0',
+  `running` tinyint(1) NOT NULL default '-1',
+  `next_run` timestamp NOT NULL default CURRENT_TIMESTAMP,
+  `execute_interval` int(11) NOT NULL default '0' COMMENT 'minimum number of minutes between function calls,0=manual mode',
+  `function` varchar(127) NOT NULL COMMENT 'name of background service function',
+  `require_once` varchar(255) default NULL COMMENT 'include file (if necessary)',
+  `sort_order` int(11) NOT NULL default '100' COMMENT 'lower numbers will be run first',
+  PRIMARY KEY  (`name`)
+) ENGINE=MyISAM;
+#EndIf
+
+#IfNotRow background_services name phimail
+INSERT INTO `background_services` (`name`, `title`, `execute_interval`, `function`, `require_once`, `sort_order`) VALUES
+('phimail', 'phiMail Direct Messaging Service', 5, 'phimail_check', '/library/direct_message_check.inc', 100);
+#EndIf
+
+#IfNotRow users username phimail-service
+INSERT INTO `users` (username,password,lname,authorized,active) 
+  VALUES ('phimail-service','NoLogin','phiMail Gateway',0,0);
+#EndIf
+
+#IfNotRow users username portal-user
+INSERT INTO `users` (username,password,lname,authorized,active) 
+  VALUES ('portal-user','NoLogin','Patient Portal User',0,0);
+#EndIf
+
+#IfNotTable direct_message_log
+CREATE TABLE IF NOT EXISTS `direct_message_log` (
+  `id` bigint(20) NOT NULL auto_increment,
+  `msg_type` char(1) NOT NULL COMMENT 'S=sent,R=received',
+  `msg_id` varchar(127) NOT NULL,
+  `sender` varchar(255) NOT NULL,
+  `recipient` varchar(255) NOT NULL,
+  `create_ts` timestamp NOT NULL default CURRENT_TIMESTAMP,
+  `status` char(1) NOT NULL COMMENT 'Q=queued,D=dispatched,R=received,F=failed',
+  `status_info` varchar(511) default NULL,
+  `status_ts` timestamp NULL default NULL,
+  `patient_id` bigint(20) default NULL,
+  `user_id` bigint(20) default NULL,
+  PRIMARY KEY  (`id`),
+  KEY `msg_id` (`msg_id`),
+  KEY `patient_id` (`patient_id`)
+) ENGINE=MyISAM;
+#EndIf
index 26d63c4..3334fb7 100644 (file)
@@ -86,6 +86,34 @@ CREATE TABLE `audit_details` (
   `entry_identification` VARCHAR(255) NOT NULL DEFAULT '1' COMMENT 'Used when multiple entry occurs from the same table.1 means no multiple entry',
   PRIMARY KEY (`id`)
 ) ENGINE=MyISAM AUTO_INCREMENT=1;
+
+--
+-- Table structure for table `background_services`
+--
+
+DROP TABLE IF EXISTS `background_services`;
+CREATE TABLE `background_services` (
+  `name` varchar(31) NOT NULL,
+  `title` varchar(127) NOT NULL COMMENT 'name for reports',
+  `active` tinyint(1) NOT NULL default '0',
+  `running` tinyint(1) NOT NULL default '-1',
+  `next_run` timestamp NOT NULL default CURRENT_TIMESTAMP,
+  `execute_interval` int(11) NOT NULL default '0' COMMENT 'minimum number of minutes between function calls,0=manual mode',
+  `function` varchar(127) NOT NULL COMMENT 'name of background service function',
+  `require_once` varchar(255) default NULL COMMENT 'include file (if necessary)',
+  `sort_order` int(11) NOT NULL default '100' COMMENT 'lower numbers will be run first',
+  PRIMARY KEY  (`name`)
+) ENGINE=MyISAM;
+
+-- 
+-- Dumping data for table `background_services`
+-- 
+
+INSERT INTO `background_services` (`name`, `title`, `execute_interval`, `function`, `require_once`, `sort_order`) VALUES
+('phimail', 'phiMail Direct Messaging Service', 5, 'phimail_check', '/library/direct_message_check.inc', 100);
+
+-- --------------------------------------------------------
+
 -- 
 -- Table structure for table `batchcom`
 -- 
@@ -598,6 +626,30 @@ CREATE TABLE `dated_reminders_link` (
 -- --------------------------------------------------------
 
 -- 
+-- Table structure for table `direct_message_log`
+-- 
+
+DROP TABLE IF EXISTS `direct_message_log`;
+CREATE TABLE `direct_message_log` (
+  `id` bigint(20) NOT NULL auto_increment,
+  `msg_type` char(1) NOT NULL COMMENT 'S=sent,R=received',
+  `msg_id` varchar(127) NOT NULL,
+  `sender` varchar(255) NOT NULL,
+  `recipient` varchar(255) NOT NULL,
+  `create_ts` timestamp NOT NULL default CURRENT_TIMESTAMP,
+  `status` char(1) NOT NULL COMMENT 'Q=queued,D=dispatched,R=received,F=failed',
+  `status_info` varchar(511) default NULL,
+  `status_ts` timestamp NULL default NULL,
+  `patient_id` bigint(20) default NULL,
+  `user_id` bigint(20) default NULL,
+  PRIMARY KEY  (`id`),
+  KEY `msg_id` (`msg_id`),
+  KEY `patient_id` (`patient_id`)
+) ENGINE=MyISAM;
+
+-- --------------------------------------------------------
+
+-- 
 -- Table structure for table `documents`
 -- 
 
@@ -5111,6 +5163,13 @@ CREATE TABLE `users` (
   PRIMARY KEY  (`id`)
 ) ENGINE=MyISAM AUTO_INCREMENT=1 ;
 
+--
+-- Dumping data for table `users`
+--
+-- NOTE THIS IS DONE AFTER INSTALLATION WHERE THE sql/official_additional_users.sql script is called durig setup
+--  (so these inserts can be found in the sql/official_additional_users.sql script)
+
+
 -- --------------------------------------------------------
 
 --
diff --git a/sql/official_additional_users.sql b/sql/official_additional_users.sql
new file mode 100644 (file)
index 0000000..029f489
--- /dev/null
@@ -0,0 +1,3 @@
+INSERT INTO `users` (username,password,lname,authorized,active) VALUES ('phimail-service','NoLogin','phiMail Gateway',0,0);
+INSERT INTO `users` (username,password,lname,authorized,active) VALUES ('portal-user','NoLogin','Patient Portal User',0,0);
+
index 3376e3f..bd75347 100644 (file)
@@ -1,6 +1,14 @@
 <form method=post enctype="multipart/form-data" action="{$FORM_ACTION}" onsubmit="return top.restoreSession()">
 <input type="hidden" name="MAX_FILE_SIZE" value="64000000" />
 
+{if (!($patient_id > 0)) }
+  <div class="text" style="color:red;">
+    {xl t="IMPORTANT: This upload tool is only for uploading documents on patients that are not yet entered into the system. To upload files for patients whom already have been entered into the system, please use the upload tool linked within the Patient Summary screen."}
+    <br/>
+    <br/>
+  </div>
+{/if}
+
 <div class="text">
     {xl t="NOTE: Uploading files with duplicate names will cause the files to be automatically renamed (for example, file.jpg will become file.1.jpg). Filenames are considered unique per patient, not per category."}
     <br/>
index 2bfc227..bc1ff9d 100644 (file)
@@ -17,7 +17,7 @@ $v_realpatch = '0';
 // is a database change in the course of development.  It is used
 // internally to determine when a database upgrade is needed.
 //
-$v_database = 87;
+$v_database = 88;
 
 // Access control version identifier, this is to be incremented whenever there
 // is a access control change in the course of development.  It is used