Merge pull request #14981 from eileenmcnaughton/load_extract
[civicrm-core.git] / api / v3 / Attachment.php
CommitLineData
56154d36 1<?php
56154d36
TO
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
56154d36 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
56154d36
TO
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 * "Attachment" is a pseudo-entity which represents a record in civicrm_file
30 * combined with a record in civicrm_entity_file as well as the underlying
31 * file content.
dc137cd4 32 * For core fields use "entity_table", for custom fields use "field_name"
56154d36
TO
33 *
34 * @code
dc137cd4 35 * // Create an attachment for a core field
56154d36
TO
36 * $result = civicrm_api3('Attachment', 'create', array(
37 * 'entity_table' => 'civicrm_activity',
38 * 'entity_id' => 123,
39 * 'name' => 'README.txt',
40 * 'mime_type' => 'text/plain',
41 * 'content' => 'Please to read the README',
42 * ));
43 * $attachment = $result['values'][$result['id']];
44 * echo sprintf("<a href='%s'>View %s</a>", $attachment['url'], $attachment['name']);
45 * @endcode
46 *
47 * @code
dc137cd4
CW
48 * // Create an attachment for a custom file field
49 * $result = civicrm_api3('Attachment', 'create', array(
50 * 'field_name' => 'custom_6',
51 * 'entity_id' => 123,
52 * 'name' => 'README.txt',
53 * 'mime_type' => 'text/plain',
54 * 'content' => 'Please to read the README',
55 * ));
56 * $attachment = $result['values'][$result['id']];
57 * echo sprintf("<a href='%s'>View %s</a>", $attachment['url'], $attachment['name']);
58 * @endcode
59 *
60 * @code
61 * // Move an existing file and save as an attachment
56154d36
TO
62 * $result = civicrm_api3('Attachment', 'create', array(
63 * 'entity_table' => 'civicrm_activity',
64 * 'entity_id' => 123,
65 * 'name' => 'README.txt',
66 * 'mime_type' => 'text/plain',
67 * 'options' => array(
68 * 'move-file' => '/tmp/upload1a2b3c4d',
69 * ),
70 * ));
71 * $attachment = $result['values'][$result['id']];
72 * echo sprintf("<a href='%s'>View %s</a>", $attachment['url'], $attachment['name']);
73 * @endcode
74 *
75 * Notes:
76 * - File content is not returned by default. One must specify 'return => content'.
77 * - Features which deal with local file system (e.g. passing "options.move-file"
78 * or returning a "path") are only valid when executed as a local API (ie
79 * "check_permissions"==false)
80 *
81 * @package CiviCRM_APIv3
56154d36
TO
82 */
83
84/**
22242c87 85 * Adjust metadata for "create" action.
56154d36 86 *
cf470720
TO
87 * @param array $spec
88 * List of fields.
56154d36
TO
89 */
90function _civicrm_api3_attachment_create_spec(&$spec) {
91 $spec = array_merge($spec, _civicrm_api3_attachment_getfields());
92 $spec['name']['api.required'] = 1;
93 $spec['mime_type']['api.required'] = 1;
56154d36
TO
94 $spec['entity_id']['api.required'] = 1;
95 $spec['upload_date']['api.default'] = 'now';
96}
97
98/**
244bbdd8 99 * Create an Attachment.
56154d36 100 *
cf470720 101 * @param array $params
22242c87 102 *
a6c01b45 103 * @return array
56154d36 104 * @throws API_Exception validation errors
2e37a19f 105 * @see Civi\API\Subscriber\DynamicFKAuthorization
56154d36
TO
106 */
107function civicrm_api3_attachment_create($params) {
dc137cd4 108 if (empty($params['id'])) {
210737b6 109 // When creating we need either entity_table or field_name.
cf8f0fff 110 civicrm_api3_verify_one_mandatory($params, NULL, ['entity_table', 'field_name']);
dc137cd4
CW
111 }
112
56154d36
TO
113 $config = CRM_Core_Config::singleton();
114 list($id, $file, $entityFile, $name, $content, $moveFile, $isTrusted, $returnContent) = _civicrm_api3_attachment_parse_params($params);
115
116 $fileDao = new CRM_Core_BAO_File();
117 $entityFileDao = new CRM_Core_DAO_EntityFile();
118
119 if ($id) {
ae2c7e00 120 $file['id'] = $fileDao->id = $id;
121
56154d36
TO
122 if (!$fileDao->find(TRUE)) {
123 throw new API_Exception("Invalid ID");
124 }
125
126 $entityFileDao->file_id = $id;
127 if (!$entityFileDao->find(TRUE)) {
128 throw new API_Exception("Cannot modify orphaned file");
129 }
130 }
131
132 if (!$id && !is_string($content) && !is_string($moveFile)) {
133 throw new API_Exception("Mandatory key(s) missing from params array: 'id' or 'content' or 'options.move-file'");
134 }
135 if (!$isTrusted && $moveFile) {
136 throw new API_Exception("options.move-file is only supported on secure calls");
137 }
138 if (is_string($content) && is_string($moveFile)) {
139 throw new API_Exception("'content' and 'options.move-file' are mutually exclusive");
140 }
141 if ($id && !$isTrusted && isset($file['upload_date']) && $file['upload_date'] != CRM_Utils_Date::isoToMysql($fileDao->upload_date)) {
cf8f0fff 142 throw new API_Exception("Cannot modify upload_date" . var_export([$file['upload_date'], $fileDao->upload_date, CRM_Utils_Date::isoToMysql($fileDao->upload_date)], TRUE));
56154d36
TO
143 }
144 if ($id && $name && $name != CRM_Utils_File::cleanFileName($fileDao->uri)) {
145 throw new API_Exception("Cannot modify name");
146 }
147
56154d36 148 if (!$id) {
ae2c7e00 149 $file['uri'] = CRM_Utils_File::makeFileName($name);
56154d36 150 }
ae2c7e00 151 $fileDao = CRM_Core_BAO_File::create($file);
152 $fileDao->find(TRUE);
56154d36
TO
153
154 $entityFileDao->copyValues($entityFile);
155 $entityFileDao->file_id = $fileDao->id;
156 $entityFileDao->save();
157
158 $path = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $fileDao->uri;
159 if (is_string($content)) {
160 file_put_contents($path, $content);
161 }
162 elseif (is_string($moveFile)) {
a436d075
ML
163 // CRM-17432 Do not use rename() since it will break file permissions.
164 // Also avoid move_uplaoded_file() because the API can use options.move-file.
165 copy($moveFile, $path);
166 unlink($moveFile);
56154d36
TO
167 }
168
dc137cd4
CW
169 // Save custom field to entity
170 if (!$id && empty($params['entity_table']) && isset($params['field_name'])) {
cf8f0fff 171 civicrm_api3('custom_value', 'create', [
dc137cd4
CW
172 'entity_id' => $params['entity_id'],
173 $params['field_name'] => $fileDao->id,
cf8f0fff 174 ]);
dc137cd4
CW
175 }
176
cf8f0fff 177 $result = [
21dfd5f5 178 $fileDao->id => _civicrm_api3_attachment_format_result($fileDao, $entityFileDao, $returnContent, $isTrusted),
cf8f0fff 179 ];
56154d36
TO
180 return civicrm_api3_create_success($result, $params, 'Attachment', 'create');
181}
182
183/**
22242c87 184 * Adjust metadata for get action.
56154d36 185 *
cf470720
TO
186 * @param array $spec
187 * List of fields.
56154d36
TO
188 */
189function _civicrm_api3_attachment_get_spec(&$spec) {
190 $spec = array_merge($spec, _civicrm_api3_attachment_getfields());
191}
192
193/**
244bbdd8 194 * Get Attachment.
dc64d047 195 *
56154d36 196 * @param array $params
dc64d047 197 *
a6c01b45 198 * @return array
72b3a70c 199 * per APIv3
56154d36
TO
200 * @throws API_Exception validation errors
201 */
202function civicrm_api3_attachment_get($params) {
203 list($id, $file, $entityFile, $name, $content, $moveFile, $isTrusted, $returnContent) = _civicrm_api3_attachment_parse_params($params);
204
205 $dao = __civicrm_api3_attachment_find($params, $id, $file, $entityFile, $isTrusted);
cf8f0fff 206 $result = [];
56154d36
TO
207 while ($dao->fetch()) {
208 $result[$dao->id] = _civicrm_api3_attachment_format_result($dao, $dao, $returnContent, $isTrusted);
209 }
210 return civicrm_api3_create_success($result, $params, 'Attachment', 'create');
211}
212
24a70b66 213/**
244bbdd8 214 * Adjust metadata for Attachment delete action.
22242c87 215 *
24a70b66
EM
216 * @param $spec
217 */
56154d36
TO
218function _civicrm_api3_attachment_delete_spec(&$spec) {
219 unset($spec['id']['api.required']);
220 $entityFileFields = CRM_Core_DAO_EntityFile::fields();
221 $spec['entity_table'] = $entityFileFields['entity_table'];
222 $spec['entity_table']['title'] = CRM_Utils_Array::value('title', $spec['entity_table'], 'Entity Table') . ' (write-once)';
223 $spec['entity_id'] = $entityFileFields['entity_id'];
224 $spec['entity_id']['title'] = CRM_Utils_Array::value('title', $spec['entity_id'], 'Entity ID') . ' (write-once)';
225}
226
227/**
244bbdd8 228 * Delete Attachment.
22242c87 229 *
16b10e64 230 * @param array $params
22242c87 231 *
56154d36
TO
232 * @return array
233 * @throws API_Exception
234 */
235function civicrm_api3_attachment_delete($params) {
236 if (!empty($params['id'])) {
237 // ok
238 }
239 elseif (!empty($params['entity_table']) && !empty($params['entity_id'])) {
240 // ok
241 }
242 else {
243 throw new API_Exception("Mandatory key(s) missing from params array: id or entity_table+entity_table");
244 }
245
246 $config = CRM_Core_Config::singleton();
247 list($id, $file, $entityFile, $name, $content, $moveFile, $isTrusted, $returnContent) = _civicrm_api3_attachment_parse_params($params);
248 $dao = __civicrm_api3_attachment_find($params, $id, $file, $entityFile, $isTrusted);
249
cf8f0fff
CW
250 $filePaths = [];
251 $fileIds = [];
56154d36 252 while ($dao->fetch()) {
35671d00 253 $filePaths[] = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $dao->uri;
56154d36
TO
254 $fileIds[] = $dao->id;
255 }
256
257 if (!empty($fileIds)) {
258 $idString = implode(',', array_filter($fileIds, 'is_numeric'));
259 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_entity_file WHERE file_id in ($idString)");
260 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_file WHERE id in ($idString)");
261 }
262
263 // unlink is non-transactional, so we do this as the last step -- just in case the other steps produce errors
264 if (!empty($filePaths)) {
265 foreach ($filePaths as $filePath) {
266 unlink($filePath);
267 }
268 }
269
cf8f0fff 270 $result = [];
56154d36
TO
271 return civicrm_api3_create_success($result, $params, 'Attachment', 'create');
272}
273
274/**
22242c87
EM
275 * Attachment find helper.
276 *
56154d36
TO
277 * @param array $params
278 * @param int|null $id the user-supplied ID of the attachment record
cf470720
TO
279 * @param array $file
280 * The user-supplied vales for the file (mime_type, description, upload_date).
281 * @param array $entityFile
906e6120 282 * The user-supplied values of the entity-file (entity_table, entity_id).
56154d36 283 * @param bool $isTrusted
22242c87 284 *
56154d36
TO
285 * @return CRM_Core_DAO
286 * @throws API_Exception
287 */
288function __civicrm_api3_attachment_find($params, $id, $file, $entityFile, $isTrusted) {
cf8f0fff 289 foreach (['name', 'content', 'path', 'url'] as $unsupportedFilter) {
56154d36
TO
290 if (!empty($params[$unsupportedFilter])) {
291 throw new API_Exception("Get by $unsupportedFilter is not currently supported");
292 }
293 }
294
295 $select = CRM_Utils_SQL_Select::from('civicrm_file cf')
296 ->join('cef', 'INNER JOIN civicrm_entity_file cef ON cf.id = cef.file_id')
cf8f0fff 297 ->select([
56154d36
TO
298 'cf.id',
299 'cf.uri',
300 'cf.mime_type',
301 'cf.description',
302 'cf.upload_date',
7aab0058 303 'cf.created_id',
56154d36
TO
304 'cef.entity_table',
305 'cef.entity_id',
cf8f0fff 306 ]);
56154d36
TO
307
308 if ($id) {
cf8f0fff 309 $select->where('cf.id = #id', ['#id' => $id]);
56154d36 310 }
22242c87 311 // Recall: $file is filtered by parse_params.
56154d36 312 foreach ($file as $key => $value) {
cf8f0fff 313 $select->where('cf.!field = @value', [
56154d36
TO
314 '!field' => $key,
315 '@value' => $value,
cf8f0fff 316 ]);
56154d36 317 }
22242c87 318 // Recall: $entityFile is filtered by parse_params.
56154d36 319 foreach ($entityFile as $key => $value) {
cf8f0fff 320 $select->where('cef.!field = @value', [
56154d36
TO
321 '!field' => $key,
322 '@value' => $value,
cf8f0fff 323 ]);
56154d36
TO
324 }
325 if (!$isTrusted) {
326 // FIXME ACLs: Add any JOIN or WHERE clauses needed to enforce access-controls for the target entity.
327 //
328 // The target entity is identified by "cef.entity_table" (aka $entityFile['entity_table']) and "cef.entity_id".
329 //
330 // As a simplification, we *require* the "get" actions to filter on a single "entity_table" which should
331 // avoid the complexity of matching ACL's against multiple entity types.
332 }
333
334 $dao = CRM_Core_DAO::executeQuery($select->toSQL());
335 return $dao;
336}
337
338/**
22242c87
EM
339 * Attachment parsing helper.
340 *
56154d36 341 * @param array $params
22242c87 342 *
dc64d047
EM
343 * @return array
344 * (0 => int $id, 1 => array $file, 2 => array $entityFile, 3 => string $name, 4 => string $content,
bed98343 345 * 5 => string $moveFile, 6 => $isTrusted, 7 => bool $returnContent)
346 * - array $file: whitelisted fields that can pass through directly to civicrm_file
347 * - array $entityFile: whitelisted fields that can pass through directly to civicrm_entity_file
348 * - string $name: the printable name
349 * - string $moveFile: the full path to a local file whose content should be loaded
350 * - bool $isTrusted: whether we trust the requester to do sketchy things (like moving files or reassigning entities)
351 * - bool $returnContent: whether we are expected to return the full content of the file
56154d36
TO
352 * @throws API_Exception validation errors
353 */
354function _civicrm_api3_attachment_parse_params($params) {
355 $id = CRM_Utils_Array::value('id', $params, NULL);
356 if ($id && !is_numeric($id)) {
357 throw new API_Exception("Malformed id");
358 }
359
cf8f0fff
CW
360 $file = [];
361 foreach (['mime_type', 'description', 'upload_date'] as $field) {
56154d36
TO
362 if (array_key_exists($field, $params)) {
363 $file[$field] = $params[$field];
364 }
365 }
366
cf8f0fff
CW
367 $entityFile = [];
368 foreach (['entity_table', 'entity_id'] as $field) {
56154d36
TO
369 if (array_key_exists($field, $params)) {
370 $entityFile[$field] = $params[$field];
371 }
372 }
373
dc137cd4
CW
374 if (empty($params['entity_table']) && isset($params['field_name'])) {
375 $tableInfo = CRM_Core_BAO_CustomField::getTableColumnGroup(intval(str_replace('custom_', '', $params['field_name'])));
376 $entityFile['entity_table'] = $tableInfo[0];
377 }
378
56154d36
TO
379 $name = NULL;
380 if (array_key_exists('name', $params)) {
381 if ($params['name'] != basename($params['name']) || preg_match(':[/\\\\]:', $params['name'])) {
382 throw new API_Exception('Malformed name');
383 }
384 $name = $params['name'];
385 }
386
387 $content = NULL;
388 if (isset($params['content'])) {
389 $content = $params['content'];
390 }
391
392 $moveFile = NULL;
393 if (isset($params['options']['move-file'])) {
394 $moveFile = $params['options']['move-file'];
395 }
396 elseif (isset($params['options.move-file'])) {
397 $moveFile = $params['options.move-file'];
398 }
399
400 $isTrusted = empty($params['check_permissions']);
401
cf8f0fff
CW
402 $returns = isset($params['return']) ? $params['return'] : [];
403 $returns = is_array($returns) ? $returns : [$returns];
56154d36
TO
404 $returnContent = in_array('content', $returns);
405
cf8f0fff 406 return [$id, $file, $entityFile, $name, $content, $moveFile, $isTrusted, $returnContent];
56154d36
TO
407}
408
409/**
22242c87
EM
410 * Attachment result formatting helper.
411 *
cf470720
TO
412 * @param CRM_Core_DAO_File $fileDao
413 * Maybe "File" or "File JOIN EntityFile".
414 * @param CRM_Core_DAO_EntityFile $entityFileDao
415 * Maybe "EntityFile" or "File JOIN EntityFile".
416 * @param bool $returnContent
417 * Whether to return the full content of the file.
418 * @param bool $isTrusted
419 * Whether the current request is trusted to perform file-specific operations.
35823763 420 *
56154d36
TO
421 * @return array
422 */
423function _civicrm_api3_attachment_format_result($fileDao, $entityFileDao, $returnContent, $isTrusted) {
424 $config = CRM_Core_Config::singleton();
425 $path = $config->customFileUploadDir . DIRECTORY_SEPARATOR . $fileDao->uri;
426
cf8f0fff 427 $result = [
56154d36
TO
428 'id' => $fileDao->id,
429 'name' => CRM_Utils_File::cleanFileName($fileDao->uri),
430 'mime_type' => $fileDao->mime_type,
431 'description' => $fileDao->description,
432 'upload_date' => is_numeric($fileDao->upload_date) ? CRM_Utils_Date::mysqlToIso($fileDao->upload_date) : $fileDao->upload_date,
433 'entity_table' => $entityFileDao->entity_table,
434 'entity_id' => $entityFileDao->entity_id,
4994819e 435 'icon' => CRM_Utils_File::getIconFromMimeType($fileDao->mime_type),
7aab0058 436 'created_id' => $fileDao->created_id,
cf8f0fff 437 ];
ff9aeadb 438 $fileHash = CRM_Core_BAO_File::generateFileHash($result['entity_id'], $result['id']);
56154d36 439 $result['url'] = CRM_Utils_System::url(
ff9aeadb 440 'civicrm/file', 'reset=1&id=' . $result['id'] . '&eid=' . $result['entity_id'] . '&fcs=' . $fileHash,
56154d36
TO
441 TRUE,
442 NULL,
443 FALSE,
444 TRUE
445 );
446 if ($isTrusted) {
447 $result['path'] = $path;
448 }
449 if ($returnContent) {
450 $result['content'] = file_get_contents($path);
451 }
452 return $result;
453}
454
455/**
22242c87
EM
456 * Attachment getfields helper.
457 *
a6c01b45 458 * @return array
72b3a70c 459 * list of fields (indexed by name)
56154d36
TO
460 */
461function _civicrm_api3_attachment_getfields() {
462 $fileFields = CRM_Core_DAO_File::fields();
463 $entityFileFields = CRM_Core_DAO_EntityFile::fields();
464
cf8f0fff 465 $spec = [];
56154d36 466 $spec['id'] = $fileFields['id'];
cf8f0fff 467 $spec['name'] = [
56154d36
TO
468 'title' => 'Name (write-once)',
469 'description' => 'The logical file name (not searchable)',
470 'type' => CRM_Utils_Type::T_STRING,
cf8f0fff
CW
471 ];
472 $spec['field_name'] = [
dc137cd4
CW
473 'title' => 'Field Name (write-once)',
474 'description' => 'Alternative to "entity_table" param - sets custom field value.',
475 'type' => CRM_Utils_Type::T_STRING,
cf8f0fff 476 ];
56154d36
TO
477 $spec['mime_type'] = $fileFields['mime_type'];
478 $spec['description'] = $fileFields['description'];
479 $spec['upload_date'] = $fileFields['upload_date'];
480 $spec['entity_table'] = $entityFileFields['entity_table'];
d1b0d05e
EM
481 // Would be hard to securely handle changes.
482 $spec['entity_table']['title'] = CRM_Utils_Array::value('title', $spec['entity_table'], 'Entity Table') . ' (write-once)';
56154d36 483 $spec['entity_id'] = $entityFileFields['entity_id'];
7c31ae57
SL
484 // would be hard to securely handle changes
485 $spec['entity_id']['title'] = CRM_Utils_Array::value('title', $spec['entity_id'], 'Entity ID') . ' (write-once)';
cf8f0fff 486 $spec['url'] = [
56154d36
TO
487 'title' => 'URL (read-only)',
488 'description' => 'URL for downloading the file (not searchable, expire-able)',
489 'type' => CRM_Utils_Type::T_STRING,
cf8f0fff
CW
490 ];
491 $spec['path'] = [
56154d36
TO
492 'title' => 'Path (read-only)',
493 'description' => 'Local file path (not searchable, local-only)',
494 'type' => CRM_Utils_Type::T_STRING,
cf8f0fff
CW
495 ];
496 $spec['content'] = [
56154d36
TO
497 'title' => 'Content',
498 'description' => 'File content (not searchable, not returned by default)',
499 'type' => CRM_Utils_Type::T_STRING,
cf8f0fff
CW
500 ];
501 $spec['created_id'] = [
ae2c7e00 502 'title' => 'Created By Contact ID',
503 'type' => CRM_Utils_Type::T_INT,
504 'description' => 'FK to civicrm_contact, who uploaded this file',
cf8f0fff 505 ];
56154d36
TO
506
507 return $spec;
508}