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
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 |
|
}
|
|
|