3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * CRM_Core_Page_AJAX_Attachment defines an end-point for AJAX operations which upload file-attachments.
15 * To upload a new file, submit a POST (multi-part encoded) to "civicrm/ajax/attachment". Inputs:
16 * - POST['entity_table']: string
17 * - POST['entity_id']: int
18 * - POST['attachment_token']: string
19 * - FILES[*]: all of the files to attach to the entity
21 * The response is a JSON document. Foreach item in FILES, there's a corresponding record in the response which
22 * describes the success or failure.
24 * Note: The permission requirements are determined by the underlying Attachment API.
26 class CRM_Core_Page_AJAX_Attachment
{
29 const ATTACHMENT_TOKEN_TTL
= 10800;
34 public static function attachFile() {
35 $result = self
::_attachFile($_POST, $_FILES, $_SERVER);
36 self
::sendResponse($result);
43 * Like global $_FILES.
44 * @param array $server
45 * Like global $_SERVER.
48 public static function _attachFile($post, $files, $server) {
49 $config = CRM_Core_Config
::singleton();
52 foreach ($files as $key => $file) {
53 if (!$config->debug
&& !self
::checkToken($post['crm_attachment_token'])) {
54 require_once 'api/v3/utils.php';
55 $results[$key] = civicrm_api3_create_error("SECURITY ALERT: Attaching files via AJAX requires a recent, valid token.",
57 'IP' => $server['REMOTE_ADDR'],
58 'level' => 'security',
59 'referer' => $server['HTTP_REFERER'],
60 'reason' => 'CSRF suspected',
64 elseif ($file['error']) {
65 require_once 'api/v3/utils.php';
66 $results[$key] = civicrm_api3_create_error("Upload failed (code=" . $file['error'] . ")");
69 CRM_Core_Transaction
::create(TRUE)
70 ->run(function (CRM_Core_Transaction
$tx) use ($key, $file, $post, &$results) {
71 // We want check_permissions=1 while creating the DB record and check_permissions=0 while moving upload,
72 // so split the work across two api calls.
75 if (isset($file['name'])) {
76 $params['name'] = $file['name'];
78 if (isset($file['type'])) {
79 $params['mime_type'] = $file['type'];
81 foreach (['entity_table', 'entity_id', 'description'] as $field) {
82 if (isset($post[$field])) {
83 $params[$field] = $post[$field];
86 $params['version'] = 3;
87 $params['check_permissions'] = 1;
88 $params['content'] = '';
89 $results[$key] = civicrm_api('Attachment', 'create', $params);
91 if (!$results[$key]['is_error']) {
93 'id' => $results[$key]['id'],
95 'options.move-file' => $file['tmp_name'],
96 // note: in this second call, check_permissions==false
98 $moveResult = civicrm_api('Attachment', 'create', $moveParams);
99 if ($moveResult['is_error']) {
100 $results[$key] = $moveResult;
112 * @param array $result
113 * List of API responses, keyed by file.
115 public static function sendResponse($result) {
117 foreach ($result as $item) {
118 $isError = $isError ||
$item['is_error'];
122 $sapi_type = php_sapi_name();
123 if (substr($sapi_type, 0, 3) == 'cgi') {
124 CRM_Utils_System
::setHttpHeader("Status", "500 Internal Server Error");
127 header("HTTP/1.1 500 Internal Server Error");
131 CRM_Utils_JSON
::output(array_merge($result));
137 public static function angularSettings() {
139 'token' => self
::createToken(),
146 public static function createToken() {
147 $signer = new CRM_Utils_Signer(CRM_Core_Key
::privateKey(), ['for', 'ts']);
148 $ts = CRM_Utils_Time
::getTimeRaw();
149 return $signer->sign([
150 'for' => 'crmAttachment',
156 * @param string $token
157 * A token supplied by the user.
159 * TRUE if the token is valid for submitting attachments
162 public static function checkToken($token) {
163 list ($signature, $ts) = explode(';;;', $token);
164 $signer = new CRM_Utils_Signer(CRM_Core_Key
::privateKey(), ['for', 'ts']);
165 if (!is_numeric($ts) || CRM_Utils_Time
::getTimeRaw() > $ts + self
::ATTACHMENT_TOKEN_TTL
) {
168 return $signer->validate($signature, [
169 'for' => 'crmAttachment',