Merge pull request #23960 from seamuslee001/fix_static_calling_trait
[civicrm-core.git] / CRM / Core / Page / AJAX / Attachment.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * CRM_Core_Page_AJAX_Attachment defines an end-point for AJAX operations which upload file-attachments.
14 *
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
20 *
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.
23 *
24 * Note: The permission requirements are determined by the underlying Attachment API.
25 */
26 class CRM_Core_Page_AJAX_Attachment {
27
28 // 3hr; 3*60*60
29 const ATTACHMENT_TOKEN_TTL = 10800;
30
31 /**
32 * (Page Callback)
33 */
34 public static function attachFile() {
35 $result = self::_attachFile($_POST, $_FILES, $_SERVER);
36 self::sendResponse($result);
37 }
38
39 /**
40 * @param array $post
41 * Like global $_POST.
42 * @param array $files
43 * Like global $_FILES.
44 * @param array $server
45 * Like global $_SERVER.
46 * @return array
47 */
48 public static function _attachFile($post, $files, $server) {
49 $config = CRM_Core_Config::singleton();
50 $results = [];
51
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.",
56 [
57 'IP' => $server['REMOTE_ADDR'],
58 'level' => 'security',
59 'referer' => $server['HTTP_REFERER'],
60 'reason' => 'CSRF suspected',
61 ]
62 );
63 }
64 elseif ($file['error']) {
65 require_once 'api/v3/utils.php';
66 $results[$key] = civicrm_api3_create_error("Upload failed (code=" . $file['error'] . ")");
67 }
68 else {
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.
73
74 $params = [];
75 if (isset($file['name'])) {
76 $params['name'] = $file['name'];
77 }
78 if (isset($file['type'])) {
79 $params['mime_type'] = $file['type'];
80 }
81 foreach (['entity_table', 'entity_id', 'description'] as $field) {
82 if (isset($post[$field])) {
83 $params[$field] = $post[$field];
84 }
85 }
86 $params['version'] = 3;
87 $params['check_permissions'] = 1;
88 $params['content'] = '';
89 $results[$key] = civicrm_api('Attachment', 'create', $params);
90
91 if (!$results[$key]['is_error']) {
92 $moveParams = [
93 'id' => $results[$key]['id'],
94 'version' => 3,
95 'options.move-file' => $file['tmp_name'],
96 // note: in this second call, check_permissions==false
97 ];
98 $moveResult = civicrm_api('Attachment', 'create', $moveParams);
99 if ($moveResult['is_error']) {
100 $results[$key] = $moveResult;
101 $tx->rollback();
102 }
103 }
104 });
105 }
106 }
107
108 return $results;
109 }
110
111 /**
112 * @param array $result
113 * List of API responses, keyed by file.
114 */
115 public static function sendResponse($result) {
116 $isError = FALSE;
117 foreach ($result as $item) {
118 $isError = $isError || $item['is_error'];
119 }
120
121 if ($isError) {
122 $sapi_type = php_sapi_name();
123 if (substr($sapi_type, 0, 3) == 'cgi') {
124 CRM_Utils_System::setHttpHeader("Status", "500 Internal Server Error");
125 }
126 else {
127 header("HTTP/1.1 500 Internal Server Error");
128 }
129 }
130
131 CRM_Utils_JSON::output(array_merge($result));
132 }
133
134 /**
135 * @return array
136 */
137 public static function angularSettings() {
138 return [
139 'token' => self::createToken(),
140 ];
141 }
142
143 /**
144 * @return string
145 */
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',
151 'ts' => $ts,
152 ]) . ';;;' . $ts;
153 }
154
155 /**
156 * @param string $token
157 * A token supplied by the user.
158 * @return bool
159 * TRUE if the token is valid for submitting attachments
160 * @throws Exception
161 */
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) {
166 return FALSE;
167 }
168 return $signer->validate($signature, [
169 'for' => 'crmAttachment',
170 'ts' => $ts,
171 ]);
172 }
173
174 }