Fixes #7590 wrongful use of break tag (#7624)
[openemr.git] / library / classes / Document.class.php
blob9b2772c7d047dc49e018b6a4ee2055b62998f0ef
1 <?php
3 /**
4 * Document - This class is the logical representation of a physical file on some system somewhere
5 * that can be referenced with a URL of some type. This URL is not necessarily a web url,
6 * it could be a file URL or reference to a BLOB in a db.
7 * It is implicit that a document can have other related tables to it at least a one document to many notes
8 * which join on a documents id and categories which do the same.
10 * @package openemr
11 * @link http://www.open-emr.org
12 * @author Unknown -- No ownership was listed on this document prior to February 5th 2021
13 * @author Stephen Nielson <stephen@nielson.org>
14 * @author Jerry Padgett <sjpadgett@gmail.com>
15 * @copyright OpenEMR contributors (c) 2021
16 * @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
17 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
20 require_once(__DIR__ . "/../pnotes.inc.php");
21 require_once(__DIR__ . "/../gprelations.inc.php");
23 use OpenEMR\Common\Acl\AclMain;
24 use OpenEMR\Common\Crypto\CryptoGen;
25 use OpenEMR\Common\ORDataObject\ORDataObject;
26 use OpenEMR\Common\Uuid\UuidRegistry;
27 use OpenEMR\Events\PatientDocuments\PatientDocumentStoreOffsite;
28 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
30 class Document extends ORDataObject
32 /**
33 * @var EventDispatcherInterface $eventDispatcher
35 private $eventDispatcher;
37 public const TABLE_NAME = "documents";
39 /**
40 * Use the native filesystem to store files at
42 public const STORAGE_METHOD_FILESYSTEM = 0;
44 /**
45 * Use CouchDb to store files at
47 public const STORAGE_METHOD_COUCHDB = 1;
49 /**
50 * Flag that the encryption is on.
52 public const ENCRYPTED_ON = 1;
54 /**
55 * Flag the encryption is off.
57 public const ENCRYPTED_OFF = 0;
59 /**
60 * Date format for the expires field
62 public const EXPIRES_DATE_FORMAT = 'Y-m-d H:i:s';
65 * Database unique identifier
66 * @public id
68 public $id;
70 /**
71 * @var string Binary of Unique User Identifier that is for both external reference to this entity and for future offline use.
73 public $uuid;
76 * DB unique identifier reference to A PATIENT RECORD, this is not unique in the document table. For actual foreign
77 * keys to a NON-Patient record use foreign_reference_id. For backwards compatability we ONLY use this for patient
78 * documents.
79 * @public int
81 public $foreign_id;
83 /**
84 * DB Unique identifier reference to another table record in the database. This is not unique in the document. The
85 * table that this record points to is in the $foreign_reference_table
86 * @public int
88 public $foreign_reference_id;
90 /**
91 * Database table name for the foreign_reference_id. This value must be populated if $foreign_reference_id is
92 * populated.
93 * @public string
95 public $foreign_reference_table;
98 * Enumerated DB field which is met information about how to use the URL
99 * @public int can also be a the properly enumerated string
101 public $type;
104 * Array mapping of possible for values for the type variable
105 * mapping is array text name to index
106 * @public array
108 public $type_array = array();
111 * Size of the document in bytes if that is available
112 * @public int
114 public $size;
117 * Date the document was first persisted
118 * @public string
120 public $date;
123 * @public string at which the document can no longer be accessed.
125 public $date_expires;
128 * URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
129 * @public string
131 public $url;
134 * URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
135 * @public string
137 public $thumb_url;
140 * Mimetype of the document if available
141 * @public string
143 public $mimetype;
146 * If the document is a multi-page format like tiff and has at least 1 page this will be 1 or greater,
147 * if a non-multi-page format this should be null or empty
148 * @public int
150 public $pages;
153 * Foreign key identifier of who initially persisited the document,
154 * potentially ownership could be changed but that would be up to an external non-document object process
155 * @public int
157 public $owner;
160 * Timestamp of the last time the document was changed and persisted, auto maintained by DB,
161 * manually change at your own peril
162 * @public int
164 public $revision;
167 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
168 * @public string
170 public $docdate;
173 * hash key of the document from when it was uploaded.
174 * @public string
176 public $hash;
179 * DB identifier reference to the lists table (the related issue), 0 if none.
180 * @public int
182 public $list_id;
184 // For name (used in OpenEMR 6.0.0+)
185 public $name = null;
187 // For label on drive (used in OpenEMR 6.0.0+)
188 public $drive_uuid = null;
190 // For tagging with the encounter
191 public $encounter_id;
192 public $encounter_check;
195 * Whether the file is already imported
196 * @public int
198 public $imported;
201 * Whether the file is encrypted
202 * @public int
204 public $encrypted;
206 // Storage method
207 public $storagemethod;
209 // For storing couch docid
210 public $couch_docid;
212 // For storing couch revid
213 public $couch_revid;
215 // For storing path depth
216 public $path_depth;
219 * Flag that marks the document as deleted or not
220 * @public int 1 if deleted, 0 if not
222 public $deleted;
225 * Constructor sets all Document attributes to their default value
226 * @param int $id optional existing id of a specific document, if omitted a "blank" document is created
228 public function __construct($id = "")
230 //call the parent constructor so we have a _db to work with
231 parent::__construct();
233 //shore up the most basic ORDataObject bits
234 $this->id = $id;
235 $this->_table = self::TABLE_NAME;
237 //load the enum type from the db using the parent helper function,
238 //this uses psuedo-class variables so it is really cheap
239 $this->type_array = $this->_load_enum("type");
241 $this->type = $this->type_array[0] ?? '';
242 $this->size = 0;
243 $this->date = date("Y-m-d H:i:s");
244 $this->date_expires = null; // typically no expiration date here
245 $this->url = "";
246 $this->mimetype = "";
247 $this->docdate = date("Y-m-d");
248 $this->hash = "";
249 $this->list_id = 0;
250 $this->encounter_id = 0;
251 $this->encounter_check = "";
252 $this->encrypted = 0;
253 $this->deleted = 0;
255 if ($id != "") {
256 $this->populate();
259 $this->eventDispatcher = $GLOBALS['kernel']->getEventDispatcher();
263 * Retrieves all of the categories associated with this document
265 public function get_categories()
267 if (empty($this->get_id())) {
268 return [];
271 $categories = "Select `id`, `name`, `value`, `parent`, `lft`, `rght`, `aco_spec`,`codes` FROM `categories` "
272 . "JOIN `categories_to_documents` `ctd` ON `ctd`.`category_id` = `categories`.`id` "
273 . "WHERE `ctd`.`document_id` = ? ";
274 $resultSet = sqlStatement($categories, [$this->get_id()]);
275 $categories = [];
276 while ($category = sqlFetchArray($resultSet)) {
277 $categories[] = $category;
279 return $categories;
284 * @return bool true if the document expiration date has expired
286 public function has_expired()
288 if (!empty($this->date_expires)) {
289 $dateTime = DateTime::createFromFormat("Y-m-d H:i:s", $this->date_expires);
290 return $dateTime->getTimestamp() >= time();
292 return false;
295 public function can_patient_access($pid)
297 $foreignId = $this->get_foreign_id();
298 // TODO: if any information blocking rule checks were to be applied, they can be done here
299 if (!empty($foreignId) && $foreignId == $pid) {
300 return true;
302 return false;
306 * Checks whether the passed in $user can access the document or not. It checks against all of the access
307 * permissions for the categories the document is in. If there are any categories that the document is tied to
308 * that the owner does NOT have access rights to, the request is denied. If there are no categories tied to the
309 * document, default access is granted.
310 * @param string|null $username The user (username) we are checking.
311 * If no user is provided it checks against the currently logged in user
312 * @return bool True if the passed in user or current user can access this document, false otherwise.
314 public function can_access($username = null)
316 $categories = $this->get_categories();
318 // no categories to prevent access
319 if (empty($categories)) {
320 return true;
323 // verify that we can access every single category this document is tied to
324 foreach ($categories as $category) {
325 if (AclMain::aclCheckAcoSpec($category['aco_spec'], $username) === false) {
326 return false;
329 return true;
333 * Checks if a document has been deleted or not
334 * @return bool true if the document is deleted, false otherwise
336 public function is_deleted()
338 return $this->get_deleted() != 0;
342 * Handles the deletion of a document
344 public function process_deleted()
346 $this->set_deleted(1);
347 $this->persist();
351 * Returns the Document deleted value. Needed for the ORM to process this value. Recommended you use
352 * is_deleted() instead of this function
353 * @return int
355 public function get_deleted()
357 return $this->deleted;
361 * Sets the Document deleted value. Used by the ORM to set this flag.
362 * @param $deleted 1 if deleted, 0 if not
364 public function set_deleted($deleted)
366 $this->deleted = $deleted;
370 * Convenience function to get an array of many document objects that are linked to a patient
371 * For really large numbers of documents there is a way more efficient way to do this
372 * by overwriting the populate method
373 * @param int $foreign_id optional id use to limit array on to a specific relation,
374 * otherwise every document object is returned
376 function documents_factory($foreign_id = "")
378 $documents = array();
380 $sqlArray = array();
382 if (empty($foreign_id)) {
383 $foreign_id_sql = " like '%'";
384 } else {
385 $foreign_id_sql = " = ?";
386 $sqlArray[] = strval($foreign_id);
389 $d = new Document();
390 $sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_id " . $foreign_id_sql;
391 $result = $d->_db->Execute($sql, $sqlArray);
393 while ($result && !$result->EOF) {
394 $documents[] = new Document($result->fields['id']);
395 $result->MoveNext();
398 return $documents;
402 * Returns an array of many documents that are linked to a foreign table. If $foreign_reference_id is populated
403 * it will return documents that are specific that that foreign record.
404 * @param string $foreign_reference_table The table name that we are retrieving documents for
405 * @param string $foreign_reference_id The table record that this document references
406 * @return array
408 public function documents_factory_for_foreign_reference(string $foreign_reference_table, $foreign_reference_id = "")
410 $documents = array();
412 $sqlArray = array($foreign_reference_table);
414 if (empty($foreign_reference_id)) {
415 $foreign_reference_id_sql = " like '%'";
416 } else {
417 $foreign_reference_id_sql = " = ?";
418 $sqlArray[] = strval($foreign_reference_id);
421 $d = new Document();
422 $sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_reference_table = ? "
423 . "AND foreign_reference_id " . $foreign_reference_id_sql;
425 (new \OpenEMR\Common\Logging\SystemLogger())->debug(
426 "documents_factory_for_foreign_reference",
427 ['sql' => $sql,
428 'sqlArray' => $sqlArray]
431 $result = $d->_db->Execute($sql, $sqlArray);
433 while ($result && !$result->EOF) {
434 $documents[] = new Document($result->fields['id']);
435 $result->MoveNext();
438 return $documents;
440 public static function getDocumentForUuid($uuid)
442 $sql = "SELECT id from " . escape_table_name(self::TABLE_NAME) . " WHERE uuid = ?";
443 $id = \OpenEMR\Common\Database\QueryUtils::fetchSingleValue($sql, 'id', [UuidRegistry::uuidToBytes($uuid)]);
444 if (!empty($id)) {
445 return new Document($id);
446 } else {
447 return null;
452 * Returns all of the documents for a specific patient
453 * @param int $patient_id
454 * @return array
456 public static function getDocumentsForPatient(int $patient_id)
458 $doc = new Document();
459 return $doc->documents_factory($patient_id);
463 * Return an array of documents that are connected to another table record in the system.
464 * @param int $foreign_id
465 * @return Document[]
467 public static function getDocumentsForForeignReferenceId(string $foreign_table, int $foreign_id)
469 $doc = new self();
470 return $doc->documents_factory_for_foreign_reference($foreign_table, $foreign_id);
474 * Convenience function to generate string debug data about the object
476 function toString($html = false)
478 $string .= "\n"
479 . "ID: " . $this->id . "\n"
480 . "FID: " . $this->foreign_id . "\n"
481 . "type: " . $this->type . "\n"
482 . "type_array: " . print_r($this->type_array, true) . "\n"
483 . "size: " . $this->size . "\n"
484 . "date: " . $this->date . "\n"
485 . "url: " . $this->url . "\n"
486 . "mimetype: " . $this->mimetype . "\n"
487 . "pages: " . $this->pages . "\n"
488 . "owner: " . $this->owner . "\n"
489 . "revision: " . $this->revision . "\n"
490 . "docdate: " . $this->docdate . "\n"
491 . "hash: " . $this->hash . "\n"
492 . "list_id: " . $this->list_id . "\n"
493 . "encounter_id: " . $this->encounter_id . "\n"
494 . "encounter_check: " . $this->encounter_check . "\n";
496 if ($html) {
497 return nl2br($string);
498 } else {
499 return $string;
503 /**#@+
504 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
505 * @param mixed new value for given attribute
507 function set_id($id)
509 $this->id = $id;
511 function get_id()
513 return $this->id;
517 * This is a Patient record id
518 * @param $fid Unique database identifier for a patient record
520 function set_foreign_id($fid)
522 $this->foreign_id = $fid;
526 * Sets the unique database identifier that this Document is referenced to. If unlinking this document
527 * with a foreign table you must set $reference_id and $table_name to be null
529 public function set_foreign_reference_id($reference_id)
531 $this->foreign_reference_id = $reference_id;
535 * Sets the table name that this Document references in the foreign_reference_id
536 * @param $table_name The database table name
538 public function set_foreign_reference_table($table_name)
540 $this->foreign_reference_table = $table_name;
544 * The unique database reference to another table record (Foreign Key)
545 * @return int|null
547 public function get_foreign_reference_id(): ?int
549 return $this->foreign_reference_id;
553 * Returns the database table name for the foreign reference id
554 * @return string|null
556 public function get_foreign_reference_table(): ?string
558 return $this->foreign_reference_table;
561 function get_foreign_id()
563 return $this->foreign_id;
565 function set_type($type)
567 $this->type = $type;
569 function get_type()
571 return $this->type;
573 function set_size($size)
575 $this->size = $size;
577 function get_size()
579 return $this->size;
581 function set_date($date)
583 $this->date = $date;
585 function get_date()
587 return $this->date;
591 * @return string|null The datetime that the document expires at
593 function get_date_expires(): ?string
595 return $this->date_expires;
597 function set_hash($hash)
599 $this->hash = $hash;
601 function get_hash()
603 return $this->hash;
605 function get_hash_algo_title()
607 if (!empty($this->hash) && strlen($this->hash) < 50) {
608 return "SHA1";
609 } else {
610 return "SHA3-512";
613 function set_url($url)
615 $this->url = $url;
617 function get_url()
619 return $this->url;
621 function set_thumb_url($url)
623 $this->thumb_url = $url;
625 function get_thumb_url()
627 return $this->thumb_url;
630 * get the url without the protocol handler
632 function get_url_filepath()
634 return preg_replace("|^(.*)://|", "", $this->url);
638 * OpenEMR installation media can be moved to other instances, to get the real filesystem path we use this method.
639 * If the document is a couch db document this will return null;
641 protected function get_filesystem_filepath()
643 if ($this->get_storagemethod() === self::STORAGE_METHOD_COUCHDB) {
644 return null;
646 //change full path to current webroot. this is for documents that may have
647 //been moved from a different filesystem and the full path in the database
648 //is not current. this is also for documents that may of been moved to
649 //different patients. Note that the path_depth is used to see how far down
650 //the path to go. For example, originally the path_depth was always 1, which
651 //only allowed things like documents/1/<file>, but now can have more structured
652 //directories. For example a path_depth of 2 can give documents/encounters/1/<file>
653 // etc.
654 // NOTE that $from_filename and basename($url) are the same thing
655 $filepath = $this->get_url_filepath();
656 $from_all = explode("/", $filepath);
657 $from_filename = array_pop($from_all);
658 $from_pathname_array = array();
659 for ($i = 0; $i < $this->get_path_depth(); $i++) {
660 $from_pathname_array[] = array_pop($from_all);
662 $from_pathname_array = array_reverse($from_pathname_array);
663 $from_pathname = implode("/", $from_pathname_array);
664 $filepath = $GLOBALS['OE_SITE_DIR'] . '/documents/' . $from_pathname . '/' . $from_filename;
665 return $filepath;
668 * get the url filename only
670 function get_url_file()
672 return basename_international(preg_replace("|^(.*)://|", "", $this->url));
675 * get the url path only
677 function get_url_path()
679 return dirname(preg_replace("|^(.*)://|", "", $this->url)) . "/";
681 function get_path_depth()
683 return $this->path_depth;
685 function set_path_depth($path_depth)
687 $this->path_depth = $path_depth;
689 function set_mimetype($mimetype)
691 $this->mimetype = $mimetype;
693 function get_mimetype()
695 return $this->mimetype;
697 function set_pages($pages)
699 $this->pages = $pages;
701 function get_pages()
703 return $this->pages;
705 function set_owner($owner)
707 $this->owner = $owner;
709 function get_owner()
711 return $this->owner;
714 * No getter for revision because it is updated automatically by the DB.
716 function set_revision($revision)
718 $this->revision = $revision;
720 function set_docdate($docdate)
722 $this->docdate = $docdate;
724 function get_docdate()
726 return $this->docdate;
728 function set_list_id($list_id)
730 $this->list_id = $list_id;
732 function get_list_id()
734 return $this->list_id;
736 function set_name($name)
738 $this->name = $name;
742 * Returns the database human readable filename of the document
743 * @return string|null
745 function get_name()
747 return $this->name;
749 function set_drive_uuid($drive_uuid)
751 $this->drive_uuid = $drive_uuid;
753 function get_drive_uuid()
755 return $this->drive_uuid;
757 function set_encounter_id($encounter_id)
759 $this->encounter_id = $encounter_id;
761 function get_encounter_id()
763 return $this->encounter_id;
765 function set_encounter_check($encounter_check)
767 $this->encounter_check = $encounter_check;
769 function get_encounter_check()
771 return $this->encounter_check;
774 function get_ccr_type($doc_id)
776 $type = sqlQuery(
777 "SELECT c.name FROM categories AS c
778 LEFT JOIN categories_to_documents AS ctd ON c.id = ctd.category_id
779 WHERE ctd.document_id = ?",
780 array($doc_id)
782 return $type['name'];
784 function set_imported($imported)
786 $this->imported = $imported;
788 function get_imported()
790 return $this->imported;
792 function update_imported($doc_id)
794 sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?", array($doc_id));
796 function set_encrypted($encrypted)
798 $this->encrypted = $encrypted;
800 function get_encrypted()
802 return $this->encrypted;
804 function is_encrypted()
806 return $this->encrypted == self::ENCRYPTED_ON;
809 * Overridden function to stor current object state in the db.
810 * current overide is to allow for a just in time foreign id, often this is needed
811 * when the object is never directly exposed and is handled as part of a larger
812 * object hierarchy.
813 * @param int $fid foreign id that should be used so that this document can be related (joined) on it later
816 function persist($fid = "")
818 if (!empty($fid)) {
819 $this->foreign_id = $fid;
822 // need to populate our uuid if its empty
824 parent::persist();
827 function set_storagemethod($str)
829 $this->storagemethod = $str;
832 function get_storagemethod()
834 return $this->storagemethod;
837 function set_couch_docid($str)
839 $this->couch_docid = $str;
842 function get_couch_docid()
844 return $this->couch_docid;
847 function set_couch_revid($str)
849 $this->couch_revid = $str;
852 function get_couch_revid()
854 return $this->couch_revid;
857 function set_uuid(?string $uuid)
859 $this->uuid = $uuid;
863 * @return string Binary representation of the uuid for this document
865 function get_uuid()
867 return $this->uuid;
870 // Function added by Rod to change the patient associated with a document.
871 // This just moves some code that used to be in C_Document.class.php,
872 // changing it as little as possible since I'm not set up to test it.
874 function change_patient($new_patient_id)
876 // Set the new patient.
877 $this->set_foreign_id($new_patient_id);
878 $this->persist();
880 // Return true for success.
881 return true;
885 * Create a new document and store its data.
886 * This is a mix of new code and code moved from C_Document.class.php.
888 * @param string $patient_id Patient pid; if not known then this may be a simple directory name
889 * @param integer $category_id The desired document category ID
890 * @param string $filename The desired filename
891 * @param string $mimetype MIME type
892 * @param string &$data The actual data to store (not encoded)
893 * @param string $higher_level_path Optional subdirectory within the local document repository
894 * @param string $path_depth Number of directory levels in $higher_level_path, if specified
895 * @param integer $owner Owner/user/service that is requesting this action
896 * @param string $tmpfile The tmp location of file (require for thumbnail generator)
897 * @param string $date_expires The datetime that the document should no longer be accessible in the system
898 * @param string $foreign_reference_id The table id to another table record in OpenEMR
899 * @param string $foreign_reference_table The table name of the foreign_reference_id this document refers to.
900 * @return string Empty string if success, otherwise error message text
902 function createDocument(
903 $patient_id,
904 $category_id,
905 $filename,
906 $mimetype,
907 &$data,
908 $higher_level_path = '',
909 $path_depth = 1,
910 $owner = 0,
911 $tmpfile = null,
912 $date_expires = null,
913 $foreign_reference_id = null,
914 $foreign_reference_table = null
916 if (
917 !empty($foreign_reference_id) && empty($foreign_reference_table)
918 || empty($foreign_reference_id) && !empty($foreign_reference_table)
920 return xl('Reference table and reference id must both be set');
922 $this->set_foreign_reference_id($foreign_reference_id);
923 $this->set_foreign_reference_table($foreign_reference_table);
924 // The original code used the encounter ID but never set it to anything.
925 // That was probably a mistake, but we reference it here for documentation
926 // and leave it empty. Logically, documents are not tied to encounters.
928 // Create a crypto object that will be used for for encryption/decryption
929 $cryptoGen = new CryptoGen();
931 if ($GLOBALS['generate_doc_thumb']) {
932 $thumb_size = ($GLOBALS['thumb_doc_max_size'] > 0) ? $GLOBALS['thumb_doc_max_size'] : null;
933 $thumbnail_class = new Thumbnail($thumb_size);
935 if (!is_null($tmpfile)) {
936 $has_thumbnail = $thumbnail_class->file_support_thumbnail($tmpfile);
937 } else {
938 $has_thumbnail = false;
941 if ($has_thumbnail) {
942 $thumbnail_resource = $thumbnail_class->create_thumbnail(null, $data);
943 if ($thumbnail_resource) {
944 $thumbnail_data = $thumbnail_class->get_string_file($thumbnail_resource);
945 } else {
946 $has_thumbnail = false;
949 } else {
950 $has_thumbnail = false;
953 $encounter_id = '';
954 $this->storagemethod = $GLOBALS['document_storage_method'];
955 $this->mimetype = $mimetype;
956 if ($this->storagemethod == self::STORAGE_METHOD_COUCHDB) {
957 // Store it using CouchDB.
958 if ($GLOBALS['couchdb_encryption']) {
959 $document = $cryptoGen->encryptStandard($data, null, 'database');
960 } else {
961 $document = base64_encode($data);
963 if ($has_thumbnail) {
964 if ($GLOBALS['couchdb_encryption']) {
965 $th_document = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
966 } else {
967 $th_document = base64_encode($thumbnail_data);
969 $this->thumb_url = $this->get_thumb_name($filename);
970 } else {
971 $th_document = false;
974 $couch = new CouchDB();
975 $docid = $couch->createDocId('documents');
976 if (!empty($th_document)) {
977 $couchdata = ['_id' => $docid, 'data' => $document, 'th_data' => $th_document];
978 } else {
979 $couchdata = ['_id' => $docid, 'data' => $document];
981 $resp = $couch->save_doc($couchdata);
982 if (!$resp->id || !$resp->rev) {
983 return xl('CouchDB save failed');
984 } else {
985 $docid = $resp->id;
986 $revid = $resp->rev;
989 $this->url = $filename;
990 $this->couch_docid = $docid;
991 $this->couch_revid = $revid;
992 } else {
993 // Store it remotely.
994 $offSiteUpload = new PatientDocumentStoreOffsite($data);
995 $offSiteUpload->setPatientId($patient_id) ?? '';
996 $offSiteUpload->setRemoteFileName($filename) ?? '';
997 $offSiteUpload->setRemoteMimeType($mimetype) ?? '';
998 $offSiteUpload->setRemoteCategory($category_id) ?? '';
1000 * There must be a return to terminate processing.
1002 $this->eventDispatcher->dispatch($offSiteUpload, PatientDocumentStoreOffsite::REMOTE_STORAGE_LOCATION);
1005 * If the response from the listener is true then the file was uploaded to another location.
1006 * Else resume the local file storage
1009 if ($GLOBALS['documentStoredRemotely'] ?? '') {
1010 return xlt("Document was uploaded to remote storage"); // terminate processing
1013 // Storing document files locally.
1014 $repository = $GLOBALS['oer_config']['documents']['repository'];
1015 $higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
1016 if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
1017 // Allow higher level directory structure in documents directory and a patient is mapped.
1018 $filepath = $repository . $higher_level_path . "/";
1019 } elseif (!empty($higher_level_path)) {
1020 // Allow higher level directory structure in documents directory and there is no patient mapping
1021 // (will create up to 10000 random directories and increment the path_depth by 1).
1022 $filepath = $repository . $higher_level_path . '/' . rand(1, 10000) . '/';
1023 ++$path_depth;
1024 } elseif (!(is_numeric($patient_id)) || !($patient_id > 0)) {
1025 // This is the default action except there is no patient mapping (when patient_id is 00 or direct)
1026 // (will create up to 10000 random directories and set the path_depth to 2).
1027 $filepath = $repository . $patient_id . '/' . rand(1, 10000) . '/';
1028 $path_depth = 2;
1029 $patient_id = 0;
1030 } else {
1031 // This is the default action where the patient is used as one level directory structure
1032 // in documents directory.
1033 $filepath = $repository . $patient_id . '/';
1034 $path_depth = 1;
1037 if (!file_exists($filepath)) {
1038 if (!mkdir($filepath, 0700, true)) {
1039 return xl('Unable to create patient document subdirectory');
1043 // collect the drive storage filename
1044 $this->drive_uuid = (new UuidRegistry(['document_drive' => true]))->createUuid();
1045 $filenameUuid = UuidRegistry::uuidToString($this->drive_uuid);
1047 $this->url = "file://" . $filepath . $filenameUuid;
1048 if (is_numeric($path_depth)) {
1049 // this is for when directory structure is more than one level
1050 $this->path_depth = $path_depth;
1053 // Store the file.
1054 if ($GLOBALS['drive_encryption']) {
1055 $storedData = $cryptoGen->encryptStandard($data, null, 'database');
1056 } else {
1057 $storedData = $data;
1059 if (file_exists($filepath . $filenameUuid)) {
1060 // this should never happen with current uuid mechanism
1061 return xl('Failed since file already exists') . " $filepath$filenameUuid";
1063 if (file_put_contents($filepath . $filenameUuid, $storedData) === false) {
1064 return xl('Failed to create') . " $filepath$filenameUuid";
1067 if ($has_thumbnail) {
1068 // Store the thumbnail.
1069 $this->thumb_url = "file://" . $filepath . $this->get_thumb_name($filenameUuid);
1070 if ($GLOBALS['drive_encryption']) {
1071 $storedThumbnailData = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
1072 } else {
1073 $storedThumbnailData = $thumbnail_data;
1075 if (file_exists($filepath . $this->get_thumb_name($filenameUuid))) {
1076 // this should never happend with current uuid mechanism
1077 return xl('Failed since file already exists') . $filepath . $this->get_thumb_name($filenameUuid);
1079 if (
1080 file_put_contents(
1081 $filepath . $this->get_thumb_name($filenameUuid),
1082 $storedThumbnailData
1083 ) === false
1085 return xl('Failed to create') . $filepath . $this->get_thumb_name($filenameUuid);
1090 if (
1091 ($GLOBALS['drive_encryption'] && ($this->storagemethod != 1))
1092 || ($GLOBALS['couchdb_encryption'] && ($this->storagemethod == 1))
1094 $this->set_encrypted(self::ENCRYPTED_ON);
1095 } else {
1096 $this->set_encrypted(self::ENCRYPTED_OFF);
1098 // we need our external unique reference identifier that can be mapped back to our table.
1099 $docUUID = (new UuidRegistry(['table_name' => $this->_table]))->createUuid();
1100 $this->set_uuid($docUUID);
1101 $this->name = $filename;
1102 $this->size = strlen($data);
1103 $this->hash = hash('sha3-512', $data);
1104 $this->type = $this->type_array['file_url'];
1105 $this->owner = $owner ? $owner : ($_SESSION['authUserID'] ?? null);
1106 $this->date_expires = $date_expires;
1107 $this->set_foreign_id($patient_id);
1108 $this->persist();
1109 $this->populate();
1110 if (is_numeric($this->get_id()) && is_numeric($category_id)) {
1111 $sql = "REPLACE INTO categories_to_documents SET category_id = ?, document_id = ?";
1112 $this->_db->Execute($sql, array($category_id, $this->get_id()));
1115 return '';
1119 * Retrieves the document data that has been saved to the filesystem or couch db. If the $force_no_decrypt flag is
1120 * set to true, it will return the encrypted version of the data for the document.
1121 * @param bool $force_no_decrypt True if the document should have its data returned encrypted, false otherwise
1122 * @throws BadMethodCallException Thrown if the method is called when the document has been marked as deleted
1123 * or expired
1124 * @return false|string Returns false if the data failed to decrypt, or a string if the data decrypts
1125 * or is unencrypted.
1127 function get_data($force_no_decrypt = false)
1129 $storagemethod = $this->get_storagemethod();
1131 if ($this->has_expired()) {
1132 throw new BadMethodCallException("Should not attempt to retrieve data from expired documents");
1134 if ($this->is_deleted()) {
1135 throw new BadMethodCallException("Should not attempt to retrieve data from deleted documents");
1138 $base64Decode = false;
1140 if ($storagemethod === self::STORAGE_METHOD_COUCHDB) {
1141 // encrypting does not use base64 encoding
1142 if (!$this->is_encrypted()) {
1143 $base64Decode = true;
1145 // Taken from ccr/display.php
1146 $couch_docid = $this->get_couch_docid();
1147 $couch_revid = $this->get_couch_revid();
1148 $couch = new CouchDB();
1149 $resp = $couch->retrieve_doc($couch_docid);
1150 $data = $resp->data;
1151 } else {
1152 $data = $this->get_content_from_filesystem();
1155 if (!empty($data)) {
1156 if ($this->is_encrypted() && !$force_no_decrypt) {
1157 $data = $this->decrypt_content($data);
1159 if ($base64Decode) {
1160 $data = base64_decode($data);
1163 return $data;
1167 * Given a document data contents it decrypts the document data
1168 * @param $data The data that needs to be decrypted
1169 * @return string Returns false if the encryption failed, otherwise it returns a string
1170 * @throws RuntimeException If the data cannot be decrypted
1172 public function decrypt_content($data)
1174 $cryptoGen = new CryptoGen();
1175 $decryptedData = $cryptoGen->decryptStandard($data, null, 'database');
1176 if ($decryptedData === false) {
1177 throw new RuntimeException("Failed to decrypt the data");
1179 return $decryptedData;
1183 * Returns the content from the filesystem for this document
1184 * @return string
1185 * @throws BadMethodCallException If you attempt to retrieve a document that is not stored on the file system
1186 * @throws RuntimeException if the filesystem file does not exist or content cannot be accessed.
1188 protected function get_content_from_filesystem()
1190 $path = $this->get_filesystem_filepath();
1191 if (empty($path)) {
1192 throw new BadMethodCallException(
1193 "Attempted to retrieve the content from the filesystem " .
1194 "for a file that uses a different storage mechanism"
1197 if (!file_exists($path)) {
1198 throw new RuntimeException("Saved filepath does not exist at location " . $path);
1200 $data = file_get_contents($path);
1201 if ($data === false) {
1202 throw new RuntimeException(
1203 "The data could not be retrieved for the file at " .
1204 $path .
1205 " Check that access rights to the file have been granted"
1208 return $data;
1212 * Return file name for thumbnail (adding 'th_')
1214 function get_thumb_name($file_name)
1216 return 'th_' . $file_name;
1220 * Post a patient note that is linked to this document.
1222 * @param string $provider Login name of the provider to receive this note.
1223 * @param integer $category_id The desired document category ID
1224 * @param string $message Any desired message text for the note.
1226 function postPatientNote($provider, $category_id, $message = '')
1228 // Build note text in a way that identifies the new document.
1229 // See pnotes_full.php which uses this to auto-display the document.
1230 $note = $this->get_url_file();
1231 for ($tmp = $category_id; $tmp;) {
1232 $catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
1233 $note = $catrow['name'] . "/$note";
1234 $tmp = $catrow['parent'];
1237 $note = "New scanned document " . $this->get_id() . ": $note";
1238 if ($message) {
1239 $note .= "\n" . $message;
1242 $noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
1243 // Link the new note to the document.
1244 setGpRelation(1, $this->get_id(), 6, $noteid);
1248 * Return note objects associated with this document using Note::notes_factory
1251 function get_notes()
1253 return (Note::notes_factory($this->get_id()));
1255 // end of Document