PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Subscribers / ImportExport / Import / Import.php
mailpoet / lib / Subscribers / ImportExport / Import Last commit date
Cli.php 2 weeks ago Import.php 2 weeks ago MailChimp.php 2 months ago MailChimpDataMapper.php 2 months ago index.php 3 years ago
Import.php
709 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Subscribers\ImportExport\Import;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\CustomFields\CustomFieldsRepository;
9 use MailPoet\Entities\CustomFieldEntity;
10 use MailPoet\Entities\SubscriberCustomFieldEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoet\Entities\SubscriberSegmentEntity;
13 use MailPoet\Entities\SubscriberTagEntity;
14 use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
15 use MailPoet\Segments\WP;
16 use MailPoet\Services\Validator;
17 use MailPoet\Subscribers\ImportExport\ImportExportFactory;
18 use MailPoet\Subscribers\ImportExport\ImportExportRepository;
19 use MailPoet\Subscribers\Source;
20 use MailPoet\Subscribers\SubscribersRepository;
21 use MailPoet\Tags\TagRepository;
22 use MailPoet\Util\DateConverter;
23 use MailPoet\Util\Helpers;
24 use MailPoet\Util\Security;
25 use MailPoet\WP\Functions as WPFunctions;
26 use MailPoetVendor\Carbon\Carbon;
27
28 class Import {
29 /** @var array */
30 public $subscribersData;
31 /** @var array */
32 public $segmentsIds;
33 /** @var string[] */
34 public $tags;
35 /** @var string */
36 public $newSubscribersStatus;
37 /** @var string */
38 public $existingSubscribersStatus;
39 /** @var bool */
40 public $updateSubscribers;
41 /** @var array */
42 public $subscribersFields;
43 /** @var array */
44 public $subscribersCustomFields;
45 /** @var int */
46 public $subscribersCount;
47 /** @var Carbon */
48 public $createdAt;
49 /** @var Carbon */
50 public $updatedAt;
51 /** @var array<string, mixed> */
52 public $requiredSubscribersFields;
53 const DB_QUERY_CHUNK_SIZE = 100;
54 const STATUS_DONT_UPDATE = 'dont_update';
55
56 public const ACTION_CREATE = 'create';
57 public const ACTION_UPDATE = 'update';
58
59 /** @var WP */
60 private $wpSegment;
61
62 /** @var CustomFieldsRepository */
63 private $customFieldsRepository;
64
65 /** @var ImportExportRepository */
66 private $importExportRepository;
67
68 /** @var NewsletterOptionsRepository */
69 private $newsletterOptionsRepository;
70
71 /** @var SubscribersRepository */
72 private $subscriberRepository;
73
74 /** @var TagRepository */
75 private $tagRepository;
76
77 /** @var Validator */
78 private $validator;
79
80 public function __construct(
81 WP $wpSegment,
82 CustomFieldsRepository $customFieldsRepository,
83 ImportExportRepository $importExportRepository,
84 NewsletterOptionsRepository $newsletterOptionsRepository,
85 SubscribersRepository $subscriberRepository,
86 TagRepository $tagRepository,
87 Validator $validator,
88 array $data
89 ) {
90 $this->wpSegment = $wpSegment;
91 $this->customFieldsRepository = $customFieldsRepository;
92 $this->importExportRepository = $importExportRepository;
93 $this->newsletterOptionsRepository = $newsletterOptionsRepository;
94 $this->subscriberRepository = $subscriberRepository;
95 $this->tagRepository = $tagRepository;
96 $this->validator = $validator;
97 $this->validateImportData($data);
98 $this->subscribersData = $this->transformSubscribersData(
99 $data['subscribers'],
100 $data['columns']
101 );
102 $this->segmentsIds = $data['segments'];
103 $this->tags = $data['tags'];
104 $this->newSubscribersStatus = $data['newSubscribersStatus'];
105 $this->existingSubscribersStatus = $data['existingSubscribersStatus'];
106 $this->updateSubscribers = $data['updateSubscribers'];
107 $this->subscribersFields = $this->getSubscribersFields(
108 array_keys($data['columns'])
109 );
110 $this->subscribersCustomFields = $this->getCustomSubscribersFields(
111 array_keys($data['columns'])
112 );
113 $this->subscribersCount = (reset($this->subscribersData) === false) ? 0 : count(reset($this->subscribersData));
114 $this->createdAt = Carbon::now()->millisecond(0);
115 $this->updatedAt = Carbon::createFromTimestamp(WPFunctions::get()->currentTime('timestamp', true) + 1);
116 $this->requiredSubscribersFields = [
117 'status' => SubscriberEntity::STATUS_SUBSCRIBED,
118 'first_name' => '',
119 'last_name' => '',
120 'created_at' => $this->createdAt,
121 ];
122 }
123
124 public function validateImportData(array $data): void {
125 $requiredDataFields = [
126 'subscribers',
127 'columns',
128 'segments',
129 'timestamp',
130 'newSubscribersStatus',
131 'existingSubscribersStatus',
132 'updateSubscribers',
133 'tags',
134 ];
135 // 1. data should contain all required fields
136 // 2. column names should only contain alphanumeric & underscore characters
137 if (
138 count(array_intersect_key(array_flip($requiredDataFields), $data)) !== count($requiredDataFields) ||
139 preg_grep('/[^a-zA-Z0-9_]/', array_keys($data['columns']))
140 ) {
141 throw new \Exception(__('Missing or invalid import data.', 'mailpoet'));
142 }
143 }
144
145 /**
146 * @return array{created: int, updated:int, segments: array, added_to_segment_with_welcome_notification:bool}
147 * @throws \Exception
148 */
149 public function process(): array {
150 // validate data based on field validation rules
151 $subscribersData = $this->validateSubscribersData($this->subscribersData);
152 if (!$subscribersData) {
153 throw new \Exception(__('No valid subscribers were found.', 'mailpoet'));
154 }
155 // permanently trash deleted subscribers
156 $this->deleteExistingTrashedSubscribers($subscribersData);
157
158 // split subscribers into "existing" and "new" and free up memory
159 $existingSubscribers = $newSubscribers = [
160 'data' => [],
161 'fields' => $this->subscribersFields,
162 ];
163 list($existingSubscribers['data'], $newSubscribers['data'], $wpUsers) =
164 $this->splitSubscribersData($subscribersData);
165 $subscribersData = null;
166
167 // create or update subscribers
168 $createdSubscribers = $updatedSubscribers = [];
169 try {
170 if ($newSubscribers['data']) {
171 // add, if required, missing required fields to new subscribers
172 $newSubscribers = $this->addMissingRequiredFields($newSubscribers);
173 $newSubscribers = $this->setSubscriptionStatusToDefault($newSubscribers, $this->newSubscribersStatus);
174 $newSubscribers = $this->setSource($newSubscribers);
175 $newSubscribers = $this->setLinkToken($newSubscribers);
176 $createdSubscribers =
177 $this->createOrUpdateSubscribers(
178 self::ACTION_CREATE,
179 $newSubscribers,
180 $this->subscribersCustomFields
181 );
182 }
183
184 $updateExistingSubscribersStatus = false;
185
186 if ($existingSubscribers['data']) {
187 $allowedStatuses = [
188 SubscriberEntity::STATUS_SUBSCRIBED,
189 SubscriberEntity::STATUS_UNSUBSCRIBED,
190 SubscriberEntity::STATUS_INACTIVE,
191 ];
192 if (in_array($this->existingSubscribersStatus, $allowedStatuses, true)) {
193 $updateExistingSubscribersStatus = true;
194 $existingSubscribers = $this->addField($existingSubscribers, 'status', $this->existingSubscribersStatus);
195 }
196 if ($this->updateSubscribers) {
197 // Update existing subscribers' info (first_name, last_name etc.)
198 // as well as status (optionally) if the status column was added above
199 $updatedSubscribers =
200 $this->createOrUpdateSubscribers(
201 self::ACTION_UPDATE,
202 $existingSubscribers,
203 $this->subscribersCustomFields
204 );
205 if ($wpUsers) {
206 $this->synchronizeWPUsers($wpUsers);
207 }
208 } elseif ($updateExistingSubscribersStatus) {
209 // Only update existing subscribers' status
210 // For this we need to remove all other fields except email and status
211 $existingSubscribers['fields'] = array_intersect($existingSubscribers['fields'], ['email', 'status']);
212 $existingSubscribers['data'] = array_intersect_key($existingSubscribers['data'], array_flip(['email', 'status']));
213 $updatedSubscribers =
214 $this->createOrUpdateSubscribers(
215 self::ACTION_UPDATE,
216 $existingSubscribers
217 );
218 }
219 }
220 } catch (\Exception $e) {
221 throw new \Exception(__('Unable to save imported subscribers.', 'mailpoet'));
222 }
223
224 // check if any subscribers were added to segments that have welcome notifications configured
225 $importFactory = new ImportExportFactory('import');
226 $segments = $importFactory->getSegments();
227 $welcomeNotificationsInSegments =
228 ($createdSubscribers || $updatedSubscribers) ?
229 $this->newsletterOptionsRepository->findWelcomeNotificationsForSegments($this->segmentsIds) :
230 false;
231
232 return [
233 'created' => is_array($createdSubscribers) ? count($createdSubscribers) : 0,
234 'updated' => is_array($updatedSubscribers) ? count($updatedSubscribers) : 0,
235 'segments' => $segments,
236 'added_to_segment_with_welcome_notification' =>
237 ($welcomeNotificationsInSegments) ? true : false,
238 ];
239 }
240
241 /**
242 * @param array $subscribersData
243 * @return false|array
244 */
245 public function validateSubscribersData(array $subscribersData) {
246 $invalidRecords = [];
247 foreach ($subscribersData as $column => &$data) {
248 if ($column === 'email') {
249 $data = array_map(
250 function($index, $email) use(&$invalidRecords) {
251 if (!$this->validator->validateNonRoleEmail($email)) {
252 $invalidRecords[] = $index;
253 }
254 return strtolower($email);
255 },
256 array_keys($data),
257 $data
258 );
259 }
260 if (in_array($column, ['created_at', 'confirmed_at'], true)) {
261 $data = $this->validateDateTime($data, $invalidRecords);
262 }
263 if (in_array($column, ['confirmed_ip', 'subscribed_ip'], true)) {
264 $data = array_map(
265 function($index, $ip) {
266 if (!filter_var($ip, FILTER_VALIDATE_IP)) {
267 // if invalid or empty, we allow the import but remove the IP
268 return null;
269 }
270 return $ip;
271 },
272 array_keys($data),
273 $data
274 );
275 }
276 // if this is a custom column
277 if (in_array($column, $this->subscribersCustomFields)) {
278 $customField = $this->customFieldsRepository->findOneById($column);
279 if (!$customField instanceof CustomFieldEntity) {
280 continue;
281 }
282 // validate date type
283 if ($customField->getType() === CustomFieldEntity::TYPE_DATE) {
284 $data = $this->validateDateTime($data, $invalidRecords);
285 }
286 }
287 }
288 if ($invalidRecords) {
289 foreach ($subscribersData as $column => &$data) {
290 $data = array_diff_key($data, array_flip($invalidRecords));
291 $data = array_values($data);
292 }
293 }
294 if (empty($subscribersData['email'])) return false;
295 return $subscribersData;
296 }
297
298 private function validateDateTime(array $data, array &$invalidRecords): array {
299 $siteUsesCustomFormat = WPFunctions::get()->getOption('date_format') === 'd/m/Y';
300 if ($siteUsesCustomFormat) {
301 return $this->validateDateTimeAttemptCustomFormat($data, $invalidRecords);
302 }
303
304 $validationRule = 'datetime';
305 return array_map(
306 function ($index, $date) use ($validationRule, &$invalidRecords) {
307 if (empty($date)) return $date;
308 $date = (new DateConverter())->convertDateToDatetime($date, $validationRule);
309 if (!$date) {
310 $invalidRecords[] = $index;
311 }
312 return $date;
313 },
314 array_keys($data),
315 $data
316 );
317 }
318
319 private function validateDateTimeAttemptCustomFormat(array $data, array &$invalidRecords): array {
320 $validationRule = 'datetime';
321 $dateTimeDates = $data;
322 $dateTimeInvalidRecords = $invalidRecords;
323 $datetimeErrorCount = 0;
324
325 $validationRuleCustom = 'd/m/Y';
326 $customFormatDates = $data;
327 $customFormatInvalidRecords = $invalidRecords;
328 $customFormatErrorCount = 0;
329
330 // We attempt converting with both date formats
331 foreach ($data as $index => $date) {
332 if (empty($date)) {
333 $dateTimeDates[$index] = $date;
334 $customFormatDates[$index] = $date;
335 continue;
336 };
337 $dateTimeDates[$index] = (new DateConverter())->convertDateToDatetime($date, $validationRule);
338 if (!$dateTimeDates[$index]) {
339 $datetimeErrorCount ++;
340 $dateTimeInvalidRecords[] = $index;
341 }
342 $customFormatDates[$index] = (new DateConverter())->convertDateToDatetime($date, $validationRuleCustom);
343 if (!$customFormatDates[$index]) {
344 $customFormatErrorCount ++;
345 $customFormatInvalidRecords[] = $index;
346 }
347 }
348
349 if ($customFormatErrorCount < $datetimeErrorCount) {
350 $invalidRecords = $customFormatInvalidRecords;
351 return $customFormatDates;
352 }
353
354 $invalidRecords = $dateTimeInvalidRecords;
355 return $dateTimeDates;
356 }
357
358 public function transformSubscribersData(array $subscribers, array $columns): array {
359 $transformedSubscribers = [];
360 foreach ($columns as $column => $data) {
361 $transformedSubscribers[$column] = array_column($subscribers, $data['index']);
362 }
363 return $transformedSubscribers;
364 }
365
366 /**
367 * @param array $subscribersData
368 * @return array{array|false,array,array|false}
369 */
370 public function splitSubscribersData(array $subscribersData): array {
371 // $subscribers_data is an two-dimensional associative array
372 // of all subscribers being imported: [field => [value1, value2], field => [value1, value2], ...]
373 $tempExistingSubscribers = [];
374 foreach (array_chunk($subscribersData['email'], self::DB_QUERY_CHUNK_SIZE) as $subscribersEmails) {
375 // create a two-dimensional indexed array of all existing subscribers
376 // with just wp_user_id and email fields: [[wp_user_id, email], [wp_user_id, email], ...]
377 $tempExistingSubscribers = array_merge(
378 $tempExistingSubscribers,
379 $this->subscriberRepository->findWpUserIdAndEmailByEmails($subscribersEmails)
380 );
381 }
382 if (!$tempExistingSubscribers) {
383 return [
384 false, // existing subscribers
385 $subscribersData, // new subscribers
386 false, // WP users
387 ];
388 }
389 // extract WP users ids into a simple indexed array: [wp_user_id_1, wp_user_id_2, ...]
390 $wpUsers = array_filter(array_column($tempExistingSubscribers, 'wp_user_id'));
391 // create a new two-dimensional associative array with existing subscribers ($existing_subscribers)
392 // and reduce $subscribers_data to only new subscribers by removing existing subscribers
393 $existingSubscribers = [];
394 $subscribersEmails = array_flip($subscribersData['email']);
395 foreach ($tempExistingSubscribers as $tempExistingSubscriber) {
396 $existingSubscriberKey = $subscribersEmails[$tempExistingSubscriber['email']];
397 foreach ($subscribersData as $field => &$value) {
398 $existingSubscribers[$field][] = $value[$existingSubscriberKey];
399 unset($value[$existingSubscriberKey]);
400 }
401 }
402 $newSubscribers = $subscribersData;
403 // reindex array after unsetting elements
404 $newSubscribers = array_map('array_values', $newSubscribers);
405 // remove empty values
406 $newSubscribers = array_filter($newSubscribers);
407 return [
408 $existingSubscribers,
409 $newSubscribers,
410 $wpUsers,
411 ];
412 }
413
414 public function deleteExistingTrashedSubscribers(array $subscribersData): void {
415 $existingTrashedRecords = array_filter(
416 array_map(function($subscriberEmails) {
417 return $this->subscriberRepository->findIdsOfDeletedByEmails($subscriberEmails);
418 }, array_chunk($subscribersData['email'], self::DB_QUERY_CHUNK_SIZE))
419 );
420 $existingTrashedRecords = Helpers::flattenArray($existingTrashedRecords);
421 if (!$existingTrashedRecords) {
422 return;
423 }
424 foreach (array_chunk($existingTrashedRecords, self::DB_QUERY_CHUNK_SIZE) as $subscriberIds) {
425 $this->subscriberRepository->bulkDelete($subscriberIds);
426 }
427 }
428
429 public function addMissingRequiredFields(array $subscribers): array {
430 foreach (array_keys($this->requiredSubscribersFields) as $requiredField) {
431 $subscribers = $this->addField($subscribers, $requiredField, $this->requiredSubscribersFields[$requiredField]);
432 }
433 return $subscribers;
434 }
435
436 /**
437 * @param array $subscribers
438 * @param string $fieldName
439 * @param mixed $fieldValue
440 * @return array
441 */
442 private function addField(array $subscribers, string $fieldName, $fieldValue): array {
443 if (in_array($fieldName, $subscribers['fields'])) return $subscribers;
444
445 $subscribersCount = count($subscribers['data'][key($subscribers['data'])]);
446 $subscribers['data'][$fieldName] = array_fill(
447 0,
448 $subscribersCount,
449 $fieldValue
450 );
451 $subscribers['fields'][] = $fieldName;
452
453 return $subscribers;
454 }
455
456 private function setSubscriptionStatusToDefault(array $subscribersData, string $defaultStatus): array {
457 if (!in_array('status', $subscribersData['fields'])) return $subscribersData;
458 $subscribersData['data']['status'] = array_map(function() use ($defaultStatus) {
459 return $defaultStatus;
460 }, $subscribersData['data']['status']);
461
462 if ($defaultStatus === SubscriberEntity::STATUS_SUBSCRIBED) {
463 if (!in_array('last_subscribed_at', $subscribersData['fields'])) {
464 $subscribersData['fields'][] = 'last_subscribed_at';
465 }
466 $subscribersData['data']['last_subscribed_at'] = array_map(function() {
467 return $this->createdAt;
468 }, $subscribersData['data']['status']);
469 }
470 return $subscribersData;
471 }
472
473 private function setSource(array $subscribersData): array {
474 $subscribersCount = count($subscribersData['data'][key($subscribersData['data'])]);
475 $subscribersData['fields'][] = 'source';
476 $subscribersData['data']['source'] = array_fill(
477 0,
478 $subscribersCount,
479 Source::IMPORTED
480 );
481 return $subscribersData;
482 }
483
484 private function setLinkToken(array $subscribersData): array {
485 $subscribersCount = count($subscribersData['data'][key($subscribersData['data'])]);
486 $subscribersData['fields'][] = 'link_token';
487 $subscribersData['data']['link_token'] = array_map(
488 function () {
489 return Security::generateRandomString(SubscriberEntity::LINK_TOKEN_LENGTH);
490 },
491 array_fill(0, $subscribersCount, null)
492 );
493 return $subscribersData;
494 }
495
496 public function getSubscribersFields(array $subscribersFields): array {
497 return array_values(
498 array_filter(
499 array_map(function($field) {
500 if (!is_int($field)) return $field;
501 }, $subscribersFields)
502 )
503 );
504 }
505
506 /**
507 * @param array $subscribersFields
508 * @return int[]
509 */
510 public function getCustomSubscribersFields(array $subscribersFields): array {
511 return array_values(
512 array_filter(
513 array_map(function($field) {
514 if (is_int($field)) return $field;
515 }, $subscribersFields)
516 )
517 );
518 }
519
520 public function createOrUpdateSubscribers(
521 string $action,
522 array $subscribersData,
523 array $subscribersCustomFields = []
524 ): ?array {
525 $subscribersCount = count($subscribersData['data'][key($subscribersData['data'])]);
526 $subscribers = array_map(function($index) use ($subscribersData) {
527 return array_map(function($field) use ($index, $subscribersData) {
528 return $subscribersData['data'][$field][$index];
529 }, $subscribersData['fields']);
530 }, range(0, $subscribersCount - 1));
531 foreach (array_chunk($subscribers, self::DB_QUERY_CHUNK_SIZE) as $data) {
532 if ($action === self::ACTION_CREATE) {
533 $this->importExportRepository->insertMultiple(
534 SubscriberEntity::class,
535 $subscribersData['fields'],
536 $data
537 );
538 } elseif ($action === self::ACTION_UPDATE) {
539 $this->importExportRepository->updateMultiple(
540 SubscriberEntity::class,
541 $subscribersData['fields'],
542 $data,
543 $this->updatedAt
544 );
545 }
546 }
547 $createdOrUpdatedSubscribers = [];
548 foreach (array_chunk($subscribersData['data']['email'], self::DB_QUERY_CHUNK_SIZE) as $emails) {
549 foreach ($this->subscriberRepository->findIdAndEmailByEmails($emails) as $createdOrUpdatedSubscriber) {
550 // ensure emails loaded from the DB are lowercased (imported emails are lowercased as well)
551 $createdOrUpdatedSubscriber['email'] = mb_strtolower($createdOrUpdatedSubscriber['email']);
552 $createdOrUpdatedSubscribers[] = $createdOrUpdatedSubscriber;
553 }
554 }
555 if (empty($createdOrUpdatedSubscribers)) return null;
556
557 $this->subscriberRepository->invalidateTotalSubscribersCache();
558 $createdOrUpdatedSubscribersIds = array_column($createdOrUpdatedSubscribers, 'id');
559 if ($subscribersCustomFields) {
560 $this->createOrUpdateCustomFields(
561 $action,
562 $createdOrUpdatedSubscribers,
563 $subscribersData,
564 $subscribersCustomFields
565 );
566 }
567 $this->addSubscribersToSegments(
568 $createdOrUpdatedSubscribersIds,
569 $this->segmentsIds
570 );
571 $this->addTagsToSubscribers(
572 $createdOrUpdatedSubscribersIds,
573 $this->tags
574 );
575 return $createdOrUpdatedSubscribers;
576 }
577
578 public function createOrUpdateCustomFields(
579 string $action,
580 array $createdOrUpdatedSubscribers,
581 array $subscribersData,
582 array $subscribersCustomFieldsIds
583 ): void {
584 // check if custom fields exist in the database
585 $subscribersCustomFieldsIds = array_map(function(CustomFieldEntity $customField): int {
586 return (int)$customField->getId();
587 }, $this->customFieldsRepository->findBy(['id' => $subscribersCustomFieldsIds, 'deletedAt' => null]));
588 if (!$subscribersCustomFieldsIds) {
589 return;
590 }
591 // assemble a two-dimensional array: [[custom_field_id, subscriber_id, value], [custom_field_id, subscriber_id, value], ...]
592 $subscribersCustomFieldsData = [];
593 $subscribersEmails = array_flip($subscribersData['data']['email']);
594 foreach ($createdOrUpdatedSubscribers as $createdOrUpdatedSubscriber) {
595 $subscriberIndex = $subscribersEmails[$createdOrUpdatedSubscriber['email']];
596 foreach ($subscribersData['data'] as $field => $values) {
597 // exclude non-custom fields
598 if (!is_int($field)) continue;
599 $subscribersCustomFieldsData[] = [
600 (int)$field,
601 $createdOrUpdatedSubscriber['id'],
602 $values[$subscriberIndex],
603 $this->createdAt,
604 ];
605 }
606 }
607 $columns = [
608 'custom_field_id',
609 'subscriber_id',
610 'value',
611 'created_at',
612 ];
613 $customFieldCount = count($subscribersCustomFieldsIds);
614 $customFieldBatchSize = (int)(round(self::DB_QUERY_CHUNK_SIZE / $customFieldCount) * $customFieldCount);
615 $customFieldBatchSize = ($customFieldBatchSize > 0) ? $customFieldBatchSize : 1;
616 foreach (array_chunk($subscribersCustomFieldsData, $customFieldBatchSize) as $subscribersCustomFieldsDataChunk) {
617 $this->importExportRepository->insertMultiple(
618 SubscriberCustomFieldEntity::class,
619 $columns,
620 $subscribersCustomFieldsDataChunk
621 );
622 if ($action === self::ACTION_UPDATE) {
623 $this->importExportRepository->updateMultiple(
624 SubscriberCustomFieldEntity::class,
625 $columns,
626 $subscribersCustomFieldsDataChunk,
627 $this->updatedAt
628 );
629 }
630 }
631 }
632
633 /**
634 * @param int[] $wpUsers
635 * @return array
636 */
637 public function synchronizeWPUsers(array $wpUsers): array {
638 $users = array_map([$this->wpSegment, 'synchronizeUser'], $wpUsers);
639 $this->subscriberRepository->invalidateTotalSubscribersCache();
640 return $users;
641 }
642
643 public function addSubscribersToSegments(array $subscribersIds, array $segmentsIds): void {
644 $columns = [
645 'subscriber_id',
646 'segment_id',
647 'created_at',
648 ];
649 foreach ($segmentsIds as $segmentId) {
650 foreach (array_chunk($subscribersIds, self::DB_QUERY_CHUNK_SIZE) as $subscriberIdsChunk) {
651 $data = [];
652 $data = array_merge($data, array_map(function ($subscriberId) use ($segmentId): array {
653 return [
654 $subscriberId,
655 $segmentId,
656 $this->createdAt,
657 ];
658 }, $subscriberIdsChunk));
659
660 $this->importExportRepository->insertMultiple(
661 SubscriberSegmentEntity::class,
662 $columns,
663 $data
664 );
665 }
666 }
667 $this->subscriberRepository->recalculateSegmentsCount($subscribersIds);
668 }
669
670 /**
671 * @param int[] $subscribersIds
672 * @param string[] $tagNames
673 */
674 public function addTagsToSubscribers(array $subscribersIds, array $tagNames): void {
675 $tagIds = [];
676 foreach ($tagNames as $tagName) {
677 $tag = $this->tagRepository->findOneBy(['name' => $tagName]);
678 if (!$tag) {
679 $tag = $this->tagRepository->createOrUpdate(['name' => $tagName]);
680 }
681 $tagIds[] = $tag->getId();
682 }
683
684 $columns = [
685 'subscriber_id',
686 'tag_id',
687 'created_at',
688 ];
689 foreach ($tagIds as $tagId) {
690 foreach (array_chunk($subscribersIds, self::DB_QUERY_CHUNK_SIZE) as $subscriberIdsChunk) {
691 $data = [];
692 $data = array_merge($data, array_map(function ($subscriberId) use ($tagId): array {
693 return [
694 $subscriberId,
695 $tagId,
696 $this->createdAt,
697 ];
698 }, $subscriberIdsChunk));
699
700 $this->importExportRepository->insertMultiple(
701 SubscriberTagEntity::class,
702 $columns,
703 $data
704 );
705 }
706 }
707 }
708 }
709