Commit | Line | Data |
---|---|---|
f344f1e3 TO |
1 | <?php |
2 | ||
12b478ef | 3 | use Psr\Http\Message\ResponseInterface; |
f344f1e3 TO |
4 | use Psr\Http\Message\RequestInterface; |
5 | ||
6 | /** | |
7 | * Additional helpers/utilities for use as Guzzle middleware. | |
8 | */ | |
9 | class CRM_Utils_GuzzleMiddleware { | |
10 | ||
ee4d3a2d TO |
11 | /** |
12 | * The authx middleware sends authenticated requests via JWT. | |
13 | * | |
14 | * To add an authentication token to a specific request, the `$options` | |
15 | * must specify `authx_user` or `authx_contact_id`. Examples: | |
16 | * | |
17 | * $http = new GuzzleHttp\Client(['authx_user' => 'admin']); | |
18 | * $http->post('civicrm/admin', ['authx_user' => 'admin']); | |
19 | * | |
20 | * Supported options: | |
21 | * - authx_ttl (int): Seconds of validity for JWT's | |
22 | * - authx_host (string): Only send tokens for the given host. | |
23 | * - authx_contact_id (int): The CiviCRM contact to authenticate with | |
24 | * - authx_user (string): The CMS user to authenticate with | |
25 | * - authx_flow (string): How to format the auth token. One of: 'param', 'xheader', 'header'. | |
26 | * | |
27 | * @return \Closure | |
28 | */ | |
29 | public static function authx($defaults = []) { | |
30 | $defaults = array_merge([ | |
31 | 'authx_ttl' => 60, | |
32 | 'authx_host' => parse_url(CIVICRM_UF_BASEURL, PHP_URL_HOST), | |
33 | 'authx_contact_id' => NULL, | |
34 | 'authx_user' => NULL, | |
35 | 'authx_flow' => 'param', | |
36 | ], $defaults); | |
37 | return function(callable $handler) use ($defaults) { | |
38 | return function (RequestInterface $request, array $options) use ($handler, $defaults) { | |
39 | if ($request->getUri()->getHost() !== $defaults['authx_host']) { | |
40 | return $handler($request, $options); | |
41 | } | |
42 | ||
43 | $options = array_merge($defaults, $options); | |
44 | if (!empty($options['authx_contact_id'])) { | |
45 | $cid = $options['authx_contact_id']; | |
46 | } | |
47 | elseif (!empty($options['authx_user'])) { | |
48 | $r = civicrm_api3("Contact", "get", ["id" => "@user:" . $options['authx_user']]); | |
49 | foreach ($r['values'] as $id => $value) { | |
50 | $cid = $id; | |
51 | break; | |
52 | } | |
53 | if (empty($cid)) { | |
54 | throw new \RuntimeException("Failed to identify user ({$options['authx_user']})"); | |
55 | } | |
56 | } | |
57 | else { | |
58 | $cid = NULL; | |
59 | } | |
60 | ||
61 | if ($cid) { | |
62 | if (!CRM_Extension_System::singleton()->getMapper()->isActiveModule('authx')) { | |
63 | throw new \RuntimeException("Authx is not enabled. Authenticated requests will not work."); | |
64 | } | |
65 | $tok = \Civi::service('crypto.jwt')->encode([ | |
66 | 'exp' => time() + $options['authx_ttl'], | |
67 | 'sub' => 'cid:' . $cid, | |
68 | 'scope' => 'authx', | |
69 | ]); | |
70 | ||
71 | switch ($options['authx_flow']) { | |
72 | case 'header': | |
73 | $request = $request->withHeader('Authorization', "Bearer $tok"); | |
74 | break; | |
75 | ||
76 | case 'xheader': | |
77 | $request = $request->withHeader('X-Civi-Auth', "Bearer $tok"); | |
78 | break; | |
79 | ||
80 | case 'param': | |
81 | if ($request->getMethod() === 'POST') { | |
82 | if (!empty($request->getHeader('Content-Type')) && !preg_grep(';application/x-www-form-urlencoded;', $request->getHeader('Content-Type'))) { | |
83 | throw new \RuntimeException("Cannot append authentication credentials to HTTP POST. Unrecognized content type."); | |
84 | } | |
85 | $query = (string) $request->getBody(); | |
86 | $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); | |
87 | $request = new GuzzleHttp\Psr7\Request( | |
88 | $request->getMethod(), | |
89 | $request->getUri(), | |
90 | $request->getHeaders(), | |
91 | http_build_query(['_authx' => "Bearer $tok"]) . ($query ? '&' : '') . $query | |
92 | ); | |
93 | } | |
94 | else { | |
95 | $query = $request->getUri()->getQuery(); | |
96 | $request = $request->withUri($request->getUri()->withQuery( | |
97 | http_build_query(['_authx' => "Bearer $tok"]) . ($query ? '&' : '') . $query | |
98 | )); | |
99 | } | |
100 | break; | |
101 | ||
102 | default: | |
103 | throw new \RuntimeException("Unrecognized authx flow: {$options['authx_flow']}"); | |
104 | } | |
105 | } | |
106 | return $handler($request, $options); | |
107 | }; | |
108 | }; | |
109 | } | |
110 | ||
f344f1e3 TO |
111 | /** |
112 | * Add this as a Guzzle handler/middleware if you wish to simplify | |
113 | * the construction of Civi-related URLs. It enables URL schemes for: | |
114 | * | |
115 | * - route://ROUTE_NAME (aka) route:ROUTE_NAME | |
1c5f00b3 TO |
116 | * - backend://ROUTE_NAME (aka) backend:ROUTE_NAME |
117 | * - frontend://ROUTE_NAME (aka) frontend:ROUTE_NAME | |
f344f1e3 TO |
118 | * - var://PATH_EXPRESSION (aka) var:PATH_EXPRESSION |
119 | * - ext://EXTENSION/FILE (aka) ext:EXTENSION/FILE | |
120 | * - assetBuilder://ASSET_NAME?PARAMS (aka) assetBuilder:ASSET_NAME?PARAMS | |
121 | * | |
122 | * Compare: | |
123 | * | |
1c5f00b3 TO |
124 | * $http->get(CRM_Utils_System::url('civicrm/dashboard', NULL, TRUE, NULL, FALSE, ??)) |
125 | * $http->get('route://civicrm/dashboard') | |
126 | * $http->get('frontend://civicrm/dashboard') | |
127 | * $http->get('backend://civicrm/dashboard') | |
f344f1e3 TO |
128 | * |
129 | * $http->get(Civi::paths()->getUrl('[civicrm.files]/foo.txt')) | |
130 | * $http->get('var:[civicrm.files]/foo.txt') | |
131 | * | |
132 | * $http->get(Civi::resources()->getUrl('my.other.ext', 'foo.js')) | |
133 | * $http->get('ext:my.other.ext/foo.js') | |
134 | * | |
135 | * $http->get(Civi::service('asset_builder')->getUrl('my-asset.css', ['a'=>1, 'b'=>2])) | |
136 | * $http->get('assetBuilder:my-asset.css?a=1&b=2') | |
137 | * | |
138 | * Note: To further simplify URL expressions, Guzzle allows you to set a 'base_uri' | |
139 | * option (which is applied as a prefix to any relative URLs). Consider using | |
140 | * `base_uri=auto:`. This allows you to implicitly use the most common types | |
141 | * (routes+variables): | |
142 | * | |
143 | * $http->get('civicrm/dashboard') | |
144 | * $http->get('[civicrm.files]/foo.txt') | |
145 | * | |
146 | * @return \Closure | |
147 | */ | |
148 | public static function url() { | |
149 | return function(callable $handler) { | |
150 | return function (RequestInterface $request, array $options) use ($handler) { | |
151 | $newUri = self::filterUri($request->getUri()); | |
152 | if ($newUri !== NULL) { | |
153 | $request = $request->withUri(\CRM_Utils_Url::parseUrl($newUri)); | |
154 | } | |
155 | ||
156 | return $handler($request, $options); | |
157 | }; | |
158 | }; | |
159 | } | |
160 | ||
161 | /** | |
162 | * @param \Psr\Http\Message\UriInterface $oldUri | |
163 | * | |
164 | * @return string|null | |
165 | * The string formation of the new URL, or NULL for unchanged URLs. | |
166 | */ | |
167 | protected static function filterUri(\Psr\Http\Message\UriInterface $oldUri) { | |
168 | // Copy the old ?query-params and #fragment-params on top of $newBase. | |
169 | $copyParams = function ($newBase) use ($oldUri) { | |
170 | if ($oldUri->getQuery()) { | |
171 | $newBase .= strpos($newBase, '?') !== FALSE ? '&' : '?'; | |
172 | $newBase .= $oldUri->getQuery(); | |
173 | } | |
174 | if ($oldUri->getFragment()) { | |
175 | $newBase .= '#' . $oldUri->getFragment(); | |
176 | } | |
177 | return $newBase; | |
178 | }; | |
179 | ||
180 | $hostPath = urldecode($oldUri->getHost() . $oldUri->getPath()); | |
181 | $scheme = $oldUri->getScheme(); | |
182 | if ($scheme === 'auto') { | |
183 | // Ex: 'auto:civicrm/my-page' ==> Router | |
184 | // Ex: 'auto:[civicrm.root]/js/foo.js' ==> Resource file | |
185 | $scheme = ($hostPath[0] === '[') ? 'var' : 'route'; | |
186 | } | |
187 | ||
1c5f00b3 TO |
188 | if ($scheme === 'route') { |
189 | $menu = CRM_Core_Menu::get($hostPath); | |
190 | $scheme = ($menu && !empty($menu['is_public'])) ? 'frontend' : 'backend'; | |
191 | } | |
192 | ||
f344f1e3 TO |
193 | switch ($scheme) { |
194 | case 'assetBuilder': | |
195 | // Ex: 'assetBuilder:dynamic.css' or 'assetBuilder://dynamic.css?foo=bar' | |
196 | // Note: It's more useful to pass params to the asset-builder than to the final HTTP request. | |
197 | $assetParams = []; | |
198 | parse_str('' . $oldUri->getQuery(), $assetParams); | |
199 | return \Civi::service('asset_builder')->getUrl($hostPath, $assetParams); | |
200 | ||
201 | case 'ext': | |
202 | // Ex: 'ext:other.ext.name/file.js' or 'ext://other.ext.name/file.js' | |
203 | [$ext, $file] = explode('/', $hostPath, 2); | |
204 | return $copyParams(\Civi::resources()->getUrl($ext, $file)); | |
205 | ||
206 | case 'var': | |
207 | // Ex: 'var:[civicrm.files]/foo.txt' or 'var://[civicrm.files]/foo.txt' | |
208 | return $copyParams(\Civi::paths()->getUrl($hostPath, 'absolute')); | |
209 | ||
1c5f00b3 TO |
210 | case 'backend': |
211 | // Ex: 'backend:civicrm/my-page' or 'backend://civicrm/my-page' | |
f344f1e3 TO |
212 | return $copyParams(\CRM_Utils_System::url($hostPath, NULL, TRUE, NULL, FALSE)); |
213 | ||
1c5f00b3 TO |
214 | case 'frontend': |
215 | // Ex: 'frontend:civicrm/my-page' or 'frontend://civicrm/my-page' | |
216 | return $copyParams(\CRM_Utils_System::url($hostPath, NULL, TRUE, NULL, FALSE, TRUE)); | |
217 | ||
f344f1e3 TO |
218 | default: |
219 | return NULL; | |
220 | } | |
221 | } | |
222 | ||
12b478ef TO |
223 | /** |
224 | * This logs the list of outgoing requests in curl format. | |
225 | */ | |
226 | public static function curlLog(\Psr\Log\LoggerInterface $logger) { | |
227 | ||
228 | $curlFmt = new class() extends \GuzzleHttp\MessageFormatter { | |
229 | ||
230 | public function format(RequestInterface $request, ResponseInterface $response = NULL, \Exception $error = NULL) { | |
231 | $cmd = '$ curl'; | |
232 | if ($request->getMethod() !== 'GET') { | |
233 | $cmd .= ' -X ' . escapeshellarg($request->getMethod()); | |
234 | } | |
235 | foreach ($request->getHeaders() as $header => $lines) { | |
236 | foreach ($lines as $line) { | |
237 | $cmd .= ' -H ' . escapeshellarg("$header: $line"); | |
238 | } | |
239 | } | |
240 | $body = (string) $request->getBody(); | |
241 | if ($body !== '') { | |
242 | $cmd .= ' -d ' . escapeshellarg($body); | |
243 | } | |
244 | $cmd .= ' ' . escapeshellarg((string) $request->getUri()); | |
245 | return $cmd; | |
246 | } | |
247 | ||
248 | }; | |
249 | ||
250 | return \GuzzleHttp\Middleware::log($logger, $curlFmt); | |
251 | } | |
252 | ||
f344f1e3 | 253 | } |