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.
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
33 * @var EventDispatcherInterface $eventDispatcher
35 private $eventDispatcher;
37 public const TABLE_NAME
= "documents";
40 * Use the native filesystem to store files at
42 public const STORAGE_METHOD_FILESYSTEM
= 0;
45 * Use CouchDb to store files at
47 public const STORAGE_METHOD_COUCHDB
= 1;
50 * Flag that the encryption is on.
52 public const ENCRYPTED_ON
= 1;
55 * Flag the encryption is off.
57 public const ENCRYPTED_OFF
= 0;
60 * Date format for the expires field
62 public const EXPIRES_DATE_FORMAT
= 'Y-m-d H:i:s';
65 * Database unique identifier
71 * @var string Binary of Unique User Identifier that is for both external reference to this entity and for future offline use.
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
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
88 public $foreign_reference_id;
91 * Database table name for the foreign_reference_id. This value must be populated if $foreign_reference_id is
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
104 * Array mapping of possible for values for the type variable
105 * mapping is array text name to index
108 public $type_array = array();
111 * Size of the document in bytes if that is available
117 * Date the document was first persisted
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
134 * URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
140 * Mimetype of the document if available
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
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
160 * Timestamp of the last time the document was changed and persisted, auto maintained by DB,
161 * manually change at your own peril
167 * Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
173 * hash key of the document from when it was uploaded.
179 * DB identifier reference to the lists table (the related issue), 0 if none.
184 // For name (used in OpenEMR 6.0.0+)
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
201 * Whether the file is encrypted
207 public $storagemethod;
209 // For storing couch docid
212 // For storing couch revid
215 // For storing path depth
219 * Flag that marks the document as deleted or not
220 * @public int 1 if deleted, 0 if not
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
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] ??
'';
243 $this->date
= date("Y-m-d H:i:s");
244 $this->date_expires
= null; // typically no expiration date here
246 $this->mimetype
= "";
247 $this->docdate
= date("Y-m-d");
250 $this->encounter_id
= 0;
251 $this->encounter_check
= "";
252 $this->encrypted
= 0;
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())) {
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()]);
276 while ($category = sqlFetchArray($resultSet)) {
277 $categories[] = $category;
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();
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) {
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)) {
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) {
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);
351 * Returns the Document deleted value. Needed for the ORM to process this value. Recommended you use
352 * is_deleted() instead of this function
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();
382 if (empty($foreign_id)) {
383 $foreign_id_sql = " like '%'";
385 $foreign_id_sql = " = ?";
386 $sqlArray[] = strval($foreign_id);
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']);
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
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 '%'";
417 $foreign_reference_id_sql = " = ?";
418 $sqlArray[] = strval($foreign_reference_id);
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",
428 'sqlArray' => $sqlArray]
431 $result = $d->_db
->Execute($sql, $sqlArray);
433 while ($result && !$result->EOF
) {
434 $documents[] = new Document($result->fields
['id']);
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)]);
445 return new Document($id);
452 * Returns all of the documents for a specific patient
453 * @param int $patient_id
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
467 public static function getDocumentsForForeignReferenceId(string $foreign_table, int $foreign_id)
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)
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";
497 return nl2br($string);
504 * Getter/Setter methods used by reflection to affect object in persist/poulate operations
505 * @param mixed new value for given attribute
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)
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)
573 function set_size($size)
581 function set_date($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)
605 function get_hash_algo_title()
607 if (!empty($this->hash
) && strlen($this->hash
) < 50) {
613 function set_url($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
) {
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>
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;
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;
705 function set_owner($owner)
707 $this->owner
= $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)
742 * Returns the database human readable filename of the document
743 * @return string|null
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)
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 = ?",
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
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 = "")
819 $this->foreign_id
= $fid;
822 // need to populate our uuid if its empty
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)
863 * @return string Binary representation of the uuid for this document
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);
880 // Return true for success.
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(
908 $higher_level_path = '',
912 $date_expires = null,
913 $foreign_reference_id = null,
914 $foreign_reference_table = null
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);
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);
946 $has_thumbnail = false;
950 $has_thumbnail = false;
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');
961 $document = base64_encode($data);
963 if ($has_thumbnail) {
964 if ($GLOBALS['couchdb_encryption']) {
965 $th_document = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
967 $th_document = base64_encode($thumbnail_data);
969 $this->thumb_url
= $this->get_thumb_name($filename);
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];
979 $couchdata = ['_id' => $docid, 'data' => $document];
981 $resp = $couch->save_doc($couchdata);
982 if (!$resp->id ||
!$resp->rev
) {
983 return xl('CouchDB save failed');
989 $this->url
= $filename;
990 $this->couch_docid
= $docid;
991 $this->couch_revid
= $revid;
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) . '/';
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) . '/';
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 . '/';
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;
1054 if ($GLOBALS['drive_encryption']) {
1055 $storedData = $cryptoGen->encryptStandard($data, null, 'database');
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');
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);
1081 $filepath . $this->get_thumb_name($filenameUuid),
1082 $storedThumbnailData
1085 return xl('Failed to create') . $filepath . $this->get_thumb_name($filenameUuid);
1091 ($GLOBALS['drive_encryption'] && ($this->storagemethod
!= 1))
1092 ||
($GLOBALS['couchdb_encryption'] && ($this->storagemethod
== 1))
1094 $this->set_encrypted(self
::ENCRYPTED_ON
);
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);
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()));
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
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
;
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);
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
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();
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 " .
1205 " Check that access rights to the file have been granted"
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";
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()));