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