PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
5.36.0 5.35.1 5.35.0 5.34.3 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 / SubscriberSubscribeController.php
mailpoet / lib / Subscribers Last commit date
ConfirmationEmailTemplate 2 months ago ImportExport 1 month ago RestApi 4 weeks ago Statistics 2 months ago BulkActionController.php 4 weeks ago BulkActionException.php 2 months ago BulkConfirmationEmailResender.php 3 months ago ConfirmationEmailCustomizer.php 3 months ago ConfirmationEmailMailer.php 3 months ago ConfirmationEmailResolver.php 3 months ago EngagementDataBackfiller.php 3 months ago InactiveSubscribersController.php 4 weeks ago LinkTokens.php 3 months ago NewSubscriberNotificationMailer.php 3 months ago RequiredCustomFieldValidator.php 3 months ago SegmentsCountRecalculator.php 1 month ago Source.php 3 months ago SubscriberActions.php 3 months ago SubscriberCustomFieldRepository.php 3 years ago SubscriberIPsRepository.php 2 years ago SubscriberLimitNotificationEvaluator.php 3 months ago SubscriberLimitNotificationMailer.php 3 months ago SubscriberLimitNotificationScheduler.php 3 months ago SubscriberListingRepository.php 1 month ago SubscriberPersonalDataEraser.php 3 months ago SubscriberSaveController.php 4 weeks ago SubscriberSegmentRepository.php 1 month ago SubscriberSubscribeController.php 2 days ago SubscriberTagRepository.php 4 years ago SubscribersCountsController.php 1 month ago SubscribersEmailCountsController.php 1 month ago SubscribersRepository.php 4 weeks ago TrackingConsentCapture.php 2 days ago TrackingConsentController.php 1 week ago index.php 3 years ago
SubscriberSubscribeController.php
455 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Subscribers;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Captcha\BehavioralSignals;
9 use MailPoet\Captcha\CaptchaConstants;
10 use MailPoet\Captcha\CaptchaSession;
11 use MailPoet\Captcha\Validator\CaptchaValidator;
12 use MailPoet\Captcha\Validator\RecaptchaValidator;
13 use MailPoet\Captcha\Validator\TurnstileValidator;
14 use MailPoet\Captcha\Validator\ValidationError;
15 use MailPoet\Entities\FormEntity;
16 use MailPoet\Entities\SubscriberEntity;
17 use MailPoet\Entities\SubscriberTagEntity;
18 use MailPoet\Form\FormsRepository;
19 use MailPoet\Form\Util\FieldNameObfuscator;
20 use MailPoet\NotFoundException;
21 use MailPoet\Segments\SubscribersFinder;
22 use MailPoet\Settings\SettingsController;
23 use MailPoet\Statistics\StatisticsFormsRepository;
24 use MailPoet\Subscription\Throttling as SubscriptionThrottling;
25 use MailPoet\Tags\TagRepository;
26 use MailPoet\UnexpectedValueException;
27 use MailPoet\WP\Functions as WPFunctions;
28
29 class SubscriberSubscribeController {
30 /** @var FormsRepository */
31 private $formsRepository;
32
33 /** @var CaptchaSession */
34 private $captchaSession;
35
36 /** @var FieldNameObfuscator */
37 private $fieldNameObfuscator;
38
39 /** @var SettingsController */
40 private $settings;
41
42 /** @var RequiredCustomFieldValidator */
43 private $requiredCustomFieldValidator;
44
45 /** @var SubscriberActions */
46 private $subscriberActions;
47
48 /** @var WPFunctions */
49 private $wp;
50
51 /** @var SubscriptionThrottling */
52 private $throttling;
53
54 /** @var StatisticsFormsRepository */
55 private $statisticsFormsRepository;
56
57 /** @var SubscribersFinder */
58 private $subscribersFinder;
59
60 /** @var TagRepository */
61 private $tagRepository;
62
63 /** @var SubscriberTagRepository */
64 private $subscriberTagRepository;
65 /** @var CaptchaValidator */
66 private $builtInCaptchaValidator;
67
68 /** @var RecaptchaValidator */
69 private $recaptchaValidator;
70
71 /** @var TurnstileValidator */
72 private $turnstileValidator;
73
74 /** @var BehavioralSignals */
75 private $behavioralSignals;
76
77 /** @var TrackingConsentCapture */
78 private $trackingConsentCapture;
79
80 public function __construct(
81 CaptchaSession $captchaSession,
82 SubscriberActions $subscriberActions,
83 SubscribersFinder $subscribersFinder,
84 SubscriptionThrottling $throttling,
85 FieldNameObfuscator $fieldNameObfuscator,
86 RequiredCustomFieldValidator $requiredCustomFieldValidator,
87 SettingsController $settings,
88 FormsRepository $formsRepository,
89 StatisticsFormsRepository $statisticsFormsRepository,
90 TagRepository $tagRepository,
91 SubscriberTagRepository $subscriberTagRepository,
92 WPFunctions $wp,
93 CaptchaValidator $builtInCaptchaValidator,
94 RecaptchaValidator $recaptchaValidator,
95 TurnstileValidator $turnstileValidator,
96 BehavioralSignals $behavioralSignals,
97 TrackingConsentCapture $trackingConsentCapture
98 ) {
99 $this->formsRepository = $formsRepository;
100 $this->captchaSession = $captchaSession;
101 $this->requiredCustomFieldValidator = $requiredCustomFieldValidator;
102 $this->fieldNameObfuscator = $fieldNameObfuscator;
103 $this->settings = $settings;
104 $this->subscriberActions = $subscriberActions;
105 $this->subscribersFinder = $subscribersFinder;
106 $this->wp = $wp;
107 $this->throttling = $throttling;
108 $this->statisticsFormsRepository = $statisticsFormsRepository;
109 $this->tagRepository = $tagRepository;
110 $this->subscriberTagRepository = $subscriberTagRepository;
111 $this->builtInCaptchaValidator = $builtInCaptchaValidator;
112 $this->recaptchaValidator = $recaptchaValidator;
113 $this->turnstileValidator = $turnstileValidator;
114 $this->behavioralSignals = $behavioralSignals;
115 $this->trackingConsentCapture = $trackingConsentCapture;
116 }
117
118 public function subscribe(array $data): array {
119 $form = $this->getForm($data);
120
121 if (!empty($data['email'])) {
122 throw new UnexpectedValueException(__('Please leave the first field empty.', 'mailpoet'));
123 }
124
125 $captchaSettings = $this->settings->get('captcha');
126 $data = $this->initCaptcha($captchaSettings, $form, $data);
127 $data = $this->deobfuscateFormPayload($data);
128
129 try {
130 $this->requiredCustomFieldValidator->validate($data, $form);
131 } catch (\Exception $e) {
132 throw new UnexpectedValueException($e->getMessage());
133 }
134
135 $segmentIds = $this->getSegmentIds($form, $data['segments'] ?? []);
136
137 // Keep `segments` in $data until after CAPTCHA validation so that, if the
138 // behavioral-baseline path stashes the submission for a deferred challenge,
139 // the stash still carries the selected segments for the resubmit.
140 $meta = $this->validateCaptcha($captchaSettings, $data, $form);
141 if (isset($meta['error'])) {
142 return $meta;
143 }
144 unset($data['segments']);
145
146 $submittedTimeZone = SubscriberEntity::sanitizeTimeZone($data[SubscriberEntity::TIME_ZONE_FIELD_NAME] ?? null);
147
148 // only accept fields defined in the form
149 $formFieldIds = array_filter(array_map(function (array $formField): ?string {
150 if (!isset($formField['id'])) {
151 return null;
152 }
153 return is_numeric($formField['id']) ? "cf_{$formField['id']}" : $formField['id'];
154 }, $form->getBlocksByTypes(FormEntity::FORM_FIELD_TYPES)));
155 $data = array_intersect_key($data, array_flip($formFieldIds));
156 if ($submittedTimeZone !== null) {
157 $data[SubscriberEntity::TIME_ZONE_FIELD_NAME] = $submittedTimeZone;
158 }
159 // The consent checkbox posts a bare 1/0. Translate it and stamp the proof
160 // server-side, then drop the raw field and any client-supplied proof so a
161 // crafted post cannot set a consent state or forge the record of it.
162 $trackingConsentData = $this->getTrackingConsentData($data, $form);
163 unset(
164 $data[TrackingConsentCapture::FIELD_ID],
165 $data['tracking_consent_method'],
166 $data['tracking_consent_copy']
167 );
168 foreach ($trackingConsentData as $consentKey => $consentValue) {
169 $data[$consentKey] = $consentValue;
170 }
171
172 // make sure we don't allow too many subscriptions with the same ip address
173 $timeout = $this->throttling->throttle();
174
175 if ($timeout > 0) {
176 $timeToWait = $this->throttling->secondsToTimeString($timeout);
177 $meta['refresh_captcha'] = true;
178 // translators: %s is the amount of time the user has to wait.
179 $meta['error'] = sprintf(__('You need to wait %s before subscribing again.', 'mailpoet'), $timeToWait);
180 return $meta;
181 }
182
183 /**
184 * Fires before a subscription gets created.
185 * To interrupt the subscription process, you can throw an MailPoet\Exception.
186 * The error message will then be displayed to the user.
187 *
188 * @param array $data The subscription data.
189 * @param array $segmentIds The segment IDs the user gets subscribed to.
190 * @param FormEntity $form The form the user used to subscribe.
191 */
192 $this->wp->doAction('mailpoet_subscription_before_subscribe', $data, $segmentIds, $form);
193
194 [$subscriber, $subscriptionMeta] = $this->subscriberActions->subscribe($data, $segmentIds);
195
196 if (
197 isset($data['captcha_session_id']) && (
198 ($captchaSettings['type'] ?? null) === CaptchaConstants::TYPE_BUILTIN
199 || CaptchaConstants::isDisabled($captchaSettings['type'] ?? null)
200 )
201 ) {
202 // Captcha has been verified, invalidate the session vars
203 $this->captchaSession->reset($data['captcha_session_id']);
204 }
205
206 // record form statistics
207 $this->statisticsFormsRepository->record($form, $subscriber);
208
209 // add tags to subscriber if they are filled
210 $formSettings = $form->getSettings();
211 $this->addTagsToSubscriber($formSettings['tags'] ?? [], $subscriber);
212
213 // Confirmation email failed. We want to show the error message
214 if ($subscriptionMeta['confirmationEmailResult'] instanceof \Exception) {
215 $meta['error'] = $subscriptionMeta['confirmationEmailResult']->getMessage();
216 return $meta;
217 }
218 if (!empty($subscriptionMeta['error'])) {
219 $meta['error'] = $subscriptionMeta['error'];
220 return $meta;
221 }
222
223 $this->wp->doAction('mailpoet_subscription_after_subscribe', $subscriber, $data, $segmentIds, $form);
224
225 if (!empty($formSettings['on_success'])) {
226 if ($formSettings['on_success'] === 'page') {
227 // redirect to a page on a success, pass the page url in the meta
228 $meta['redirect_url'] = $this->wp->getPermalink($formSettings['success_page']);
229 } else if ($formSettings['on_success'] === 'url') {
230 $meta['redirect_url'] = $formSettings['success_url'];
231 }
232 }
233
234 return $meta;
235 }
236
237 /**
238 * Turns the form's tracking-consent checkbox into a consent record.
239 *
240 * The posted value is a bare 1/0, so it is translated here and the proof of
241 * how and against what wording consent was given is stamped server-side —
242 * the same shape Manage.php uses for the manage-subscription page. The raw
243 * field never reaches the subscriber data, so a crafted post cannot set a
244 * consent state directly.
245 *
246 * @param array<string, mixed> $data
247 * @return array<string, string>
248 */
249 private function getTrackingConsentData(array $data, FormEntity $form): array {
250 if (!array_key_exists(TrackingConsentCapture::FIELD_ID, $data)) {
251 return [];
252 }
253
254 $email = $data['email'] ?? null;
255 $method = SubscriberEntity::TRACKING_CONSENT_METHOD_FORM;
256
257 return $this->trackingConsentCapture->getConsentData(
258 (bool)$data[TrackingConsentCapture::FIELD_ID],
259 $method,
260 $this->trackingConsentCapture->getCopy($method, $this->getFormConsentCopy($form)),
261 $this->trackingConsentCapture->isNewSubscriber(is_string($email) ? $email : null)
262 );
263 }
264
265 /**
266 * The wording the form actually shows next to its consent checkbox, so the
267 * stored proof matches what the subscriber read.
268 */
269 private function getFormConsentCopy(FormEntity $form): ?string {
270 foreach ($form->getBlocksByTypes([FormEntity::CHECKBOX_BLOCK_TYPE]) as $block) {
271 if (($block['id'] ?? null) !== TrackingConsentCapture::FIELD_ID) {
272 continue;
273 }
274 $value = $block['params']['values'][0]['value'] ?? null;
275 return is_string($value) ? $value : null;
276 }
277 return null;
278 }
279
280 /**
281 * Checks if the subscriber is subscribed to any segments in the form
282 *
283 * @param FormEntity $form The form entity
284 * @param SubscriberEntity $subscriber The subscriber entity
285 * @return bool True if the subscriber is subscribed to any of the segments in the form
286 */
287 public function isSubscribedToAnyFormSegments(FormEntity $form, SubscriberEntity $subscriber): bool {
288 $formSegments = array_merge($form->getSegmentBlocksSegmentIds(), $form->getSettingsSegmentIds());
289
290 $subscribersFound = $this->subscribersFinder->findSubscribersInSegments([$subscriber->getId()], $formSegments);
291 if (!empty($subscribersFound)) return true;
292
293 return false;
294 }
295
296 private function deobfuscateFormPayload($data): array {
297 return $this->fieldNameObfuscator->deobfuscateFormPayload($data);
298 }
299
300 private function initCaptcha(?array $captchaSettings, FormEntity $form, array $data): array {
301 $type = $captchaSettings['type'] ?? null;
302
303 if ($type === CaptchaConstants::TYPE_BUILTIN) {
304 // When serving the built-in CAPTCHA for the first time, generate a new session ID.
305 if (!isset($data['captcha_session_id'])) {
306 $data['captcha_session_id'] = $this->captchaSession->generateSessionId();
307 }
308 $sessionId = $data['captcha_session_id'];
309
310 if (!isset($data['captcha'])) {
311 // Save form data to session
312 $this->captchaSession->setFormData($sessionId, array_merge($data, ['form_id' => $form->getId()]));
313 } elseif ($this->captchaSession->getFormData($sessionId)) {
314 // Restore form data from session, but keep the current request's captcha
315 // and behavioral signals so the resubmit reflects accumulated interaction
316 // rather than the (possibly bot-like) snapshot from the first submit.
317 $preserve = ['captcha' => $data['captcha']];
318 if (isset($data[BehavioralSignals::FIELD_NAME])) {
319 $preserve[BehavioralSignals::FIELD_NAME] = $data[BehavioralSignals::FIELD_NAME];
320 }
321 $data = array_merge($this->captchaSession->getFormData($sessionId), $preserve);
322 }
323 return $data;
324 }
325
326 // Disabled with behavioral baseline: restore stashed form data on resubmit
327 // (after a previous behavioral escalation). The first submit stashes inside
328 // the escalation path; here we only handle the restore side.
329 if (
330 CaptchaConstants::isDisabled($type)
331 && isset($data['captcha_session_id'], $data['captcha'])
332 ) {
333 $stashed = $this->captchaSession->getFormData($data['captcha_session_id']);
334 if (is_array($stashed)) {
335 // Keep the current request's behavioral signals over the stash so the
336 // resubmit's signal check reflects accumulated interaction, not the
337 // (possibly bot-like) snapshot that triggered the original challenge.
338 $preserve = [
339 'captcha' => $data['captcha'],
340 'captcha_session_id' => $data['captcha_session_id'],
341 ];
342 if (isset($data[BehavioralSignals::FIELD_NAME])) {
343 $preserve[BehavioralSignals::FIELD_NAME] = $data[BehavioralSignals::FIELD_NAME];
344 }
345 $data = array_merge($stashed, $preserve);
346 }
347 }
348
349 return $data;
350 }
351
352 private function validateCaptcha($captchaSettings, $data, FormEntity $form): array {
353 $type = $captchaSettings['type'] ?? null;
354 try {
355 if (CaptchaConstants::isDisabled($type)) {
356 $this->enforceBehavioralBaseline($data, $form);
357 return [];
358 }
359 if ($type === CaptchaConstants::TYPE_BUILTIN) {
360 $this->builtInCaptchaValidator->validate($data);
361 $this->requireHumanSignals($data, $form);
362 }
363 if (CaptchaConstants::isReCaptcha($type)) {
364 $this->recaptchaValidator->validate($data);
365 }
366 if (CaptchaConstants::isTurnstile($type)) {
367 $this->turnstileValidator->validate($data);
368 }
369 } catch (ValidationError $error) {
370 return $error->getMeta();
371 }
372 return [];
373 }
374
375 /**
376 * Baseline protection when no CAPTCHA is configured: behavioral signals must
377 * look human, otherwise escalate to the built-in CAPTCHA inline challenge.
378 * isRequired()'s IP-history heuristic is intentionally bypassed here — the
379 * decision is made on per-submission signals, not on the IP's CAPTCHA history.
380 * On resubmit (after a previous escalation), signals are re-checked so that
381 * solving the CAPTCHA alone isn't enough to bypass the baseline.
382 */
383 private function enforceBehavioralBaseline(array $data, FormEntity $form): void {
384 if (!empty($data['captcha_session_id'])) {
385 $this->builtInCaptchaValidator->validateChallenge($data);
386 }
387 $this->requireHumanSignals($data, $form);
388 }
389
390 /**
391 * Throws a fresh CAPTCHA challenge unless behavioral signals look human.
392 * Admin/editor exempt. The suspect signals are dropped from the stash so the
393 * resubmit is evaluated on the current request's freshest counters (via
394 * initCaptcha's preserve step).
395 */
396 private function requireHumanSignals(array $data, FormEntity $form): void {
397 if ($this->builtInCaptchaValidator->isUserExemptFromCaptcha()) {
398 return;
399 }
400 if ($this->behavioralSignals->looksHuman($data)) {
401 return;
402 }
403 $stash = array_merge($data, ['form_id' => $form->getId()]);
404 unset($stash[BehavioralSignals::FIELD_NAME]);
405 $challenge = $this->builtInCaptchaValidator->getInlineCaptchaChallenge($stash);
406 throw new ValidationError(__('Please fill in the CAPTCHA.', 'mailpoet'), $challenge);
407 }
408
409 private function getSegmentIds(FormEntity $form, array $segmentIds): array {
410
411 // If form contains segment selection blocks allow only segments ids configured in those blocks
412 $segmentBlocksSegmentIds = $form->getSegmentBlocksSegmentIds();
413 if (!empty($segmentBlocksSegmentIds)) {
414 $segmentIds = array_intersect($segmentIds, $segmentBlocksSegmentIds);
415 } else {
416 $segmentIds = $form->getSettingsSegmentIds();
417 }
418
419 if (empty($segmentIds)) {
420 throw new UnexpectedValueException(__('Please select a list.', 'mailpoet'));
421 }
422
423 return $segmentIds;
424 }
425
426 private function getForm(array $data): FormEntity {
427 $formId = (isset($data['form_id']) ? (int)$data['form_id'] : false);
428 $form = $this->formsRepository->findOneById($formId);
429
430 if (!$form) {
431 throw new NotFoundException(__('Please specify a valid form ID.', 'mailpoet'));
432 }
433
434 return $form;
435 }
436
437 /**
438 * @param string[] $tagNames
439 */
440 private function addTagsToSubscriber(array $tagNames, SubscriberEntity $subscriber): void {
441 foreach ($tagNames as $tagName) {
442 $tag = $this->tagRepository->createOrUpdate(['name' => $tagName]);
443
444 $subscriberTag = $subscriber->getSubscriberTag($tag);
445 if (!$subscriberTag) {
446 $subscriberTag = new SubscriberTagEntity($tag, $subscriber);
447 $subscriber->getSubscriberTags()->add($subscriberTag);
448 $this->subscriberTagRepository->persist($subscriberTag);
449 $this->subscriberTagRepository->flush();
450 $this->wp->doAction('mailpoet_subscriber_tag_added', $subscriberTag);
451 }
452 }
453 }
454 }
455