another minor fix to prior commit
[openemr.git] / library / classes / Document.class.php
blobe5f16bd66db84617be93ccc4ae821ae912d11444
1 <?php
3 require_once(dirname(__FILE__) . "/ORDataObject.class.php");
4 require_once(dirname(__FILE__) . "/CouchDB.class.php");
5 require_once(dirname(__FILE__) . "/thumbnail/Thumbnail.class.php");
6 require_once(dirname(__FILE__) . "/../pnotes.inc");
7 require_once(dirname(__FILE__) . "/../gprelations.inc.php");
9 /**
10 * class Document
11 * This class is the logical representation of a physical file on some system somewhere that can be referenced with a URL
12 * 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.
13 * 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
14 * id and categories which do the same.
17 class Document extends ORDataObject{
20 * Database unique identifier
21 * @var id
23 var $id;
26 * DB unique identifier reference to some other table, this is not unique in the document table
27 * @var int
29 var $foreign_id;
32 * Enumerated DB field which is met information about how to use the URL
33 * @var int can also be a the properly enumerated string
35 var $type;
38 * Array mapping of possible for values for the type variable
39 * mapping is array text name to index
40 * @var array
42 var $type_array = array();
45 * Size of the document in bytes if that is available
46 * @var int
48 var $size;
51 * Date the document was first persisted
52 * @var string
54 var $date;
57 * URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
58 * @var string
60 var $url;
63 * URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
64 * @var string
66 var $thumb_url;
69 * Mimetype of the document if available
70 * @var string
72 var $mimetype;
75 * 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
76 * @var int
78 var $pages;
81 * Foreign key identifier of who initially persisited the document,
82 * potentially ownership could be changed but that would be up to an external non-document object process
83 * @var int
85 var $owner;
88 * Timestamp of the last time the document was changed and persisted, auto maintained by DB, manually change at your own peril
89 * @var int
91 var $revision;
94 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
95 * @var string
97 var $docdate;
100 * 40-character sha1 hash key of the document from when it was uploaded.
101 * @var string
103 var $hash;
106 * DB identifier reference to the lists table (the related issue), 0 if none.
107 * @var int
109 var $list_id;
111 // For tagging with the encounter
112 var $encounter_id;
113 var $encounter_check;
116 * Whether the file is already imported
117 * @var int
119 var $imported;
122 * Constructor sets all Document attributes to their default value
123 * @param int $id optional existing id of a specific document, if omitted a "blank" document is created
125 function __construct($id = "") {
126 //call the parent constructor so we have a _db to work with
127 parent::__construct();
129 //shore up the most basic ORDataObject bits
130 $this->id = $id;
131 $this->_table = "documents";
133 //load the enum type from the db using the parent helper function, this uses psuedo-class variables so it is really cheap
134 $this->type_array = $this->_load_enum("type");
136 $this->type = $this->type_array[0];
137 $this->size = 0;
138 $this->date = date("Y-m-d H:i:s");
139 $this->url = "";
140 $this->mimetype = "";
141 $this->docdate = date("Y-m-d");
142 $this->hash = "";
143 $this->list_id = 0;
144 $this->encounter_id = 0;
145 $this->encounter_check = "";
147 if ($id != "") {
148 $this->populate();
153 * Convenience function to get an array of many document objects
154 * For really large numbers of documents there is a way more efficient way to do this by overwriting the populate method
155 * @param int $foreign_id optional id use to limit array on to a specific relation, otherwise every document object is returned
157 function documents_factory($foreign_id = "") {
158 $documents = array();
160 if (empty($foreign_id)) {
161 $foreign_id= "like '%'";
163 else {
164 $foreign_id= " = '" . add_escape_custom(strval($foreign_id)) . "'";
167 $d = new Document();
168 $sql = "SELECT id FROM " . $d->_table . " WHERE foreign_id " .$foreign_id ;
169 $result = $d->_db->Execute($sql);
171 while ($result && !$result->EOF) {
172 $documents[] = new Document($result->fields['id']);
173 $result->MoveNext();
176 return $documents;
180 * Convenience function to get a document object from a url
181 * Checks to see if there is an existing document with that URL and if so returns that object, otherwise
182 * creates a new one, persists it and returns it
183 * @param string $url
184 * @return object new or existing document object with the specified URL
186 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']);
202 else {
203 $sql = "DELETE FROM " . $d->_table . " WHERE id= '" . $result->fields['id'] ."'";
204 $result = $d->_db->Execute($sql);
205 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");
208 else {
209 $file_command = $GLOBALS['oer_config']['document']['file_command_path'] ;
210 $cmd_args = "-i ".escapeshellarg($new_path.$fname);
212 $command = $file_command." ".$cmd_args;
213 $mimetype = exec($command);
214 $mime_array = explode(":", $mimetype);
215 $mimetype = $mime_array[1];
216 $d->set_mimetype($mimetype);
217 $d->url = $url;
218 $d->size = filesize($filename);
219 $d->type = $d->type_array['file_url'];
220 $d->persist();
221 $d->populate();
224 return $d;
228 * Convenience function to generate string debug data about the object
230 function toString($html = false) {
231 $string .= "\n"
232 . "ID: " . $this->id."\n"
233 . "FID: " . $this->foreign_id."\n"
234 . "type: " . $this->type . "\n"
235 . "type_array: " . print_r($this->type_array,true) . "\n"
236 . "size: " . $this->size . "\n"
237 . "date: " . $this->date . "\n"
238 . "url: " . $this->url . "\n"
239 . "mimetype: " . $this->mimetype . "\n"
240 . "pages: " . $this->pages . "\n"
241 . "owner: " . $this->owner . "\n"
242 . "revision: " . $this->revision . "\n"
243 . "docdate: " . $this->docdate . "\n"
244 . "hash: " . $this->hash . "\n"
245 . "list_id: " . $this->list_id . "\n"
246 . "encounter_id: " . $this->encounter_id . "\n"
247 . "encounter_check: " . $this->encounter_check . "\n";
249 if ($html) {
250 return nl2br($string);
252 else {
253 return $string;
257 /**#@+
258 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
259 * @param mixed new value for given attribute
261 function set_id($id) {
262 $this->id = $id;
264 function get_id() {
265 return $this->id;
267 function set_foreign_id($fid) {
268 $this->foreign_id = $fid;
270 function get_foreign_id() {
271 return $this->foreign_id;
273 function set_type($type) {
274 $this->type = $type;
276 function get_type() {
277 return $this->type;
279 function set_size($size) {
280 $this->size = $size;
282 function get_size() {
283 return $this->size;
285 function set_date($date) {
286 $this->date = $date;
288 function get_date() {
289 return $this->date;
291 function set_hash($hash) {
292 $this->hash = $hash;
294 function get_hash() {
295 return $this->hash;
297 function set_url($url) {
298 $this->url = $url;
300 function get_url() {
301 return $this->url;
303 function set_thumb_url($url) {
304 $this->thumb_url = $url;
306 function get_thumb_url() {
307 return $this->thumb_url;
310 * this returns the url stripped down to basename
312 function get_url_web() {
313 return basename($this->url);
316 * get the url without the protocol handler
318 function get_url_filepath() {
319 return preg_replace("|^(.*)://|","",$this->url);
322 * get the url filename only
324 function get_url_file() {
325 return basename(preg_replace("|^(.*)://|","",$this->url));
328 * get the url path only
330 function get_url_path() {
331 return dirname(preg_replace("|^(.*)://|","",$this->url)) ."/";
333 function get_path_depth() {
334 return $this->path_depth;
336 function set_path_depth($path_depth) {
337 $this->path_depth = $path_depth;
339 function set_mimetype($mimetype) {
340 $this->mimetype = $mimetype;
342 function get_mimetype() {
343 return $this->mimetype;
345 function set_pages($pages) {
346 $this->pages = $pages;
348 function get_pages() {
349 return $this->pages;
351 function set_owner($owner) {
352 $this->owner = $owner;
354 function get_owner() {
355 return $this->owner;
358 * No getter for revision because it is updated automatically by the DB.
360 function set_revision($revision) {
361 $this->revision = $revision;
363 function set_docdate($docdate) {
364 $this->docdate = $docdate;
366 function get_docdate() {
367 return $this->docdate;
369 function set_list_id($list_id) {
370 $this->list_id = $list_id;
372 function get_list_id() {
373 return $this->list_id;
375 function set_encounter_id($encounter_id) {
376 $this->encounter_id = $encounter_id;
378 function get_encounter_id() {
379 return $this->encounter_id;
381 function set_encounter_check($encounter_check) {
382 $this->encounter_check = $encounter_check;
384 function get_encounter_check() {
385 return $this->encounter_check;
388 function get_ccr_type($doc_id){
389 $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));
390 return $type['name'];
392 function set_imported($imported) {
393 $this->imported = $imported;
395 function get_imported() {
396 return $this->imported;
398 function update_imported($doc_id) {
399 sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?",array($doc_id));
402 * Overridden function to stor current object state in the db.
403 * current overide is to allow for a just in time foreign id, often this is needed
404 * when the object is never directly exposed and is handled as part of a larger
405 * object hierarchy.
406 * @param int $fid foreign id that should be used so that this document can be related (joined) on it later
409 function persist($fid ="") {
410 if (!empty($fid)) {
411 $this->foreign_id = $fid;
413 parent::persist();
416 function set_storagemethod($str) {
417 $this->storagemethod = $str;
420 function get_storagemethod() {
421 return $this->storagemethod;
424 function set_couch_docid($str) {
425 $this->couch_docid = $str;
428 function get_couch_docid() {
429 return $this->couch_docid;
432 function set_couch_revid($str) {
433 $this->couch_revid = $str;
436 function get_couch_revid() {
437 return $this->couch_revid;
440 function get_couch_url($pid,$encounter){
441 $couch_docid = $this->get_couch_docid();
442 $couch_url = $this->get_url();
443 $couch = new CouchDB();
444 $data = array($GLOBALS['couchdb_dbase'],$couch_docid,$pid,$encounter);
445 $resp = $couch->retrieve_doc($data);
446 $content = $resp->data;
447 $temp_url=$couch_url;
448 $temp_url = $GLOBALS['OE_SITE_DIR'] . '/documents/temp/' . $pid . '_' . $couch_url;
449 $f_CDB = fopen($temp_url,'w');
450 fwrite($f_CDB,base64_decode($content));
451 fclose($f_CDB);
452 return $temp_url;
455 // Function added by Rod to change the patient associated with a document.
456 // This just moves some code that used to be in C_Document.class.php,
457 // changing it as little as possible since I'm not set up to test it.
459 function change_patient($new_patient_id) {
460 $couch_docid = $this->get_couch_docid();
461 $couch_revid = $this->get_couch_revid();
463 // Set the new patient in CouchDB.
464 if ($couch_docid && $couch_revid) {
465 $couch = new CouchDB();
466 $db = $GLOBALS['couchdb_dbase'];
467 $data = array($db, $couch_docid);
468 $couchresp = $couch->retrieve_doc($data);
469 // CouchDB doesnot support updating a single value in a document.
470 // Have to retrieve the entire document, update the necessary value and save again
471 list ($db, $docid, $revid, $patient_id, $encounter, $type, $json) = $data;
472 $data = array($db, $couch_docid, $couch_revid, $new_patient_id, $couchresp->encounter,
473 $couchresp->mimetype, json_encode($couchresp->data));
474 $resp = $couch->update_doc($data);
475 // Sometimes the response from CouchDB is not available, still it would
476 // have saved in the DB. Hence check one more time.
477 if(!$resp->_id || !$resp->_rev){
478 $data = array($db, $couch_docid, $new_patient_id, $couchresp->encounter);
479 $resp = $couch->retrieve_doc($data);
481 if($resp->_rev == $couch_revid) {
482 return false;
484 else {
485 $this->set_couch_revid($resp->_rev);
489 // Set the new patient in mysql.
490 $this->set_foreign_id($new_patient_id);
491 $this->persist();
493 // Return true for success.
494 return true;
498 * Create a new document and store its data.
499 * This is a mix of new code and code moved from C_Document.class.php.
501 * @param string $patient_id Patient pid; if not known then this may be a simple directory name
502 * @param integer $category_id The desired document category ID
503 * @param string $filename Desired filename, may be modified for uniqueness
504 * @param string $mimetype MIME type
505 * @param string &$data The actual data to store (not encoded)
506 * @param string $higher_level_path Optional subdirectory within the local document repository
507 * @param string $path_depth Number of directory levels in $higher_level_path, if specified
508 * @param integer $owner Owner/user/service that is requesting this action
509 * @param string $tmpfile The tmp location of file (require for thumbnail generator)
510 * @return string Empty string if success, otherwise error message text
512 function createDocument($patient_id, $category_id, $filename, $mimetype, &$data,
513 $higher_level_path='', $path_depth=1, $owner=0, $tmpfile = null) {
514 // The original code used the encounter ID but never set it to anything.
515 // That was probably a mistake, but we reference it here for documentation
516 // and leave it empty. Logically, documents are not tied to encounters.
518 if($GLOBALS['generate_doc_thumb']) {
520 $thumb_size = ($GLOBALS['thumb_doc_max_size'] > 0) ? $GLOBALS['thumb_doc_max_size'] : null;
521 $thumbnail_class = new Thumbnail($thumb_size);
523 if(!is_null($tmpfile)) {
524 $has_thumbnail = $thumbnail_class->file_support_thumbnail($tmpfile);
525 } else {
526 $has_thumbnail = false;
529 if ($has_thumbnail) {
530 $thumbnail_resource = $thumbnail_class->create_thumbnail(null, $data);
531 if ($thumbnail_resource) {
532 $thumbnail_data = $thumbnail_class->get_string_file($thumbnail_resource);
533 } else {
534 $has_thumbnail = false;
537 } else {
538 $has_thumbnail = false;
541 $encounter_id = '';
542 $this->storagemethod = $GLOBALS['document_storage_method'];
543 $this->mimetype = $mimetype;
544 if ($this->storagemethod == 1) {
545 // Store it using CouchDB.
546 $couch = new CouchDB();
547 $docname = $_SESSION['authId'] . $filename . $patient_id . $encounter_id . date("%Y-%m-%d H:i:s");
548 $docid = $couch->stringToId($docname);
549 $json = json_encode(base64_encode($data));
550 if ($has_thumbnail) {
551 $th_json = json_encode(base64_encode($thumbnail_data));
552 $this->thumb_url = $this->get_thumb_name($filename);
553 } else {
554 $th_json = false;
556 $db = $GLOBALS['couchdb_dbase'];
557 $couchdata = array($db, $docid, $patient_id, $encounter_id, $mimetype, $json, $th_json);
558 $resp = $couch->check_saveDOC($couchdata);
559 if(!$resp->id || !$resp->_rev) {
560 // Not sure what this is supposed to do. The references to id, rev,
561 // _id and _rev seem pretty weird.
562 $couchdata = array($db, $docid, $patient_id, $encounter_id);
563 $resp = $couch->retrieve_doc($couchdata);
564 $docid = $resp->_id;
565 $revid = $resp->_rev;
567 else {
568 $docid = $resp->id;
569 $revid = $resp->rev;
571 if(!$docid && !$revid) {
572 return xl('CouchDB save failed');
574 $this->url = $filename;
575 $this->couch_docid = $docid;
576 $this->couch_revid = $revid;
578 else {
579 // Storing document files locally.
580 $repository = $GLOBALS['oer_config']['documents']['repository'];
581 $higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
582 if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
583 // Allow higher level directory structure in documents directory and a patient is mapped.
584 $filepath = $repository . $higher_level_path . "/";
586 else if (!empty($higher_level_path)) {
587 // Allow higher level directory structure in documents directory and there is no patient mapping
588 // (will create up to 10000 random directories and increment the path_depth by 1).
589 $filepath = $repository . $higher_level_path . '/' . rand(1,10000) . '/';
590 ++$path_depth;
592 else if (!(is_numeric($patient_id)) || !($patient_id > 0)) {
593 // This is the default action except there is no patient mapping (when patient_id is 00 or direct)
594 // (will create up to 10000 random directories and set the path_depth to 2).
595 $filepath = $repository . $patient_id . '/' . rand(1,10000) . '/';
596 $path_depth = 2;
597 $patient_id = 0;
599 else {
600 // This is the default action where the patient is used as one level directory structure in documents directory.
601 $filepath = $repository . $patient_id . '/';
602 $path_depth = 1;
604 if (!file_exists($filepath)) {
605 if (!mkdir($filepath, 0700, true)) {
606 return xl('Unable to create patient document subdirectory');
609 // Filename modification to force valid characters and uniqueness.
610 $filename = preg_replace("/[^a-zA-Z0-9_.]/", "_", $filename);
611 $fnsuffix = 0;
612 $fn1 = $filename;
613 $fn2 = '';
614 $fn3 = '';
615 $dotpos = strrpos($filename, '.');
616 if ($dotpos !== FALSE) {
617 $fn1 = substr($filename, 0, $dotpos);
618 $fn2 = '.';
619 $fn3 = substr($filename, $dotpos + 1);
621 while (file_exists($filepath . $filename)) {
622 if (++$fnsuffix > 10000) return xl('Failed to compute a unique filename');
623 $filename = $fn1 . '_' . $fnsuffix . $fn2 . $fn3;
625 $this->url = "file://" . $filepath . $filename;
626 if (is_numeric($path_depth)) {
627 // this is for when directory structure is more than one level
628 $this->path_depth = $path_depth;
630 // Store the file into its proper directory.
631 if (file_put_contents($filepath . $filename, $data) === FALSE) {
632 return xl('Failed to create') . " $filepath$filename";
634 if( $has_thumbnail ) {
635 $this->thumb_url = "file://" . $filepath . $this->get_thumb_name($filename);
636 // Store the file into its proper directory.
637 if (file_put_contents($filepath . $this->get_thumb_name($filename), $thumbnail_data) === FALSE) {
638 return xl('Failed to create') . $filepath . $this->get_thumb_name($filename);
643 $this->size = strlen($data);
644 $this->hash = sha1($data);
645 $this->type = $this->type_array['file_url'];
646 $this->owner = $owner ? $owner : $_SESSION['authUserID'];
647 $this->set_foreign_id($patient_id);
648 $this->persist();
649 $this->populate();
650 if (is_numeric($this->get_id()) && is_numeric($category_id)){
651 $sql = "REPLACE INTO categories_to_documents set " .
652 "category_id = '$category_id', " .
653 "document_id = '" . $this->get_id() . "'";
654 $this->_db->Execute($sql);
656 return '';
660 * Return file name for thumbnail (adding 'th_')
662 function get_thumb_name($file_name) {
663 return 'th_' . $file_name;
667 * Post a patient note that is linked to this document.
669 * @param string $provider Login name of the provider to receive this note.
670 * @param integer $category_id The desired document category ID
671 * @param string $message Any desired message text for the note.
673 function postPatientNote($provider, $category_id, $message='') {
674 // Build note text in a way that identifies the new document.
675 // See pnotes_full.php which uses this to auto-display the document.
676 $note = $this->get_url_file();
677 for ($tmp = $category_id; $tmp;) {
678 $catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
679 $note = $catrow['name'] . "/$note";
680 $tmp = $catrow['parent'];
682 $note = "New scanned document " . $this->get_id() . ": $note";
683 if ($message) $note .= "\n" . $message;
684 $noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
685 // Link the new note to the document.
686 setGpRelation(1, $this->get_id(), 6, $noteid);
689 } // end of Document