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 / Segments / WP.php
mailpoet / lib / Segments Last commit date
DynamicSegments 1 month ago RestApi 1 month ago SegmentDependencyValidator.php 3 years ago SegmentListingRepository.php 1 month ago SegmentSaveController.php 2 weeks ago SegmentSubscribersRepository.php 2 weeks ago SegmentsFinder.php 3 years ago SegmentsRepository.php 2 weeks ago SegmentsSimpleListRepository.php 2 months ago SubscribersFinder.php 1 month ago WP.php 2 weeks ago WooCommerce.php 2 weeks ago index.php 3 years ago
WP.php
597 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Segments;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Config\SubscriberChangesNotifier;
9 use MailPoet\DI\ContainerWrapper;
10 use MailPoet\Doctrine\WPDB\Connection;
11 use MailPoet\Entities\SegmentEntity;
12 use MailPoet\Entities\SubscriberEntity;
13 use MailPoet\Entities\SubscriberSegmentEntity;
14 use MailPoet\Logging\LoggerFactory;
15 use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
16 use MailPoet\Services\Validator;
17 use MailPoet\Settings\SettingsController;
18 use MailPoet\Subscribers\ConfirmationEmailMailer;
19 use MailPoet\Subscribers\SegmentsCountRecalculator;
20 use MailPoet\Subscribers\Source;
21 use MailPoet\Subscribers\SubscriberSegmentRepository;
22 use MailPoet\Subscribers\SubscribersRepository;
23 use MailPoet\Util\DBCollationChecker;
24 use MailPoet\WooCommerce\Helper as WooCommerceHelper;
25 use MailPoet\WP\Functions as WPFunctions;
26 use MailPoetVendor\Carbon\Carbon;
27 use MailPoetVendor\Doctrine\ORM\EntityManager;
28
29 class WP {
30
31 /** @var WPFunctions */
32 private $wp;
33
34 /** @var WelcomeScheduler */
35 private $welcomeScheduler;
36
37 /** @var WooCommerceHelper */
38 private $wooHelper;
39
40 /** @var SubscribersRepository */
41 private $subscribersRepository;
42
43 /** @var SubscriberChangesNotifier */
44 private $subscriberChangesNotifier;
45
46 /** @var SubscriberSegmentRepository */
47 private $subscriberSegmentRepository;
48
49 /** @var Validator */
50 private $validator;
51
52 /** @var SegmentsRepository */
53 private $segmentsRepository;
54
55 /** @var EntityManager */
56 private $entityManager;
57
58 /** @var DBCollationChecker */
59 private $collationChecker;
60
61 /** @var string */
62 private $subscribersTable;
63
64 /** @var \MailPoetVendor\Doctrine\DBAL\Connection */
65 private $databaseConnection;
66
67 /** @var SegmentsCountRecalculator */
68 private $segmentsCountRecalculator;
69
70 public function __construct(
71 WPFunctions $wp,
72 WelcomeScheduler $welcomeScheduler,
73 WooCommerceHelper $wooHelper,
74 SubscribersRepository $subscribersRepository,
75 SubscriberSegmentRepository $subscriberSegmentRepository,
76 SubscriberChangesNotifier $subscriberChangesNotifier,
77 Validator $validator,
78 SegmentsRepository $segmentsRepository,
79 EntityManager $entityManager,
80 DBCollationChecker $collationChecker,
81 SegmentsCountRecalculator $segmentsCountRecalculator
82 ) {
83 $this->wp = $wp;
84 $this->welcomeScheduler = $welcomeScheduler;
85 $this->wooHelper = $wooHelper;
86 $this->subscribersRepository = $subscribersRepository;
87 $this->subscriberSegmentRepository = $subscriberSegmentRepository;
88 $this->subscriberChangesNotifier = $subscriberChangesNotifier;
89 $this->validator = $validator;
90 $this->segmentsRepository = $segmentsRepository;
91 $this->entityManager = $entityManager;
92 $this->collationChecker = $collationChecker;
93 $this->segmentsCountRecalculator = $segmentsCountRecalculator;
94 $this->databaseConnection = $this->entityManager->getConnection();
95 $this->subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
96 }
97
98 /**
99 * @param int $wpUserId
100 * @param array|false $oldWpUserData
101 */
102 public function synchronizeUser(int $wpUserId, $oldWpUserData = false): void {
103 $wpUser = \get_userdata($wpUserId);
104 if ($wpUser === false) return;
105
106 $subscriber = $this->subscribersRepository->findOneBy(['wpUserId' => $wpUserId]);
107
108 $currentFilter = $this->wp->currentFilter();
109 // Delete
110 if (in_array($currentFilter, ['delete_user', 'deleted_user', 'remove_user_from_blog'])) {
111 if ($subscriber instanceof SubscriberEntity) {
112 $this->unlinkSubscriberFromWpUser($subscriber, $wpUser);
113 }
114 return;
115 }
116 $this->handleCreatingOrUpdatingSubscriber($currentFilter, $wpUser, $subscriber, $oldWpUserData);
117
118 // In WP::synchronizeUser, after the subscriber is created
119 $this->wp->doAction('mailpoet_user_registered', $wpUserId, $subscriber);
120 }
121
122 private function deleteSubscriber(SubscriberEntity $subscriber): void {
123 $this->entityManager->wrapInTransaction(function() use ($subscriber): void {
124 $this->subscriberSegmentRepository->deleteAllBySubscriber($subscriber);
125 $this->subscribersRepository->remove($subscriber);
126 $this->subscribersRepository->flush();
127 });
128 }
129
130 private function unlinkSubscriberFromWpUser(SubscriberEntity $subscriber, \WP_User $wpUser): void {
131 // Backwards-compat escape hatch for sites that need the legacy hard-delete behavior
132 // (e.g. GDPR-driven account deletion flows). Returning true reproduces pre-STOMAIL-8018 behavior.
133 $hardDelete = (bool)$this->wp->applyFilters(
134 'mailpoet_delete_subscriber_on_wp_user_delete',
135 false,
136 $subscriber,
137 (int)$wpUser->ID // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
138 );
139 if ($hardDelete) {
140 $this->deleteSubscriber($subscriber);
141 return;
142 }
143
144 $this->entityManager->wrapInTransaction(function() use ($subscriber, $wpUser): void {
145 $wpSegment = $this->segmentsRepository->getWPUsersSegment();
146
147 // Remove only the WP-Users segment membership; other list subscriptions stay intact.
148 $this->entityManager->createQueryBuilder()
149 ->delete(SubscriberSegmentEntity::class, 'ss')
150 ->where('ss.subscriber = :subscriber AND ss.segment = :segment')
151 ->setParameter('subscriber', $subscriber)
152 ->setParameter('segment', $wpSegment)
153 ->getQuery()
154 ->execute();
155
156 $subscriber->setWpUserId(null);
157 $subscriber->setSource(Source::WORDPRESS_USER_DELETED);
158
159 // If the subscriber was only on the WP-Users list and is not a WC customer,
160 // they had no list of their own to remain on — trash them instead of leaving a floating row.
161 // Skip when the subscriber was already trashed (e.g. manually by an admin) so we keep
162 // the original deleted_at and status as audit information.
163 $hasOtherActiveSegments = $this->hasOtherActiveSegments($subscriber);
164 $isWooCustomer = $this->wooHelper->isWooCommerceActive() && in_array('customer', $wpUser->roles, true); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
165 if (!$hasOtherActiveSegments && !$isWooCustomer && $subscriber->getDeletedAt() === null) {
166 $subscriber->setStatus(SubscriberEntity::STATUS_UNCONFIRMED);
167 $subscriber->setDeletedAt(Carbon::now()->millisecond(0));
168 }
169
170 $this->subscribersRepository->persist($subscriber);
171 $this->subscribersRepository->flush();
172 });
173 $this->segmentsCountRecalculator->recalculateForSubscribers([(int)$subscriber->getId()]);
174 }
175
176 private function hasOtherActiveSegments(SubscriberEntity $subscriber): bool {
177 $subscriberId = $subscriber->getId();
178 if ($subscriberId === null) {
179 return false;
180 }
181
182 $count = $this->entityManager->createQueryBuilder()
183 ->select('COUNT(segment.id)')
184 ->from(SubscriberSegmentEntity::class, 'subscriberSegment')
185 ->innerJoin('subscriberSegment.segment', 'segment')
186 ->where('subscriberSegment.subscriber = :subscriber')
187 ->andWhere('segment.type != :wpType')
188 ->andWhere('segment.deletedAt IS NULL')
189 ->setParameter('subscriber', $subscriber)
190 ->setParameter('wpType', SegmentEntity::TYPE_WP_USERS)
191 ->getQuery()
192 ->getSingleScalarResult();
193
194 return is_numeric($count) && (int)$count > 0;
195 }
196
197 /**
198 * @param string $currentFilter
199 * @param \WP_User $wpUser
200 * @param ?SubscriberEntity $subscriber
201 * @param array|false $oldWpUserData
202 */
203 private function handleCreatingOrUpdatingSubscriber(string $currentFilter, \WP_User $wpUser, ?SubscriberEntity $subscriber = null, $oldWpUserData = false): void {
204 // Add or update
205 $wpSegment = $this->segmentsRepository->getWPUsersSegment();
206
207 // find subscriber by email when is null
208 if (is_null($subscriber)) {
209 $subscriber = $this->subscribersRepository->findOneBy(['email' => $wpUser->user_email]); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
210 }
211
212 // get first name & last name
213 $firstName = html_entity_decode($wpUser->first_name, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
214 $lastName = html_entity_decode($wpUser->last_name, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
215 if (empty($wpUser->first_name) && empty($wpUser->last_name)) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
216 $firstName = html_entity_decode($wpUser->display_name, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
217 }
218 $signupConfirmationEnabled = SettingsController::getInstance()->get('signup_confirmation.enabled');
219 $status = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
220 // we want to mark a new subscriber as unsubscribe when the checkbox from registration is unchecked
221 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- type narrowing only, value is read as bool
222 $mailpoetPost = isset($_POST['mailpoet']) && is_array($_POST['mailpoet']) ? $_POST['mailpoet'] : [];
223 if (isset($mailpoetPost['subscribe_on_register_active']) && (bool)$mailpoetPost['subscribe_on_register_active'] === true) {
224 $status = SubscriberEntity::STATUS_UNSUBSCRIBED;
225 }
226
227 // subscriber data
228 $wpUserId = (int)$wpUser->ID; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
229 $data = [
230 'wp_user_id' => $wpUserId,
231 'email' => $wpUser->user_email, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
232 'first_name' => $firstName,
233 'last_name' => $lastName,
234 'status' => $status,
235 'source' => Source::WORDPRESS_USER,
236 ];
237
238 $isRelinkingDeletedWpUser = $subscriber !== null
239 && $subscriber->getSource() === Source::WORDPRESS_USER_DELETED
240 && $subscriber->getWpUserId() !== $wpUserId;
241 if (!is_null($subscriber)) {
242 $data['id'] = $subscriber->getId();
243 unset($data['status']); // don't override status for existing users
244 unset($data['source']); // don't override source for existing users
245 if ($isRelinkingDeletedWpUser) {
246 // Restore the live WP-user source on any re-link, not only the auto-trashed case.
247 // Only revive deleted_at when the subscriber was actually trashed by the unlink
248 // (subscribers kept on other lists were never trashed and must keep deleted_at IS NULL).
249 $data['source'] = Source::WORDPRESS_USER;
250 if ($subscriber->getDeletedAt() !== null) {
251 $data['deleted_at'] = null;
252 }
253 }
254 }
255
256 $addingNewUserToDisabledWPSegment = $wpSegment->getDeletedAt() !== null && $currentFilter === 'user_register';
257
258 $otherActiveSegments = [];
259 if ($subscriber) {
260 $otherActiveSegments = array_filter($subscriber->getSegments()->toArray() ?? [], function (SegmentEntity $segment) {
261 return $segment->getType() !== SegmentEntity::TYPE_WP_USERS && $segment->getDeletedAt() === null;
262 });
263 }
264 $isWooCustomer = $this->wooHelper->isWooCommerceActive() && in_array('customer', $wpUser->roles, true);
265 // When WP Segment is disabled force trashed state and unconfirmed status for new WPUsers without active segment
266 // or who are not WooCommerce customers at the same time since customers are added to the WooCommerce list
267 if ($addingNewUserToDisabledWPSegment && !$isRelinkingDeletedWpUser && !$otherActiveSegments && !$isWooCustomer) {
268 $data['deleted_at'] = Carbon::now()->millisecond(0);
269 $data['status'] = SubscriberEntity::STATUS_UNCONFIRMED;
270 }
271
272 // Apply filter to allow modifying subscriber data before save
273 $data = $this->wp->applyFilters('mailpoet_subscriber_data_before_save', $data);
274
275 // Ensure data is an array
276 if (!is_array($data)) {
277 // If the filter returned a non-array, log it and use the original data
278 $logger = LoggerFactory::getInstance()->getLogger();
279 $logger->error(
280 'Filter mailpoet_subscriber_data_before_save returned non-array data.',
281 ['data_type' => gettype($data)]
282 );
283 return;
284 }
285
286 // When updating an existing subscriber's email, remove any other subscriber
287 // that already holds the new email to avoid a unique constraint violation.
288 // This can happen when a WP user registers with email A, checks out with
289 // email B (creating a second subscriber), then changes their account email
290 // from A to B.
291 // Uses bulkDelete() to properly clean up related data (segments, tags,
292 // custom fields) and to restrict deletion to safe duplicates (non-WP,
293 // non-WooCommerce subscribers).
294 if ($subscriber !== null && $subscriber->getEmail() !== $data['email']) {
295 $existingSubscriber = $this->subscribersRepository->findOneBy(['email' => $data['email']]);
296 if ($existingSubscriber !== null && $existingSubscriber->getId() !== $subscriber->getId()) {
297 $duplicateId = $existingSubscriber->getId();
298 $this->entityManager->detach($existingSubscriber);
299 $deletedCount = $this->subscribersRepository->bulkDelete([$duplicateId]);
300 if ($deletedCount === 0) {
301 // The duplicate is a WP user or WooCommerce customer and cannot be
302 // safely removed. Skip the email update to avoid a constraint violation.
303 $logger = LoggerFactory::getInstance()->getLogger();
304 $logger->warning(
305 'Cannot update subscriber email: duplicate subscriber is a WP user or WooCommerce customer',
306 ['subscriber_id' => $subscriber->getId(), 'duplicate_id' => $duplicateId, 'email' => $data['email']]
307 );
308 return;
309 }
310 }
311 }
312
313 try {
314 $subscriber = $this->createOrUpdateSubscriber($data, $subscriber);
315 } catch (\Exception $e) {
316 return; // fails silently as this was the behavior before the Doctrine refactor.
317 }
318
319 // add subscriber to the WP Users segment
320 $this->subscriberSegmentRepository->subscribeToSegments(
321 $subscriber,
322 [$wpSegment]
323 );
324
325 if (!$signupConfirmationEnabled && $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED && $currentFilter === 'user_register') {
326 $subscriberSegment = $this->subscriberSegmentRepository->findOneBy([
327 'subscriber' => $subscriber->getId(),
328 'segment' => $wpSegment->getId(),
329 ]);
330
331 if (!is_null($subscriberSegment)) {
332 $this->wp->doAction('mailpoet_segment_subscribed', $subscriberSegment);
333 }
334 }
335
336 $subscribeOnRegisterEnabled = SettingsController::getInstance()->get('subscribe.on_register.enabled');
337 $sendConfirmationEmail =
338 $signupConfirmationEnabled
339 && $subscribeOnRegisterEnabled
340 && $currentFilter !== 'profile_update'
341 && !$addingNewUserToDisabledWPSegment;
342
343 if ($sendConfirmationEmail && ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED)) {
344 /** @var ConfirmationEmailMailer $confirmationEmailMailer */
345 $confirmationEmailMailer = ContainerWrapper::getInstance()->get(ConfirmationEmailMailer::class);
346 try {
347 // Per-list confirmation settings are not resolved here because this path
348 // subscribes to the WordPress Users segment (TYPE_WP_USERS),
349 // which does not support custom confirmation overrides.
350 $confirmationEmailMailer->sendConfirmationEmailOnce($subscriber);
351 } catch (\Exception $e) {
352 // ignore errors
353 }
354 }
355
356 // welcome email
357 $scheduleWelcomeNewsletter = false;
358 if (in_array($currentFilter, ['profile_update', 'user_register', 'add_user_role', 'set_user_role'])) {
359 $scheduleWelcomeNewsletter = true;
360 }
361 if ($scheduleWelcomeNewsletter === true) {
362 $this->welcomeScheduler->scheduleWPUserWelcomeNotification(
363 $subscriber->getId(),
364 (array)$wpUser,
365 (array)$oldWpUserData
366 );
367 }
368 }
369
370 private function createOrUpdateSubscriber(array $data, ?SubscriberEntity $subscriber = null): SubscriberEntity {
371 if (is_null($subscriber)) {
372 $subscriber = new SubscriberEntity();
373 }
374
375 $subscriber->setWpUserId($data['wp_user_id']);
376 $subscriber->setEmail($data['email']);
377
378 // Only set first_name if it's present in the data array
379 if (isset($data['first_name'])) {
380 $subscriber->setFirstName($data['first_name']);
381 }
382
383 // Only set last_name if it's present in the data array
384 if (isset($data['last_name'])) {
385 $subscriber->setLastName($data['last_name']);
386 }
387
388 if (isset($data['status'])) {
389 $subscriber->setStatus($data['status']);
390 }
391
392 if (isset($data['source'])) {
393 $subscriber->setSource($data['source']);
394 }
395
396 if (array_key_exists('deleted_at', $data)) {
397 $subscriber->setDeletedAt($data['deleted_at']);
398 }
399
400 $this->subscribersRepository->persist($subscriber);
401 $this->subscribersRepository->flush();
402
403 return $subscriber;
404 }
405
406 public function synchronizeUsers(): bool {
407 // Temporarily skip synchronization in WP Playground.
408 // Some of the queries are not yet supported by the SQLite integration.
409 if (Connection::isSQLite()) {
410 return true;
411 }
412
413 // Save timestamp about changes and update before insert
414 $this->subscriberChangesNotifier->subscribersBatchCreate();
415 $this->subscriberChangesNotifier->subscribersBatchUpdate();
416
417 $updatedUsersEmails = $this->updateSubscribersEmails();
418 $insertedUsersEmails = $this->insertSubscribers();
419 $this->removeUpdatedSubscribersWithInvalidEmail(array_merge($updatedUsersEmails, $insertedUsersEmails));
420 // There is high chance that an update will be made
421 $this->subscriberChangesNotifier->subscribersBatchUpdate();
422 unset($updatedUsersEmails);
423 unset($insertedUsersEmails);
424 $this->updateFirstNames();
425 $this->updateLastNames();
426 $this->updateFirstNameIfMissing();
427 $this->insertUsersToSegment();
428 $this->removeOrphanedSubscribers();
429 // insertUsersToSegment adds WP users to the WP-Users segment via raw SQL,
430 // so refresh segments_count for that segment's members.
431 // recalculateForSegment() only sees subscribers that still have a membership
432 // row. Orphans that are hard-deleted by removeOrphanedSubscribers() are fine
433 // (row gone, count moot). Orphans whose membership is deleted but who survive
434 // (soft-trashed or still on other lists) are recalculated explicitly inside
435 // removeOrphanedSubscribersFromWpSegment() before the membership DELETE.
436 $this->segmentsCountRecalculator->recalculateForSegment((int)$this->segmentsRepository->getWPUsersSegment()->getId());
437 $this->subscribersRepository->invalidateTotalSubscribersCache();
438 $this->subscribersRepository->refreshAll();
439
440 return true;
441 }
442
443 private function removeUpdatedSubscribersWithInvalidEmail(array $updatedEmails): void {
444 $invalidWpUserIds = array_map(function($item) {
445 return $item['id'];
446 },
447 array_filter($updatedEmails, function($updatedEmail) {
448 return !$this->validator->validateEmail($updatedEmail['email']) && $updatedEmail['id'] !== null;
449 }));
450 if (!$invalidWpUserIds) {
451 return;
452 }
453
454 $this->subscribersRepository->removeByWpUserIds($invalidWpUserIds);
455 }
456
457 private function updateSubscribersEmails(): array {
458 global $wpdb;
459
460 $stmt = $this->databaseConnection->executeQuery('SELECT NOW();');
461 $startTime = $stmt->fetchOne();
462
463 if (!is_string($startTime)) {
464 throw new \RuntimeException("Failed to fetch the current time.");
465 }
466
467 $updateSql =
468 "UPDATE IGNORE {$this->subscribersTable} s
469 INNER JOIN {$wpdb->users} as wu ON s.wp_user_id = wu.id
470 SET s.email = wu.user_email";
471 $this->databaseConnection->executeStatement($updateSql);
472
473 $selectSql =
474 "SELECT wp_user_id as id, email FROM {$this->subscribersTable}
475 WHERE updated_at >= '{$startTime}'";
476 $updatedEmails = $this->databaseConnection->fetchAllAssociative($selectSql);
477
478 return $updatedEmails;
479 }
480
481 private function insertSubscribers(): array {
482 global $wpdb;
483 $wpSegment = $this->segmentsRepository->getWPUsersSegment();
484
485 if ($wpSegment->getDeletedAt() !== null) {
486 $subscriberStatus = SubscriberEntity::STATUS_UNCONFIRMED;
487 $deletedAt = 'CURRENT_TIMESTAMP()';
488 } else {
489 $signupConfirmationEnabled = SettingsController::getInstance()->get('signup_confirmation.enabled');
490 $subscriberStatus = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
491 $deletedAt = 'null';
492 }
493
494 // Fetch users that are not in the subscribers table
495 $selectSql =
496 "SELECT u.id, u.user_email as email
497 FROM {$wpdb->users} u
498 LEFT JOIN {$this->subscribersTable} AS s ON s.wp_user_id = u.id
499 WHERE s.wp_user_id IS NULL AND u.user_email != ''";
500 $insertedUserIds = $this->databaseConnection->fetchAllAssociative($selectSql);
501
502 // Insert new users into the subscribers table.
503 // The email-based join can cross a collation boundary when wp_users.user_email and
504 // mailpoet_subscribers.email use different (but compatible) collations — force a
505 // matching collation to avoid "Illegal mix of collations" errors.
506 $emailCollation = $this->collationChecker->getCollateIfNeeded(
507 $wpdb->users,
508 'user_email',
509 $this->subscribersTable,
510 'email'
511 );
512 $insertSql =
513 "INSERT IGNORE INTO {$this->subscribersTable} (wp_user_id, email, status, created_at, `source`, deleted_at)
514 SELECT wu.id, wu.user_email, :subscriberStatus, CURRENT_TIMESTAMP(), :source, {$deletedAt}
515 FROM {$wpdb->users} wu
516 LEFT JOIN {$this->subscribersTable} s ON wu.id = s.wp_user_id
517 LEFT JOIN {$this->subscribersTable} existingSubscriber ON wu.user_email = existingSubscriber.email {$emailCollation}
518 WHERE s.wp_user_id IS NULL AND wu.user_email != ''
519 ON DUPLICATE KEY UPDATE
520 wp_user_id = wu.id,
521 deleted_at = IF(
522 existingSubscriber.`source` = :deletedSource AND existingSubscriber.deleted_at IS NOT NULL,
523 NULL,
524 existingSubscriber.deleted_at
525 ),
526 `source` = IF(
527 existingSubscriber.`source` = :deletedSource,
528 :source,
529 existingSubscriber.`source`
530 )";
531 $stmt = $this->databaseConnection->prepare($insertSql);
532 $stmt->bindValue('subscriberStatus', $subscriberStatus);
533 $stmt->bindValue('source', Source::WORDPRESS_USER);
534 $stmt->bindValue('deletedSource', Source::WORDPRESS_USER_DELETED);
535 $stmt->executeStatement();
536
537 return $insertedUserIds;
538 }
539
540 private function updateFirstNames(): void {
541 global $wpdb;
542
543 $sql =
544 "UPDATE {$this->subscribersTable} s
545 JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'first_name'
546 SET s.first_name = SUBSTRING(wpum.meta_value, 1, 255)
547 WHERE s.first_name = ''
548 AND s.wp_user_id IS NOT NULL
549 AND wpum.meta_value IS NOT NULL";
550
551 $this->databaseConnection->executeStatement($sql);
552 }
553
554 private function updateLastNames(): void {
555 global $wpdb;
556
557 $sql =
558 "UPDATE {$this->subscribersTable} s
559 JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'last_name'
560 SET s.last_name = SUBSTRING(wpum.meta_value, 1, 255)
561 WHERE s.last_name = ''
562 AND s.wp_user_id IS NOT NULL
563 AND wpum.meta_value IS NOT NULL";
564
565 $this->databaseConnection->executeStatement($sql);
566 }
567
568 private function updateFirstNameIfMissing(): void {
569 global $wpdb;
570
571 $sql =
572 "UPDATE {$this->subscribersTable} s
573 JOIN {$wpdb->users} wu ON s.wp_user_id = wu.id
574 SET s.first_name = wu.display_name
575 WHERE s.first_name = ''
576 AND s.wp_user_id IS NOT NULL";
577
578 $this->databaseConnection->executeStatement($sql);
579 }
580
581 private function insertUsersToSegment(): void {
582 $wpSegment = $this->segmentsRepository->getWPUsersSegment();
583 $subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
584
585 $sql =
586 "INSERT IGNORE INTO {$subscribersSegmentTable} (subscriber_id, segment_id, created_at)
587 SELECT s.id, '{$wpSegment->getId()}', CURRENT_TIMESTAMP() FROM {$this->subscribersTable} s
588 WHERE s.wp_user_id > 0";
589
590 $this->databaseConnection->executeStatement($sql);
591 }
592
593 private function removeOrphanedSubscribers(): void {
594 $this->subscribersRepository->removeOrphanedSubscribersFromWpSegment();
595 }
596 }
597