move dicom viewer to iframe (#1395)
[openemr.git] / library / classes / Document.class.php
blob09259c944f9078b49bb5a24fbb59ae20bfb6b38d
1 <?php
3 require_once(dirname(__FILE__) . "/../pnotes.inc");
4 require_once(dirname(__FILE__) . "/../gprelations.inc.php");
6 /**
7 * class Document
8 * This class is the logical representation of a physical file on some system somewhere that can be referenced with a URL
9 * of some type. This URL is not necessarily a web url, it could be a file URL or reference to a BLOB in a db.
10 * It is implicit that a document can have other related tables to it at least a one document to many notes which join on a documents
11 * id and categories which do the same.
14 class Document extends ORDataObject
18 * Database unique identifier
19 * @var id
21 var $id;
24 * DB unique identifier reference to some other table, this is not unique in the document table
25 * @var int
27 var $foreign_id;
30 * Enumerated DB field which is met information about how to use the URL
31 * @var int can also be a the properly enumerated string
33 var $type;
36 * Array mapping of possible for values for the type variable
37 * mapping is array text name to index
38 * @var array
40 var $type_array = array();
43 * Size of the document in bytes if that is available
44 * @var int
46 var $size;
49 * Date the document was first persisted
50 * @var string
52 var $date;
55 * URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
56 * @var string
58 var $url;
61 * URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
62 * @var string
64 var $thumb_url;
67 * Mimetype of the document if available
68 * @var string
70 var $mimetype;
73 * If the document is a multi-page format like tiff and has at least 1 page this will be 1 or greater, if a non-multi-page format this should be null or empty
74 * @var int
76 var $pages;
79 * Foreign key identifier of who initially persisited the document,
80 * potentially ownership could be changed but that would be up to an external non-document object process
81 * @var int
83 var $owner;
86 * Timestamp of the last time the document was changed and persisted, auto maintained by DB, manually change at your own peril
87 * @var int
89 var $revision;
92 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
93 * @var string
95 var $docdate;
98 * 40-character sha1 hash key of the document from when it was uploaded.
99 * @var string
101 var $hash;
104 * DB identifier reference to the lists table (the related issue), 0 if none.
105 * @var int
107 var $list_id;
109 // For tagging with the encounter
110 var $encounter_id;
111 var $encounter_check;
114 * Whether the file is already imported
115 * @var int
117 var $imported;
120 * Constructor sets all Document attributes to their default value
121 * @param int $id optional existing id of a specific document, if omitted a "blank" document is created
123 function __construct($id = "")
125 //call the parent constructor so we have a _db to work with
126 parent::__construct();
128 //shore up the most basic ORDataObject bits
129 $this->id = $id;
130 $this->_table = "documents";
132 //load the enum type from the db using the parent helper function, this uses psuedo-class variables so it is really cheap
133 $this->type_array = $this->_load_enum("type");
135 $this->type = $this->type_array[0];
136 $this->size = 0;
137 $this->date = date("Y-m-d H:i:s");
138 $this->url = "";
139 $this->mimetype = "";
140 $this->docdate = date("Y-m-d");
141 $this->hash = "";
142 $this->list_id = 0;
143 $this->encounter_id = 0;
144 $this->encounter_check = "";
146 if ($id != "") {
147 $this->populate();
152 * Convenience function to get an array of many document objects
153 * For really large numbers of documents there is a way more efficient way to do this by overwriting the populate method
154 * @param int $foreign_id optional id use to limit array on to a specific relation, otherwise every document object is returned
156 function documents_factory($foreign_id = "")
158 $documents = array();
160 if (empty($foreign_id)) {
161 $foreign_id= "like '%'";
162 } else {
163 $foreign_id= " = '" . add_escape_custom(strval($foreign_id)) . "'";
166 $d = new Document();
167 $sql = "SELECT id FROM " . $d->_table . " WHERE foreign_id " .$foreign_id ;
168 $result = $d->_db->Execute($sql);
170 while ($result && !$result->EOF) {
171 $documents[] = new Document($result->fields['id']);
172 $result->MoveNext();
175 return $documents;
179 * Convenience function to get a document object from a url
180 * Checks to see if there is an existing document with that URL and if so returns that object, otherwise
181 * creates a new one, persists it and returns it
182 * @param string $url
183 * @return object new or existing document object with the specified URL
185 function document_factory_url($url)
187 $d = new Document();
188 //strip url handler, for now we always assume file://
189 $filename = preg_replace("|^(.*)://|", "", $url);
191 if (!file_exists($filename)) {
192 die("An invalid URL was specified to crete a new document, this would only be caused if files are being deleted as you are working through the queue. '$filename'\n");
195 $sql = "SELECT id FROM " . $d->_table . " WHERE url= '" . add_escape_custom($url) ."'" ;
196 $result = $d->_db->Execute($sql);
198 if ($result && !$result->EOF) {
199 if (file_exists($filename)) {
200 $d = new Document($result->fields['id']);
201 } else {
202 $sql = "DELETE FROM " . $d->_table . " WHERE id= '" . $result->fields['id'] . "'";
203 $result = $d->_db->Execute($sql);
204 echo("There is a database for the file but it no longer exists on the file system. Its document entry has been deleted. '$filename'\n");
206 } else {
207 $file_command = $GLOBALS['oer_config']['document']['file_command_path'];
208 $cmd_args = "-i " . escapeshellarg($new_path . $fname);
210 $command = $file_command . " " . $cmd_args;
211 $mimetype = exec($command);
212 $mime_array = explode(":", $mimetype);
213 $mimetype = $mime_array[1];
214 if ($mimetype == 'application/octet-stream') { // windows most likely...
215 $parts = pathinfo($fname);
216 if (strtolower($parts['extension']) == 'dcm') { // cheat for dicom on windows because MS must be different!!!
217 $mimetype = 'application/dicom';
220 $d->set_mimetype($mimetype);
221 $d->url = $url;
222 $d->size = filesize($filename);
223 $d->type = $d->type_array['file_url'];
224 $d->persist();
225 $d->populate();
228 return $d;
232 * Convenience function to generate string debug data about the object
234 function toString($html = false)
236 $string .= "\n"
237 . "ID: " . $this->id."\n"
238 . "FID: " . $this->foreign_id."\n"
239 . "type: " . $this->type . "\n"
240 . "type_array: " . print_r($this->type_array, true) . "\n"
241 . "size: " . $this->size . "\n"
242 . "date: " . $this->date . "\n"
243 . "url: " . $this->url . "\n"
244 . "mimetype: " . $this->mimetype . "\n"
245 . "pages: " . $this->pages . "\n"
246 . "owner: " . $this->owner . "\n"
247 . "revision: " . $this->revision . "\n"
248 . "docdate: " . $this->docdate . "\n"
249 . "hash: " . $this->hash . "\n"
250 . "list_id: " . $this->list_id . "\n"
251 . "encounter_id: " . $this->encounter_id . "\n"
252 . "encounter_check: " . $this->encounter_check . "\n";
254 if ($html) {
255 return nl2br($string);
256 } else {
257 return $string;
261 /**#@+
262 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
263 * @param mixed new value for given attribute
265 function set_id($id)
267 $this->id = $id;
269 function get_id()
271 return $this->id;
273 function set_foreign_id($fid)
275 $this->foreign_id = $fid;
277 function get_foreign_id()
279 return $this->foreign_id;
281 function set_type($type)
283 $this->type = $type;
285 function get_type()
287 return $this->type;
289 function set_size($size)
291 $this->size = $size;
293 function get_size()
295 return $this->size;
297 function set_date($date)
299 $this->date = $date;
301 function get_date()
303 return $this->date;
305 function set_hash($hash)
307 $this->hash = $hash;
309 function get_hash()
311 return $this->hash;
313 function set_url($url)
315 $this->url = $url;
317 function get_url()
319 return $this->url;
321 function set_thumb_url($url)
323 $this->thumb_url = $url;
325 function get_thumb_url()
327 return $this->thumb_url;
330 * this returns the url stripped down to basename
332 function get_url_web()
334 return basename_international($this->url);
337 * get the url without the protocol handler
339 function get_url_filepath()
341 return preg_replace("|^(.*)://|", "", $this->url);
344 * get the url filename only
346 function get_url_file()
348 return basename_international(preg_replace("|^(.*)://|", "", $this->url));
351 * get the url path only
353 function get_url_path()
355 return dirname(preg_replace("|^(.*)://|", "", $this->url)) ."/";
357 function get_path_depth()
359 return $this->path_depth;
361 function set_path_depth($path_depth)
363 $this->path_depth = $path_depth;
365 function set_mimetype($mimetype)
367 $this->mimetype = $mimetype;
369 function get_mimetype()
371 return $this->mimetype;
373 function set_pages($pages)
375 $this->pages = $pages;
377 function get_pages()
379 return $this->pages;
381 function set_owner($owner)
383 $this->owner = $owner;
385 function get_owner()
387 return $this->owner;
390 * No getter for revision because it is updated automatically by the DB.
392 function set_revision($revision)
394 $this->revision = $revision;
396 function set_docdate($docdate)
398 $this->docdate = $docdate;
400 function get_docdate()
402 return $this->docdate;
404 function set_list_id($list_id)
406 $this->list_id = $list_id;
408 function get_list_id()
410 return $this->list_id;
412 function set_encounter_id($encounter_id)
414 $this->encounter_id = $encounter_id;
416 function get_encounter_id()
418 return $this->encounter_id;
420 function set_encounter_check($encounter_check)
422 $this->encounter_check = $encounter_check;
424 function get_encounter_check()
426 return $this->encounter_check;
429 function get_ccr_type($doc_id)
431 $type = sqlQuery("SELECT c.name FROM categories AS c LEFT JOIN categories_to_documents AS ctd ON c.id = ctd.category_id WHERE ctd.document_id = ?", array($doc_id));
432 return $type['name'];
434 function set_imported($imported)
436 $this->imported = $imported;
438 function get_imported()
440 return $this->imported;
442 function update_imported($doc_id)
444 sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?", array($doc_id));
447 * Overridden function to stor current object state in the db.
448 * current overide is to allow for a just in time foreign id, often this is needed
449 * when the object is never directly exposed and is handled as part of a larger
450 * object hierarchy.
451 * @param int $fid foreign id that should be used so that this document can be related (joined) on it later
454 function persist($fid = "")
456 if (!empty($fid)) {
457 $this->foreign_id = $fid;
460 parent::persist();
463 function set_storagemethod($str)
465 $this->storagemethod = $str;
468 function get_storagemethod()
470 return $this->storagemethod;
473 function set_couch_docid($str)
475 $this->couch_docid = $str;
478 function get_couch_docid()
480 return $this->couch_docid;
483 function set_couch_revid($str)
485 $this->couch_revid = $str;
488 function get_couch_revid()
490 return $this->couch_revid;
493 function get_couch_url($pid, $encounter)
495 $couch_docid = $this->get_couch_docid();
496 $couch_url = $this->get_url();
497 $couch = new CouchDB();
498 $data = array($GLOBALS['couchdb_dbase'],$couch_docid,$pid,$encounter);
499 $resp = $couch->retrieve_doc($data);
500 $content = $resp->data;
501 $temp_url=$couch_url;
502 $temp_url = $GLOBALS['OE_SITE_DIR'] . '/documents/temp/' . $pid . '_' . $couch_url;
503 $f_CDB = fopen($temp_url, 'w');
504 fwrite($f_CDB, base64_decode($content));
505 fclose($f_CDB);
506 return $temp_url;
509 // Function added by Rod to change the patient associated with a document.
510 // This just moves some code that used to be in C_Document.class.php,
511 // changing it as little as possible since I'm not set up to test it.
513 function change_patient($new_patient_id)
515 $couch_docid = $this->get_couch_docid();
516 $couch_revid = $this->get_couch_revid();
518 // Set the new patient in CouchDB.
519 if ($couch_docid && $couch_revid) {
520 $couch = new CouchDB();
521 $db = $GLOBALS['couchdb_dbase'];
522 $data = array($db, $couch_docid);
523 $couchresp = $couch->retrieve_doc($data);
524 // CouchDB doesnot support updating a single value in a document.
525 // Have to retrieve the entire document, update the necessary value and save again
526 list ($db, $docid, $revid, $patient_id, $encounter, $type, $json) = $data;
527 $data = array($db, $couch_docid, $couch_revid, $new_patient_id, $couchresp->encounter,
528 $couchresp->mimetype, json_encode($couchresp->data), json_encode($couchresp->th_data));
529 $resp = $couch->update_doc($data);
530 // Sometimes the response from CouchDB is not available, still it would
531 // have saved in the DB. Hence check one more time.
532 if (!$resp->_id || !$resp->_rev) {
533 $data = array($db, $couch_docid, $new_patient_id, $couchresp->encounter);
534 $resp = $couch->retrieve_doc($data);
537 if ($resp->_rev == $couch_revid) {
538 return false;
539 } else {
540 $this->set_couch_revid($resp->_rev);
544 // Set the new patient in mysql.
545 $this->set_foreign_id($new_patient_id);
546 $this->persist();
548 // Return true for success.
549 return true;
553 * Create a new document and store its data.
554 * This is a mix of new code and code moved from C_Document.class.php.
556 * @param string $patient_id Patient pid; if not known then this may be a simple directory name
557 * @param integer $category_id The desired document category ID
558 * @param string $filename Desired filename, may be modified for uniqueness
559 * @param string $mimetype MIME type
560 * @param string &$data The actual data to store (not encoded)
561 * @param string $higher_level_path Optional subdirectory within the local document repository
562 * @param string $path_depth Number of directory levels in $higher_level_path, if specified
563 * @param integer $owner Owner/user/service that is requesting this action
564 * @param string $tmpfile The tmp location of file (require for thumbnail generator)
565 * @return string Empty string if success, otherwise error message text
567 function createDocument(
568 $patient_id,
569 $category_id,
570 $filename,
571 $mimetype,
572 &$data,
573 $higher_level_path = '',
574 $path_depth = 1,
575 $owner = 0,
576 $tmpfile = null
578 // The original code used the encounter ID but never set it to anything.
579 // That was probably a mistake, but we reference it here for documentation
580 // and leave it empty. Logically, documents are not tied to encounters.
582 if ($GLOBALS['generate_doc_thumb']) {
583 $thumb_size = ($GLOBALS['thumb_doc_max_size'] > 0) ? $GLOBALS['thumb_doc_max_size'] : null;
584 $thumbnail_class = new Thumbnail($thumb_size);
586 if (!is_null($tmpfile)) {
587 $has_thumbnail = $thumbnail_class->file_support_thumbnail($tmpfile);
588 } else {
589 $has_thumbnail = false;
592 if ($has_thumbnail) {
593 $thumbnail_resource = $thumbnail_class->create_thumbnail(null, $data);
594 if ($thumbnail_resource) {
595 $thumbnail_data = $thumbnail_class->get_string_file($thumbnail_resource);
596 } else {
597 $has_thumbnail = false;
600 } else {
601 $has_thumbnail = false;
604 $encounter_id = '';
605 $this->storagemethod = $GLOBALS['document_storage_method'];
606 $this->mimetype = $mimetype;
607 if ($this->storagemethod == 1) {
608 // Store it using CouchDB.
609 $couch = new CouchDB();
610 $docname = $_SESSION['authId'] . $filename . $patient_id . $encounter_id . date("%Y-%m-%d H:i:s");
611 $docid = $couch->stringToId($docname);
612 $json = json_encode(base64_encode($data));
613 if ($has_thumbnail) {
614 $th_json = json_encode(base64_encode($thumbnail_data));
615 $this->thumb_url = $this->get_thumb_name($filename);
616 } else {
617 $th_json = false;
620 $db = $GLOBALS['couchdb_dbase'];
621 $couchdata = array($db, $docid, $patient_id, $encounter_id, $mimetype, $json, $th_json);
622 $resp = $couch->check_saveDOC($couchdata);
623 if (!$resp->id || !$resp->_rev) {
624 // Not sure what this is supposed to do. The references to id, rev,
625 // _id and _rev seem pretty weird.
626 $couchdata = array($db, $docid, $patient_id, $encounter_id);
627 $resp = $couch->retrieve_doc($couchdata);
628 $docid = $resp->_id;
629 $revid = $resp->_rev;
630 } else {
631 $docid = $resp->id;
632 $revid = $resp->rev;
635 if (!$docid && !$revid) {
636 return xl('CouchDB save failed');
639 $this->url = $filename;
640 $this->couch_docid = $docid;
641 $this->couch_revid = $revid;
642 } else {
643 // Storing document files locally.
644 $repository = $GLOBALS['oer_config']['documents']['repository'];
645 $higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
646 if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
647 // Allow higher level directory structure in documents directory and a patient is mapped.
648 $filepath = $repository . $higher_level_path . "/";
649 } else if (!empty($higher_level_path)) {
650 // Allow higher level directory structure in documents directory and there is no patient mapping
651 // (will create up to 10000 random directories and increment the path_depth by 1).
652 $filepath = $repository . $higher_level_path . '/' . rand(1, 10000) . '/';
653 ++$path_depth;
654 } else if (!(is_numeric($patient_id)) || !($patient_id > 0)) {
655 // This is the default action except there is no patient mapping (when patient_id is 00 or direct)
656 // (will create up to 10000 random directories and set the path_depth to 2).
657 $filepath = $repository . $patient_id . '/' . rand(1, 10000) . '/';
658 $path_depth = 2;
659 $patient_id = 0;
660 } else {
661 // This is the default action where the patient is used as one level directory structure in documents directory.
662 $filepath = $repository . $patient_id . '/';
663 $path_depth = 1;
666 if (!file_exists($filepath)) {
667 if (!mkdir($filepath, 0700, true)) {
668 return xl('Unable to create patient document subdirectory');
672 // Filename modification to force valid characters and uniqueness.
673 $filename = preg_replace("/[^a-zA-Z0-9_.]/", "_", $filename);
675 $fileExtension = pathinfo($filename, PATHINFO_EXTENSION);
676 if (empty($fileExtension)) {
677 return xl('Your file doesn\'t have an extension');
680 $fnsuffix = 0;
681 $fn1 = $filename;
682 $fn2 = '';
683 $fn3 = '';
684 $dotpos = strrpos($filename, '.');
685 if ($dotpos !== false) {
686 $fn1 = substr($filename, 0, $dotpos);
687 $fn2 = '.';
688 $fn3 = substr($filename, $dotpos + 1);
691 while (file_exists($filepath . $filename)) {
692 if (++$fnsuffix > 10000) {
693 return xl('Failed to compute a unique filename');
696 $filename = $fn1 . '_' . $fnsuffix . $fn2 . $fn3;
699 $this->url = "file://" . $filepath . $filename;
700 if (is_numeric($path_depth)) {
701 // this is for when directory structure is more than one level
702 $this->path_depth = $path_depth;
705 // Store the file into its proper directory.
706 if (file_put_contents($filepath . $filename, $data) === false) {
707 return xl('Failed to create') . " $filepath$filename";
710 if ($has_thumbnail) {
711 $this->thumb_url = "file://" . $filepath . $this->get_thumb_name($filename);
712 // Store the file into its proper directory.
713 if (file_put_contents($filepath . $this->get_thumb_name($filename), $thumbnail_data) === false) {
714 return xl('Failed to create') . $filepath . $this->get_thumb_name($filename);
719 $this->size = strlen($data);
720 $this->hash = sha1($data);
721 $this->type = $this->type_array['file_url'];
722 $this->owner = $owner ? $owner : $_SESSION['authUserID'];
723 $this->set_foreign_id($patient_id);
724 $this->persist();
725 $this->populate();
726 if (is_numeric($this->get_id()) && is_numeric($category_id)) {
727 $sql = "REPLACE INTO categories_to_documents set " .
728 "category_id = '$category_id', " .
729 "document_id = '" . $this->get_id() . "'";
730 $this->_db->Execute($sql);
733 return '';
737 * Return file name for thumbnail (adding 'th_')
739 function get_thumb_name($file_name)
741 return 'th_' . $file_name;
745 * Post a patient note that is linked to this document.
747 * @param string $provider Login name of the provider to receive this note.
748 * @param integer $category_id The desired document category ID
749 * @param string $message Any desired message text for the note.
751 function postPatientNote($provider, $category_id, $message = '')
753 // Build note text in a way that identifies the new document.
754 // See pnotes_full.php which uses this to auto-display the document.
755 $note = $this->get_url_file();
756 for ($tmp = $category_id; $tmp;) {
757 $catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
758 $note = $catrow['name'] . "/$note";
759 $tmp = $catrow['parent'];
762 $note = "New scanned document " . $this->get_id() . ": $note";
763 if ($message) {
764 $note .= "\n" . $message;
767 $noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
768 // Link the new note to the document.
769 setGpRelation(1, $this->get_id(), 6, $noteid);
773 * Return note objects associated with this document using Note::notes_factory
776 function get_notes()
778 return (Note::notes_factory($this->get_id()));
780 } // end of Document