Merge pull request #3460 from totten/master-file-cleanup
[civicrm-core.git] / CRM / Core / BAO / File.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2014
32 * $Id$
33 *
34 */
35
36 /**
37 * BAO object for crm_log table
38 */
39 class CRM_Core_BAO_File extends CRM_Core_DAO_File {
40
41 static $_signableFields = array('entityTable', 'entityID', 'fileID');
42
43 /**
44 * @param $fileID
45 * @param $entityID
46 * @param null $entityTable
47 *
48 * @return array
49 */
50 static function path($fileID, $entityID, $entityTable = NULL) {
51 $entityFileDAO = new CRM_Core_DAO_EntityFile();
52 if ($entityTable) {
53 $entityFileDAO->entity_table = $entityTable;
54 }
55 $entityFileDAO->entity_id = $entityID;
56 $entityFileDAO->file_id = $fileID;
57
58 if ($entityFileDAO->find(TRUE)) {
59 $fileDAO = new CRM_Core_DAO_File();
60 $fileDAO->id = $fileID;
61 if ($fileDAO->find(TRUE)) {
62 $config = CRM_Core_Config::singleton();
63 $path = $config->customFileUploadDir . $fileDAO->uri;
64
65 if (file_exists($path) && is_readable($path)) {
66 return array($path, $fileDAO->mime_type);
67 }
68 }
69 }
70
71 return array(NULL, NULL);
72 }
73
74
75 /**
76 * @param $data
77 * @param $fileTypeID
78 * @param $entityTable
79 * @param $entityID
80 * @param $entitySubtype
81 * @param bool $overwrite
82 * @param null $fileParams
83 * @param string $uploadName
84 * @param null $mimeType
85 *
86 * @throws Exception
87 */
88 static function filePostProcess(
89 $data,
90 $fileTypeID,
91 $entityTable,
92 $entityID,
93 $entitySubtype,
94 $overwrite = TRUE,
95 $fileParams = NULL,
96 $uploadName = 'uploadFile',
97 $mimeType = NULL
98 ) {
99 if (!$mimeType) {
100 CRM_Core_Error::fatal(ts('Mime Type is now a required parameter'));
101 }
102
103 $config = CRM_Core_Config::singleton();
104
105 $path = explode('/', $data);
106 $filename = $path[count($path) - 1];
107
108 // rename this file to go into the secure directory
109 if ($entitySubtype) {
110 $directoryName = $config->customFileUploadDir . $entitySubtype . DIRECTORY_SEPARATOR . $entityID;
111 }
112 else {
113 $directoryName = $config->customFileUploadDir;
114 }
115
116 CRM_Utils_File::createDir($directoryName);
117
118 if (!rename($data, $directoryName . DIRECTORY_SEPARATOR . $filename)) {
119 CRM_Core_Error::fatal(ts('Could not move custom file to custom upload directory'));
120 break;
121 }
122
123 // to get id's
124 if ($overwrite && $fileTypeID) {
125 list($sql, $params) = self::sql($entityTable, $entityID, $fileTypeID);
126 }
127 else {
128 list($sql, $params) = self::sql($entityTable, $entityID, 0);
129 }
130
131 $dao = CRM_Core_DAO::executeQuery($sql, $params);
132 $dao->fetch();
133
134 $fileDAO = new CRM_Core_DAO_File();
135 $op = 'create';
136 if (isset($dao->cfID) && $dao->cfID) {
137 $op = 'edit';
138 $fileDAO->id = $dao->cfID;
139 unlink($directoryName . DIRECTORY_SEPARATOR . $dao->uri);
140 }
141
142 if (!empty($fileParams)) {
143 $fileDAO->copyValues($fileParams);
144 }
145
146 $fileDAO->uri = $filename;
147 $fileDAO->mime_type = $mimeType;
148 $fileDAO->file_type_id = $fileTypeID;
149 $fileDAO->upload_date = date('Ymdhis');
150 $fileDAO->save();
151
152 // need to add/update civicrm_entity_file
153 $entityFileDAO = new CRM_Core_DAO_EntityFile();
154 if (isset($dao->cefID) && $dao->cefID) {
155 $entityFileDAO->id = $dao->cefID;
156 }
157 $entityFileDAO->entity_table = $entityTable;
158 $entityFileDAO->entity_id = $entityID;
159 $entityFileDAO->file_id = $fileDAO->id;
160 $entityFileDAO->save();
161
162 //save static tags
163 if (!empty($fileParams['tag'])) {
164 CRM_Core_BAO_EntityTag::create($fileParams['tag'], 'civicrm_file', $entityFileDAO->id);
165 }
166
167 //save free tags
168 if (isset($fileParams['attachment_taglist']) && !empty($fileParams['attachment_taglist'])) {
169 CRM_Core_Form_Tag::postProcess($fileParams['attachment_taglist'], $entityFileDAO->id, 'civicrm_file', CRM_Core_DAO::$_nullObject);
170 }
171
172 // lets call the post hook here so attachments code can do the right stuff
173 CRM_Utils_Hook::post($op, 'File', $fileDAO->id, $fileDAO);
174 }
175
176 /**
177 * A static function wrapper that deletes the various objects that are
178 * connected to a file object (i.e. file, entityFile and customValue
179 */
180 public static function deleteFileReferences($fileID, $entityID, $fieldID) {
181 $fileDAO = new CRM_Core_DAO_File();
182 $fileDAO->id = $fileID;
183 if (!$fileDAO->find(TRUE)) {
184 CRM_Core_Error::fatal();
185 }
186
187 // lets call a pre hook before the delete, so attachments hooks can get the info before things
188 // disappear
189 CRM_Utils_Hook::pre('delete', 'File', $fileID, $fileDAO);
190
191 // get the table and column name
192 list($tableName, $columnName, $groupID) = CRM_Core_BAO_CustomField::getTableColumnGroup($fieldID);
193
194 $entityFileDAO = new CRM_Core_DAO_EntityFile();
195 $entityFileDAO->file_id = $fileID;
196 $entityFileDAO->entity_id = $entityID;
197 $entityFileDAO->entity_table = $tableName;
198
199 if (!$entityFileDAO->find(TRUE)) {
200 CRM_Core_Error::fatal();
201 }
202
203 $entityFileDAO->delete();
204 $fileDAO->delete();
205
206 // also set the value to null of the table and column
207 $query = "UPDATE $tableName SET $columnName = null WHERE $columnName = %1";
208 $params = array(1 => array($fileID, 'Integer'));
209 CRM_Core_DAO::executeQuery($query, $params);
210 }
211
212 /**
213 * The $useWhere is used so that the signature matches the parent class
214 */
215 public function delete($useWhere = FALSE) {
216 list($fileID, $entityID, $fieldID) = func_get_args();
217
218 self::deleteFileReferences($fileID, $entityID, $fieldID);
219 }
220
221 /**
222 * delete all the files and associated object associated with this
223 * combination
224 */
225 static function deleteEntityFile($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
226 if (empty($entityTable) || empty($entityID)) {
227 return;
228 }
229
230 $config = CRM_Core_Config::singleton();
231
232 list($sql, $params) = self::sql($entityTable, $entityID, $fileTypeID, $fileID);
233 $dao = CRM_Core_DAO::executeQuery($sql, $params);
234
235 $cfIDs = array();
236 $cefIDs = array();
237 while ($dao->fetch()) {
238 $cfIDs[$dao->cfID] = $dao->uri;
239 $cefIDs[] = $dao->cefID;
240 }
241
242 if (!empty($cefIDs)) {
243 $cefIDs = implode(',', $cefIDs);
244 $sql = "DELETE FROM civicrm_entity_file where id IN ( $cefIDs )";
245 CRM_Core_DAO::executeQuery($sql);
246 }
247
248 if (!empty($cfIDs)) {
249 // Delete file only if there no any entity using this file.
250 $deleteFiles = array();
251 foreach ($cfIDs as $fId => $fUri) {
252 //delete tags from entity tag table
253 $tagParams = array(
254 'entity_table' => 'civicrm_file',
255 'entity_id' => $fId
256 );
257
258 CRM_Core_BAO_EntityTag::del($tagParams);
259
260 if (!CRM_Core_DAO::getFieldValue('CRM_Core_DAO_EntityFile', $fId, 'id', 'file_id')) {
261 unlink($config->customFileUploadDir . DIRECTORY_SEPARATOR . $fUri);
262 $deleteFiles[$fId] = $fId;
263 }
264 }
265
266 if (!empty($deleteFiles)) {
267 $deleteFiles = implode(',', $deleteFiles);
268 $sql = "DELETE FROM civicrm_file where id IN ( $deleteFiles )";
269 CRM_Core_DAO::executeQuery($sql);
270 }
271 }
272 }
273
274 /**
275 * get all the files and associated object associated with this
276 * combination
277 */
278 static function getEntityFile($entityTable, $entityID, $addDeleteArgs = FALSE) {
279 if (empty($entityTable) || !$entityID) {
280 $results = NULL;
281 return $results;
282 }
283
284 $config = CRM_Core_Config::singleton();
285
286 list($sql, $params) = self::sql($entityTable, $entityID, NULL);
287 $dao = CRM_Core_DAO::executeQuery($sql, $params);
288 $results = array();
289 while ($dao->fetch()) {
290 $result['fileID'] = $dao->cfID;
291 $result['entityID'] = $dao->cefID;
292 $result['mime_type'] = $dao->mime_type;
293 $result['fileName'] = $dao->uri;
294 $result['description'] = $dao->description;
295 $result['cleanName'] = CRM_Utils_File::cleanFileName($dao->uri);
296 $result['fullPath'] = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $dao->uri;
297 $result['url'] = CRM_Utils_System::url('civicrm/file', "reset=1&id={$dao->cfID}&eid={$entityID}");
298 $result['href'] = "<a href=\"{$result['url']}\">{$result['cleanName']}</a>";
299 $result['tag'] = CRM_Core_BAO_EntityTag::getTag($dao->cfID, 'civicrm_file');
300 if ($addDeleteArgs) {
301 $result['deleteURLArgs'] = self::deleteURLArgs($entityTable, $entityID, $dao->cfID);
302 }
303 $results[$dao->cfID] = $result;
304 }
305
306 //fix tag names
307 $tags = CRM_Core_PseudoConstant::get('CRM_Core_DAO_EntityTag', 'tag_id', array('onlyActive' => FALSE));
308
309 foreach ($results as &$values) {
310 if (!empty($values['tag'])) {
311 $tagNames = array();
312 foreach ($values['tag'] as $tid) {
313 $tagNames[] = $tags[$tid];
314 }
315 $values['tag'] = implode(', ', $tagNames);
316 }
317 else {
318 $values['tag'] = '';
319 }
320 }
321
322 $dao->free();
323 return $results;
324 }
325
326 /**
327 * @param $entityTable
328 * @param $entityID
329 * @param null $fileTypeID
330 * @param null $fileID
331 *
332 * @return array
333 */
334 static function sql($entityTable, $entityID, $fileTypeID = NULL, $fileID = NULL) {
335 $sql = "
336 SELECT CF.id as cfID,
337 CF.uri as uri,
338 CF.mime_type as mime_type,
339 CF.description as description,
340 CEF.id as cefID
341 FROM civicrm_file AS CF
342 LEFT JOIN civicrm_entity_file AS CEF ON ( CEF.file_id = CF.id )
343 WHERE CEF.entity_table = %1
344 AND CEF.entity_id = %2";
345
346 $params = array(
347 1 => array($entityTable, 'String'),
348 2 => array($entityID, 'Integer'),
349 );
350
351 if ($fileTypeID !== NULL) {
352 $sql .= " AND CF.file_type_id = %3";
353 $params[3] = array($fileTypeID, 'Integer');
354 }
355
356 if ($fileID !== NULL) {
357 $sql .= " AND CF.id = %4";
358 $params[4] = array($fileID, 'Integer');
359 }
360
361 return array($sql, $params);
362 }
363
364 /**
365 * @param $form
366 * @param $entityTable
367 * @param null $entityID
368 * @param null $numAttachments
369 * @param bool $ajaxDelete
370 */
371 static function buildAttachment(&$form, $entityTable, $entityID = NULL, $numAttachments = NULL, $ajaxDelete = FALSE) {
372
373 if (!$numAttachments) {
374 $numAttachments = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'max_attachments');
375 }
376 // Assign maxAttachments count to template for help message
377 $form->assign('maxAttachments', $numAttachments);
378
379 $config = CRM_Core_Config::singleton();
380 // set default max file size as 2MB
381 $maxFileSize = $config->maxFileSize ? $config->maxFileSize : 2;
382
383 $currentAttachmentInfo = self::getEntityFile($entityTable, $entityID, TRUE);
384 $totalAttachments = 0;
385 if ($currentAttachmentInfo) {
386 $totalAttachments = count($currentAttachmentInfo);
387 $form->add('checkbox', 'is_delete_attachment', ts('Delete All Attachment(s)'));
388 $form->assign('currentAttachmentInfo', $currentAttachmentInfo);
389 }
390 else {
391 $form->assign('currentAttachmentInfo', NULL);
392 }
393
394 if ($totalAttachments) {
395 if ($totalAttachments >= $numAttachments) {
396 $numAttachments = 0;
397 }
398 else {
399 $numAttachments -= $totalAttachments;
400 }
401 }
402
403 $form->assign('numAttachments', $numAttachments);
404
405 $tags = CRM_Core_BAO_Tag::getTags('civicrm_file');
406
407 // get tagset info
408 $parentNames = CRM_Core_BAO_Tag::getTagSet('civicrm_file');
409
410 // add attachments
411 for ($i = 1; $i <= $numAttachments; $i++) {
412 $form->addElement('file', "attachFile_$i", ts('Attach File'), 'size=30 maxlength=60');
413 $form->addUploadElement("attachFile_$i");
414 $form->setMaxFileSize($maxFileSize * 1024 * 1024);
415 $form->addRule("attachFile_$i",
416 ts('File size should be less than %1 MByte(s)',
417 array(1 => $maxFileSize)
418 ),
419 'maxfilesize',
420 $maxFileSize * 1024 * 1024
421 );
422 $form->addElement('text', "attachDesc_$i", NULL, array(
423 'size' => 40,
424 'maxlength' => 255,
425 'placeholder' => ts('Description')
426 ));
427
428 if (!empty($tags)) {
429 $form->add('select', "tag_$i", ts('Tags'), $tags, FALSE,
430 array(
431 'id' => "tags_$i",
432 'multiple' => 'multiple',
433 'class' => 'huge crm-select2',
434 'placeholder' => ts('- none -')
435 )
436 );
437 }
438 CRM_Core_Form_Tag::buildQuickForm($form, $parentNames, 'civicrm_file', NULL, FALSE, TRUE, "file_taglist_$i");
439 }
440 }
441
442 /**
443 * Function to return a clean url string and the number of attachment for a
444 * given entityTable, entityID
445 *
446 * @param $entityTable string The entityTable to which the file is attached
447 * @param $entityID int The id of the object in the above entityTable
448 * @param $separator string The string separator where to implode the urls
449 *
450 * @return array An array with 2 elements. The string and the number of attachments
451 * @static
452 */
453 static function attachmentInfo($entityTable, $entityID, $separator = '<br />') {
454 if (!$entityID) {
455 return NULL;
456 }
457
458 $currentAttachments = self::getEntityFile($entityTable, $entityID);
459 if (!empty($currentAttachments)) {
460 $currentAttachmentURL = array();
461 foreach ($currentAttachments as $fileID => $attach) {
462 $currentAttachmentURL[] = $attach['href'];
463 }
464 return implode($separator, $currentAttachmentURL);
465 }
466 return NULL;
467 }
468
469 /**
470 * @param $formValues
471 * @param $params
472 * @param $entityTable
473 * @param null $entityID
474 */
475 static function formatAttachment(
476 &$formValues,
477 &$params,
478 $entityTable,
479 $entityID = NULL
480 ) {
481
482 // delete current attachments if applicable
483 if ($entityID && !empty($formValues['is_delete_attachment'])) {
484 CRM_Core_BAO_File::deleteEntityFile($entityTable, $entityID);
485 }
486
487 $numAttachments = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'max_attachments');
488
489 $now = date('Ymdhis');
490
491 // setup all attachments
492 for ($i = 1; $i <= $numAttachments; $i++) {
493 $attachName = "attachFile_$i";
494 $attachDesc = "attachDesc_$i";
495 $attachTags = "tag_$i";
496 $attachFreeTags = "file_taglist_$i";
497 if (isset($formValues[$attachName]) && !empty($formValues[$attachName])) {
498 // add static tags if selects
499 $tagParams = array();
500 if (!empty($formValues[$attachTags])) {
501 foreach ($formValues[$attachTags] as $tag) {
502 $tagParams[$tag] = 1;
503 }
504 }
505
506 // we dont care if the file is empty or not
507 // CRM-7448
508 $fileParams = array(
509 'uri' => $formValues[$attachName]['name'],
510 'type' => $formValues[$attachName]['type'],
511 'location' => $formValues[$attachName]['name'],
512 'description' => $formValues[$attachDesc],
513 'upload_date' => $now,
514 'tag' => $tagParams,
515 'attachment_taglist' => CRM_Utils_Array::value($attachFreeTags, $formValues, array())
516 );
517
518 $params[$attachName] = $fileParams;
519 }
520 }
521 }
522
523 /**
524 * @param $params
525 * @param $entityTable
526 * @param $entityID
527 */
528 static function processAttachment(&$params, $entityTable, $entityID) {
529 $numAttachments = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'max_attachments');
530
531 for ($i = 1; $i <= $numAttachments; $i++) {
532 if (
533 isset($params["attachFile_$i"]) &&
534 is_array($params["attachFile_$i"])
535 ) {
536 self::filePostProcess(
537 $params["attachFile_$i"]['location'],
538 NULL,
539 $entityTable,
540 $entityID,
541 NULL,
542 TRUE,
543 $params["attachFile_$i"],
544 "attachFile_$i",
545 $params["attachFile_$i"]['type']
546 );
547 }
548 }
549 }
550
551 /**
552 * @return array
553 */
554 static function uploadNames() {
555 $numAttachments = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'max_attachments');
556
557 $names = array();
558 for ($i = 1; $i <= $numAttachments; $i++) {
559 $names[] = "attachFile_{$i}";
560 }
561 $names[] = 'uploadFile';
562 return $names;
563 }
564
565 /*
566 * Function to copy/attach an existing file to a different entity
567 * table and id.
568 */
569 /**
570 * @param $oldEntityTable
571 * @param $oldEntityId
572 * @param $newEntityTable
573 * @param $newEntityId
574 */
575 static function copyEntityFile($oldEntityTable, $oldEntityId, $newEntityTable, $newEntityId) {
576 $oldEntityFile = new CRM_Core_DAO_EntityFile();
577 $oldEntityFile->entity_id = $oldEntityId;
578 $oldEntityFile->entity_table = $oldEntityTable;
579 $oldEntityFile->find();
580
581 while ($oldEntityFile->fetch()) {
582 $newEntityFile = new CRM_Core_DAO_EntityFile();
583 $newEntityFile->entity_id = $newEntityId;
584 $newEntityFile->entity_table = $newEntityTable;
585 $newEntityFile->file_id = $oldEntityFile->file_id;
586 $newEntityFile->save();
587 }
588 }
589
590 /**
591 * @param $entityTable
592 * @param $entityID
593 * @param $fileID
594 *
595 * @return string
596 */
597 static function deleteURLArgs($entityTable, $entityID, $fileID) {
598 $params['entityTable'] = $entityTable;
599 $params['entityID'] = $entityID;
600 $params['fileID'] = $fileID;
601
602 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$_signableFields);
603 $params['_sgn'] = $signer->sign($params);
604 return CRM_Utils_System::makeQueryString($params);
605 }
606
607 /**
608 * function to delete a file attachment from an entity table / entity ID
609 *
610 * @static
611 * @access public
612 */
613 static function deleteAttachment() {
614 $params = array();
615 $params['entityTable'] = CRM_Utils_Request::retrieve('entityTable', 'String', CRM_Core_DAO::$_nullObject, TRUE);
616 $params['entityID'] = CRM_Utils_Request::retrieve('entityID', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
617 $params['fileID'] = CRM_Utils_Request::retrieve('fileID', 'Positive', CRM_Core_DAO::$_nullObject, TRUE);
618
619 $signature = CRM_Utils_Request::retrieve('_sgn', 'String', CRM_Core_DAO::$_nullObject, TRUE);
620
621 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$_signableFields);
622 if (!$signer->validate($signature, $params)) {
623 CRM_Core_Error::fatal('Request signature is invalid');
624 }
625
626 CRM_Core_BAO_File::deleteEntityFile($params['entityTable'], $params['entityID'], NULL, $params['fileID']);
627 }
628
629
630 /**
631 * function to display paper icon for a file attachment -- CRM-13624
632 *
633 * @param $entityTable string The entityTable to which the file is attached. eg "civicrm_contact", "civicrm_note", "civicrm_activity"
634 * @param $entityID int The id of the object in the above entityTable
635 *
636 * @return array|NULL list of HTML snippets; one HTML snippet for each attachment. If none found, then NULL
637 *
638 * @static
639 * @access public
640 */
641 static function paperIconAttachment($entityTable, $entityID) {
642 if (empty($entityTable) || !$entityID) {
643 $results = NULL;
644 return $results;
645 }
646 $currentAttachmentInfo = self::getEntityFile($entityTable, $entityID);
647 foreach ($currentAttachmentInfo as $fileKey => $fileValue) {
648 $fileID = $fileValue['fileID'];
649 $fileType = $fileValue['mime_type'];
650 $eid = $entityID;
651 if ($fileID) {
652 if ($fileType == 'image/jpeg' ||
653 $fileType == 'image/pjpeg' ||
654 $fileType == 'image/gif' ||
655 $fileType == 'image/x-png' ||
656 $fileType == 'image/png'
657 ) {
658 $url = $fileValue['url'];
659 $alt = $fileValue['cleanName'];
660 $file_url[$fileID] = "
661 <a href=\"$url\" class='crm-image-popup'>
662 <div class='icon paper-icon' title=\"$alt\" alt=\"$alt\"></div>
663 </a>";
664 // for non image files
665 }
666 else {
667 $url = $fileValue['url'];
668 $alt = $fileValue['cleanName'];
669 $file_url[$fileID] = "<a href=\"$url\"><div class='icon paper-icon' title=\"$alt\" alt=\"$alt\"></div></a>";
670 }
671 }
672 }
673 if (empty($file_url)) {
674 $results = NULL;
675 }
676 else {
677 $results = $file_url;
678 }
679 return $results;
680 }
681 }