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 / Captcha / Validator / CaptchaValidator.php
mailpoet / lib / Captcha / Validator Last commit date
CaptchaValidator.php 1 month ago RecaptchaValidator.php 2 months ago TurnstileValidator.php 2 months ago ValidationError.php 2 months ago index.php 1 year ago
CaptchaValidator.php
186 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Captcha\Validator;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Captcha\CaptchaPhrase;
9 use MailPoet\Captcha\CaptchaSession;
10 use MailPoet\Captcha\CaptchaUrlFactory;
11 use MailPoet\Subscribers\SubscriberIPsRepository;
12 use MailPoet\Subscribers\SubscribersRepository;
13 use MailPoet\Util\Helpers;
14 use MailPoet\WP\Functions as WPFunctions;
15
16 class CaptchaValidator {
17 /** @var CaptchaUrlFactory */
18 private $captchaUrlFactory;
19
20 /** @var CaptchaPhrase */
21 private $captchaPhrase;
22
23 /** @var WPFunctions */
24 private $wp;
25
26 /** @var SubscriberIPsRepository */
27 private $subscriberIPsRepository;
28
29 /** @var SubscribersRepository */
30 private $subscribersRepository;
31
32 /** @var CaptchaSession */
33 private $captchaSession;
34
35 public function __construct(
36 CaptchaUrlFactory $urlFactory,
37 CaptchaPhrase $captchaPhrase,
38 WPFunctions $wp,
39 SubscriberIPsRepository $subscriberIPsRepository,
40 SubscribersRepository $subscribersRepository,
41 CaptchaSession $captchaSession
42 ) {
43 $this->captchaUrlFactory = $urlFactory;
44 $this->captchaPhrase = $captchaPhrase;
45 $this->wp = $wp;
46 $this->subscriberIPsRepository = $subscriberIPsRepository;
47 $this->subscribersRepository = $subscribersRepository;
48 $this->captchaSession = $captchaSession;
49 }
50
51 public function validate(array $data): bool {
52 $isBuiltinCaptchaRequired = $this->isRequired(isset($data['email']) ? $data['email'] : null);
53 if (!$isBuiltinCaptchaRequired) {
54 return true;
55 }
56 return $this->validateChallenge($data);
57 }
58
59 /**
60 * Validates the user's CAPTCHA answer without consulting isRequired().
61 * Use this when the caller has already decided a challenge is needed
62 * (e.g. the behavioral-signal escalation path).
63 */
64 public function validateChallenge(array $data): bool {
65 // session ID must be set at this point
66 $sessionId = $data['captcha_session_id'] ?? null;
67 if (!$sessionId) {
68 throw new ValidationError(__('CAPTCHA verification failed.', 'mailpoet'));
69 }
70
71 if (empty($data['captcha'])) {
72 $this->captchaPhrase->createPhrase($sessionId);
73 throw new ValidationError(
74 __('Please fill in the CAPTCHA.', 'mailpoet'),
75 $this->getInlineCaptchaData($sessionId)
76 );
77 }
78
79 $captchaHash = $this->captchaPhrase->getPhrase($sessionId);
80 if (empty($captchaHash)) {
81 $this->captchaPhrase->createPhrase($sessionId);
82 throw new ValidationError(
83 __('Please regenerate the CAPTCHA.', 'mailpoet'),
84 $this->getInlineCaptchaData($sessionId)
85 );
86 }
87
88 if (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) {
89 $this->captchaPhrase->createPhrase($sessionId);
90 throw new ValidationError(
91 __('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'),
92 [
93 'refresh_captcha' => true,
94 // Keep redirect_url for non-JavaScript form submissions
95 'redirect_url' => $this->captchaUrlFactory->getCaptchaUrlForMPForm($sessionId),
96 ]
97 );
98 }
99
100 return true;
101 }
102
103 /**
104 * Creates a fresh inline CAPTCHA challenge: generates a session, primes a
105 * phrase, and returns the meta the client uses to render it inline.
106 * Optionally stashes form data so the non-JS captcha-page fallback can
107 * restore the original submission on resubmit.
108 *
109 * @param array<string, mixed>|null $formData Form data to stash, or null to skip stashing.
110 */
111 public function getInlineCaptchaChallenge(?array $formData = null): array {
112 $sessionId = $this->captchaSession->generateSessionId();
113 $this->captchaPhrase->createPhrase($sessionId);
114 if ($formData !== null) {
115 $this->captchaSession->setFormData($sessionId, $formData);
116 }
117 return $this->getInlineCaptchaData($sessionId);
118 }
119
120 public function isRequired($subscriberEmail = null) {
121 if ($this->isUserExemptFromCaptcha()) {
122 return false;
123 }
124
125 $subscriptionCaptchaRecipientLimit = $this->wp->applyFilters('mailpoet_subscription_captcha_recipient_limit', 0);
126 if ($subscriptionCaptchaRecipientLimit === 0) {
127 return true;
128 }
129
130 // Check limits per recipient if enabled
131 if ($subscriberEmail) {
132 $subscriber = $this->subscribersRepository->findOneBy(['email' => $subscriberEmail]);
133 if (
134 $subscriber && $subscriber->getConfirmationsCount() >= $subscriptionCaptchaRecipientLimit
135 ) {
136 return true;
137 }
138 }
139
140 // Check limits per IP address
141 /** @var int|string $subscriptionCaptchaWindow */
142 $subscriptionCaptchaWindow = $this->wp->applyFilters('mailpoet_subscription_captcha_window', MONTH_IN_SECONDS);
143
144 $subscriberIp = Helpers::getIP();
145 if (empty($subscriberIp)) {
146 return false;
147 }
148
149 $subscriptionCount = $this->subscriberIPsRepository->getCountByIPAndCreatedAtAfterTimeInSeconds(
150 $subscriberIp,
151 (int)$subscriptionCaptchaWindow
152 );
153
154 if ($subscriptionCount > 0) {
155 return true;
156 }
157
158 return false;
159 }
160
161 public function isUserExemptFromCaptcha(): bool {
162 if (!$this->wp->isUserLoggedIn()) {
163 return false;
164 }
165 $user = $this->wp->wpGetCurrentUser();
166 $roles = $this->wp->applyFilters('mailpoet_subscription_captcha_exclude_roles', ['administrator', 'editor']);
167 $stringRoles = is_array($roles) ? array_values(array_filter($roles, 'is_string')) : [];
168 return !empty(array_intersect($stringRoles, $user->roles));
169 }
170
171 /**
172 * Returns data needed to display the captcha inline within the form.
173 * Includes redirect_url as a fallback for non-JavaScript submissions.
174 */
175 private function getInlineCaptchaData(string $sessionId): array {
176 return [
177 'show_captcha' => true,
178 'captcha_session_id' => $sessionId,
179 'captcha_image_url' => $this->captchaUrlFactory->getCaptchaImageUrl($sessionId),
180 'captcha_audio_url' => $this->captchaUrlFactory->getCaptchaAudioUrl($sessionId),
181 // Keep redirect_url for non-JavaScript form submissions
182 'redirect_url' => $this->captchaUrlFactory->getCaptchaUrlForMPForm($sessionId),
183 ];
184 }
185 }
186