Merge pull request #17008 from ivan-compucorp/CPS-70-fix-radio-value
[civicrm-core.git] / CRM / Core / BAO / File.php
CommitLineData
6a488035 1<?php
0c56e4c8
TO
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
0c56e4c8 5 | |
bc77d7c0
TO
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 |
0c56e4c8 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
0c56e4c8
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17
0c56e4c8
TO
18/**
19 * BAO object for crm_log table
20 */
21class CRM_Core_BAO_File extends CRM_Core_DAO_File {
22
518fa0ee 23 public static $_signableFields = ['entityTable', 'entityID', 'fileID'];
0c56e4c8 24
8913e915
D
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
ae2c7e00 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
0c56e4c8 60 /**
100fef9d
CW
61 * @param int $fileID
62 * @param int $entityID
0c56e4c8
TO
63 *
64 * @return array
65 */
9bbc34f1 66 public static function path($fileID, $entityID) {
0c56e4c8 67 $entityFileDAO = new CRM_Core_DAO_EntityFile();
0c56e4c8
TO
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)) {
be2fb01f 79 return [$path, $fileDAO->mime_type];
0c56e4c8
TO
80 }
81 }
82 }
83
be2fb01f 84 return [NULL, NULL];
0c56e4c8
TO
85 }
86
0c56e4c8
TO
87 /**
88 * @param $data
100fef9d 89 * @param int $fileTypeID
0c56e4c8 90 * @param $entityTable
100fef9d 91 * @param int $entityID
0c56e4c8
TO
92 * @param $entitySubtype
93 * @param bool $overwrite
75fd1335 94 * @param null|array $fileParams
0c56e4c8
TO
95 * @param string $uploadName
96 * @param null $mimeType
97 *
98 * @throws Exception
99 */
ae5ffbb7 100 public static function filePostProcess(
0c56e4c8
TO
101 $data,
102 $fileTypeID,
103 $entityTable,
104 $entityID,
105 $entitySubtype,
106 $overwrite = TRUE,
107 $fileParams = NULL,
108 $uploadName = 'uploadFile',
109 $mimeType = NULL
110 ) {
111 if (!$mimeType) {
b93376a8 112 CRM_Core_Error::statusBounce(ts('Mime Type is now a required parameter for file upload'));
0c56e4c8
TO
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)) {
b93376a8 131 CRM_Core_Error::statusBounce(ts('Could not move custom file to custom upload directory'));
0c56e4c8
TO
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;
77ca21d6 160 $fileDAO->upload_date = date('YmdHis');
0c56e4c8
TO
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'])) {
1273d77c 180 CRM_Core_Form_Tag::postProcess($fileParams['attachment_taglist'], $entityFileDAO->id, 'civicrm_file');
0c56e4c8
TO
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 /**
ad37ac8e 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
0c56e4c8
TO
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)) {
eea8ec44 202 throw new CRM_Core_Exception(ts('File not found'));
0c56e4c8
TO
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)) {
eea8ec44 218 throw new CRM_Core_Exception(sprintf('No record found for given file ID - %d and entity ID - %d', $fileID, $entityID));
0c56e4c8
TO
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";
be2fb01f 226 $params = [1 => [$fileID, 'Integer']];
0c56e4c8
TO
227 CRM_Core_DAO::executeQuery($query, $params);
228 }
229
230 /**
231 * The $useWhere is used so that the signature matches the parent class
0527cd81 232 *
353ffa53
TO
233 * public function delete($useWhere = FALSE) {
234 * list($fileID, $entityID, $fieldID) = func_get_args();
235 *
236 * self::deleteFileReferences($fileID, $entityID, $fieldID);
237 * } */
0c56e4c8
TO
238
239 /**
ad37ac8e 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 *
7b8bc7a1
SB
247 * @return bool
248 * Was file deleted?
0c56e4c8 249 */
00be9182 250 public static function deleteEntityFile($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
7b8bc7a1 251 $isDeleted = FALSE;
0c56e4c8 252 if (empty($entityTable) || empty($entityID)) {
7b8bc7a1 253 return $isDeleted;
0c56e4c8
TO
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
be2fb01f
CW
261 $cfIDs = [];
262 $cefIDs = [];
0c56e4c8
TO
263 while ($dao->fetch()) {
264 $cfIDs[$dao->cfID] = $dao->uri;
265 $cefIDs[] = $dao->cefID;
266 }
267
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);
7b8bc7a1 272 $isDeleted = TRUE;
0c56e4c8
TO
273 }
274
275 if (!empty($cfIDs)) {
276 // Delete file only if there no any entity using this file.
be2fb01f 277 $deleteFiles = [];
0c56e4c8
TO
278 foreach ($cfIDs as $fId => $fUri) {
279 //delete tags from entity tag table
be2fb01f 280 $tagParams = [
0c56e4c8 281 'entity_table' => 'civicrm_file',
21dfd5f5 282 'entity_id' => $fId,
be2fb01f 283 ];
0c56e4c8
TO
284
285 CRM_Core_BAO_EntityTag::del($tagParams);
286
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;
290 }
291 }
292
293 if (!empty($deleteFiles)) {
294 $deleteFiles = implode(',', $deleteFiles);
295 $sql = "DELETE FROM civicrm_file where id IN ( $deleteFiles )";
296 CRM_Core_DAO::executeQuery($sql);
297 }
7b8bc7a1 298 $isDeleted = TRUE;
0c56e4c8 299 }
7b8bc7a1 300 return $isDeleted;
0c56e4c8
TO
301 }
302
303 /**
ad37ac8e 304 * Get all the files and associated object associated with this combination.
305 *
306 * @param string $entityTable
307 * @param int $entityID
308 * @param bool $addDeleteArgs
309 *
310 * @return array|null
0c56e4c8 311 */
00be9182 312 public static function getEntityFile($entityTable, $entityID, $addDeleteArgs = FALSE) {
0c56e4c8
TO
313 if (empty($entityTable) || !$entityID) {
314 $results = NULL;
315 return $results;
316 }
317
318 $config = CRM_Core_Config::singleton();
319
320 list($sql, $params) = self::sql($entityTable, $entityID, NULL);
321 $dao = CRM_Core_DAO::executeQuery($sql, $params);
be2fb01f 322 $results = [];
0c56e4c8 323 while ($dao->fetch()) {
ff9aeadb 324 $fileHash = self::generateFileHash($dao->entity_id, $dao->cfID);
0c56e4c8
TO
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;
ff9aeadb 332 $result['url'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$dao->entity_id}&fcs={$fileHash}");
0c56e4c8
TO
333 $result['href'] = "<a href=\"{$result['url']}\">{$result['cleanName']}</a>";
334 $result['tag'] = CRM_Core_BAO_EntityTag::getTag($dao->cfID, 'civicrm_file');
30a0455f 335 $result['icon'] = CRM_Utils_File::getIconFromMimeType($dao->mime_type);
0c56e4c8 336 if ($addDeleteArgs) {
06faabfa 337 $result['deleteURLArgs'] = self::deleteURLArgs($dao->entity_table, $dao->entity_id, $dao->cfID);
0c56e4c8
TO
338 }
339 $results[$dao->cfID] = $result;
340 }
341
342 //fix tag names
be2fb01f 343 $tags = CRM_Core_PseudoConstant::get('CRM_Core_DAO_EntityTag', 'tag_id', ['onlyActive' => FALSE]);
0c56e4c8
TO
344
345 foreach ($results as &$values) {
346 if (!empty($values['tag'])) {
be2fb01f 347 $tagNames = [];
0c56e4c8
TO
348 foreach ($values['tag'] as $tid) {
349 $tagNames[] = $tags[$tid];
350 }
351 $values['tag'] = implode(', ', $tagNames);
352 }
353 else {
354 $values['tag'] = '';
355 }
356 }
357
0c56e4c8
TO
358 return $results;
359 }
360
361 /**
6a0b768e
TO
362 * @param string $entityTable
363 * Table-name or "*" (to reference files directly by file-id).
06faabfa 364 * @param int $entityID
100fef9d
CW
365 * @param int $fileTypeID
366 * @param int $fileID
0c56e4c8
TO
367 *
368 * @return array
369 */
00be9182 370 public static function sql($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
06faabfa
TO
371 if ($entityTable == '*') {
372 // $entityID is the ID of a specific file
373 $sql = "
374SELECT CF.id as cfID,
375 CF.uri as uri,
376 CF.mime_type as mime_type,
377 CF.description as description,
378 CEF.id as cefID,
379 CEF.entity_table as entity_table,
380 CEF.entity_id as entity_id
381FROM civicrm_file AS CF
382LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
383WHERE CF.id = %2";
384
0db6c3e1
TO
385 }
386 else {
06faabfa 387 $sql = "
0c56e4c8 388SELECT CF.id as cfID,
6a488035
TO
389 CF.uri as uri,
390 CF.mime_type as mime_type,
391 CF.description as description,
06faabfa
TO
392 CEF.id as cefID,
393 CEF.entity_table as entity_table,
394 CEF.entity_id as entity_id
0c56e4c8
TO
395FROM civicrm_file AS CF
396LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
397WHERE CEF.entity_table = %1
398AND CEF.entity_id = %2";
06faabfa 399 }
0c56e4c8 400
be2fb01f
CW
401 $params = [
402 1 => [$entityTable, 'String'],
403 2 => [$entityID, 'Integer'],
404 ];
0c56e4c8
TO
405
406 if ($fileTypeID !== NULL) {
407 $sql .= " AND CF.file_type_id = %3";
be2fb01f 408 $params[3] = [$fileTypeID, 'Integer'];
0c56e4c8
TO
409 }
410
411 if ($fileID !== NULL) {
412 $sql .= " AND CF.id = %4";
be2fb01f 413 $params[4] = [$fileID, 'Integer'];
0c56e4c8
TO
414 }
415
be2fb01f 416 return [$sql, $params];
0c56e4c8
TO
417 }
418
419 /**
75fd1335
TO
420 * @param CRM_Core_Form $form
421 * @param string $entityTable
100fef9d 422 * @param int $entityID
0c56e4c8
TO
423 * @param null $numAttachments
424 * @param bool $ajaxDelete
425 */
00be9182 426 public static function buildAttachment(&$form, $entityTable, $entityID = NULL, $numAttachments = NULL, $ajaxDelete = FALSE) {
0c56e4c8
TO
427
428 if (!$numAttachments) {
aaffa79f 429 $numAttachments = Civi::settings()->get('max_attachments');
0c56e4c8
TO
430 }
431 // Assign maxAttachments count to template for help message
432 $form->assign('maxAttachments', $numAttachments);
433
434 $config = CRM_Core_Config::singleton();
435 // set default max file size as 2MB
436 $maxFileSize = $config->maxFileSize ? $config->maxFileSize : 2;
437
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);
444 }
445 else {
446 $form->assign('currentAttachmentInfo', NULL);
447 }
448
449 if ($totalAttachments) {
450 if ($totalAttachments >= $numAttachments) {
451 $numAttachments = 0;
452 }
453 else {
454 $numAttachments -= $totalAttachments;
455 }
456 }
457
458 $form->assign('numAttachments', $numAttachments);
459
e0f9d6a2 460 CRM_Core_BAO_Tag::getTags('civicrm_file', $tags, NULL,
461 '&nbsp;&nbsp;', TRUE);
0c56e4c8
TO
462
463 // get tagset info
464 $parentNames = CRM_Core_BAO_Tag::getTagSet('civicrm_file');
465
466 // add attachments
467 for ($i = 1; $i <= $numAttachments; $i++) {
d00e9ef0 468 $form->addElement('file', "attachFile_$i", ts('Attach File'), 'size=30 maxlength=221');
0c56e4c8
TO
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)',
be2fb01f 473 [1 => $maxFileSize]
0c56e4c8
TO
474 ),
475 'maxfilesize',
476 $maxFileSize * 1024 * 1024
477 );
be2fb01f 478 $form->addElement('text', "attachDesc_$i", NULL, [
0c56e4c8
TO
479 'size' => 40,
480 'maxlength' => 255,
21dfd5f5 481 'placeholder' => ts('Description'),
be2fb01f 482 ]);
0c56e4c8
TO
483
484 if (!empty($tags)) {
485 $form->add('select', "tag_$i", ts('Tags'), $tags, FALSE,
be2fb01f 486 [
0c56e4c8
TO
487 'id' => "tags_$i",
488 'multiple' => 'multiple',
489 'class' => 'huge crm-select2',
21dfd5f5 490 'placeholder' => ts('- none -'),
be2fb01f 491 ]
0c56e4c8
TO
492 );
493 }
494 CRM_Core_Form_Tag::buildQuickForm($form, $parentNames, 'civicrm_file', NULL, FALSE, TRUE, "file_taglist_$i");
495 }
496 }
497
498 /**
100fef9d 499 * Return a clean url string and the number of attachment for a
0c56e4c8
TO
500 * given entityTable, entityID
501 *
5a4f6742
CW
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.
0c56e4c8 508 *
a6c01b45
CW
509 * @return array
510 * An array with 2 elements. The string and the number of attachments
0c56e4c8 511 */
00be9182 512 public static function attachmentInfo($entityTable, $entityID, $separator = '<br />') {
0c56e4c8
TO
513 if (!$entityID) {
514 return NULL;
515 }
516
517 $currentAttachments = self::getEntityFile($entityTable, $entityID);
518 if (!empty($currentAttachments)) {
be2fb01f 519 $currentAttachmentURL = [];
0c56e4c8
TO
520 foreach ($currentAttachments as $fileID => $attach) {
521 $currentAttachmentURL[] = $attach['href'];
522 }
523 return implode($separator, $currentAttachmentURL);
524 }
525 return NULL;
526 }
527
528 /**
529 * @param $formValues
c490a46a 530 * @param array $params
0c56e4c8 531 * @param $entityTable
100fef9d 532 * @param int $entityID
0c56e4c8 533 */
ae5ffbb7 534 public static function formatAttachment(
0c56e4c8
TO
535 &$formValues,
536 &$params,
537 $entityTable,
538 $entityID = NULL
539 ) {
540
541 // delete current attachments if applicable
542 if ($entityID && !empty($formValues['is_delete_attachment'])) {
543 CRM_Core_BAO_File::deleteEntityFile($entityTable, $entityID);
544 }
545
aaffa79f 546 $numAttachments = Civi::settings()->get('max_attachments');
0c56e4c8 547
0c56e4c8
TO
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
be2fb01f 556 $tagParams = [];
0c56e4c8
TO
557 if (!empty($formValues[$attachTags])) {
558 foreach ($formValues[$attachTags] as $tag) {
559 $tagParams[$tag] = 1;
560 }
561 }
562
563 // we dont care if the file is empty or not
564 // CRM-7448
be2fb01f 565 $extraParams = [
0c56e4c8 566 'description' => $formValues[$attachDesc],
0c56e4c8 567 'tag' => $tagParams,
6187cca4 568 'attachment_taglist' => $formValues[$attachFreeTags] ?? [],
be2fb01f 569 ];
0c56e4c8 570
90a73810 571 CRM_Utils_File::formatFile($formValues, $attachName, $extraParams);
cde03305 572
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];
577 }
0c56e4c8
TO
578 }
579 }
580 }
581
582 /**
c490a46a 583 * @param array $params
0c56e4c8 584 * @param $entityTable
100fef9d 585 * @param int $entityID
0c56e4c8 586 */
00be9182 587 public static function processAttachment(&$params, $entityTable, $entityID) {
8913e915 588 $numAttachments = Civi::settings()->get('max_attachments_backend') ?? self::DEFAULT_MAX_ATTACHMENTS_BACKEND;
0c56e4c8
TO
589
590 for ($i = 1; $i <= $numAttachments; $i++) {
8913e915
D
591 if (isset($params["attachFile_$i"])) {
592 /**
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.
596 */
597 if (is_array($params["attachFile_$i"])) {
598 self::filePostProcess(
599 $params["attachFile_$i"]['location'],
600 NULL,
601 $entityTable,
602 $entityID,
603 NULL,
604 TRUE,
605 $params["attachFile_$i"],
606 "attachFile_$i",
607 $params["attachFile_$i"]['type']
608 );
609 }
610 }
611 else {
612 /**
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.
617 */
618 break;
0c56e4c8
TO
619 }
620 }
621 }
622
623 /**
624 * @return array
625 */
00be9182 626 public static function uploadNames() {
aaffa79f 627 $numAttachments = Civi::settings()->get('max_attachments');
0c56e4c8 628
be2fb01f 629 $names = [];
0c56e4c8
TO
630 for ($i = 1; $i <= $numAttachments; $i++) {
631 $names[] = "attachFile_{$i}";
632 }
633 $names[] = 'uploadFile';
634 return $names;
635 }
636
d424ffde 637 /**
c490a46a 638 * copy/attach an existing file to a different entity
0c56e4c8 639 * table and id.
d424ffde 640 *
0c56e4c8 641 * @param $oldEntityTable
100fef9d 642 * @param int $oldEntityId
0c56e4c8 643 * @param $newEntityTable
100fef9d 644 * @param int $newEntityId
0c56e4c8 645 */
00be9182 646 public static function copyEntityFile($oldEntityTable, $oldEntityId, $newEntityTable, $newEntityId) {
6a488035
TO
647 $oldEntityFile = new CRM_Core_DAO_EntityFile();
648 $oldEntityFile->entity_id = $oldEntityId;
649 $oldEntityFile->entity_table = $oldEntityTable;
650 $oldEntityFile->find();
651
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();
658 }
659 }
660
0c56e4c8
TO
661 /**
662 * @param $entityTable
100fef9d
CW
663 * @param int $entityID
664 * @param int $fileID
0c56e4c8
TO
665 *
666 * @return string
667 */
00be9182 668 public static function deleteURLArgs($entityTable, $entityID, $fileID) {
6a488035 669 $params['entityTable'] = $entityTable;
0c56e4c8
TO
670 $params['entityID'] = $entityID;
671 $params['fileID'] = $fileID;
6a488035
TO
672
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);
676 }
677
678 /**
100fef9d 679 * Delete a file attachment from an entity table / entity ID
ac15829d 680 * @throws CRM_Core_Exception
6a488035 681 */
00be9182 682 public static function deleteAttachment() {
be2fb01f 683 $params = [];
0c56e4c8
TO
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);
6a488035 687
0c56e4c8 688 $signature = CRM_Utils_Request::retrieve('_sgn', 'String', CRM_Core_DAO::$_nullObject, TRUE);
6a488035
TO
689
690 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$_signableFields);
0c56e4c8 691 if (!$signer->validate($signature, $params)) {
ac15829d 692 throw new CRM_Core_Exception('Request signature is invalid');
6a488035
TO
693 }
694
33d245c8 695 self::deleteEntityFile($params['entityTable'], $params['entityID'], NULL, $params['fileID']);
6a488035
TO
696 }
697
34f51a07 698 /**
100fef9d 699 * Display paper icon for a file attachment -- CRM-13624
34f51a07 700 *
5a4f6742
CW
701 * @param string $entityTable
702 * The entityTable to which the file is attached. eg "civicrm_contact", "civicrm_note", "civicrm_activity".
06faabfa 703 * If you have the ID of a specific row in civicrm_file, use $entityTable='*'
5a4f6742
CW
704 * @param int $entityID
705 * The id of the object in the above entityTable.
1a7ab71e 706 *
72b3a70c
CW
707 * @return array|NULL
708 * list of HTML snippets; one HTML snippet for each attachment. If none found, then NULL
1a7ab71e 709 *
34f51a07 710 */
00be9182 711 public static function paperIconAttachment($entityTable, $entityID) {
0c56e4c8
TO
712 if (empty($entityTable) || !$entityID) {
713 $results = NULL;
714 return $results;
715 }
716 $currentAttachmentInfo = self::getEntityFile($entityTable, $entityID);
717 foreach ($currentAttachmentInfo as $fileKey => $fileValue) {
34f51a07 718 $fileID = $fileValue['fileID'];
0c56e4c8 719 if ($fileID) {
b9773de0
CW
720 $fileType = $fileValue['mime_type'];
721 $url = $fileValue['url'];
722 $title = $fileValue['cleanName'];
34f51a07 723 if ($fileType == 'image/jpeg' ||
0c56e4c8
TO
724 $fileType == 'image/pjpeg' ||
725 $fileType == 'image/gif' ||
726 $fileType == 'image/x-png' ||
727 $fileType == 'image/png'
728 ) {
13a3d214
AH
729 $file_url[$fileID] = <<<HEREDOC
730 <a href="$url" class="crm-image-popup" title="$title">
731 <i class="crm-i fa-file-image-o" aria-hidden="true"></i>
732 </a>
733HEREDOC;
34f51a07 734 }
b9773de0 735 // for non image files
34f51a07 736 else {
13a3d214
AH
737 $file_url[$fileID] = <<<HEREDOC
738 <a href="$url" title="$title">
739 <i class="crm-i fa-paperclip" aria-hidden="true"></i>
740 </a>
741HEREDOC;
34f51a07
N
742 }
743 }
744 }
0c56e4c8
TO
745 if (empty($file_url)) {
746 $results = NULL;
34f51a07
N
747 }
748 else {
0c56e4c8 749 $results = $file_url;
34f51a07
N
750 }
751 return $results;
752 }
6cccc6d4
TO
753
754 /**
755 * Get a reference to the file-search service (if one is available).
756 *
757 * @return CRM_Core_FileSearchInterface|NULL
758 */
00be9182 759 public static function getSearchService() {
be2fb01f 760 $fileSearches = [];
6cccc6d4
TO
761 CRM_Utils_Hook::fileSearches($fileSearches);
762
763 // use the first available search
764 foreach ($fileSearches as $fileSearch) {
765 /** @var $fileSearch CRM_Core_FileSearchInterface */
766 return $fileSearch;
767 }
768 return NULL;
769 }
96025800 770
ff9aeadb 771 /**
7618eb9d
TO
772 * Generates an access-token for downloading a specific file.
773 *
efddd94f
TO
774 * @param int $entityId entity id the file is attached to
775 * @param int $fileId file ID
518fa0ee
SL
776 * @param int $genTs
777 * @param int $life
ff9aeadb
SL
778 * @return string
779 */
efddd94f 780 public static function generateFileHash($entityId = NULL, $fileId = NULL, $genTs = NULL, $life = NULL) {
ff9aeadb 781 // Use multiple (but stable) inputs for hash information.
65d060f5
TO
782 $siteKey = CRM_Utils_Constant::value('CIVICRM_SITE_KEY');
783 if (!$siteKey) {
784 throw new \CRM_Core_Exception("Cannot generate file access token. Please set CIVICRM_SITE_KEY.");
785 }
b68895ad
SL
786
787 if (!$genTs) {
788 $genTs = time();
789 }
790 if (!$life) {
54a043e4 791 $days = Civi::settings()->get('checksum_timeout');
efddd94f 792 $life = 24 * $days;
b68895ad 793 }
ff9aeadb
SL
794 // Trim 8 chars off the string, make it slightly easier to find
795 // but reveals less information from the hash.
efddd94f 796 $cs = hash_hmac('sha256', "entity={$entityId}&file={$fileId}&life={$life}", $siteKey);
b68895ad 797 return "{$cs}_{$genTs}_{$life}";
ff9aeadb
SL
798 }
799
c5c40df7 800 /**
7618eb9d
TO
801 * Validate a file access token.
802 *
c5c40df7 803 * @param string $hash
7618eb9d
TO
804 * @param int $entityId Entity Id the file is attached to
805 * @param int $fileId File Id
c5c40df7
TO
806 * @return bool
807 */
7618eb9d 808 public static function validateFileHash($hash, $entityId, $fileId) {
c5c40df7 809 $input = CRM_Utils_System::explode('_', $hash, 3);
9c1bc317
CW
810 $inputTs = $input[1] ?? NULL;
811 $inputLF = $input[2] ?? NULL;
7618eb9d 812 $testHash = CRM_Core_BAO_File::generateFileHash($entityId, $fileId, $inputTs, $inputLF);
c5c40df7
TO
813 if (hash_equals($testHash, $hash)) {
814 $now = time();
815 if ($inputTs + ($inputLF * 60 * 60) >= $now) {
816 return TRUE;
817 }
818 else {
819 return FALSE;
820 }
821 }
822 return FALSE;
823 }
824
b2aaa85e 825}