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