You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1214 lines
38 KiB

<?php
/**
* Document - This class is the logical representation of a physical file on some system somewhere
* that can be referenced with a URL 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.
* 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 id and categories which do the same.
*
* @package openemr
* @link http://www.open-emr.org
* @author Unknown -- No ownership was listed on this document prior to February 5th 2021
* @author Stephen Nielson <stephen@nielson.org>
* @author Jerry Padgett <sjpadgett@gmail.com>
* @copyright OpenEMR contributors (c) 2021
* @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/
require_once(__DIR__ . "/../pnotes.inc");
require_once(__DIR__ . "/../gprelations.inc.php");
use OpenEMR\Common\Acl\AclMain;
use OpenEMR\Common\Crypto\CryptoGen;
use OpenEMR\Common\ORDataObject\ORDataObject;
use OpenEMR\Common\Uuid\UuidRegistry;
class Document extends ORDataObject
{
public const TABLE_NAME = "documents";
/**
* Use the native filesystem to store files at
*/
public const STORAGE_METHOD_FILESYSTEM = 0;
/**
* Use CouchDb to store files at
*/
public const STORAGE_METHOD_COUCHDB = 1;
/**
* Flag that the encryption is on.
*/
public const ENCRYPTED_ON = 1;
/**
* Flag the encryption is off.
*/
public const ENCRYPTED_OFF = 0;
/**
* Date format for the expires field
*/
public const EXPIRES_DATE_FORMAT = 'Y-m-d H:i:s';
/*
* Database unique identifier
* @public id
*/
public $id;
/**
* @var Unique User Identifier that is for both external reference to this entity and for future offline use.
*/
public $uuid;
/*
* DB unique identifier reference to A PATIENT RECORD, this is not unique in the document table. For actual foreign
* keys to a NON-Patient record use foreign_reference_id. For backwards compatability we ONLY use this for patient
* documents.
* @public int
*/
public $foreign_id;
/**
* DB Unique identifier reference to another table record in the database. This is not unique in the document. The
* table that this record points to is in the $foreign_reference_table
* @public int
*/
public $foreign_reference_id;
/**
* Database table name for the foreign_reference_id. This value must be populated if $foreign_reference_id is
* populated.
* @public string
*/
public $foreign_reference_table;
/*
* Enumerated DB field which is met information about how to use the URL
* @public int can also be a the properly enumerated string
*/
public $type;
/*
* Array mapping of possible for values for the type variable
* mapping is array text name to index
* @public array
*/
public $type_array = array();
/*
* Size of the document in bytes if that is available
* @public int
*/
public $size;
/*
* Date the document was first persisted
* @public string
*/
public $date;
/**
* @public string at which the document can no longer be accessed.
*/
public $date_expires;
/*
* URL which point to the document, may be a file URL, a web URL, a db BLOB URL, or others
* @public string
*/
public $url;
/*
* URL which point to the thumbnail document, may be a file URL, a web URL, a db BLOB URL, or others
* @public string
*/
public $thumb_url;
/*
* Mimetype of the document if available
* @public string
*/
public $mimetype;
/*
* 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
* @public int
*/
public $pages;
/*
* Foreign key identifier of who initially persisited the document,
* potentially ownership could be changed but that would be up to an external non-document object process
* @public int
*/
public $owner;
/*
* Timestamp of the last time the document was changed and persisted, auto maintained by DB,
* manually change at your own peril
* @public int
*/
public $revision;
/*
* Date (YYYY-MM-DD) logically associated with the document, e.g. when a picture was taken.
* @public string
*/
public $docdate;
/*
* hash key of the document from when it was uploaded.
* @public string
*/
public $hash;
/*
* DB identifier reference to the lists table (the related issue), 0 if none.
* @public int
*/
public $list_id;
// For name (used in OpenEMR 6.0.0+)
public $name = null;
// For label on drive (used in OpenEMR 6.0.0+)
public $drive_uuid = null;
// For tagging with the encounter
public $encounter_id;
public $encounter_check;
/*
* Whether the file is already imported
* @public int
*/
public $imported;
/*
* Whether the file is encrypted
* @public int
*/
public $encrypted;
// Storage method
public $storagemethod;
// For storing couch docid
public $couch_docid;
// For storing couch revid
public $couch_revid;
// For storing path depth
public $path_depth;
/**
* Flag that marks the document as deleted or not
* @public int 1 if deleted, 0 if not
*/
public $deleted;
/**
* Constructor sets all Document attributes to their default value
* @param int $id optional existing id of a specific document, if omitted a "blank" document is created
*/
public function __construct($id = "")
{
//call the parent constructor so we have a _db to work with
parent::__construct();
//shore up the most basic ORDataObject bits
$this->id = $id;
$this->_table = self::TABLE_NAME;
//load the enum type from the db using the parent helper function,
//this uses psuedo-class variables so it is really cheap
$this->type_array = $this->_load_enum("type");
$this->type = $this->type_array[0] ?? '';
$this->size = 0;
$this->date = date("Y-m-d H:i:s");
$this->date_expires = null; // typically no expiration date here
$this->url = "";
$this->mimetype = "";
$this->docdate = date("Y-m-d");
$this->hash = "";
$this->list_id = 0;
$this->encounter_id = 0;
$this->encounter_check = "";
$this->encrypted = 0;
$this->deleted = 0;
if ($id != "") {
$this->populate();
}
}
/**
* Retrieves all of the categories associated with this document
*/
public function get_categories()
{
if (empty($this->get_id())) {
return [];
}
$categories = "Select `id`, `name`, `value`, `parent`, `lft`, `rght`, `aco_spec`,`codes` FROM `categories` "
. "JOIN `categories_to_documents` `ctd` ON `ctd`.`category_id` = `categories`.`id` "
. "WHERE `ctd`.`document_id` = ? ";
$resultSet = sqlStatement($categories, [$this->get_id()]);
$categories = [];
while ($category = sqlGetAssoc($resultSet)) {
$categories[] = $category;
}
return $categories;
}
/**
*
* @return bool true if the document expiration date has expired
*/
public function has_expired()
{
if (!empty($this->date_expires)) {
$dateTime = DateTime::createFromFormat("Y-m-d H:i:s", $this->date_expires);
return $dateTime->getTimestamp() >= time();
}
return false;
}
/**
* Checks whether the passed in $user can access the document or not. It checks against all of the access
* permissions for the categories the document is in. If there are any categories that the document is tied to
* that the owner does NOT have access rights to, the request is denied. If there are no categories tied to the
* document, default access is granted.
* @param string|null $username The user (username) we are checking.
* If no user is provided it checks against the currently logged in user
* @return bool True if the passed in user or current user can access this document, false otherwise.
*/
public function can_access($username = null)
{
$categories = $this->get_categories();
// no categories to prevent access
if (empty($categories)) {
return true;
}
// verify that we can access every single category this document is tied to
foreach ($categories as $category) {
if (AclMain::aclCheckAcoSpec($category['aco_spec'], $username) === false) {
return false;
}
}
return true;
}
/**
* Checks if a document has been deleted or not
* @return bool true if the document is deleted, false otherwise
*/
public function is_deleted()
{
return $this->get_deleted() != 0;
}
/**
* Handles the deletion of a document
*/
public function process_deleted()
{
$this->set_deleted(1);
$this->persist();
}
/**
* Returns the Document deleted value. Needed for the ORM to process this value. Recommended you use
* is_deleted() instead of this function
* @return int
*/
public function get_deleted()
{
return $this->deleted;
}
/**
* Sets the Document deleted value. Used by the ORM to set this flag.
* @param $deleted 1 if deleted, 0 if not
*/
public function set_deleted($deleted)
{
$this->deleted = $deleted;
}
/**
* Convenience function to get an array of many document objects that are linked to a patient
* For really large numbers of documents there is a way more efficient way to do this
* by overwriting the populate method
* @param int $foreign_id optional id use to limit array on to a specific relation,
* otherwise every document object is returned
*/
function documents_factory($foreign_id = "")
{
$documents = array();
$sqlArray = array();
if (empty($foreign_id)) {
$foreign_id_sql = " like '%'";
} else {
$foreign_id_sql = " = ?";
$sqlArray[] = strval($foreign_id);
}
$d = new Document();
$sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_id " . $foreign_id_sql;
$result = $d->_db->Execute($sql, $sqlArray);
while ($result && !$result->EOF) {
$documents[] = new Document($result->fields['id']);
$result->MoveNext();
}
return $documents;
}
/**
* Returns an array of many documents that are linked to a foreign table. If $foreign_reference_id is populated
* it will return documents that are specific that that foreign record.
* @param string $foreign_reference_table The table name that we are retrieving documents for
* @param string $foreign_reference_id The table record that this document references
* @return array
*/
public function documents_factory_for_foreign_reference(string $foreign_reference_table, $foreign_reference_id = "")
{
$documents = array();
$sqlArray = array($foreign_reference_table);
if (empty($foreign_reference_id)) {
$foreign_reference_id_sql = " like '%'";
} else {
$foreign_reference_id_sql = " = ?";
$sqlArray[] = strval($foreign_reference_id);
}
$d = new Document();
$sql = "SELECT id FROM " . escape_table_name($d->_table) . " WHERE foreign_reference_table = ? "
. "AND foreign_reference_id " . $foreign_reference_id_sql;
(new \OpenEMR\Common\Logging\SystemLogger())->debug(
"documents_factory_for_foreign_reference",
['sql' => $sql,
'sqlArray' => $sqlArray]
);
$result = $d->_db->Execute($sql, $sqlArray);
while ($result && !$result->EOF) {
$documents[] = new Document($result->fields['id']);
$result->MoveNext();
}
return $documents;
}
public static function getDocumentForUuid($uuid)
{
$sql = "SELECT id from " . escape_table_name(self::TABLE_NAME) . " WHERE uuid = ?";
$id = \OpenEMR\Common\Database\QueryUtils::fetchSingleValue($sql, 'id', [UuidRegistry::uuidToBytes($uuid)]);
if (!empty($id)) {
return new Document($id);
} else {
return null;
}
}
/**
* Returns all of the documents for a specific patient
* @param int $patient_id
* @return array
*/
public static function getDocumentsForPatient(int $patient_id)
{
$doc = new Document();
return $doc->documents_factory($patient_id);
}
/**
* Return an array of documents that are connected to another table record in the system.
* @param int $foreign_id
* @return Document[]
*/
public static function getDocumentsForForeignReferenceId(string $foreign_table, int $foreign_id)
{
$doc = new self();
return $doc->documents_factory_for_foreign_reference($foreign_table, $foreign_id);
}
/**
* Convenience function to generate string debug data about the object
*/
function toString($html = false)
{
$string .= "\n"
. "ID: " . $this->id . "\n"
. "FID: " . $this->foreign_id . "\n"
. "type: " . $this->type . "\n"
. "type_array: " . print_r($this->type_array, true) . "\n"
. "size: " . $this->size . "\n"
. "date: " . $this->date . "\n"
. "url: " . $this->url . "\n"
. "mimetype: " . $this->mimetype . "\n"
. "pages: " . $this->pages . "\n"
. "owner: " . $this->owner . "\n"
. "revision: " . $this->revision . "\n"
. "docdate: " . $this->docdate . "\n"
. "hash: " . $this->hash . "\n"
. "list_id: " . $this->list_id . "\n"
. "encounter_id: " . $this->encounter_id . "\n"
. "encounter_check: " . $this->encounter_check . "\n";
if ($html) {
return nl2br($string);
} else {
return $string;
}
}
/**#@+
* Getter/Setter methods used by reflection to affect object in persist/poulate operations
* @param mixed new value for given attribute
*/
function set_id($id)
{
$this->id = $id;
}
function get_id()
{
return $this->id;
}
/**
* This is a Patient record id
* @param $fid Unique database identifier for a patient record
*/
function set_foreign_id($fid)
{
$this->foreign_id = $fid;
}
/**
* Sets the unique database identifier that this Document is referenced to. If unlinking this document
* with a foreign table you must set $reference_id and $table_name to be null
*/
public function set_foreign_reference_id($reference_id)
{
$this->foreign_reference_id = $reference_id;
}
/**
* Sets the table name that this Document references in the foreign_reference_id
* @param $table_name The database table name
*/
public function set_foreign_reference_table($table_name)
{
$this->foreign_reference_table = $table_name;
}
/**
* The unique database reference to another table record (Foreign Key)
* @return int|null
*/
public function get_foreign_reference_id(): ?int
{
return $this->foreign_reference_id;
}
/**
* Returns the database table name for the foreign reference id
* @return string|null
*/
public function get_foreign_reference_table(): ?string
{
return $this->foreign_reference_table;
}
function get_foreign_id()
{
return $this->foreign_id;
}
function set_type($type)
{
$this->type = $type;
}
function get_type()
{
return $this->type;
}
function set_size($size)
{
$this->size = $size;
}
function get_size()
{
return $this->size;
}
function set_date($date)
{
$this->date = $date;
}
function get_date()
{
return $this->date;
}
/**
* @return string|null The datetime that the document expires at
*/
function get_date_expires(): ?string
{
return $this->date_expires;
}
function set_hash($hash)
{
$this->hash = $hash;
}
function get_hash()
{
return $this->hash;
}
function get_hash_algo_title()
{
if (!empty($this->hash) && strlen($this->hash) < 50) {
return "SHA1";
} else {
return "SHA3-512";
}
}
function set_url($url)
{
$this->url = $url;
}
function get_url()
{
return $this->url;
}
function set_thumb_url($url)
{
$this->thumb_url = $url;
}
function get_thumb_url()
{
return $this->thumb_url;
}
/**
* get the url without the protocol handler
*/
function get_url_filepath()
{
return preg_replace("|^(.*)://|", "", $this->url);
}
/**
* OpenEMR installation media can be moved to other instances, to get the real filesystem path we use this method.
* If the document is a couch db document this will return null;
*/
protected function get_filesystem_filepath()
{
if ($this->get_storagemethod() === self::STORAGE_METHOD_COUCHDB) {
return null;
}
//change full path to current webroot. this is for documents that may have
//been moved from a different filesystem and the full path in the database
//is not current. this is also for documents that may of been moved to
//different patients. Note that the path_depth is used to see how far down
//the path to go. For example, originally the path_depth was always 1, which
//only allowed things like documents/1/<file>, but now can have more structured
//directories. For example a path_depth of 2 can give documents/encounters/1/<file>
// etc.
// NOTE that $from_filename and basename($url) are the same thing
$filepath = $this->get_url_filepath();
$from_all = explode("/", $filepath);
$from_filename = array_pop($from_all);
$from_pathname_array = array();
for ($i = 0; $i < $this->get_path_depth(); $i++) {
$from_pathname_array[] = array_pop($from_all);
}
$from_pathname_array = array_reverse($from_pathname_array);
$from_pathname = implode("/", $from_pathname_array);
$filepath = $GLOBALS['OE_SITE_DIR'] . '/documents/' . $from_pathname . '/' . $from_filename;
return $filepath;
}
/**
* get the url filename only
*/
function get_url_file()
{
return basename_international(preg_replace("|^(.*)://|", "", $this->url));
}
/**
* get the url path only
*/
function get_url_path()
{
return dirname(preg_replace("|^(.*)://|", "", $this->url)) . "/";
}
function get_path_depth()
{
return $this->path_depth;
}
function set_path_depth($path_depth)
{
$this->path_depth = $path_depth;
}
function set_mimetype($mimetype)
{
$this->mimetype = $mimetype;
}
function get_mimetype()
{
return $this->mimetype;
}
function set_pages($pages)
{
$this->pages = $pages;
}
function get_pages()
{
return $this->pages;
}
function set_owner($owner)
{
$this->owner = $owner;
}
function get_owner()
{
return $this->owner;
}
/*
* No getter for revision because it is updated automatically by the DB.
*/
function set_revision($revision)
{
$this->revision = $revision;
}
function set_docdate($docdate)
{
$this->docdate = $docdate;
}
function get_docdate()
{
return $this->docdate;
}
function set_list_id($list_id)
{
$this->list_id = $list_id;
}
function get_list_id()
{
return $this->list_id;
}
function set_name($name)
{
$this->name = $name;
}
/**
* Returns the database human readable filename of the document
* @return string|null
*/
function get_name()
{
return $this->name;
}
function set_drive_uuid($drive_uuid)
{
$this->drive_uuid = $drive_uuid;
}
function get_drive_uuid()
{
return $this->drive_uuid;
}
function set_encounter_id($encounter_id)
{
$this->encounter_id = $encounter_id;
}
function get_encounter_id()
{
return $this->encounter_id;
}
function set_encounter_check($encounter_check)
{
$this->encounter_check = $encounter_check;
}
function get_encounter_check()
{
return $this->encounter_check;
}
function get_ccr_type($doc_id)
{
$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)
);
return $type['name'];
}
function set_imported($imported)
{
$this->imported = $imported;
}
function get_imported()
{
return $this->imported;
}
function update_imported($doc_id)
{
sqlQuery("UPDATE documents SET imported = 1 WHERE id = ?", array($doc_id));
}
function set_encrypted($encrypted)
{
$this->encrypted = $encrypted;
}
function get_encrypted()
{
return $this->encrypted;
}
function is_encrypted()
{
return $this->encrypted == self::ENCRYPTED_ON;
}
/*
* Overridden function to stor current object state in the db.
* current overide is to allow for a just in time foreign id, often this is needed
* when the object is never directly exposed and is handled as part of a larger
* object hierarchy.
* @param int $fid foreign id that should be used so that this document can be related (joined) on it later
*/
function persist($fid = "")
{
if (!empty($fid)) {
$this->foreign_id = $fid;
}
// need to populate our uuid if its empty
parent::persist();
}
function set_storagemethod($str)
{
$this->storagemethod = $str;
}
function get_storagemethod()
{
return $this->storagemethod;
}
function set_couch_docid($str)
{
$this->couch_docid = $str;
}
function get_couch_docid()
{
return $this->couch_docid;
}
function set_couch_revid($str)
{
$this->couch_revid = $str;
}
function get_couch_revid()
{
return $this->couch_revid;
}
function set_uuid($uuid)
{
$this->uuid = $uuid;
}
function get_uuid()
{
return $this->uuid;
}
// Function added by Rod to change the patient associated with a document.
// This just moves some code that used to be in C_Document.class.php,
// changing it as little as possible since I'm not set up to test it.
//
function change_patient($new_patient_id)
{
// Set the new patient.
$this->set_foreign_id($new_patient_id);
$this->persist();
// Return true for success.
return true;
}
/**
* Create a new document and store its data.
* This is a mix of new code and code moved from C_Document.class.php.
*
* @param string $patient_id Patient pid; if not known then this may be a simple directory name
* @param integer $category_id The desired document category ID
* @param string $filename The desired filename
* @param string $mimetype MIME type
* @param string &$data The actual data to store (not encoded)
* @param string $higher_level_path Optional subdirectory within the local document repository
* @param string $path_depth Number of directory levels in $higher_level_path, if specified
* @param integer $owner Owner/user/service that is requesting this action
* @param string $tmpfile The tmp location of file (require for thumbnail generator)
* @param string $date_expires The datetime that the document should no longer be accessible in the system
* @param string $foreign_reference_id The table id to another table record in OpenEMR
* @param string $foreign_reference_table The table name of the foreign_reference_id this document refers to.
* @return string Empty string if success, otherwise error message text
*/
function createDocument(
$patient_id,
$category_id,
$filename,
$mimetype,
&$data,
$higher_level_path = '',
$path_depth = 1,
$owner = 0,
$tmpfile = null,
$date_expires = null,
$foreign_reference_id = null,
$foreign_reference_table = null
) {
if (
!empty($foreign_reference_id) && empty($foreign_reference_table)
|| empty($foreign_reference_id) && !empty($foreign_reference_table)
) {
return xl('Reference table and reference id must both be set');
}
$this->set_foreign_reference_id($foreign_reference_id);
$this->set_foreign_reference_table($foreign_reference_table);
// The original code used the encounter ID but never set it to anything.
// That was probably a mistake, but we reference it here for documentation
// and leave it empty. Logically, documents are not tied to encounters.
// Create a crypto object that will be used for for encryption/decryption
$cryptoGen = new CryptoGen();
if ($GLOBALS['generate_doc_thumb']) {
$thumb_size = ($GLOBALS['thumb_doc_max_size'] > 0) ? $GLOBALS['thumb_doc_max_size'] : null;
$thumbnail_class = new Thumbnail($thumb_size);
if (!is_null($tmpfile)) {
$has_thumbnail = $thumbnail_class->file_support_thumbnail($tmpfile);
} else {
$has_thumbnail = false;
}
if ($has_thumbnail) {
$thumbnail_resource = $thumbnail_class->create_thumbnail(null, $data);
if ($thumbnail_resource) {
$thumbnail_data = $thumbnail_class->get_string_file($thumbnail_resource);
} else {
$has_thumbnail = false;
}
}
} else {
$has_thumbnail = false;
}
$encounter_id = '';
$this->storagemethod = $GLOBALS['document_storage_method'];
$this->mimetype = $mimetype;
if ($this->storagemethod == self::STORAGE_METHOD_COUCHDB) {
// Store it using CouchDB.
if ($GLOBALS['couchdb_encryption']) {
$document = $cryptoGen->encryptStandard($data, null, 'database');
} else {
$document = base64_encode($data);
}
if ($has_thumbnail) {
if ($GLOBALS['couchdb_encryption']) {
$th_document = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
} else {
$th_document = base64_encode($thumbnail_data);
}
$this->thumb_url = $this->get_thumb_name($filename);
} else {
$th_document = false;
}
$couch = new CouchDB();
$docid = $couch->createDocId('documents');
if (!empty($th_document)) {
$couchdata = ['_id' => $docid, 'data' => $document, 'th_data' => $th_document];
} else {
$couchdata = ['_id' => $docid, 'data' => $document];
}
$resp = $couch->save_doc($couchdata);
if (!$resp->id || !$resp->rev) {
return xl('CouchDB save failed');
} else {
$docid = $resp->id;
$revid = $resp->rev;
}
$this->url = $filename;
$this->couch_docid = $docid;
$this->couch_revid = $revid;
} else {
// Storing document files locally.
$repository = $GLOBALS['oer_config']['documents']['repository'];
$higher_level_path = preg_replace("/[^A-Za-z0-9\/]/", "_", $higher_level_path);
if ((!empty($higher_level_path)) && (is_numeric($patient_id) && $patient_id > 0)) {
// Allow higher level directory structure in documents directory and a patient is mapped.
$filepath = $repository . $higher_level_path . "/";
} elseif (!empty($higher_level_path)) {
// Allow higher level directory structure in documents directory and there is no patient mapping
// (will create up to 10000 random directories and increment the path_depth by 1).
$filepath = $repository . $higher_level_path . '/' . rand(1, 10000) . '/';
++$path_depth;
} elseif (!(is_numeric($patient_id)) || !($patient_id > 0)) {
// This is the default action except there is no patient mapping (when patient_id is 00 or direct)
// (will create up to 10000 random directories and set the path_depth to 2).
$filepath = $repository . $patient_id . '/' . rand(1, 10000) . '/';
$path_depth = 2;
$patient_id = 0;
} else {
// This is the default action where the patient is used as one level directory structure
// in documents directory.
$filepath = $repository . $patient_id . '/';
$path_depth = 1;
}
if (!file_exists($filepath)) {
if (!mkdir($filepath, 0700, true)) {
return xl('Unable to create patient document subdirectory');
}
}
// collect the drive storage filename
$this->drive_uuid = (new UuidRegistry(['document_drive' => true]))->createUuid();
$filenameUuid = UuidRegistry::uuidToString($this->drive_uuid);
$this->url = "file://" . $filepath . $filenameUuid;
if (is_numeric($path_depth)) {
// this is for when directory structure is more than one level
$this->path_depth = $path_depth;
}
// Store the file.
if ($GLOBALS['drive_encryption']) {
$storedData = $cryptoGen->encryptStandard($data, null, 'database');
} else {
$storedData = $data;
}
if (file_exists($filepath . $filenameUuid)) {
// this should never happen with current uuid mechanism
return xl('Failed since file already exists') . " $filepath$filenameUuid";
}
if (file_put_contents($filepath . $filenameUuid, $storedData) === false) {
return xl('Failed to create') . " $filepath$filenameUuid";
}
if ($has_thumbnail) {
// Store the thumbnail.
$this->thumb_url = "file://" . $filepath . $this->get_thumb_name($filenameUuid);
if ($GLOBALS['drive_encryption']) {
$storedThumbnailData = $cryptoGen->encryptStandard($thumbnail_data, null, 'database');
} else {
$storedThumbnailData = $thumbnail_data;
}
if (file_exists($filepath . $this->get_thumb_name($filenameUuid))) {
// this should never happend with current uuid mechanism
return xl('Failed since file already exists') . $filepath . $this->get_thumb_name($filenameUuid);
}
if (
file_put_contents(
$filepath . $this->get_thumb_name($filenameUuid),
$storedThumbnailData
) === false
) {
return xl('Failed to create') . $filepath . $this->get_thumb_name($filenameUuid);
}
}
}
if (
($GLOBALS['drive_encryption'] && ($this->storagemethod != 1))
|| ($GLOBALS['couchdb_encryption'] && ($this->storagemethod == 1))
) {
$this->set_encrypted(self::ENCRYPTED_ON);
} else {
$this->set_encrypted(self::ENCRYPTED_OFF);
}
// we need our external unique reference identifier that can be mapped back to our table.
$docUUID = (new UuidRegistry(['table_name' => $this->_table]))->createUuid();
$this->set_uuid($docUUID);
$this->name = $filename;
$this->size = strlen($data);
$this->hash = hash('sha3-512', $data);
$this->type = $this->type_array['file_url'];
$this->owner = $owner ? $owner : ($_SESSION['authUserID'] ?? null);
$this->date_expires = $date_expires;
$this->set_foreign_id($patient_id);
$this->persist();
$this->populate();
if (is_numeric($this->get_id()) && is_numeric($category_id)) {
$sql = "REPLACE INTO categories_to_documents SET category_id = ?, document_id = ?";
$this->_db->Execute($sql, array($category_id, $this->get_id()));
}
return '';
}
/**
* Retrieves the document data that has been saved to the filesystem or couch db. If the $force_no_decrypt flag is
* set to true, it will return the encrypted version of the data for the document.
* @param bool $force_no_decrypt True if the document should have its data returned encrypted, false otherwise
* @throws BadMethodCallException Thrown if the method is called when the document has been marked as deleted
* or expired
* @return false|string Returns false if the data failed to decrypt, or a string if the data decrypts
* or is unencrypted.
*/
function get_data($force_no_decrypt = false)
{
$storagemethod = $this->get_storagemethod();
if ($this->has_expired()) {
throw new BadMethodCallException("Should not attempt to retrieve data from expired documents");
}
if ($this->is_deleted()) {
throw new BadMethodCallException("Should not attempt to retrieve data from deleted documents");
}
$base64Decode = false;
if ($storagemethod === self::STORAGE_METHOD_COUCHDB) {
// encrypting does not use base64 encoding
if (!$this->is_encrypted()) {
$base64Decode = true;
}
// Taken from ccr/display.php
$couch_docid = $this->get_couch_docid();
$couch_revid = $this->get_couch_revid();
$couch = new CouchDB();
$resp = $couch->retrieve_doc($couch_docid);
$data = $resp->data;
} else {
$data = $this->get_content_from_filesystem();
}
if (!empty($data)) {
if ($this->is_encrypted() && !$force_no_decrypt) {
$data = $this->decrypt_content($data);
}
if ($base64Decode) {
$data = base64_decode($data);
}
}
return $data;
}
/**
* Given a document data contents it decrypts the document data
* @param $data The data that needs to be decrypted
* @return string Returns false if the encryption failed, otherwise it returns a string
* @throws RuntimeException If the data cannot be decrypted
*/
public function decrypt_content($data)
{
$cryptoGen = new CryptoGen();
$decryptedData = $cryptoGen->decryptStandard($data, null, 'database');
if ($decryptedData === false) {
throw new RuntimeException("Failed to decrypt the data");
}
return $decryptedData;
}
/**
* Returns the content from the filesystem for this document
* @return string
* @throws BadMethodCallException If you attempt to retrieve a document that is not stored on the file system
* @throws RuntimeException if the filesystem file does not exist or content cannot be accessed.
*/
protected function get_content_from_filesystem()
{
$path = $this->get_filesystem_filepath();
if (empty($path)) {
throw new BadMethodCallException(
"Attempted to retrieve the content from the filesystem " .
"for a file that uses a different storage mechanism"
);
}
if (!file_exists($path)) {
throw new RuntimeException("Saved filepath does not exist at location " . $path);
}
$data = file_get_contents($path);
if ($data === false) {
throw new RuntimeException(
"The data could not be retrieved for the file at " .
$path .
" Check that access rights to the file have been granted"
);
}
return $data;
}
/**
* Return file name for thumbnail (adding 'th_')
*/
function get_thumb_name($file_name)
{
return 'th_' . $file_name;
}
/**
* Post a patient note that is linked to this document.
*
* @param string $provider Login name of the provider to receive this note.
* @param integer $category_id The desired document category ID
* @param string $message Any desired message text for the note.
*/
function postPatientNote($provider, $category_id, $message = '')
{
// Build note text in a way that identifies the new document.
// See pnotes_full.php which uses this to auto-display the document.
$note = $this->get_url_file();
for ($tmp = $category_id; $tmp;) {
$catrow = sqlQuery("SELECT name, parent FROM categories WHERE id = ?", array($tmp));
$note = $catrow['name'] . "/$note";
$tmp = $catrow['parent'];
}
$note = "New scanned document " . $this->get_id() . ": $note";
if ($message) {
$note .= "\n" . $message;
}
$noteid = addPnote($this->get_foreign_id(), $note, 0, '1', 'New Document', $provider);
// Link the new note to the document.
setGpRelation(1, $this->get_id(), 6, $noteid);
}
/**
* Return note objects associated with this document using Note::notes_factory
*
*/
function get_notes()
{
return (Note::notes_factory($this->get_id()));
}
// end of Document
}