PHP7 project, commit 1.
[openemr.git] / library / classes / Document.class.php
blobe5dd18f5e25e657f98608b0557de719e3ca841c5
1 <?php
3 require_once(dirname(__FILE__) . "/ORDataObject.class.php");
4 require_once(dirname(__FILE__) . "/CouchDB.class.php");
5 require_once(dirname(__FILE__) . "/../pnotes.inc");
6 require_once(dirname(__FILE__) . "/../gprelations.inc.php");
8 /**
9 * class Document
10 * This class is the logical representation of a physical file on some system somewhere that can be referenced with a URL
11 * 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.
12 * 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
13 * id and categories which do the same.
16 class Document extends ORDataObject{
19 * Database unique identifier
20 * @var id
22 var $id;
25 * DB unique identifier reference to some other table, this is not unique in the document table
26 * @var int
28 var $foreign_id;
31 * Enumerated DB field which is met information about how to use the URL
32 * @var int can also be a the properly enumerated string
34 var $type;
37 * Array mapping of possible for values for the type variable
38 * mapping is array text name to index
39 * @var array
41 var $type_array = array();
44 * Size of the document in bytes if that is available
45 * @var int
47 var $size;
50 * Date the document was first persisted
51 * @var string
53 var $date;
56 * URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
57 * @var string
59 var $url;
62 * Mimetype of the document if available
63 * @var string
65 var $mimetype;
68 * 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
69 * @var int
71 var $pages;
74 * Foreign key identifier of who initially persisited the document,
75 * potentially ownership could be changed but that would be up to an external non-document object process
76 * @var int
78 var $owner;
81 * Timestamp of the last time the document was changed and persisted, auto maintained by DB, manually change at your own peril
82 * @var int
84 var $revision;
87 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
88 * @var string
90 var $docdate;
93 * 40-character sha1 hash key of the document from when it was uploaded.
94 * @var string
96 var $hash;
99 * DB identifier reference to the lists table (the related issue), 0 if none.
100 * @var int
102 var $list_id;
104 // For tagging with the encounter
105 var $encounter_id;
106 var $encounter_check;
109 * Whether the file is already imported
110 * @var int
112 var $imported;
115 * Constructor sets all Document attributes to their default value
116 * @param int $id optional existing id of a specific document, if omitted a "blank" document is created
118 function Document($id = "") {
119 //call the parent constructor so we have a _db to work with
120 parent::ORDataObject();
122 //shore up the most basic ORDataObject bits
123 $this->id = $id;
124 $this->_table = "documents";
126 //load the enum type from the db using the parent helper function, this uses psuedo-class variables so it is really cheap
127 $this->type_array = $this->_load_enum("type");
129 $this->type = $this->type_array[0];
130 $this->size = 0;
131 $this->date = date("Y-m-d H:i:s");
132 $this->url = "";
133 $this->mimetype = "";
134 $this->docdate = date("Y-m-d");
135 $this->hash = "";
136 $this->list_id = 0;
137 $this->encounter_id = 0;
138 $this->encounter_check = "";
140 if ($id != "") {
141 $this->populate();
146 * Convenience function to get an array of many document objects
147 * For really large numbers of documents there is a way more efficient way to do this by overwriting the populate method
148 * @param int $foreign_id optional id use to limit array on to a specific relation, otherwise every document object is returned
150 function documents_factory($foreign_id = "") {
151 $documents = array();
153 if (empty($foreign_id)) {
154 $foreign_id= "like '%'";
156 else {
157 $foreign_id= " = '" . add_escape_custom(strval($foreign_id)) . "'";
160 $d = new Document();
161 $sql = "SELECT id FROM " . $d->_table . " WHERE foreign_id " .$foreign_id ;
162 $result = $d->_db->Execute($sql);
164 while ($result && !$result->EOF) {
165 $documents[] = new Document($result->fields['id']);
166 $result->MoveNext();
169 return $documents;
173 * Convenience function to get a document object from a url
174 * Checks to see if there is an existing document with that URL and if so returns that object, otherwise
175 * creates a new one, persists it and returns it
176 * @param string $url
177 * @return object new or existing document object with the specified URL
179 function document_factory_url($url) {
180 $d = new Document();
181 //strip url handler, for now we always assume file://
182 $filename = preg_replace("|^(.*)://|","",$url);
184 if (!file_exists($filename)) {
185 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");
188 $sql = "SELECT id FROM " . $d->_table . " WHERE url= '" . add_escape_custom($url) ."'" ;
189 $result = $d->_db->Execute($sql);
191 if ($result && !$result->EOF) {
192 if (file_exists($filename)) {
193 $d = new Document($result->fields['id']);
195 else {
196 $sql = "DELETE FROM " . $d->_table . " WHERE id= '" . $result->fields['id'] ."'";
197 $result = $d->_db->Execute($sql);
198 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");
201 else {
202 $file_command = $GLOBALS['oer_config']['document']['file_command_path'] ;
203 $cmd_args = "-i ".escapeshellarg($new_path.$fname);
205 $command = $file_command." ".$cmd_args;
206 $mimetype = exec($command);
207 $mime_array = split(":", $mimetype);
208 $mimetype = $mime_array[1];
209 $d->set_mimetype($mimetype);
210 $d->url = $url;
211 $d->size = filesize($filename);
212 $d->type = $d->type_array['file_url'];
213 $d->persist();
214 $d->populate();
217 return $d;
221 * Convenience function to generate string debug data about the object
223 function toString($html = false) {
224 $string .= "\n"
225 . "ID: " . $this->id."\n"
226 . "FID: " . $this->foreign_id."\n"
227 . "type: " . $this->type . "\n"
228 . "type_array: " . print_r($this->type_array,true) . "\n"
229 . "size: " . $this->size . "\n"
230 . "date: " . $this->date . "\n"
231 . "url: " . $this->url . "\n"
232 . "mimetype: " . $this->mimetype . "\n"
233 . "pages: " . $this->pages . "\n"
234 . "owner: " . $this->owner . "\n"
235 . "revision: " . $this->revision . "\n"
236 . "docdate: " . $this->docdate . "\n"
237 . "hash: " . $this->hash . "\n"
238 . "list_id: " . $this->list_id . "\n"
239 . "encounter_id: " . $this->encounter_id . "\n"
240 . "encounter_check: " . $this->encounter_check . "\n";
242 if ($html) {
243 return nl2br($string);
245 else {
246 return $string;
250 /**#@+
251 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
252 * @param mixed new value for given attribute
254 function set_id($id) {
255 $this->id = $id;
257 function get_id() {
258 return $this->id;
260 function set_foreign_id($fid) {
261 $this->foreign_id = $fid;
263 function get_foreign_id() {
264 return $this->foreign_id;
266 function set_type($type) {
267 $this->type = $type;
269 function get_type() {
270 return $this->type;
272 function set_size($size) {
273 $this->size = $size;
275 function get_size() {
276 return $this->size;
278 function set_date($date) {
279 $this->date = $date;
281 function get_date() {
282 return $this->date;
284 function set_hash($hash) {
285 $this->hash = $hash;
287 function get_hash() {
288 return $this->hash;
290 function set_url($url) {
291 $this->url = $url;
293 function get_url() {
294 return $this->url;
297 * this returns the url stripped down to basename
299 function get_url_web() {
300 return basename($this->url);
303 * get the url without the protocol handler
305 function get_url_filepath() {
306 return preg_replace("|^(.*)://|","",$this->url);
309 * get the url filename only
311 function get_url_file() {
312 return basename(preg_replace("|^(.*)://|","",$this->url));
315 * get the url path only
317 function get_url_path() {
318 return dirname(preg_replace("|^(.*)://|","",$this->url)) ."/";
320 function get_path_depth() {
321 return $this->path_depth;
323 function set_path_depth($path_depth) {
324 $this->path_depth = $path_depth;
326 function set_mimetype($mimetype) {
327 $this->mimetype = $mimetype;
329 function get_mimetype() {
330 return $this->mimetype;
332 function set_pages($pages) {
333 $this->pages = $pages;
335 function get_pages() {
336 return $this->pages;
338 function set_owner($owner) {
339 $this->owner = $owner;
341 function get_owner() {
342 return $this->owner;
345 * No getter for revision because it is updated automatically by the DB.
347 function set_revision($revision) {
348 $this->revision = $revision;
350 function set_docdate($docdate) {
351 $this->docdate = $docdate;
353 function get_docdate() {
354 return $this->docdate;
356 function set_list_id($list_id) {
357 $this->list_id = $list_id;
359 function get_list_id() {
360 return $this->list_id;
362 function set_encounter_id($encounter_id) {
363 $this->encounter_id = $encounter_id;
365 function get_encounter_id() {
366 return $this->encounter_id;
368 function set_encounter_check($encounter_check) {
369 $this->encounter_check = $encounter_check;
371 function get_encounter_check() {
372 return $this->encounter_check;
375 function get_ccr_type($doc_id){
376 $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));
377 return $type['name'];
379 function set_imported($imported) {
380 $this->imported = $imported;
382 function get_imported() {
383 return $this->imported;
385 function update_imported($doc_id) {
386 sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?",array($doc_id));
389 * Overridden function to stor current object state in the db.
390 * current overide is to allow for a just in time foreign id, often this is needed
391 * when the object is never directly exposed and is handled as part of a larger
392 * object hierarchy.
393 * @param int $fid foreign id that should be used so that this document can be related (joined) on it later
396 function persist($fid ="") {
397 if (!empty($fid)) {
398 $this->foreign_id = $fid;
400 parent::persist();
403 function set_storagemethod($str) {
404 $this->storagemethod = $str;
407 function get_storagemethod() {
408 return $this->storagemethod;
411 function set_couch_docid($str) {
412 $this->couch_docid = $str;
415 function get_couch_docid() {
416 return $this->couch_docid;
419 function set_couch_revid($str) {
420 $this->couch_revid = $str;
423 function get_couch_revid() {
424 return $this->couch_revid;
427 function get_couch_url($pid,$encounter){
428 $couch_docid = $this->get_couch_docid();
429 $couch_url = $this->get_url();
430 $couch = new CouchDB();
431 $data = array($GLOBALS['couchdb_dbase'],$couch_docid,$pid,$encounter);
432 $resp = $couch->retrieve_doc($data);
433 $content = $resp->data;
434 $temp_url=$couch_url;
435 $temp_url = $GLOBALS['OE_SITE_DIR'] . '/documents/temp/' . $pid . '_' . $couch_url;
436 $f_CDB = fopen($temp_url,'w');
437 fwrite($f_CDB,base64_decode($content));
438 fclose($f_CDB);
439 return $temp_url;
442 // Function added by Rod to change the patient associated with a document.
443 // This just moves some code that used to be in C_Document.class.php,
444 // changing it as little as possible since I'm not set up to test it.
446 function change_patient($new_patient_id) {
447 $couch_docid = $this->get_couch_docid();
448 $couch_revid = $this->get_couch_revid();
450 // Set the new patient in CouchDB.
451 if ($couch_docid && $couch_revid) {
452 $couch = new CouchDB();
453 $db = $GLOBALS['couchdb_dbase'];
454 $data = array($db, $couch_docid);
455 $couchresp = $couch->retrieve_doc($data);
456 // CouchDB doesnot support updating a single value in a document.
457 // Have to retrieve the entire document, update the necessary value and save again
458 list ($db, $docid, $revid, $patient_id, $encounter, $type, $json) = $data;
459 $data = array($db, $couch_docid, $couch_revid, $new_patient_id, $couchresp->encounter,
460 $couchresp->mimetype, json_encode($couchresp->data));
461 $resp = $couch->update_doc($data);
462 // Sometimes the response from CouchDB is not available, still it would
463 // have saved in the DB. Hence check one more time.
464 if(!$resp->_id || !$resp->_rev){
465 $data = array($db, $couch_docid, $new_patient_id, $couchresp->encounter);
466 $resp = $couch->retrieve_doc($data);
468 if($resp->_rev == $couch_revid) {
469 return false;
471 else {
472 $this->set_couch_revid($resp->_rev);
476 // Set the new patient in mysql.
477 $this->set_foreign_id($new_patient_id);
478 $this->persist();
480 // Return true for success.
481 return true;
485 * Create a new document and store its data.
486 * This is a mix of new code and code moved from C_Document.class.php.
488 * @param string $patient_id Patient pid; if not known then this may be a simple directory name
489 * @param integer $category_id The desired document category ID
490 * @param string $filename Desired filename, may be modified for uniqueness
491 * @param string $mimetype MIME type
492 * @param string &$data The actual data to store (not encoded)
493 * @param string $higher_level_path Optional subdirectory within the local document repository
494 * @param string $path_depth Number of directory levels in $higher_level_path, if specified
495 * @param integer $owner Owner/user/service that is requesting this action
496 * @return string Empty string if success, otherwise error message text
498 function createDocument($patient_id, $category_id, $filename, $mimetype, &$data,
499 $higher_level_path='', $path_depth=1, $owner=0) {
500 // The original code used the encounter ID but never set it to anything.
501 // That was probably a mistake, but we reference it here for documentation
502 // and leave it empty. Logically, documents are not tied to encounters.
503 $encounter_id = '';
504 $this->storagemethod = $GLOBALS['document_storage_method'];
505 $this->mimetype = $mimetype;
506 if ($this->storagemethod == 1) {
507 // Store it using CouchDB.
508 $couch = new CouchDB();
509 $docname = $_SESSION['authId'] . $filename . $patient_id . $encounter_id . date("%Y-%m-%d H:i:s");
510 $docid = $couch->stringToId($docname);
511 $json = json_encode(base64_encode($data));
512 $db = $GLOBALS['couchdb_dbase'];
513 $couchdata = array($db, $docid, $patient_id, $encounter_id, $mimetype, $json);
514 $resp = $couch->check_saveDOC($couchdata);
515 if(!$resp->id || !$resp->_rev) {
516 // Not sure what this is supposed to do. The references to id, rev,
517 // _id and _rev seem pretty weird.
518 $couchdata = array($db, $docid, $patient_id, $encounter_id);
519 $resp = $couch->retrieve_doc($couchdata);
520 $docid = $resp->_id;
521 $revid = $resp->_rev;
523 else {
524 $docid = $resp->id;
525 $revid = $resp->rev;
527 if(!$docid && !$revid) {
528 return xl('CouchDB save failed');
530 $this->url = $filename;
531 $this->couch_docid = $docid;
532 $this->couch_revid = $revid;
534 else {
535 // Storing document files locally.
536 $repository = $GLOBALS['oer_config']['documents']['repository'];
537 $higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
538 if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
539 // Allow higher level directory structure in documents directory and a patient is mapped.
540 $filepath = $repository . $higher_level_path . "/";
542 else if (!empty($higher_level_path)) {
543 // Allow higher level directory structure in documents directory and there is no patient mapping
544 // (will create up to 10000 random directories and increment the path_depth by 1).
545 $filepath = $repository . $higher_level_path . '/' . rand(1,10000) . '/';
546 ++$path_depth;
548 else if (!(is_numeric($patient_id)) || !($patient_id > 0)) {
549 // This is the default action except there is no patient mapping (when patient_id is 00 or direct)
550 // (will create up to 10000 random directories and set the path_depth to 2).
551 $filepath = $repository . $patient_id . '/' . rand(1,10000) . '/';
552 $path_depth = 2;
553 $patient_id = 0;
555 else {
556 // This is the default action where the patient is used as one level directory structure in documents directory.
557 $filepath = $repository . $patient_id . '/';
558 $path_depth = 1;
560 if (!file_exists($filepath)) {
561 if (!mkdir($filepath, 0700, true)) {
562 return xl('Unable to create patient document subdirectory');
565 // Filename modification to force valid characters and uniqueness.
566 $filename = preg_replace("/[^a-zA-Z0-9_.]/", "_", $filename);
567 $fnsuffix = 0;
568 $fn1 = $filename;
569 $fn2 = '';
570 $fn3 = '';
571 $dotpos = strrpos($filename, '.');
572 if ($dotpos !== FALSE) {
573 $fn1 = substr($filename, 0, $dotpos);
574 $fn2 = '.';
575 $fn3 = substr($filename, $dotpos + 1);
577 while (file_exists($filepath . $filename)) {
578 if (++$fnsuffix > 10000) return xl('Failed to compute a unique filename');
579 $filename = $fn1 . '_' . $fnsuffix . $fn2 . $fn3;
581 $this->url = "file://" . $filepath . $filename;
582 if (is_numeric($path_depth)) {
583 // this is for when directory structure is more than one level
584 $this->path_depth = $path_depth;
586 // Store the file into its proper directory.
587 if (file_put_contents($filepath . $filename, $data) === FALSE) {
588 return xl('Failed to create') . " $filepath$filename";
591 $this->size = strlen($data);
592 $this->hash = sha1($data);
593 $this->type = $this->type_array['file_url'];
594 $this->owner = $owner ? $owner : $_SESSION['authUserID'];
595 $this->set_foreign_id($patient_id);
596 $this->persist();
597 $this->populate();
598 if (is_numeric($this->get_id()) && is_numeric($category_id)){
599 $sql = "REPLACE INTO categories_to_documents set " .
600 "category_id = '$category_id', " .
601 "document_id = '" . $this->get_id() . "'";
602 $this->_db->Execute($sql);
604 return '';
608 * Post a patient note that is linked to this document.
610 * @param string $provider Login name of the provider to receive this note.
611 * @param integer $category_id The desired document category ID
612 * @param string $message Any desired message text for the note.
614 function postPatientNote($provider, $category_id, $message='') {
615 // Build note text in a way that identifies the new document.
616 // See pnotes_full.php which uses this to auto-display the document.
617 $note = $this->get_url_file();
618 for ($tmp = $category_id; $tmp;) {
619 $catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
620 $note = $catrow['name'] . "/$note";
621 $tmp = $catrow['parent'];
623 $note = "New scanned document " . $this->get_id() . ": $note";
624 if ($message) $note .= "\n" . $message;
625 $noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
626 // Link the new note to the document.
627 setGpRelation(1, $this->get_id(), 6, $noteid);
630 } // end of Document