Refactor Mailchimp import to API v3

[MAILPOET-3404]
This commit is contained in:
Jan Lysý
2021-02-26 14:22:27 +01:00
committed by Veljko V
parent 878e3eb28f
commit d940c9365e
2 changed files with 151 additions and 80 deletions

View File

@ -6,113 +6,93 @@ use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
class MailChimp { class MailChimp {
private const API_BASE_URI = 'https://user:%s@%s.api.mailchimp.com/3.0/';
private const API_KEY_REGEX = '/[a-zA-Z0-9]{32}-[a-zA-Z0-9]{2,4}$/';
private const API_BATCH_SIZE = 100;
/** @var false|string */
public $apiKey; public $apiKey;
/** @var int */
public $maxPostSize; public $maxPostSize;
/** @var false|string */
public $dataCenter; public $dataCenter;
private $exportUrl; /** @var MailChimpDataMapper */
private $listsUrl; private $mapper;
const API_KEY_REGEX = '/[a-zA-Z0-9]{32}-[a-zA-Z0-9]{2,4}$/';
public function __construct($apiKey) { public function __construct($apiKey) {
$this->apiKey = $this->getAPIKey($apiKey); $this->apiKey = $this->getAPIKey($apiKey);
$this->maxPostSize = Helpers::getMaxPostSize('bytes'); $this->maxPostSize = (int)Helpers::getMaxPostSize('bytes');
$this->dataCenter = $this->getDataCenter($this->apiKey); $this->dataCenter = $this->getDataCenter($this->apiKey);
$this->listsUrl = 'https://%s.api.mailchimp.com/2.0/lists/list?apikey=%s'; $this->mapper = new MailChimpDataMapper();
$this->exportUrl = 'https://%s.api.mailchimp.com/export/1.0/list/?apikey=%s&id=%s';
} }
public function getLists() { public function getLists(): array {
if (!$this->apiKey || !$this->dataCenter) { if (!$this->apiKey || !$this->dataCenter) {
return $this->throwException('API'); $this->throwException('API');
}
$url = sprintf($this->listsUrl, $this->dataCenter, $this->apiKey);
$connection = @fopen($url, 'r');
if (!$connection) {
return $this->throwException('connection');
} else {
$response = '';
while (!feof($connection)) {
$buffer = fgets($connection, 4096);
if (!is_string($buffer)) {
return $this->throwException('connection');
}
if (trim($buffer) !== '') {
$response .= $buffer;
}
}
fclose($connection);
}
$response = json_decode($response);
if (!$response) {
return $this->throwException('API');
} }
$lists = []; $lists = [];
foreach ($response->data as $list) { $count = 0;
$lists[] = [ while (true) {
'id' => $list->id, $data = $this->getApiData('lists', $count);
'name' => $list->name, if ($data === null) {
]; $this->throwException('lists');
break;
}
$count += count($data['lists']);
foreach ($data['lists'] as $list) {
$lists[] = [
'id' => $list['id'],
'name' => $list['name'],
];
}
if ($data['total_items'] <= $count) {
break;
}
} }
return $lists; return $lists;
} }
public function getSubscribers($lists = []) { public function getSubscribers($lists = []): array {
if (!$this->apiKey || !$this->dataCenter) { if (!$this->apiKey || !$this->dataCenter) {
return $this->throwException('API'); $this->throwException('API');
} }
if (!$lists) { if (!$lists) {
return $this->throwException('lists'); $this->throwException('lists');
} }
$bytesFetched = 0;
$subscribers = []; $subscribers = [];
$duplicate = []; $duplicate = [];
$header = [];
foreach ($lists as $list) { foreach ($lists as $list) {
$url = sprintf($this->exportUrl, $this->dataCenter, $this->apiKey, $list); $count = 0;
$connection = @fopen($url, 'r'); while (true) {
if (!$connection) { $data = $this->getApiData("lists/{$list}/members", $count);
return $this->throwException('connection'); if ($data === null) {
} $this->throwException('lists');
$i = 0; break;
while (!feof($connection)) { }
$buffer = fgets($connection, 4096); $count += count($data['members']);
if (trim((string)$buffer) !== '') { foreach ($data['members'] as $member) {
$obj = json_decode((string)$buffer); $emailAddress = $member['email_address'];
if ($i === 0) { if (isset($subscribers[$emailAddress])) {
$header = $obj; $duplicate[$emailAddress] = $this->mapper->mapMember($member);
if (is_object($header) && isset($header->error)) {
return $this->throwException('lists');
}
if (!isset($headerHash)) {
$headerHash = md5(implode(',', $header));
} elseif (md5(implode(',', $header)) !== $headerHash) {
return $this->throwException('headers');
}
} elseif (isset($subscribers[$obj[0]])) {
$duplicate[] = $obj[0];
} else { } else {
$subscribers[$obj[0]] = $obj; $subscribers[$emailAddress] = $this->mapper->mapMember($member);
} }
$i++;
} }
$bytesFetched += strlen((string)$buffer);
if ($bytesFetched > $this->maxPostSize) { if ($data['total_items'] <= $count) {
return $this->throwException('size'); break;
} }
} }
fclose($connection);
} }
if (!count($subscribers)) { if (!count($subscribers)) {
return $this->throwException('subscribers'); $this->throwException('subscribers');
} }
return [ return [
@ -120,7 +100,7 @@ class MailChimp {
'invalid' => [], 'invalid' => [],
'duplicate' => $duplicate, 'duplicate' => $duplicate,
'role' => [], 'role' => [],
'header' => $header, 'header' => $this->mapper->getMembersHeader(),
'subscribersCount' => count($subscribers), 'subscribersCount' => count($subscribers),
]; ];
} }
@ -135,18 +115,16 @@ class MailChimp {
return (preg_match(self::API_KEY_REGEX, $apiKey)) ? $apiKey : false; return (preg_match(self::API_KEY_REGEX, $apiKey)) ? $apiKey : false;
} }
public function throwException($error) { /**
* @param string $error
* @throws \Exception
*/
public function throwException(string $error): void {
$errorMessage = WPFunctions::get()->__('Unknown MailChimp error.', 'mailpoet'); $errorMessage = WPFunctions::get()->__('Unknown MailChimp error.', 'mailpoet');
switch ($error) { switch ($error) {
case 'API': case 'API':
$errorMessage = WPFunctions::get()->__('Invalid API Key.', 'mailpoet'); $errorMessage = WPFunctions::get()->__('Invalid API Key.', 'mailpoet');
break; break;
case 'connection':
$errorMessage = WPFunctions::get()->__('Could not connect to your MailChimp account.', 'mailpoet');
break;
case 'headers':
$errorMessage = WPFunctions::get()->__('The selected lists do not have matching columns (headers).', 'mailpoet');
break;
case 'size': case 'size':
$errorMessage = WPFunctions::get()->__('The information received from MailChimp is too large for processing. Please limit the number of lists!', 'mailpoet'); $errorMessage = WPFunctions::get()->__('The information received from MailChimp is too large for processing. Please limit the number of lists!', 'mailpoet');
break; break;
@ -159,4 +137,36 @@ class MailChimp {
} }
throw new \Exception($errorMessage); throw new \Exception($errorMessage);
} }
private function getApiData(string $endpoint, int $offset): ?array {
$url = sprintf(self::API_BASE_URI, $this->apiKey, $this->dataCenter);
$url .= $endpoint . '?' . http_build_query([
'count' => self::API_BATCH_SIZE,
'offset' => $offset,
]);
$connection = @fopen($url, 'r');
if (!$connection) {
return null;
}
$bytesFetched = 0;
$response = '';
while (!feof($connection)) {
$buffer = fgets($connection, 4096);
if (!is_string($buffer)) {
return null;
}
if (trim($buffer) !== '') {
$response .= $buffer;
}
$bytesFetched += strlen((string)$buffer);
if ($bytesFetched > $this->maxPostSize) {
$this->throwException('size');
}
}
fclose($connection);
return json_decode($response, true);
}
} }

View File

@ -0,0 +1,61 @@
<?php
namespace MailPoet\Subscribers\ImportExport\Import;
class MailChimpDataMapper {
public function getMembersHeader(): array {
return [
'email_address',
'status',
'first_name',
'last_name',
'address',
'phone',
'birthday',
'ip_signup',
'timestamp_signup',
'ip_opt',
'timestamp_opt',
'member_rating',
'last_changed',
'language',
'vip',
'email_client',
'latitude',
'longitude',
'gmtoff',
'dstoff',
'country_code',
'timezone',
'source',
];
}
public function mapMember(array $member): array {
return [
$member['email_address'],
$member['status'],
$member['merge_fields']['FNAME'] ?? '',
$member['merge_fields']['LNAME'] ?? '',
is_array($member['merge_fields']['ADDRESS']) ? implode(' ', $member['merge_fields']['ADDRESS'] ?? []) : $member['merge_fields']['ADDRESS'],
$member['merge_fields']['PHONE'] ?? '',
$member['merge_fields']['BIRTHDAY'] ?? '',
$member['ip_signup'],
$member['timestamp_signup'],
$member['ip_opt'],
$member['timestamp_opt'],
$member['member_rating'],
$member['last_changed'],
$member['language'],
$member['vip'],
$member['email_client'],
$member['location']['latitude'] ?? '',
$member['location']['longitude'] ?? '',
$member['location']['gmtoff'] ?? '',
$member['location']['dstoff'] ?? '',
$member['location']['country_code'] ?? '',
$member['location']['timezone'] ?? '',
$member['source'],
];
}
}