Merge pull request #23044 from braders/https-maps-links
[civicrm-core.git] / CRM / Core / BAO / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * BAO object for crm_log table
20 */
21 class CRM_Core_BAO_File extends CRM_Core_DAO_File {
22
23 public static $_signableFields = ['entityTable', 'entityID', 'fileID'];
24
25 /**
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
28 * process.
29 */
30 const DEFAULT_MAX_ATTACHMENTS_BACKEND = 100;
31
32 /**
33 * Takes an associative array and creates a File object.
34 *
35 * @param array $params
36 * (reference ) an assoc array of name/value pairs.
37 *
38 * @return CRM_Core_BAO_File
39 */
40 public static function create($params) {
41 $fileDAO = new CRM_Core_DAO_File();
42
43 $op = empty($params['id']) ? 'create' : 'edit';
44
45 CRM_Utils_Hook::pre($op, 'File', CRM_Utils_Array::value('id', $params), $params);
46
47 $fileDAO->copyValues($params);
48
49 if (empty($params['id']) && empty($params['created_id'])) {
50 $fileDAO->created_id = CRM_Core_Session::getLoggedInContactID();
51 }
52
53 $fileDAO->save();
54
55 CRM_Utils_Hook::post($op, 'File', $fileDAO->id, $fileDAO);
56
57 return $fileDAO;
58 }
59
60 /**
61 * @param int $fileID
62 * @param int $entityID
63 *
64 * @return array
65 */
66 public static function path($fileID, $entityID) {
67 $entityFileDAO = new CRM_Core_DAO_EntityFile();
68 $entityFileDAO->entity_id = $entityID;
69 $entityFileDAO->file_id = $fileID;
70
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;
77
78 if (file_exists($path) && is_readable($path)) {
79 return [$path, $fileDAO->mime_type];
80 }
81 }
82 }
83
84 return [NULL, NULL];
85 }
86
87 /**
88 * @param string $data
89 * @param int $fileTypeID
90 * @param string $entityTable
91 * @param int $entityID
92 * @param string|false $entitySubtype
93 * @param bool $overwrite
94 * @param null|array $fileParams
95 * @param string $uploadName
96 * @param string $mimeType
97 *
98 * @throws Exception
99 */
100 public static function filePostProcess(
101 $data,
102 $fileTypeID,
103 $entityTable,
104 $entityID,
105 $entitySubtype = FALSE,
106 $overwrite = TRUE,
107 $fileParams = NULL,
108 $uploadName = 'uploadFile',
109 $mimeType = NULL
110 ) {
111 if (!$mimeType) {
112 CRM_Core_Error::statusBounce(ts('Mime Type is now a required parameter for file upload'));
113 }
114
115 $config = CRM_Core_Config::singleton();
116
117 $path = explode('/', $data);
118 $filename = $path[count($path) - 1];
119
120 // rename this file to go into the secure directory
121 if ($entitySubtype) {
122 $directoryName = $config->customFileUploadDir . $entitySubtype . DIRECTORY_SEPARATOR . $entityID;
123 }
124 else {
125 $directoryName = $config->customFileUploadDir;
126 }
127
128 CRM_Utils_File::createDir($directoryName);
129
130 if (!rename($data, $directoryName . DIRECTORY_SEPARATOR . $filename)) {
131 CRM_Core_Error::statusBounce(ts('Could not move custom file to custom upload directory'));
132 }
133
134 // to get id's
135 if ($overwrite && $fileTypeID) {
136 list($sql, $params) = self::sql($entityTable, $entityID, $fileTypeID);
137 }
138 else {
139 list($sql, $params) = self::sql($entityTable, $entityID, 0);
140 }
141
142 $dao = CRM_Core_DAO::executeQuery($sql, $params);
143 $dao->fetch();
144
145 $fileDAO = new CRM_Core_DAO_File();
146 $op = 'create';
147 if (isset($dao->cfID) && $dao->cfID) {
148 $op = 'edit';
149 $fileDAO->id = $dao->cfID;
150 unlink($directoryName . DIRECTORY_SEPARATOR . $dao->uri);
151 }
152
153 if (!empty($fileParams)) {
154 $fileDAO->copyValues($fileParams);
155 }
156
157 $fileDAO->uri = $filename;
158 $fileDAO->mime_type = $mimeType;
159 $fileDAO->file_type_id = $fileTypeID;
160 $fileDAO->upload_date = date('YmdHis');
161 $fileDAO->save();
162
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;
167 }
168 $entityFileDAO->entity_table = $entityTable;
169 $entityFileDAO->entity_id = $entityID;
170 $entityFileDAO->file_id = $fileDAO->id;
171 $entityFileDAO->save();
172
173 //save static tags
174 if (!empty($fileParams['tag'])) {
175 CRM_Core_BAO_EntityTag::create($fileParams['tag'], 'civicrm_file', $entityFileDAO->id);
176 }
177
178 //save free tags
179 if (isset($fileParams['attachment_taglist']) && !empty($fileParams['attachment_taglist'])) {
180 CRM_Core_Form_Tag::postProcess($fileParams['attachment_taglist'], $entityFileDAO->id, 'civicrm_file');
181 }
182
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);
185 }
186
187 /**
188 * A static function wrapper that deletes the various objects.
189 *
190 * Objects are those hat are connected to a file object (i.e. file, entityFile and customValue.
191 *
192 * @param int $fileID
193 * @param int $entityID
194 * @param int $fieldID
195 *
196 * @throws \Exception
197 */
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'));
203 }
204
205 // lets call a pre hook before the delete, so attachments hooks can get the info before things
206 // disappear
207 CRM_Utils_Hook::pre('delete', 'File', $fileID, $fileDAO);
208
209 // get the table and column name
210 list($tableName, $columnName, $groupID) = CRM_Core_BAO_CustomField::getTableColumnGroup($fieldID);
211
212 $entityFileDAO = new CRM_Core_DAO_EntityFile();
213 $entityFileDAO->file_id = $fileID;
214 $entityFileDAO->entity_id = $entityID;
215 $entityFileDAO->entity_table = $tableName;
216
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));
219 }
220
221 $entityFileDAO->delete();
222 $fileDAO->delete();
223
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);
228 }
229
230 /**
231 * The $useWhere is used so that the signature matches the parent class
232 *
233 * public function delete($useWhere = FALSE) {
234 * list($fileID, $entityID, $fieldID) = func_get_args();
235 *
236 * self::deleteFileReferences($fileID, $entityID, $fieldID);
237 * } */
238
239 /**
240 * Delete all the files and associated object associated with this combination.
241 *
242 * @param string $entityTable
243 * @param int $entityID
244 * @param int $fileTypeID
245 * @param int $fileID
246 *
247 * @return bool
248 * Was file deleted?
249 */
250 public static function deleteEntityFile($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
251 $isDeleted = FALSE;
252 if (empty($entityTable) || empty($entityID)) {
253 return $isDeleted;
254 }
255
256 $config = CRM_Core_Config::singleton();
257
258 list($sql, $params) = self::sql($entityTable, $entityID, $fileTypeID, $fileID);
259 $dao = CRM_Core_DAO::executeQuery($sql, $params);
260
261 $cfIDs = [];
262 $cefIDs = [];
263 while ($dao->fetch()) {
264 $cfIDs[$dao->cfID] = $dao->uri;
265 $cefIDs[] = $dao->cefID;
266 }
267
268 // Delete tags from entity tag table.
269 if (!empty($cfIDs)) {
270 $deleteFiles = [];
271 foreach ($cfIDs as $fId => $fUri) {
272 $tagParams = [
273 'entity_table' => 'civicrm_file',
274 'entity_id' => $fId,
275 ];
276 CRM_Core_BAO_EntityTag::del($tagParams);
277 }
278 }
279
280 // Delete entries from entity file table.
281 if (!empty($cefIDs)) {
282 $cefIDs = implode(',', $cefIDs);
283 $sql = "DELETE FROM civicrm_entity_file where id IN ( $cefIDs )";
284 CRM_Core_DAO::executeQuery($sql);
285 $isDeleted = TRUE;
286 }
287
288 if (!empty($cfIDs)) {
289 $deleteFiles = [];
290 foreach ($cfIDs as $fId => $fUri) {
291 // Delete file only if there are no longer any entities using this file.
292 if (!CRM_Core_DAO::getFieldValue('CRM_Core_DAO_EntityFile', $fId, 'id', 'file_id')) {
293 unlink($config->customFileUploadDir . DIRECTORY_SEPARATOR . $fUri);
294 $deleteFiles[$fId] = $fId;
295 }
296 }
297
298 // Delete entries from file table.
299 if (!empty($deleteFiles)) {
300 $deleteFiles = implode(',', $deleteFiles);
301 $sql = "DELETE FROM civicrm_file where id IN ( $deleteFiles )";
302 CRM_Core_DAO::executeQuery($sql);
303 }
304 $isDeleted = TRUE;
305 }
306 return $isDeleted;
307 }
308
309 /**
310 * Get all the files and associated object associated with this combination.
311 *
312 * @param string $entityTable
313 * @param int $entityID
314 * @param bool $addDeleteArgs
315 *
316 * @return array|null
317 */
318 public static function getEntityFile($entityTable, $entityID, $addDeleteArgs = FALSE) {
319 if (empty($entityTable) || !$entityID) {
320 $results = NULL;
321 return $results;
322 }
323
324 $config = CRM_Core_Config::singleton();
325
326 list($sql, $params) = self::sql($entityTable, $entityID, NULL);
327 $dao = CRM_Core_DAO::executeQuery($sql, $params);
328 $results = [];
329 while ($dao->fetch()) {
330 $fileHash = self::generateFileHash($dao->entity_id, $dao->cfID);
331 $result['fileID'] = $dao->cfID;
332 $result['entityID'] = $dao->cefID;
333 $result['mime_type'] = $dao->mime_type;
334 $result['fileName'] = $dao->uri;
335 $result['description'] = $dao->description;
336 $result['cleanName'] = CRM_Utils_File::cleanFileName($dao->uri);
337 $result['fullPath'] = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $dao->uri;
338 $result['url'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$dao->entity_id}&fcs={$fileHash}");
339 $result['href'] = "<a href=\"{$result['url']}\">{$result['cleanName']}</a>";
340 $result['tag'] = CRM_Core_BAO_EntityTag::getTag($dao->cfID, 'civicrm_file');
341 $result['icon'] = CRM_Utils_File::getIconFromMimeType($dao->mime_type);
342 if ($addDeleteArgs) {
343 $result['deleteURLArgs'] = self::deleteURLArgs($dao->entity_table, $dao->entity_id, $dao->cfID);
344 }
345 $results[$dao->cfID] = $result;
346 }
347
348 //fix tag names
349 $tags = CRM_Core_PseudoConstant::get('CRM_Core_DAO_EntityTag', 'tag_id', ['onlyActive' => FALSE]);
350
351 foreach ($results as &$values) {
352 if (!empty($values['tag'])) {
353 $tagNames = [];
354 foreach ($values['tag'] as $tid) {
355 $tagNames[] = $tags[$tid];
356 }
357 $values['tag'] = implode(', ', $tagNames);
358 }
359 else {
360 $values['tag'] = '';
361 }
362 }
363
364 return $results;
365 }
366
367 /**
368 * @param string $entityTable
369 * Table-name or "*" (to reference files directly by file-id).
370 * @param int $entityID
371 * @param int $fileTypeID
372 * @param int $fileID
373 *
374 * @return array
375 */
376 public static function sql($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
377 if ($entityTable == '*') {
378 // $entityID is the ID of a specific file
379 $sql = "
380 SELECT CF.id as cfID,
381 CF.uri as uri,
382 CF.mime_type as mime_type,
383 CF.description as description,
384 CEF.id as cefID,
385 CEF.entity_table as entity_table,
386 CEF.entity_id as entity_id
387 FROM civicrm_file AS CF
388 LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
389 WHERE CF.id = %2";
390
391 }
392 else {
393 $sql = "
394 SELECT CF.id as cfID,
395 CF.uri as uri,
396 CF.mime_type as mime_type,
397 CF.description as description,
398 CEF.id as cefID,
399 CEF.entity_table as entity_table,
400 CEF.entity_id as entity_id
401 FROM civicrm_file AS CF
402 LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
403 WHERE CEF.entity_table = %1
404 AND CEF.entity_id = %2";
405 }
406
407 $params = [
408 1 => [$entityTable, 'String'],
409 2 => [$entityID, 'Integer'],
410 ];
411
412 if ($fileTypeID !== NULL) {
413 $sql .= " AND CF.file_type_id = %3";
414 $params[3] = [$fileTypeID, 'Integer'];
415 }
416
417 if ($fileID !== NULL) {
418 $sql .= " AND CF.id = %4";
419 $params[4] = [$fileID, 'Integer'];
420 }
421
422 return [$sql, $params];
423 }
424
425 /**
426 * @param CRM_Core_Form $form
427 * @param string $entityTable
428 * @param int $entityID
429 * @param null $numAttachments
430 * @param bool $ajaxDelete
431 */
432 public static function buildAttachment(&$form, $entityTable, $entityID = NULL, $numAttachments = NULL, $ajaxDelete = FALSE) {
433
434 if (!$numAttachments) {
435 $numAttachments = Civi::settings()->get('max_attachments');
436 }
437 // Assign maxAttachments count to template for help message
438 $form->assign('maxAttachments', $numAttachments);
439
440 $config = CRM_Core_Config::singleton();
441 // set default max file size as 2MB
442 $maxFileSize = $config->maxFileSize ? $config->maxFileSize : 2;
443
444 $currentAttachmentInfo = self::getEntityFile($entityTable, $entityID, TRUE);
445 $totalAttachments = 0;
446 if ($currentAttachmentInfo) {
447 $totalAttachments = count($currentAttachmentInfo);
448 $form->add('checkbox', 'is_delete_attachment', ts('Delete All Attachment(s)'));
449 $form->assign('currentAttachmentInfo', $currentAttachmentInfo);
450 }
451 else {
452 $form->assign('currentAttachmentInfo', NULL);
453 }
454
455 if ($totalAttachments) {
456 if ($totalAttachments >= $numAttachments) {
457 $numAttachments = 0;
458 }
459 else {
460 $numAttachments -= $totalAttachments;
461 }
462 }
463
464 $form->assign('numAttachments', $numAttachments);
465
466 CRM_Core_BAO_Tag::getTags('civicrm_file', $tags, NULL,
467 '&nbsp;&nbsp;', TRUE);
468
469 // get tagset info
470 $parentNames = CRM_Core_BAO_Tag::getTagSet('civicrm_file');
471
472 // add attachments
473 for ($i = 1; $i <= $numAttachments; $i++) {
474 $form->addElement('file', "attachFile_$i", ts('Attach File'), 'size=30 maxlength=221');
475 $form->addUploadElement("attachFile_$i");
476 $form->setMaxFileSize($maxFileSize * 1024 * 1024);
477 $form->addRule("attachFile_$i",
478 ts('File size should be less than %1 MByte(s)',
479 [1 => $maxFileSize]
480 ),
481 'maxfilesize',
482 $maxFileSize * 1024 * 1024
483 );
484 $form->addElement('text', "attachDesc_$i", NULL, [
485 'size' => 40,
486 'maxlength' => 255,
487 'placeholder' => ts('Description'),
488 ]);
489
490 if (!empty($tags)) {
491 $form->add('select', "tag_$i", ts('Tags'), $tags, FALSE,
492 [
493 'id' => "tags_$i",
494 'multiple' => 'multiple',
495 'class' => 'huge crm-select2',
496 'placeholder' => ts('- none -'),
497 ]
498 );
499 }
500 CRM_Core_Form_Tag::buildQuickForm($form, $parentNames, 'civicrm_file', NULL, FALSE, TRUE, "file_taglist_$i");
501 }
502 }
503
504 /**
505 * Return a HTML string, separated by $separator,
506 * where each item is an anchor link to the file,
507 * with the filename as the link text.
508 *
509 * @param string $entityTable
510 * The entityTable to which the file is attached.
511 * @param int $entityID
512 * The id of the object in the above entityTable.
513 * @param string $separator
514 * The string separator where to implode the urls.
515 *
516 * @return string|null
517 * HTML list of attachment links, or null if no attachments
518 */
519 public static function attachmentInfo($entityTable, $entityID, $separator = '<br />') {
520 if (!$entityID) {
521 return NULL;
522 }
523
524 $currentAttachments = self::getEntityFile($entityTable, $entityID);
525 if (!empty($currentAttachments)) {
526 $currentAttachmentURL = [];
527 foreach ($currentAttachments as $fileID => $attach) {
528 $currentAttachmentURL[] = $attach['href'];
529 }
530 return implode($separator, $currentAttachmentURL);
531 }
532 return NULL;
533 }
534
535 /**
536 * @param $formValues
537 * @param array $params
538 * @param $entityTable
539 * @param int $entityID
540 */
541 public static function formatAttachment(
542 &$formValues,
543 &$params,
544 $entityTable,
545 $entityID = NULL
546 ) {
547
548 // delete current attachments if applicable
549 if ($entityID && !empty($formValues['is_delete_attachment'])) {
550 CRM_Core_BAO_File::deleteEntityFile($entityTable, $entityID);
551 }
552
553 $numAttachments = Civi::settings()->get('max_attachments');
554
555 // setup all attachments
556 for ($i = 1; $i <= $numAttachments; $i++) {
557 $attachName = "attachFile_$i";
558 $attachDesc = "attachDesc_$i";
559 $attachTags = "tag_$i";
560 $attachFreeTags = "file_taglist_$i";
561 if (isset($formValues[$attachName]) && !empty($formValues[$attachName])) {
562 // add static tags if selects
563 $tagParams = [];
564 if (!empty($formValues[$attachTags])) {
565 foreach ($formValues[$attachTags] as $tag) {
566 $tagParams[$tag] = 1;
567 }
568 }
569
570 // we dont care if the file is empty or not
571 // CRM-7448
572 $extraParams = [
573 'description' => $formValues[$attachDesc],
574 'tag' => $tagParams,
575 'attachment_taglist' => $formValues[$attachFreeTags] ?? [],
576 ];
577
578 CRM_Utils_File::formatFile($formValues, $attachName, $extraParams);
579
580 // set the formatted attachment attributes to $params, later used
581 // to send mail with desired attachments
582 if (!empty($formValues[$attachName])) {
583 $params[$attachName] = $formValues[$attachName];
584 }
585 }
586 }
587 }
588
589 /**
590 * @param array $params
591 * @param $entityTable
592 * @param int $entityID
593 */
594 public static function processAttachment(&$params, $entityTable, $entityID) {
595 $numAttachments = Civi::settings()->get('max_attachments_backend') ?? self::DEFAULT_MAX_ATTACHMENTS_BACKEND;
596
597 for ($i = 1; $i <= $numAttachments; $i++) {
598 if (isset($params["attachFile_$i"])) {
599 /**
600 * Moved the second condition into its own if block to avoid changing
601 * how it works if there happens to be an entry that is not an array,
602 * since we now might exit loop early via newly added break below.
603 */
604 if (is_array($params["attachFile_$i"])) {
605 self::filePostProcess(
606 $params["attachFile_$i"]['location'],
607 NULL,
608 $entityTable,
609 $entityID,
610 NULL,
611 TRUE,
612 $params["attachFile_$i"],
613 "attachFile_$i",
614 $params["attachFile_$i"]['type']
615 );
616 }
617 }
618 else {
619 /**
620 * No point looping 100 times if there aren't any more.
621 * This assumes the array is continuous and doesn't skip array keys,
622 * but (a) where would it be doing that, and (b) it would have caused
623 * problems before anyway if there were skipped keys.
624 */
625 break;
626 }
627 }
628 }
629
630 /**
631 * @return array
632 */
633 public static function uploadNames() {
634 $numAttachments = Civi::settings()->get('max_attachments');
635
636 $names = [];
637 for ($i = 1; $i <= $numAttachments; $i++) {
638 $names[] = "attachFile_{$i}";
639 }
640 $names[] = 'uploadFile';
641 return $names;
642 }
643
644 /**
645 * copy/attach an existing file to a different entity
646 * table and id.
647 *
648 * @param $oldEntityTable
649 * @param int $oldEntityId
650 * @param $newEntityTable
651 * @param int $newEntityId
652 */
653 public static function copyEntityFile($oldEntityTable, $oldEntityId, $newEntityTable, $newEntityId) {
654 $oldEntityFile = new CRM_Core_DAO_EntityFile();
655 $oldEntityFile->entity_id = $oldEntityId;
656 $oldEntityFile->entity_table = $oldEntityTable;
657 $oldEntityFile->find();
658
659 while ($oldEntityFile->fetch()) {
660 $newEntityFile = new CRM_Core_DAO_EntityFile();
661 $newEntityFile->entity_id = $newEntityId;
662 $newEntityFile->entity_table = $newEntityTable;
663 $newEntityFile->file_id = $oldEntityFile->file_id;
664 $newEntityFile->save();
665 }
666 }
667
668 /**
669 * @param $entityTable
670 * @param int $entityID
671 * @param int $fileID
672 *
673 * @return string
674 */
675 public static function deleteURLArgs($entityTable, $entityID, $fileID) {
676 $params['entityTable'] = $entityTable;
677 $params['entityID'] = $entityID;
678 $params['fileID'] = $fileID;
679
680 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$_signableFields);
681 $params['_sgn'] = $signer->sign($params);
682 return CRM_Utils_System::makeQueryString($params);
683 }
684
685 /**
686 * Delete a file attachment from an entity table / entity ID
687 * @throws CRM_Core_Exception
688 */
689 public static function deleteAttachment() {
690 $params = [];
691 $params['entityTable'] = CRM_Utils_Request::retrieve('entityTable', 'String', CRM_Core_DAO::$_nullObject, TRUE);
692 $params['entityID'] = CRM_Utils_Request::retrieve('entityID', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
693 $params['fileID'] = CRM_Utils_Request::retrieve('fileID', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
694
695 $signature = CRM_Utils_Request::retrieve('_sgn', 'String', CRM_Core_DAO::$_nullObject, TRUE);
696
697 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$_signableFields);
698 if (!$signer->validate($signature, $params)) {
699 throw new CRM_Core_Exception('Request signature is invalid');
700 }
701
702 self::deleteEntityFile($params['entityTable'], $params['entityID'], NULL, $params['fileID']);
703 }
704
705 /**
706 * Display paper icon for a file attachment -- CRM-13624
707 *
708 * @param string $entityTable
709 * The entityTable to which the file is attached. eg "civicrm_contact", "civicrm_note", "civicrm_activity".
710 * If you have the ID of a specific row in civicrm_file, use $entityTable='*'
711 * @param int $entityID
712 * The id of the object in the above entityTable.
713 *
714 * @return array|NULL
715 * list of HTML snippets; one HTML snippet for each attachment. If none found, then NULL
716 *
717 */
718 public static function paperIconAttachment($entityTable, $entityID) {
719 if (empty($entityTable) || !$entityID) {
720 $results = NULL;
721 return $results;
722 }
723 $currentAttachmentInfo = self::getEntityFile($entityTable, $entityID);
724 foreach ($currentAttachmentInfo as $fileKey => $fileValue) {
725 $fileID = $fileValue['fileID'];
726 if ($fileID) {
727 $fileType = $fileValue['mime_type'];
728 $url = $fileValue['url'];
729 $title = $fileValue['cleanName'];
730 if ($fileType == 'image/jpeg' ||
731 $fileType == 'image/pjpeg' ||
732 $fileType == 'image/gif' ||
733 $fileType == 'image/x-png' ||
734 $fileType == 'image/png'
735 ) {
736 $file_url[$fileID] = <<<HEREDOC
737 <a href="$url" class="crm-image-popup" title="$title">
738 <i class="crm-i fa-file-image-o" aria-hidden="true"></i>
739 </a>
740 HEREDOC;
741 }
742 // for non image files
743 else {
744 $file_url[$fileID] = <<<HEREDOC
745 <a href="$url" title="$title">
746 <i class="crm-i fa-paperclip" aria-hidden="true"></i>
747 </a>
748 HEREDOC;
749 }
750 }
751 }
752 if (empty($file_url)) {
753 $results = NULL;
754 }
755 else {
756 $results = $file_url;
757 }
758 return $results;
759 }
760
761 /**
762 * Get a reference to the file-search service (if one is available).
763 *
764 * @return CRM_Core_FileSearchInterface|NULL
765 */
766 public static function getSearchService() {
767 $fileSearches = [];
768 CRM_Utils_Hook::fileSearches($fileSearches);
769
770 // use the first available search
771 foreach ($fileSearches as $fileSearch) {
772 /** @var $fileSearch CRM_Core_FileSearchInterface */
773 return $fileSearch;
774 }
775 return NULL;
776 }
777
778 /**
779 * Generates an access-token for downloading a specific file.
780 *
781 * @param int $entityId entity id the file is attached to
782 * @param int $fileId file ID
783 * @param int $genTs
784 * @param int $life
785 * @return string
786 */
787 public static function generateFileHash($entityId = NULL, $fileId = NULL, $genTs = NULL, $life = NULL) {
788 // Use multiple (but stable) inputs for hash information.
789 $siteKey = CRM_Utils_Constant::value('CIVICRM_SITE_KEY');
790 if (!$siteKey) {
791 throw new \CRM_Core_Exception("Cannot generate file access token. Please set CIVICRM_SITE_KEY.");
792 }
793
794 if (!$genTs) {
795 $genTs = time();
796 }
797 if (!$life) {
798 $days = Civi::settings()->get('checksum_timeout');
799 $life = 24 * $days;
800 }
801 // Trim 8 chars off the string, make it slightly easier to find
802 // but reveals less information from the hash.
803 $cs = hash_hmac('sha256', "entity={$entityId}&file={$fileId}&life={$life}", $siteKey);
804 return "{$cs}_{$genTs}_{$life}";
805 }
806
807 /**
808 * Validate a file access token.
809 *
810 * @param string $hash
811 * @param int $entityId Entity Id the file is attached to
812 * @param int $fileId File Id
813 * @return bool
814 */
815 public static function validateFileHash($hash, $entityId, $fileId) {
816 $input = CRM_Utils_System::explode('_', $hash, 3);
817 $inputTs = $input[1] ?? NULL;
818 $inputLF = $input[2] ?? NULL;
819 $testHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileId, $inputTs, $inputLF);
820 if (hash_equals($testHash, $hash)) {
821 $now = time();
822 if ($inputTs + ($inputLF * 60 * 60) >= $now) {
823 return TRUE;
824 }
825 else {
826 return FALSE;
827 }
828 }
829 return FALSE;
830 }
831
832 }