Tag documents to procedures (#4465)
[openemr.git] / library / classes / Document.class.php
blob9b6c327bf5a8176ed97698bb4caf975adec5821f
1 <?php
3 /**
4 * Document - This class is the logical representation of a physical file on some system somewhere that can be referenced with a URL
5 * 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.
6 * 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
7 * id and categories which do the same.
9 * @package openemr
10 * @link http://www.open-emr.org
11 * @author Unknown -- No ownership was listed on this document prior to February 5th 2021
12 * @author Stephen Nielson <stephen@nielson.org>
13 * @author Jerry Padgett <sjpadgett@gmail.com>
14 * @copyright OpenEMR contributors (c) 2021
15 * @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
16 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
19 require_once(__DIR__ . "/../pnotes.inc");
20 require_once(__DIR__ . "/../gprelations.inc.php");
22 use OpenEMR\Common\Acl\AclMain;
23 use OpenEMR\Common\Crypto\CryptoGen;
24 use OpenEMR\Common\ORDataObject\ORDataObject;
25 use OpenEMR\Common\Uuid\UuidRegistry;
27 class Document extends ORDataObject
30 /**
31 * Use the native filesystem to store files at
33 const STORAGE_METHOD_FILESYSTEM = 0;
35 /**
36 * Use CouchDb to store files at
38 const STORAGE_METHOD_COUCHDB = 1;
40 /**
41 * Flag that the encryption is on.
43 const ENCRYPTED_ON = 1;
45 /**
46 * Flag the encryption is off.
48 const ENCRYPTED_OFF = 0;
50 /**
51 * Date format for the expires field
53 const EXPIRES_DATE_FORMAT = 'Y-m-d H:i:s';
56 * Database unique identifier
57 * @public id
59 public $id;
62 * DB unique identifier reference to A PATIENT RECORD, this is not unique in the document table. For actual foreign
63 * keys to a NON-Patient record use foreign_reference_id. For backwards compatability we ONLY use this for patient
64 * documents.
65 * @public int
67 public $foreign_id;
69 /**
70 * DB Unique identifier reference to another table record in the database. This is not unique in the document. The
71 * table that this record points to is in the $foreign_reference_table
72 * @public int
74 public $foreign_reference_id;
76 /**
77 * Database table name for the foreign_reference_id. This value must be populated if $foreign_reference_id is
78 * populated.
79 * @public string
81 public $foreign_reference_table;
84 * Enumerated DB field which is met information about how to use the URL
85 * @public int can also be a the properly enumerated string
87 public $type;
90 * Array mapping of possible for values for the type variable
91 * mapping is array text name to index
92 * @public array
94 public $type_array = array();
97 * Size of the document in bytes if that is available
98 * @public int
100 public $size;
103 * Date the document was first persisted
104 * @public string
106 public $date;
109 * @public string at which the document can no longer be accessed.
111 public $date_expires;
114 * URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
115 * @public string
117 public $url;
120 * URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
121 * @public string
123 public $thumb_url;
126 * Mimetype of the document if available
127 * @public string
129 public $mimetype;
132 * 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
133 * @public int
135 public $pages;
138 * Foreign key identifier of who initially persisited the document,
139 * potentially ownership could be changed but that would be up to an external non-document object process
140 * @public int
142 public $owner;
145 * Timestamp of the last time the document was changed and persisted, auto maintained by DB, manually change at your own peril
146 * @public int
148 public $revision;
151 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
152 * @public string
154 public $docdate;
157 * hash key of the document from when it was uploaded.
158 * @public string
160 public $hash;
163 * DB identifier reference to the lists table (the related issue), 0 if none.
164 * @public int
166 public $list_id;
168 // For name (used in OpenEMR 6.0.0+)
169 public $name = null;
171 // For label on drive (used in OpenEMR 6.0.0+)
172 public $drive_uuid = null;
174 // For tagging with the encounter
175 public $encounter_id;
176 public $encounter_check;
179 * Whether the file is already imported
180 * @public int
182 public $imported;
185 * Whether the file is encrypted
186 * @public int
188 public $encrypted;
190 // Storage method
191 public $storagemethod;
193 // For storing couch docid
194 public $couch_docid;
196 // For storing couch revid
197 public $couch_revid;
199 // For storing path depth
200 public $path_depth;
203 * Flag that marks the document as deleted or not
204 * @public int 1 if deleted, 0 if not
206 public $deleted;
209 * Constructor sets all Document attributes to their default value
210 * @param int $id optional existing id of a specific document, if omitted a "blank" document is created
212 public function __construct($id = "")
214 //call the parent constructor so we have a _db to work with
215 parent::__construct();
217 //shore up the most basic ORDataObject bits
218 $this->id = $id;
219 $this->_table = "documents";
221 //load the enum type from the db using the parent helper function, this uses psuedo-class variables so it is really cheap
222 $this->type_array = $this->_load_enum("type");
224 $this->type = $this->type_array[0] ?? '';
225 $this->size = 0;
226 $this->date = date("Y-m-d H:i:s");
227 $this->date_expires = null; // typically no expiration date here
228 $this->url = "";
229 $this->mimetype = "";
230 $this->docdate = date("Y-m-d");
231 $this->hash = "";
232 $this->list_id = 0;
233 $this->encounter_id = 0;
234 $this->encounter_check = "";
235 $this->encrypted = 0;
236 $this->deleted = 0;
238 if ($id != "") {
239 $this->populate();
244 * Retrieves all of the categories associated with this document
246 public function get_categories()
248 if (empty($this->get_id())) {
249 return [];
252 $categories = "Select `id`, `name`, `value`, `parent`, `lft`, `rght`, `aco_spec` FROM `categories` "
253 . "JOIN `categories_to_documents` `ctd` ON `ctd`.`category_id` = `categories`.`id` "
254 . "WHERE `ctd`.`document_id` = ? ";
255 $resultSet = sqlStatement($categories, [$this->get_id()]);
256 $categories = [];
257 while ($category = sqlGetAssoc($resultSet)) {
258 $categories[] = $category;
260 return $categories;
265 * @return bool true if the document expiration date has expired
267 public function has_expired()
269 if (!empty($this->date_expires)) {
270 $dateTime = DateTime::createFromFormat("Y-m-d H:i:s", $this->date_expires);
271 return $dateTime->getTimestamp() >= time();
273 return false;
277 * Checks whether the passed in $user can access the document or not. It checks against all of the access
278 * permissions for the categories the document is in. If there are any categories that the document is tied to
279 * that the owner does NOT have access rights to, the request is denied. If there are no categories tied to the
280 * document, default access is granted.
281 * @param int|null $user The user we are checking. If no user is provided it checks against the currently logged in user
282 * @return bool True if the passed in user or current user can access this document, false otherwise.
284 public function can_access($user = null)
286 $categories = $this->get_categories();
288 // no categories to prevent access
289 if (empty($categories)) {
290 return true;
293 // verify that we can access every single category this document is tied to
294 foreach ($categories as $category) {
295 if (AclMain::aclCheckAcoSpec($category['aco_spec'], $user) === false) {
296 return false;
299 return true;
303 * Checks if a document has been deleted or not
304 * @return bool true if the document is deleted, false otherwise
306 public function is_deleted()
308 return $this->get_deleted() != 0;
312 * Handles the deletion of a document
314 public function process_deleted()
316 $this->set_deleted(1);
317 $this->persist();
321 * Returns the Document deleted value. Needed for the ORM to process this value. Recommended you use
322 * is_deleted() instead of this function
323 * @return int
325 public function get_deleted()
327 return $this->deleted;
331 * Sets the Document deleted value. Used by the ORM to set this flag.
332 * @param $deleted 1 if deleted, 0 if not
334 public function set_deleted($deleted)
336 $this->deleted = $deleted;
340 * Convenience function to get an array of many document objects that are linked to a patient
341 * For really large numbers of documents there is a way more efficient way to do this by overwriting the populate method
342 * @param int $foreign_id optional id use to limit array on to a specific relation, otherwise every document object is returned
344 function documents_factory($foreign_id = "")
346 $documents = array();
348 $sqlArray = array();
350 if (empty($foreign_id)) {
351 $foreign_id_sql = " like '%'";
352 } else {
353 $foreign_id_sql = " = ?";
354 $sqlArray[] = strval($foreign_id);
357 $d = new Document();
358 $sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_id " . $foreign_id_sql;
359 $result = $d->_db->Execute($sql, $sqlArray);
361 while ($result && !$result->EOF) {
362 $documents[] = new Document($result->fields['id']);
363 $result->MoveNext();
366 return $documents;
370 * Returns an array of many documents that are linked to a foreign table. If $foreign_reference_id is populated
371 * it will return documents that are specific that that foreign record.
372 * @param string $foreign_reference_table The table name that we are retrieving documents for
373 * @param string $foreign_reference_id The table record that this document references
374 * @return array
376 public function documents_factory_for_foreign_reference(string $foreign_reference_table, $foreign_reference_id = "")
378 $documents = array();
380 $sqlArray = array($foreign_reference_table);
382 if (empty($foreign_reference_id)) {
383 $foreign_reference_id_sql = " like '%'";
384 } else {
385 $foreign_reference_id_sql = " = ?";
386 $sqlArray[] = strval($foreign_reference_id);
389 $d = new Document();
390 $sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_reference_table = ? "
391 . "AND foreign_reference_id " . $foreign_reference_id_sql;
393 (new \OpenEMR\Common\Logging\SystemLogger())->debug("documents_factory_for_foreign_reference", ['sql' => $sql, 'sqlArray' => $sqlArray]);
395 $result = $d->_db->Execute($sql, $sqlArray);
397 while ($result && !$result->EOF) {
398 $documents[] = new Document($result->fields['id']);
399 $result->MoveNext();
402 return $documents;
406 * Return an array of documents that are connected to another table record in the system.
407 * @param int $foreign_id
408 * @return Document[]
410 public static function getDocumentsForForeignReferenceId(string $foreign_table, int $foreign_id)
412 $doc = new self();
413 return $doc->documents_factory_for_foreign_reference($foreign_table, $foreign_id);
417 * Convenience function to generate string debug data about the object
419 function toString($html = false)
421 $string .= "\n"
422 . "ID: " . $this->id . "\n"
423 . "FID: " . $this->foreign_id . "\n"
424 . "type: " . $this->type . "\n"
425 . "type_array: " . print_r($this->type_array, true) . "\n"
426 . "size: " . $this->size . "\n"
427 . "date: " . $this->date . "\n"
428 . "url: " . $this->url . "\n"
429 . "mimetype: " . $this->mimetype . "\n"
430 . "pages: " . $this->pages . "\n"
431 . "owner: " . $this->owner . "\n"
432 . "revision: " . $this->revision . "\n"
433 . "docdate: " . $this->docdate . "\n"
434 . "hash: " . $this->hash . "\n"
435 . "list_id: " . $this->list_id . "\n"
436 . "encounter_id: " . $this->encounter_id . "\n"
437 . "encounter_check: " . $this->encounter_check . "\n";
439 if ($html) {
440 return nl2br($string);
441 } else {
442 return $string;
446 /**#@+
447 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
448 * @param mixed new value for given attribute
450 function set_id($id)
452 $this->id = $id;
454 function get_id()
456 return $this->id;
460 * This is a Patient record id
461 * @param $fid Unique database identifier for a patient record
463 function set_foreign_id($fid)
465 $this->foreign_id = $fid;
469 * Sets the unique database identifier that this Document is referenced to. If unlinking this document
470 * with a foreign table you must set $reference_id and $table_name to be null
472 public function set_foreign_reference_id($reference_id)
474 $this->foreign_reference_id = $reference_id;
478 * Sets the table name that this Document references in the foreign_reference_id
479 * @param $table_name The database table name
481 public function set_foreign_reference_table($table_name)
483 $this->foreign_reference_table = $table_name;
487 * The unique database reference to another table record (Foreign Key)
488 * @return int|null
490 public function get_foreign_reference_id(): ?int
492 return $this->foreign_reference_id;
496 * Returns the database table name for the foreign reference id
497 * @return string|null
499 public function get_foreign_reference_table(): ?string
501 return $this->foreign_reference_table;
504 function get_foreign_id()
506 return $this->foreign_id;
508 function set_type($type)
510 $this->type = $type;
512 function get_type()
514 return $this->type;
516 function set_size($size)
518 $this->size = $size;
520 function get_size()
522 return $this->size;
524 function set_date($date)
526 $this->date = $date;
528 function get_date()
530 return $this->date;
534 * @return string|null The datetime that the document expires at
536 function get_date_expires(): ?string
538 return $this->date_expires;
540 function set_hash($hash)
542 $this->hash = $hash;
544 function get_hash()
546 return $this->hash;
548 function get_hash_algo_title()
550 if (!empty($this->hash) && strlen($this->hash) < 50) {
551 return "SHA1";
552 } else {
553 return "SHA3-512";
556 function set_url($url)
558 $this->url = $url;
560 function get_url()
562 return $this->url;
564 function set_thumb_url($url)
566 $this->thumb_url = $url;
568 function get_thumb_url()
570 return $this->thumb_url;
573 * get the url without the protocol handler
575 function get_url_filepath()
577 return preg_replace("|^(.*)://|", "", $this->url);
581 * OpenEMR installation media can be moved to other instances, to get the real filesystem path we use this method.
582 * If the document is a couch db document this will return null;
584 protected function get_filesystem_filepath()
586 if ($this->get_storagemethod() === self::STORAGE_METHOD_COUCHDB) {
587 return null;
589 //change full path to current webroot. this is for documents that may have
590 //been moved from a different filesystem and the full path in the database
591 //is not current. this is also for documents that may of been moved to
592 //different patients. Note that the path_depth is used to see how far down
593 //the path to go. For example, originally the path_depth was always 1, which
594 //only allowed things like documents/1/<file>, but now can have more structured
595 //directories. For example a path_depth of 2 can give documents/encounters/1/<file>
596 // etc.
597 // NOTE that $from_filename and basename($url) are the same thing
598 $filepath = $this->get_url_filepath();
599 $from_all = explode("/", $filepath);
600 $from_filename = array_pop($from_all);
601 $from_pathname_array = array();
602 for ($i = 0; $i < $this->get_path_depth(); $i++) {
603 $from_pathname_array[] = array_pop($from_all);
605 $from_pathname_array = array_reverse($from_pathname_array);
606 $from_pathname = implode("/", $from_pathname_array);
607 $filepath = $GLOBALS['OE_SITE_DIR'] . '/documents/' . $from_pathname . '/' . $from_filename;
608 return $filepath;
611 * get the url filename only
613 function get_url_file()
615 return basename_international(preg_replace("|^(.*)://|", "", $this->url));
618 * get the url path only
620 function get_url_path()
622 return dirname(preg_replace("|^(.*)://|", "", $this->url)) . "/";
624 function get_path_depth()
626 return $this->path_depth;
628 function set_path_depth($path_depth)
630 $this->path_depth = $path_depth;
632 function set_mimetype($mimetype)
634 $this->mimetype = $mimetype;
636 function get_mimetype()
638 return $this->mimetype;
640 function set_pages($pages)
642 $this->pages = $pages;
644 function get_pages()
646 return $this->pages;
648 function set_owner($owner)
650 $this->owner = $owner;
652 function get_owner()
654 return $this->owner;
657 * No getter for revision because it is updated automatically by the DB.
659 function set_revision($revision)
661 $this->revision = $revision;
663 function set_docdate($docdate)
665 $this->docdate = $docdate;
667 function get_docdate()
669 return $this->docdate;
671 function set_list_id($list_id)
673 $this->list_id = $list_id;
675 function get_list_id()
677 return $this->list_id;
679 function set_name($name)
681 $this->name = $name;
683 function get_name()
685 return $this->name;
687 function set_drive_uuid($drive_uuid)
689 $this->drive_uuid = $drive_uuid;
691 function get_drive_uuid()
693 return $this->drive_uuid;
695 function set_encounter_id($encounter_id)
697 $this->encounter_id = $encounter_id;
699 function get_encounter_id()
701 return $this->encounter_id;
703 function set_encounter_check($encounter_check)
705 $this->encounter_check = $encounter_check;
707 function get_encounter_check()
709 return $this->encounter_check;
712 function get_ccr_type($doc_id)
714 $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));
715 return $type['name'];
717 function set_imported($imported)
719 $this->imported = $imported;
721 function get_imported()
723 return $this->imported;
725 function update_imported($doc_id)
727 sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?", array($doc_id));
729 function set_encrypted($encrypted)
731 $this->encrypted = $encrypted;
733 function get_encrypted()
735 return $this->encrypted;
737 function is_encrypted()
739 return $this->encrypted == self::ENCRYPTED_ON;
742 * Overridden function to stor current object state in the db.
743 * current overide is to allow for a just in time foreign id, often this is needed
744 * when the object is never directly exposed and is handled as part of a larger
745 * object hierarchy.
746 * @param int $fid foreign id that should be used so that this document can be related (joined) on it later
749 function persist($fid = "")
751 if (!empty($fid)) {
752 $this->foreign_id = $fid;
755 parent::persist();
758 function set_storagemethod($str)
760 $this->storagemethod = $str;
763 function get_storagemethod()
765 return $this->storagemethod;
768 function set_couch_docid($str)
770 $this->couch_docid = $str;
773 function get_couch_docid()
775 return $this->couch_docid;
778 function set_couch_revid($str)
780 $this->couch_revid = $str;
783 function get_couch_revid()
785 return $this->couch_revid;
788 // Function added by Rod to change the patient associated with a document.
789 // This just moves some code that used to be in C_Document.class.php,
790 // changing it as little as possible since I'm not set up to test it.
792 function change_patient($new_patient_id)
794 // Set the new patient.
795 $this->set_foreign_id($new_patient_id);
796 $this->persist();
798 // Return true for success.
799 return true;
803 * Create a new document and store its data.
804 * This is a mix of new code and code moved from C_Document.class.php.
806 * @param string $patient_id Patient pid; if not known then this may be a simple directory name
807 * @param integer $category_id The desired document category ID
808 * @param string $filename The desired filename
809 * @param string $mimetype MIME type
810 * @param string &$data The actual data to store (not encoded)
811 * @param string $higher_level_path Optional subdirectory within the local document repository
812 * @param string $path_depth Number of directory levels in $higher_level_path, if specified
813 * @param integer $owner Owner/user/service that is requesting this action
814 * @param string $tmpfile The tmp location of file (require for thumbnail generator)
815 * @param string $date_expires The datetime that the document should no longer be accessible in the system
816 * @param string $foreign_reference_id The table id to another table record in OpenEMR
817 * @param string $foreign_reference_table The table name of the foreign_reference_id this document refers to.
818 * @return string Empty string if success, otherwise error message text
820 function createDocument(
821 $patient_id,
822 $category_id,
823 $filename,
824 $mimetype,
825 &$data,
826 $higher_level_path = '',
827 $path_depth = 1,
828 $owner = 0,
829 $tmpfile = null,
830 $date_expires = null,
831 $foreign_reference_id = null,
832 $foreign_reference_table = null
834 if (
835 !empty($foreign_reference_id) && empty($foreign_reference_table)
836 || empty($foreign_reference_id) && !empty($foreign_reference_table)
838 return xl('Reference table and reference id must both be set');
840 $this->set_foreign_reference_id($foreign_reference_id);
841 $this->set_foreign_reference_table($foreign_reference_table);
842 // The original code used the encounter ID but never set it to anything.
843 // That was probably a mistake, but we reference it here for documentation
844 // and leave it empty. Logically, documents are not tied to encounters.
846 // Create a crypto object that will be used for for encryption/decryption
847 $cryptoGen = new CryptoGen();
849 if ($GLOBALS['generate_doc_thumb']) {
850 $thumb_size = ($GLOBALS['thumb_doc_max_size'] > 0) ? $GLOBALS['thumb_doc_max_size'] : null;
851 $thumbnail_class = new Thumbnail($thumb_size);
853 if (!is_null($tmpfile)) {
854 $has_thumbnail = $thumbnail_class->file_support_thumbnail($tmpfile);
855 } else {
856 $has_thumbnail = false;
859 if ($has_thumbnail) {
860 $thumbnail_resource = $thumbnail_class->create_thumbnail(null, $data);
861 if ($thumbnail_resource) {
862 $thumbnail_data = $thumbnail_class->get_string_file($thumbnail_resource);
863 } else {
864 $has_thumbnail = false;
867 } else {
868 $has_thumbnail = false;
871 $encounter_id = '';
872 $this->storagemethod = $GLOBALS['document_storage_method'];
873 $this->mimetype = $mimetype;
874 if ($this->storagemethod == self::STORAGE_METHOD_COUCHDB) {
875 // Store it using CouchDB.
876 if ($GLOBALS['couchdb_encryption']) {
877 $document = $cryptoGen->encryptStandard($data, null, 'database');
878 } else {
879 $document = base64_encode($data);
881 if ($has_thumbnail) {
882 if ($GLOBALS['couchdb_encryption']) {
883 $th_document = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
884 } else {
885 $th_document = base64_encode($thumbnail_data);
887 $this->thumb_url = $this->get_thumb_name($filename);
888 } else {
889 $th_document = false;
892 $couch = new CouchDB();
893 $docid = $couch->createDocId('documents');
894 if (!empty($th_document)) {
895 $couchdata = ['_id' => $docid, 'data' => $document, 'th_data' => $th_document];
896 } else {
897 $couchdata = ['_id' => $docid, 'data' => $document];
899 $resp = $couch->save_doc($couchdata);
900 if (!$resp->id || !$resp->rev) {
901 return xl('CouchDB save failed');
902 } else {
903 $docid = $resp->id;
904 $revid = $resp->rev;
907 $this->url = $filename;
908 $this->couch_docid = $docid;
909 $this->couch_revid = $revid;
910 } else {
911 // Storing document files locally.
912 $repository = $GLOBALS['oer_config']['documents']['repository'];
913 $higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
914 if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
915 // Allow higher level directory structure in documents directory and a patient is mapped.
916 $filepath = $repository . $higher_level_path . "/";
917 } elseif (!empty($higher_level_path)) {
918 // Allow higher level directory structure in documents directory and there is no patient mapping
919 // (will create up to 10000 random directories and increment the path_depth by 1).
920 $filepath = $repository . $higher_level_path . '/' . rand(1, 10000) . '/';
921 ++$path_depth;
922 } elseif (!(is_numeric($patient_id)) || !($patient_id > 0)) {
923 // This is the default action except there is no patient mapping (when patient_id is 00 or direct)
924 // (will create up to 10000 random directories and set the path_depth to 2).
925 $filepath = $repository . $patient_id . '/' . rand(1, 10000) . '/';
926 $path_depth = 2;
927 $patient_id = 0;
928 } else {
929 // This is the default action where the patient is used as one level directory structure in documents directory.
930 $filepath = $repository . $patient_id . '/';
931 $path_depth = 1;
934 if (!file_exists($filepath)) {
935 if (!mkdir($filepath, 0700, true)) {
936 return xl('Unable to create patient document subdirectory');
940 // collect the drive storage filename
941 $this->drive_uuid = (new UuidRegistry(['document_drive' => true]))->createUuid();
942 $filenameUuid = UuidRegistry::uuidToString($this->drive_uuid);
944 $this->url = "file://" . $filepath . $filenameUuid;
945 if (is_numeric($path_depth)) {
946 // this is for when directory structure is more than one level
947 $this->path_depth = $path_depth;
950 // Store the file.
951 if ($GLOBALS['drive_encryption']) {
952 $storedData = $cryptoGen->encryptStandard($data, null, 'database');
953 } else {
954 $storedData = $data;
956 if (file_exists($filepath . $filenameUuid)) {
957 // this should never happend with current uuid mechanism
958 return xl('Failed since file already exists') . " $filepath$filenameUuid";
960 if (file_put_contents($filepath . $filenameUuid, $storedData) === false) {
961 return xl('Failed to create') . " $filepath$filenameUuid";
964 if ($has_thumbnail) {
965 // Store the thumbnail.
966 $this->thumb_url = "file://" . $filepath . $this->get_thumb_name($filenameUuid);
967 if ($GLOBALS['drive_encryption']) {
968 $storedThumbnailData = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
969 } else {
970 $storedThumbnailData = $thumbnail_data;
972 if (file_exists($filepath . $this->get_thumb_name($filenameUuid))) {
973 // this should never happend with current uuid mechanism
974 return xl('Failed since file already exists') . $filepath . $this->get_thumb_name($filenameUuid);
976 if (file_put_contents($filepath . $this->get_thumb_name($filenameUuid), $storedThumbnailData) === false) {
977 return xl('Failed to create') . $filepath . $this->get_thumb_name($filenameUuid);
982 if (($GLOBALS['drive_encryption'] && ($this->storagemethod != 1)) || ($GLOBALS['couchdb_encryption'] && ($this->storagemethod == 1))) {
983 $this->set_encrypted(self::ENCRYPTED_ON);
984 } else {
985 $this->set_encrypted(self::ENCRYPTED_OFF);
987 $this->name = $filename;
988 $this->size = strlen($data);
989 $this->hash = hash('sha3-512', $data);
990 $this->type = $this->type_array['file_url'];
991 $this->owner = $owner ? $owner : $_SESSION['authUserID'];
992 $this->date_expires = $date_expires;
993 $this->set_foreign_id($patient_id);
994 $this->persist();
995 $this->populate();
996 if (is_numeric($this->get_id()) && is_numeric($category_id)) {
997 $sql = "REPLACE INTO categories_to_documents SET category_id = ?, document_id = ?";
998 $this->_db->Execute($sql, array($category_id, $this->get_id()));
1001 return '';
1005 * Retrieves the document data that has been saved to the filesystem or couch db. If the $force_no_decrypt flag is
1006 * set to true, it will return the encrypted version of the data for the document.
1007 * @param bool $force_no_decrypt True if the document should have its data returned encrypted, false otherwise
1008 * @throws BadMethodCallException Thrown if the method is called when the document has been marked as deleted or expired
1009 * @return false|string Returns false if the data failed to decrypt, or a string if the data decrypts or is unencrypted.
1011 function get_data($force_no_decrypt = false)
1013 $storagemethod = $this->get_storagemethod();
1015 if ($this->has_expired()) {
1016 throw new BadMethodCallException("Should not attempt to retrieve data from expired documents");
1018 if ($this->is_deleted()) {
1019 throw new BadMethodCallException("Should not attempt to retrieve data from deleted documents");
1022 $base64Decode = false;
1024 if ($storagemethod === self::STORAGE_METHOD_COUCHDB) {
1025 // encrypting does not use base64 encoding
1026 if (!$this->is_encrypted()) {
1027 $base64Decode = true;
1029 // Taken from ccr/display.php
1030 $couch_docid = $this->get_couch_docid();
1031 $couch_revid = $this->get_couch_revid();
1032 $couch = new CouchDB();
1033 $resp = $couch->retrieve_doc($couch_docid);
1034 $data = $resp->data;
1035 } else {
1036 $data = $this->get_content_from_filesystem();
1039 if (!empty($data)) {
1040 if ($this->is_encrypted() && !$force_no_decrypt) {
1041 $data = $this->decrypt_content($data);
1043 if ($base64Decode) {
1044 $data = base64_decode($data);
1047 return $data;
1051 * Given a document data contents it decrypts the document data
1052 * @param $data The data that needs to be decrypted
1053 * @return string Returns false if the encryption failed, otherwise it returns a string
1054 * @throws RuntimeException If the data cannot be decrypted
1056 public function decrypt_content($data)
1058 $cryptoGen = new CryptoGen();
1059 $decryptedData = $cryptoGen->decryptStandard($data, null, 'database');
1060 if ($decryptedData === false) {
1061 throw new RuntimeException("Failed to decrypt the data");
1063 return $decryptedData;
1067 * Returns the content from the filesystem for this document
1068 * @return string
1069 * @throws BadMethodCallException If you attempt to retrieve a document that is not stored on the file system
1070 * @throws RuntimeException if the filesystem file does not exist or content cannot be accessed.
1072 protected function get_content_from_filesystem()
1074 $path = $this->get_filesystem_filepath();
1075 if (empty($path)) {
1076 throw new BadMethodCallException("Attempted to retrieve the content from the filesystem for a file that uses a different storage mechanism");
1078 if (!file_exists($path)) {
1079 throw new RuntimeException("Saved filepath does not exist at location " . $path);
1081 $data = file_get_contents($path);
1082 if ($data === false) {
1083 throw new RuntimeException("The data could not be retrieved for the file at " . $path . " Check that access rights to the file have been granted");
1085 return $data;
1089 * Return file name for thumbnail (adding 'th_')
1091 function get_thumb_name($file_name)
1093 return 'th_' . $file_name;
1097 * Post a patient note that is linked to this document.
1099 * @param string $provider Login name of the provider to receive this note.
1100 * @param integer $category_id The desired document category ID
1101 * @param string $message Any desired message text for the note.
1103 function postPatientNote($provider, $category_id, $message = '')
1105 // Build note text in a way that identifies the new document.
1106 // See pnotes_full.php which uses this to auto-display the document.
1107 $note = $this->get_url_file();
1108 for ($tmp = $category_id; $tmp;) {
1109 $catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
1110 $note = $catrow['name'] . "/$note";
1111 $tmp = $catrow['parent'];
1114 $note = "New scanned document " . $this->get_id() . ": $note";
1115 if ($message) {
1116 $note .= "\n" . $message;
1119 $noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
1120 // Link the new note to the document.
1121 setGpRelation(1, $this->get_id(), 6, $noteid);
1125 * Return note objects associated with this document using Note::notes_factory
1128 function get_notes()
1130 return (Note::notes_factory($this->get_id()));
1132 } // end of Document