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