PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.11
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.11
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Integrations / MailChimp / MailChimp.php

MailChimp.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.11, at app/Services/Integrations/MailChimp/MailChimp.php

478 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Integrations\MailChimp;
4
5 /**
6 * Super-simple, minimum abstraction MailChimp API v3 wrapper
7 * MailChimp API v3: http://developer.mailchimp.com
8 * This wrapper: https://github.com/drewm/mailchimp-api
9 *
10 * @author Drew McLellan <drew.mclellan@gmail.com>
11 *
12 * @version 2.4
13 */
14 class MailChimp
15 {
16 private $api_key;
17 private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
18
19 public const TIMEOUT = 10;
20
21 /* SSL Verification
22 Read before disabling:
23 http://snippets.webaware.com.au/howto/stop-turning-off-curlopt_ssl_verifypeer-and-fix-your-php-config/
24 */
25 public $verify_ssl = true;
26
27 private $request_successful = false;
28 private $last_error = '';
29 private $last_response = [];
30 private $last_request = [];
31
32 /**
33 * Create a new instance
34 *
35 * @param string $api_key Your MailChimp API key
36 * @param string $api_endpoint Optional custom API endpoint
37 *
38 * @throws \Exception
39 */
40 public function __construct($api_key, $api_endpoint = null)
41 {
42 $this->api_key = $api_key;
43
44 if (null === $api_endpoint) {
45 if (false === strpos($this->api_key, '-')) {
46 throw new \Exception(sprintf('Invalid Mailchimp API key `%s` supplied.', esc_html($api_key)));
47 }
48 list(, $data_center) = explode('-', $this->api_key);
49 $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
50 } else {
51 $this->api_endpoint = $api_endpoint;
52 }
53
54 $this->last_response = ['headers' => null, 'body' => null];
55 }
56
57 /**
58 * @return string The url to the API endpoint
59 */
60 public function getApiEndpoint()
61 {
62 return $this->api_endpoint;
63 }
64
65 /**
66 * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
67 *
68 * @param string $email The subscriber's email address
69 *
70 * @return string Hashed version of the input
71 */
72 public function subscriberHash($email)
73 {
74 return md5(strtolower($email));
75 }
76
77 /**
78 * Was the last request successful?
79 *
80 * @return bool True for success, false for failure
81 */
82 public function success()
83 {
84 return $this->request_successful;
85 }
86
87 /**
88 * Get the last error returned by either the network transport, or by the API.
89 * If something didn't work, this should contain the string describing the problem.
90 *
91 * @return string|false describing the error
92 */
93 public function getLastError()
94 {
95 return $this->last_error ?: false;
96 }
97
98 /**
99 * Get an array containing the HTTP headers and the body of the API response.
100 *
101 * @return array Assoc array with keys 'headers' and 'body'
102 */
103 public function getLastResponse()
104 {
105 return $this->last_response;
106 }
107
108 /**
109 * Get an array containing the HTTP headers and the body of the API request.
110 *
111 * @return array Assoc array
112 */
113 public function getLastRequest()
114 {
115 return $this->last_request;
116 }
117
118 /**
119 * Make an HTTP DELETE request - for deleting data
120 *
121 * @param string $method URL of the API request method
122 * @param array $args Assoc array of arguments (if any)
123 * @param int $timeout Timeout limit for request in seconds
124 *
125 * @return array|false Assoc array of API response, decoded from JSON
126 */
127 public function delete($method, $args = [], $timeout = self::TIMEOUT)
128 {
129 return $this->makeRequest('delete', $method, $args, $timeout);
130 }
131
132 /**
133 * Make an HTTP GET request - for retrieving data
134 *
135 * @param string $method URL of the API request method
136 * @param array $args Assoc array of arguments (usually your data)
137 * @param int $timeout Timeout limit for request in seconds
138 *
139 * @return array|false Assoc array of API response, decoded from JSON
140 */
141 public function get($method, $args = [], $timeout = self::TIMEOUT)
142 {
143 return $this->makeRequest('get', $method, $args, $timeout);
144 }
145
146 /**
147 * Make an HTTP PATCH request - for performing partial updates
148 *
149 * @param string $method URL of the API request method
150 * @param array $args Assoc array of arguments (usually your data)
151 * @param int $timeout Timeout limit for request in seconds
152 *
153 * @return array|false Assoc array of API response, decoded from JSON
154 */
155 public function patch($method, $args = [], $timeout = self::TIMEOUT)
156 {
157 return $this->makeRequest('patch', $method, $args, $timeout);
158 }
159
160 /**
161 * Make an HTTP POST request - for creating and updating items
162 *
163 * @param string $method URL of the API request method
164 * @param array $args Assoc array of arguments (usually your data)
165 * @param int $timeout Timeout limit for request in seconds
166 *
167 * @return array|false Assoc array of API response, decoded from JSON
168 */
169 public function post($method, $args = [], $timeout = self::TIMEOUT)
170 {
171 return $this->makeRequest('post', $method, $args, $timeout);
172 }
173
174 /**
175 * Make an HTTP PUT request - for creating new items
176 *
177 * @param string $method URL of the API request method
178 * @param array $args Assoc array of arguments (usually your data)
179 * @param int $timeout Timeout limit for request in seconds
180 *
181 * @return array|false Assoc array of API response, decoded from JSON
182 *
183 * @throws \Exception
184 */
185 public function put($method, $args = [], $timeout = self::TIMEOUT)
186 {
187 return $this->makeRequest('put', $method, $args, $timeout);
188 }
189
190 /**
191 * Performs the underlying HTTP request. Not very exciting.
192 *
193 * @param string $http_verb The HTTP verb to use: get, post, put, patch, delete
194 * @param string $method The API method to be called
195 * @param array $args Assoc array of parameters to be passed
196 * @param int $timeout
197 *
198 * @return array|false Assoc array of decoded result
199 *
200 * @throws \Exception
201 */
202 private function makeRequest($http_verb, $method, $args = [], $timeout = self::TIMEOUT)
203 {
204 $timeout = apply_filters('fluentform/mailchimp_api_timeout', $timeout);
205
206 $url = $this->api_endpoint . '/' . $method;
207
208 $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout);
209
210 $headers = [
211 'Accept' => 'application/vnd.api+json',
212 'Content-Type' => 'application/vnd.api+json',
213 'Authorization' => 'apikey ' . $this->api_key,
214 'User-Agent' => 'DrewM/MailChimp-API/3.0 (github.com/drewm/mailchimp-api)',
215 ];
216
217 if (isset($args['language'])) {
218 $headers['Accept-Language'] = $args['language'];
219 unset($args['language']);
220 }
221
222 $request_args = [
223 'method' => strtoupper($http_verb),
224 'timeout' => $timeout,
225 'headers' => $headers,
226 'sslverify' => $this->verify_ssl,
227 'httpversion' => '1.0',
228 ];
229
230 // Handle GET requests - append query string to URL
231 if ('get' === $http_verb) {
232 if (! empty($args)) {
233 $query = http_build_query($args, '', '&');
234 $url = $url . '?' . $query;
235 }
236 } else {
237 // For POST, PUT, PATCH, DELETE - encode body as JSON
238 $encoded = json_encode($args);
239 $this->last_request['body'] = $encoded;
240 $request_args['body'] = $encoded;
241 }
242
243 $wp_response = wp_remote_request($url, $request_args);
244
245 if (is_wp_error($wp_response)) {
246 $this->last_error = $wp_response->get_error_message();
247 $response['headers'] = null;
248 $response['body'] = null;
249 $this->last_response = $response;
250 return false;
251 }
252
253 $response_code = wp_remote_retrieve_response_code($wp_response);
254 $response_body = wp_remote_retrieve_body($wp_response);
255 $response_headers = wp_remote_retrieve_headers($wp_response);
256
257 // Build response structure similar to cURL format for compatibility
258 $response['headers'] = [
259 'http_code' => $response_code,
260 'content_type' => wp_remote_retrieve_header($wp_response, 'content-type'),
261 'total_time' => isset($wp_response['headers']['total_time']) ? $wp_response['headers']['total_time'] : 0,
262 ];
263
264 $response['httpHeaders'] = $this->convertWpHeadersToArray($response_headers);
265 $response['body'] = $response_body;
266
267 // Store request headers for debugging
268 $this->last_request['headers'] = $headers;
269
270 $formattedResponse = $this->formatResponse($response);
271
272 $this->determineSuccess($response, $formattedResponse, $timeout);
273
274 return $formattedResponse;
275 }
276
277 /**
278 * @param string $http_verb
279 * @param string $method
280 * @param string $url
281 * @param integer $timeout
282 */
283 private function prepareStateForRequest($http_verb, $method, $url, $timeout)
284 {
285 $this->last_error = '';
286
287 $this->request_successful = false;
288
289 $this->last_response = [
290 'headers' => null, // array of details from wp_remote_retrieve_response_code()
291 'httpHeaders' => null, // array of HTTP headers
292 'body' => null, // content of the response
293 ];
294
295 $this->last_request = [
296 'method' => $http_verb,
297 'path' => $method,
298 'url' => $url,
299 'body' => '',
300 'timeout' => $timeout,
301 ];
302
303 return $this->last_response;
304 }
305
306 /**
307 * Get the HTTP headers as an array of header-name => header-value pairs.
308 *
309 * The "Link" header is parsed into an associative array based on the
310 * rel names it contains. The original value is available under
311 * the "_raw" key.
312 *
313 * @param string $headersAsString
314 *
315 * @return array
316 */
317 private function getHeadersAsArray($headersAsString)
318 {
319 $headers = [];
320
321 foreach (explode("\r\n", $headersAsString) as $i => $line) {
322 if (0 === $i) { // HTTP code
323 continue;
324 }
325
326 $line = trim($line);
327 if (empty($line)) {
328 continue;
329 }
330
331 list($key, $value) = explode(': ', $line);
332
333 if ('Link' == $key) {
334 $value = array_merge(
335 ['_raw' => $value],
336 $this->getLinkHeaderAsArray($value)
337 );
338 }
339
340 $headers[$key] = $value;
341 }
342
343 return $headers;
344 }
345
346 /**
347 * Extract all rel => URL pairs from the provided Link header value
348 *
349 * Mailchimp only implements the URI reference and relation type from
350 * RFC 5988, so the value of the header is something like this:
351 *
352 * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy", <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
353 *
354 * @param string $linkHeaderAsString
355 *
356 * @return array
357 */
358 private function getLinkHeaderAsArray($linkHeaderAsString)
359 {
360 $urls = [];
361
362 if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
363 foreach ($matches[2] as $i => $relName) {
364 $urls[$relName] = $matches[1][$i];
365 }
366 }
367
368 return $urls;
369 }
370
371 /**
372 * Convert WordPress headers object to array format
373 *
374 * @param \WP_HTTP_Requests_Response|\Requests_Utility_CaseInsensitiveDictionary $wp_headers WordPress headers object
375 *
376 * @return array Associative array of headers
377 */
378 private function convertWpHeadersToArray($wp_headers)
379 {
380 $headers = [];
381
382 if (is_object($wp_headers) && method_exists($wp_headers, 'getAll')) {
383 $wp_headers = $wp_headers->getAll();
384 }
385
386 if (is_array($wp_headers)) {
387 foreach ($wp_headers as $key => $value) {
388 if (is_array($value)) {
389 $value = implode(', ', $value);
390 }
391 $headers[$key] = $value;
392
393 // Handle Link header parsing
394 if ('Link' === $key || 'link' === strtolower($key)) {
395 $headers[$key] = array_merge(
396 ['_raw' => $value],
397 $this->getLinkHeaderAsArray($value)
398 );
399 }
400 }
401 }
402
403 return $headers;
404 }
405
406 /**
407 * Decode the response and format any error messages for debugging
408 *
409 * @param array $response The response from the WordPress HTTP request
410 *
411 * @return array|false The JSON decoded into an array
412 */
413 private function formatResponse($response)
414 {
415 $this->last_response = $response;
416
417 if (! empty($response['body'])) {
418 return json_decode($response['body'], true);
419 }
420
421 return false;
422 }
423
424
425 /**
426 * Check if the response was successful or a failure. If it failed, store the error.
427 *
428 * @param array $response The response from the WordPress HTTP request
429 * @param array|false $formattedResponse The response body payload from the WordPress HTTP request
430 * @param int $timeout The timeout supplied to the WordPress HTTP request.
431 *
432 * @return bool If the request was successful
433 */
434 private function determineSuccess($response, $formattedResponse, $timeout)
435 {
436 $status = $this->findHTTPStatus($response, $formattedResponse);
437
438 if ($status >= 200 && $status <= 299) {
439 $this->request_successful = true;
440 return true;
441 }
442
443 if (isset($formattedResponse['detail'])) {
444 $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
445 return false;
446 }
447
448 if ($timeout > 0 && $response['headers'] && isset($response['headers']['total_time']) && $response['headers']['total_time'] >= $timeout) {
449 $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
450 return false;
451 }
452
453 $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
454 return false;
455 }
456
457 /**
458 * Find the HTTP status code from the headers or API response body
459 *
460 * @param array $response The response from the WordPress HTTP request
461 * @param array|false $formattedResponse The response body payload from the WordPress HTTP request
462 *
463 * @return int HTTP status code
464 */
465 private function findHTTPStatus($response, $formattedResponse)
466 {
467 if (! empty($response['headers']) && isset($response['headers']['http_code'])) {
468 return (int) $response['headers']['http_code'];
469 }
470
471 if (! empty($response['body']) && isset($formattedResponse['status'])) {
472 return (int) $formattedResponse['status'];
473 }
474
475 return 418;
476 }
477 }
478