3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * BAO object for crm_log table
21 class CRM_Core_BAO_File
extends CRM_Core_DAO_File
{
23 public static $_signableFields = ['entityTable', 'entityID', 'fileID'];
26 * If there is no setting configured on the admin screens, maximum number
27 * of attachments to try to process when given a list of attachments to
30 const DEFAULT_MAX_ATTACHMENTS_BACKEND
= 100;
33 * Takes an associative array and creates a File object.
35 * @param array $params
36 * (reference ) an assoc array of name/value pairs.
38 * @return CRM_Core_BAO_File
40 public static function create($params) {
41 $fileDAO = new CRM_Core_DAO_File();
43 $op = empty($params['id']) ?
'create' : 'edit';
45 CRM_Utils_Hook
::pre($op, 'File', CRM_Utils_Array
::value('id', $params), $params);
47 $fileDAO->copyValues($params);
49 if (empty($params['id']) && empty($params['created_id'])) {
50 $fileDAO->created_id
= CRM_Core_Session
::getLoggedInContactID();
55 CRM_Utils_Hook
::post($op, 'File', $fileDAO->id
, $fileDAO);
62 * @param int $entityID
66 public static function path($fileID, $entityID) {
67 $entityFileDAO = new CRM_Core_DAO_EntityFile();
68 $entityFileDAO->entity_id
= $entityID;
69 $entityFileDAO->file_id
= $fileID;
71 if ($entityFileDAO->find(TRUE)) {
72 $fileDAO = new CRM_Core_DAO_File();
73 $fileDAO->id
= $fileID;
74 if ($fileDAO->find(TRUE)) {
75 $config = CRM_Core_Config
::singleton();
76 $path = $config->customFileUploadDir
. $fileDAO->uri
;
78 if (file_exists($path) && is_readable($path)) {
79 return [$path, $fileDAO->mime_type
];
89 * @param int $fileTypeID
91 * @param int $entityID
92 * @param $entitySubtype
93 * @param bool $overwrite
94 * @param null|array $fileParams
95 * @param string $uploadName
96 * @param null $mimeType
100 public static function filePostProcess(
108 $uploadName = 'uploadFile',
112 CRM_Core_Error
::statusBounce(ts('Mime Type is now a required parameter for file upload'));
115 $config = CRM_Core_Config
::singleton();
117 $path = explode('/', $data);
118 $filename = $path[count($path) - 1];
120 // rename this file to go into the secure directory
121 if ($entitySubtype) {
122 $directoryName = $config->customFileUploadDir
. $entitySubtype . DIRECTORY_SEPARATOR
. $entityID;
125 $directoryName = $config->customFileUploadDir
;
128 CRM_Utils_File
::createDir($directoryName);
130 if (!rename($data, $directoryName . DIRECTORY_SEPARATOR
. $filename)) {
131 CRM_Core_Error
::statusBounce(ts('Could not move custom file to custom upload directory'));
135 if ($overwrite && $fileTypeID) {
136 list($sql, $params) = self
::sql($entityTable, $entityID, $fileTypeID);
139 list($sql, $params) = self
::sql($entityTable, $entityID, 0);
142 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
145 $fileDAO = new CRM_Core_DAO_File();
147 if (isset($dao->cfID
) && $dao->cfID
) {
149 $fileDAO->id
= $dao->cfID
;
150 unlink($directoryName . DIRECTORY_SEPARATOR
. $dao->uri
);
153 if (!empty($fileParams)) {
154 $fileDAO->copyValues($fileParams);
157 $fileDAO->uri
= $filename;
158 $fileDAO->mime_type
= $mimeType;
159 $fileDAO->file_type_id
= $fileTypeID;
160 $fileDAO->upload_date
= date('YmdHis');
163 // need to add/update civicrm_entity_file
164 $entityFileDAO = new CRM_Core_DAO_EntityFile();
165 if (isset($dao->cefID
) && $dao->cefID
) {
166 $entityFileDAO->id
= $dao->cefID
;
168 $entityFileDAO->entity_table
= $entityTable;
169 $entityFileDAO->entity_id
= $entityID;
170 $entityFileDAO->file_id
= $fileDAO->id
;
171 $entityFileDAO->save();
174 if (!empty($fileParams['tag'])) {
175 CRM_Core_BAO_EntityTag
::create($fileParams['tag'], 'civicrm_file', $entityFileDAO->id
);
179 if (isset($fileParams['attachment_taglist']) && !empty($fileParams['attachment_taglist'])) {
180 CRM_Core_Form_Tag
::postProcess($fileParams['attachment_taglist'], $entityFileDAO->id
, 'civicrm_file');
183 // lets call the post hook here so attachments code can do the right stuff
184 CRM_Utils_Hook
::post($op, 'File', $fileDAO->id
, $fileDAO);
188 * A static function wrapper that deletes the various objects.
190 * Objects are those hat are connected to a file object (i.e. file, entityFile and customValue.
193 * @param int $entityID
194 * @param int $fieldID
198 public static function deleteFileReferences($fileID, $entityID, $fieldID) {
199 $fileDAO = new CRM_Core_DAO_File();
200 $fileDAO->id
= $fileID;
201 if (!$fileDAO->find(TRUE)) {
202 throw new CRM_Core_Exception(ts('File not found'));
205 // lets call a pre hook before the delete, so attachments hooks can get the info before things
207 CRM_Utils_Hook
::pre('delete', 'File', $fileID, $fileDAO);
209 // get the table and column name
210 list($tableName, $columnName, $groupID) = CRM_Core_BAO_CustomField
::getTableColumnGroup($fieldID);
212 $entityFileDAO = new CRM_Core_DAO_EntityFile();
213 $entityFileDAO->file_id
= $fileID;
214 $entityFileDAO->entity_id
= $entityID;
215 $entityFileDAO->entity_table
= $tableName;
217 if (!$entityFileDAO->find(TRUE)) {
218 throw new CRM_Core_Exception(sprintf('No record found for given file ID - %d and entity ID - %d', $fileID, $entityID));
221 $entityFileDAO->delete();
224 // also set the value to null of the table and column
225 $query = "UPDATE $tableName SET $columnName = null WHERE $columnName = %1";
226 $params = [1 => [$fileID, 'Integer']];
227 CRM_Core_DAO
::executeQuery($query, $params);
231 * The $useWhere is used so that the signature matches the parent class
233 * public function delete($useWhere = FALSE) {
234 * list($fileID, $entityID, $fieldID) = func_get_args();
236 * self::deleteFileReferences($fileID, $entityID, $fieldID);
240 * Delete all the files and associated object associated with this combination.
242 * @param string $entityTable
243 * @param int $entityID
244 * @param int $fileTypeID
250 public static function deleteEntityFile($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
252 if (empty($entityTable) ||
empty($entityID)) {
256 $config = CRM_Core_Config
::singleton();
258 list($sql, $params) = self
::sql($entityTable, $entityID, $fileTypeID, $fileID);
259 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
263 while ($dao->fetch()) {
264 $cfIDs[$dao->cfID
] = $dao->uri
;
265 $cefIDs[] = $dao->cefID
;
268 if (!empty($cefIDs)) {
269 $cefIDs = implode(',', $cefIDs);
270 $sql = "DELETE FROM civicrm_entity_file where id IN ( $cefIDs )";
271 CRM_Core_DAO
::executeQuery($sql);
275 if (!empty($cfIDs)) {
276 // Delete file only if there no any entity using this file.
278 foreach ($cfIDs as $fId => $fUri) {
279 //delete tags from entity tag table
281 'entity_table' => 'civicrm_file',
285 CRM_Core_BAO_EntityTag
::del($tagParams);
287 if (!CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_EntityFile', $fId, 'id', 'file_id')) {
288 unlink($config->customFileUploadDir
. DIRECTORY_SEPARATOR
. $fUri);
289 $deleteFiles[$fId] = $fId;
293 if (!empty($deleteFiles)) {
294 $deleteFiles = implode(',', $deleteFiles);
295 $sql = "DELETE FROM civicrm_file where id IN ( $deleteFiles )";
296 CRM_Core_DAO
::executeQuery($sql);
304 * Get all the files and associated object associated with this combination.
306 * @param string $entityTable
307 * @param int $entityID
308 * @param bool $addDeleteArgs
312 public static function getEntityFile($entityTable, $entityID, $addDeleteArgs = FALSE) {
313 if (empty($entityTable) ||
!$entityID) {
318 $config = CRM_Core_Config
::singleton();
320 list($sql, $params) = self
::sql($entityTable, $entityID, NULL);
321 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
323 while ($dao->fetch()) {
324 $fileHash = self
::generateFileHash($dao->entity_id
, $dao->cfID
);
325 $result['fileID'] = $dao->cfID
;
326 $result['entityID'] = $dao->cefID
;
327 $result['mime_type'] = $dao->mime_type
;
328 $result['fileName'] = $dao->uri
;
329 $result['description'] = $dao->description
;
330 $result['cleanName'] = CRM_Utils_File
::cleanFileName($dao->uri
);
331 $result['fullPath'] = $config->customFileUploadDir
. DIRECTORY_SEPARATOR
. $dao->uri
;
332 $result['url'] = CRM_Utils_System
::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$dao->entity_id}&fcs={$fileHash}");
333 $result['href'] = "<a href=\"{$result['url']}\">{$result['cleanName']}</a>";
334 $result['tag'] = CRM_Core_BAO_EntityTag
::getTag($dao->cfID
, 'civicrm_file');
335 $result['icon'] = CRM_Utils_File
::getIconFromMimeType($dao->mime_type
);
336 if ($addDeleteArgs) {
337 $result['deleteURLArgs'] = self
::deleteURLArgs($dao->entity_table
, $dao->entity_id
, $dao->cfID
);
339 $results[$dao->cfID
] = $result;
343 $tags = CRM_Core_PseudoConstant
::get('CRM_Core_DAO_EntityTag', 'tag_id', ['onlyActive' => FALSE]);
345 foreach ($results as &$values) {
346 if (!empty($values['tag'])) {
348 foreach ($values['tag'] as $tid) {
349 $tagNames[] = $tags[$tid];
351 $values['tag'] = implode(', ', $tagNames);
362 * @param string $entityTable
363 * Table-name or "*" (to reference files directly by file-id).
364 * @param int $entityID
365 * @param int $fileTypeID
370 public static function sql($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
371 if ($entityTable == '*') {
372 // $entityID is the ID of a specific file
374 SELECT CF.id as cfID,
376 CF.mime_type as mime_type,
377 CF.description as description,
379 CEF.entity_table as entity_table,
380 CEF.entity_id as entity_id
381 FROM civicrm_file AS CF
382 LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
388 SELECT CF.id as cfID,
390 CF.mime_type as mime_type,
391 CF.description as description,
393 CEF.entity_table as entity_table,
394 CEF.entity_id as entity_id
395 FROM civicrm_file AS CF
396 LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
397 WHERE CEF.entity_table = %1
398 AND CEF.entity_id = %2";
402 1 => [$entityTable, 'String'],
403 2 => [$entityID, 'Integer'],
406 if ($fileTypeID !== NULL) {
407 $sql .= " AND CF.file_type_id = %3";
408 $params[3] = [$fileTypeID, 'Integer'];
411 if ($fileID !== NULL) {
412 $sql .= " AND CF.id = %4";
413 $params[4] = [$fileID, 'Integer'];
416 return [$sql, $params];
420 * @param CRM_Core_Form $form
421 * @param string $entityTable
422 * @param int $entityID
423 * @param null $numAttachments
424 * @param bool $ajaxDelete
426 public static function buildAttachment(&$form, $entityTable, $entityID = NULL, $numAttachments = NULL, $ajaxDelete = FALSE) {
428 if (!$numAttachments) {
429 $numAttachments = Civi
::settings()->get('max_attachments');
431 // Assign maxAttachments count to template for help message
432 $form->assign('maxAttachments', $numAttachments);
434 $config = CRM_Core_Config
::singleton();
435 // set default max file size as 2MB
436 $maxFileSize = $config->maxFileSize ?
$config->maxFileSize
: 2;
438 $currentAttachmentInfo = self
::getEntityFile($entityTable, $entityID, TRUE);
439 $totalAttachments = 0;
440 if ($currentAttachmentInfo) {
441 $totalAttachments = count($currentAttachmentInfo);
442 $form->add('checkbox', 'is_delete_attachment', ts('Delete All Attachment(s)'));
443 $form->assign('currentAttachmentInfo', $currentAttachmentInfo);
446 $form->assign('currentAttachmentInfo', NULL);
449 if ($totalAttachments) {
450 if ($totalAttachments >= $numAttachments) {
454 $numAttachments -= $totalAttachments;
458 $form->assign('numAttachments', $numAttachments);
460 CRM_Core_BAO_Tag
::getTags('civicrm_file', $tags, NULL,
461 ' ', TRUE);
464 $parentNames = CRM_Core_BAO_Tag
::getTagSet('civicrm_file');
467 for ($i = 1; $i <= $numAttachments; $i++
) {
468 $form->addElement('file', "attachFile_$i", ts('Attach File'), 'size=30 maxlength=221');
469 $form->addUploadElement("attachFile_$i");
470 $form->setMaxFileSize($maxFileSize * 1024 * 1024);
471 $form->addRule("attachFile_$i",
472 ts('File size should be less than %1 MByte(s)',
476 $maxFileSize * 1024 * 1024
478 $form->addElement('text', "attachDesc_$i", NULL, [
481 'placeholder' => ts('Description'),
485 $form->add('select', "tag_$i", ts('Tags'), $tags, FALSE,
488 'multiple' => 'multiple',
489 'class' => 'huge crm-select2',
490 'placeholder' => ts('- none -'),
494 CRM_Core_Form_Tag
::buildQuickForm($form, $parentNames, 'civicrm_file', NULL, FALSE, TRUE, "file_taglist_$i");
499 * Return a clean url string and the number of attachment for a
500 * given entityTable, entityID
502 * @param string $entityTable
503 * The entityTable to which the file is attached.
504 * @param int $entityID
505 * The id of the object in the above entityTable.
506 * @param string $separator
507 * The string separator where to implode the urls.
510 * An array with 2 elements. The string and the number of attachments
512 public static function attachmentInfo($entityTable, $entityID, $separator = '<br />') {
517 $currentAttachments = self
::getEntityFile($entityTable, $entityID);
518 if (!empty($currentAttachments)) {
519 $currentAttachmentURL = [];
520 foreach ($currentAttachments as $fileID => $attach) {
521 $currentAttachmentURL[] = $attach['href'];
523 return implode($separator, $currentAttachmentURL);
530 * @param array $params
531 * @param $entityTable
532 * @param int $entityID
534 public static function formatAttachment(
541 // delete current attachments if applicable
542 if ($entityID && !empty($formValues['is_delete_attachment'])) {
543 CRM_Core_BAO_File
::deleteEntityFile($entityTable, $entityID);
546 $numAttachments = Civi
::settings()->get('max_attachments');
548 // setup all attachments
549 for ($i = 1; $i <= $numAttachments; $i++
) {
550 $attachName = "attachFile_$i";
551 $attachDesc = "attachDesc_$i";
552 $attachTags = "tag_$i";
553 $attachFreeTags = "file_taglist_$i";
554 if (isset($formValues[$attachName]) && !empty($formValues[$attachName])) {
555 // add static tags if selects
557 if (!empty($formValues[$attachTags])) {
558 foreach ($formValues[$attachTags] as $tag) {
559 $tagParams[$tag] = 1;
563 // we dont care if the file is empty or not
566 'description' => $formValues[$attachDesc],
568 'attachment_taglist' => $formValues[$attachFreeTags] ??
[],
571 CRM_Utils_File
::formatFile($formValues, $attachName, $extraParams);
573 // set the formatted attachment attributes to $params, later used by
574 // CRM_Activity_BAO_Activity::sendEmail(...) to send mail with desired attachments
575 if (!empty($formValues[$attachName])) {
576 $params[$attachName] = $formValues[$attachName];
583 * @param array $params
584 * @param $entityTable
585 * @param int $entityID
587 public static function processAttachment(&$params, $entityTable, $entityID) {
588 $numAttachments = Civi
::settings()->get('max_attachments_backend') ?? self
::DEFAULT_MAX_ATTACHMENTS_BACKEND
;
590 for ($i = 1; $i <= $numAttachments; $i++
) {
591 if (isset($params["attachFile_$i"])) {
593 * Moved the second condition into its own if block to avoid changing
594 * how it works if there happens to be an entry that is not an array,
595 * since we now might exit loop early via newly added break below.
597 if (is_array($params["attachFile_$i"])) {
598 self
::filePostProcess(
599 $params["attachFile_$i"]['location'],
605 $params["attachFile_$i"],
607 $params["attachFile_$i"]['type']
613 * No point looping 100 times if there aren't any more.
614 * This assumes the array is continuous and doesn't skip array keys,
615 * but (a) where would it be doing that, and (b) it would have caused
616 * problems before anyway if there were skipped keys.
626 public static function uploadNames() {
627 $numAttachments = Civi
::settings()->get('max_attachments');
630 for ($i = 1; $i <= $numAttachments; $i++
) {
631 $names[] = "attachFile_{$i}";
633 $names[] = 'uploadFile';
638 * copy/attach an existing file to a different entity
641 * @param $oldEntityTable
642 * @param int $oldEntityId
643 * @param $newEntityTable
644 * @param int $newEntityId
646 public static function copyEntityFile($oldEntityTable, $oldEntityId, $newEntityTable, $newEntityId) {
647 $oldEntityFile = new CRM_Core_DAO_EntityFile();
648 $oldEntityFile->entity_id
= $oldEntityId;
649 $oldEntityFile->entity_table
= $oldEntityTable;
650 $oldEntityFile->find();
652 while ($oldEntityFile->fetch()) {
653 $newEntityFile = new CRM_Core_DAO_EntityFile();
654 $newEntityFile->entity_id
= $newEntityId;
655 $newEntityFile->entity_table
= $newEntityTable;
656 $newEntityFile->file_id
= $oldEntityFile->file_id
;
657 $newEntityFile->save();
662 * @param $entityTable
663 * @param int $entityID
668 public static function deleteURLArgs($entityTable, $entityID, $fileID) {
669 $params['entityTable'] = $entityTable;
670 $params['entityID'] = $entityID;
671 $params['fileID'] = $fileID;
673 $signer = new CRM_Utils_Signer(CRM_Core_Key
::privateKey(), self
::$_signableFields);
674 $params['_sgn'] = $signer->sign($params);
675 return CRM_Utils_System
::makeQueryString($params);
679 * Delete a file attachment from an entity table / entity ID
680 * @throws CRM_Core_Exception
682 public static function deleteAttachment() {
684 $params['entityTable'] = CRM_Utils_Request
::retrieve('entityTable', 'String', CRM_Core_DAO
::$_nullObject, TRUE);
685 $params['entityID'] = CRM_Utils_Request
::retrieve('entityID', 'Positive', CRM_Core_DAO
::$_nullObject, TRUE);
686 $params['fileID'] = CRM_Utils_Request
::retrieve('fileID', 'Positive', CRM_Core_DAO
::$_nullObject, TRUE);
688 $signature = CRM_Utils_Request
::retrieve('_sgn', 'String', CRM_Core_DAO
::$_nullObject, TRUE);
690 $signer = new CRM_Utils_Signer(CRM_Core_Key
::privateKey(), self
::$_signableFields);
691 if (!$signer->validate($signature, $params)) {
692 throw new CRM_Core_Exception('Request signature is invalid');
695 self
::deleteEntityFile($params['entityTable'], $params['entityID'], NULL, $params['fileID']);
699 * Display paper icon for a file attachment -- CRM-13624
701 * @param string $entityTable
702 * The entityTable to which the file is attached. eg "civicrm_contact", "civicrm_note", "civicrm_activity".
703 * If you have the ID of a specific row in civicrm_file, use $entityTable='*'
704 * @param int $entityID
705 * The id of the object in the above entityTable.
708 * list of HTML snippets; one HTML snippet for each attachment. If none found, then NULL
711 public static function paperIconAttachment($entityTable, $entityID) {
712 if (empty($entityTable) ||
!$entityID) {
716 $currentAttachmentInfo = self
::getEntityFile($entityTable, $entityID);
717 foreach ($currentAttachmentInfo as $fileKey => $fileValue) {
718 $fileID = $fileValue['fileID'];
720 $fileType = $fileValue['mime_type'];
721 $url = $fileValue['url'];
722 $title = $fileValue['cleanName'];
723 if ($fileType == 'image/jpeg' ||
724 $fileType == 'image/pjpeg' ||
725 $fileType == 'image/gif' ||
726 $fileType == 'image/x-png' ||
727 $fileType == 'image/png'
729 $file_url[$fileID] = "
730 <a href='$url' class='crm-image-popup' title='$title'>
731 <i class='crm-i fa-file-image-o'></i>
734 // for non image files
736 $file_url[$fileID] = "
737 <a href='$url' title='$title'>
738 <i class='crm-i fa-paperclip'></i>
743 if (empty($file_url)) {
747 $results = $file_url;
753 * Get a reference to the file-search service (if one is available).
755 * @return CRM_Core_FileSearchInterface|NULL
757 public static function getSearchService() {
759 CRM_Utils_Hook
::fileSearches($fileSearches);
761 // use the first available search
762 foreach ($fileSearches as $fileSearch) {
763 /** @var $fileSearch CRM_Core_FileSearchInterface */
770 * Generates an access-token for downloading a specific file.
772 * @param int $entityId entity id the file is attached to
773 * @param int $fileId file ID
778 public static function generateFileHash($entityId = NULL, $fileId = NULL, $genTs = NULL, $life = NULL) {
779 // Use multiple (but stable) inputs for hash information.
780 $siteKey = CRM_Utils_Constant
::value('CIVICRM_SITE_KEY');
782 throw new \
CRM_Core_Exception("Cannot generate file access token. Please set CIVICRM_SITE_KEY.");
789 $days = Civi
::settings()->get('checksum_timeout');
792 // Trim 8 chars off the string, make it slightly easier to find
793 // but reveals less information from the hash.
794 $cs = hash_hmac('sha256', "entity={$entityId}&file={$fileId}&life={$life}", $siteKey);
795 return "{$cs}_{$genTs}_{$life}";
799 * Validate a file access token.
801 * @param string $hash
802 * @param int $entityId Entity Id the file is attached to
803 * @param int $fileId File Id
806 public static function validateFileHash($hash, $entityId, $fileId) {
807 $input = CRM_Utils_System
::explode('_', $hash, 3);
808 $inputTs = $input[1] ??
NULL;
809 $inputLF = $input[2] ??
NULL;
810 $testHash = CRM_Core_BAO_File
::generateFileHash($entityId, $fileId, $inputTs, $inputLF);
811 if (hash_equals($testHash, $hash)) {
813 if ($inputTs +
($inputLF * 60 * 60) >= $now) {