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 / WooCommerce / Subscription.php
mailpoet / lib / WooCommerce Last commit date
Emails 3 months ago Integrations 3 months ago MultichannelMarketing 2 years ago TransactionalEmails 10 months ago WooCommerceBookings 1 month ago WooCommerceSubscriptions 3 months ago CouponPreProcessor.php 3 months ago Emails.php 9 months ago Helper.php 2 months ago MailPoetTask.php 2 years ago NonPersistablePreviewData.php 1 month ago OrderAttributionFields.php 1 month ago OrderAttributionRevenueReader.php 1 month ago OrderAttributionWriter.php 1 month ago RandomCouponCodeGenerator.php 3 months ago Settings.php 1 year ago SubscriberEngagement.php 3 years ago Subscription.php 21 hours ago Tracker.php 2 years ago TransactionalEmailHooks.php 1 year ago TransactionalEmails.php 1 year ago WooSystemInfo.php 2 months ago WooSystemInfoController.php 1 year ago index.php 3 years ago
Subscription.php
328 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\WooCommerce;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\SubscriberEntity;
9 use MailPoet\Segments\SegmentsRepository;
10 use MailPoet\Settings\SettingsController;
11 use MailPoet\Subscribers\ConfirmationEmailMailer;
12 use MailPoet\Subscribers\Source;
13 use MailPoet\Subscribers\SubscriberSegmentRepository;
14 use MailPoet\Subscribers\SubscribersRepository;
15 use MailPoet\Subscribers\TrackingConsentCapture;
16 use MailPoet\Util\Helpers;
17 use MailPoet\WP\Functions as WPFunctions;
18 use MailPoetVendor\Carbon\Carbon;
19
20 class Subscription {
21 const CHECKOUT_OPTIN_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin';
22 const CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin_present';
23 const CHECKOUT_TRACKING_CONSENT_INPUT_NAME = 'mailpoet_woocommerce_checkout_tracking_consent';
24 const OPTIN_ENABLED_SETTING_NAME = 'woocommerce.optin_on_checkout.enabled';
25 const OPTIN_SEGMENTS_SETTING_NAME = 'woocommerce.optin_on_checkout.segments';
26 const OPTIN_MESSAGE_SETTING_NAME = 'woocommerce.optin_on_checkout.message';
27 const OPTIN_POSITION_SETTING_NAME = 'woocommerce.optin_on_checkout.position';
28
29 private $allowedHtml = [
30 'input' => [
31 'type' => true,
32 'name' => true,
33 'id' => true,
34 'class' => true,
35 'value' => true,
36 'checked' => true,
37 ],
38 'span' => [
39 'class' => true,
40 ],
41 'label' => [
42 'class' => true,
43 'data-automation-id' => true,
44 'for' => true,
45 ],
46 'p' => [
47 'class' => true,
48 'id' => true,
49 'data-priority' => true,
50 ],
51 ];
52
53 /** @var SettingsController */
54 private $settings;
55
56 /** @var WPFunctions */
57 private $wp;
58
59 /** @var Helper */
60 private $wcHelper;
61
62 /** @var ConfirmationEmailMailer */
63 private $confirmationEmailMailer;
64
65 /** @var SubscribersRepository */
66 private $subscribersRepository;
67
68 /** @var SegmentsRepository */
69 private $segmentsRepository;
70
71 /** @var SubscriberSegmentRepository */
72 private $subscriberSegmentRepository;
73
74 /** @var TrackingConsentCapture */
75 private $trackingConsentCapture;
76
77 public function __construct(
78 SettingsController $settings,
79 ConfirmationEmailMailer $confirmationEmailMailer,
80 WPFunctions $wp,
81 Helper $wcHelper,
82 SubscribersRepository $subscribersRepository,
83 SegmentsRepository $segmentsRepository,
84 SubscriberSegmentRepository $subscriberSegmentRepository,
85 TrackingConsentCapture $trackingConsentCapture
86 ) {
87 $this->settings = $settings;
88 $this->wp = $wp;
89 $this->wcHelper = $wcHelper;
90 $this->confirmationEmailMailer = $confirmationEmailMailer;
91 $this->subscribersRepository = $subscribersRepository;
92 $this->segmentsRepository = $segmentsRepository;
93 $this->subscriberSegmentRepository = $subscriberSegmentRepository;
94 $this->trackingConsentCapture = $trackingConsentCapture;
95 }
96
97 public function extendWooCommerceCheckoutForm() {
98 $inputName = self::CHECKOUT_OPTIN_INPUT_NAME;
99 $checked = false;
100 if (!empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME])) {
101 $checked = true;
102 }
103 $labelString = $this->settings->get(self::OPTIN_MESSAGE_SETTING_NAME);
104 $defaultTemplate = wp_kses(
105 $this->getSubscriptionField($inputName, $checked, $labelString),
106 $this->allowedHtml
107 );
108 $filtered = $this->wp->applyFilters(
109 'mailpoet_woocommerce_checkout_optin_template',
110 $defaultTemplate,
111 $inputName,
112 $checked,
113 $labelString
114 );
115 $template = is_string($filtered) ? $filtered : $defaultTemplate;
116 // The template has been sanitized above and can be considered safe.
117 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
118 echo $template;
119 if ($template) {
120 $field = $this->getSubscriptionPresenceCheckField();
121 echo wp_kses($field, $this->allowedHtml);
122 // A second, independent control. Consent to open and click tracking can
123 // never be bundled with the marketing opt-in above it.
124 echo wp_kses($this->getTrackingConsentField(), $this->allowedHtml);
125 }
126 }
127
128 /**
129 * The tracking-consent checkbox, shown only on sites that chose to ask. It is
130 * never pre-ticked: a pre-ticked consent box is not valid consent (CJEU
131 * Planet49).
132 */
133 private function getTrackingConsentField(): string {
134 if (!$this->trackingConsentCapture->isCaptureEnabled()) {
135 return '';
136 }
137 $inputName = self::CHECKOUT_TRACKING_CONSENT_INPUT_NAME;
138 $copy = $this->trackingConsentCapture->getCopy(
139 SubscriberEntity::TRACKING_CONSENT_METHOD_WOOCOMMERCE_CHECKOUT
140 );
141
142 return '<label class="woocommerce-form__label woocommerce-form__label-for-checkbox checkbox" data-automation-id="woo-commerce-tracking-consent">
143 <input id="' . $this->wp->escAttr($inputName) . '" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" type="checkbox" name="' . $this->wp->escAttr($inputName) . '" value="1" />
144 <span>' . $this->wp->escHtml($copy) . '</span>
145 </label>';
146 }
147
148 private function getSubscriptionField($inputName, $checked, $labelString) {
149 $checked = checked($checked, true, false);
150
151 return '<label class="woocommerce-form__label woocommerce-form__label-for-checkbox checkbox" data-automation-id="woo-commerce-subscription-opt-in">
152 <input id="mailpoet_woocommerce_checkout_optin" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" ' . $checked . ' type="checkbox" name="' . $this->wp->escAttr($inputName) . '" value="1" />
153 <span>' . $this->wp->escHtml($labelString) . '</span>
154 </label>';
155 }
156
157 private function getSubscriptionPresenceCheckField() {
158 $field = $this->wcHelper->woocommerceFormField(
159 self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
160 [
161 'type' => 'hidden',
162 'return' => true,
163 ],
164 1
165 );
166 if ($field) {
167 return $field;
168 }
169 // Workaround for older WooCommerce versions (below 4.6.0) that don't support hidden fields
170 // We can remove it after we drop support of older WooCommerce
171 $field = $this->wcHelper->woocommerceFormField(
172 self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
173 [
174 'type' => 'text',
175 'return' => true,
176 ],
177 1
178 );
179 return str_replace('type="text"', 'type="hidden"', $field);
180 }
181
182 public function subscribeOnOrderPay($orderId) {
183 $wcOrder = $this->wcHelper->wcGetOrder($orderId);
184 if (!$wcOrder instanceof \WC_Order) {
185 return null;
186 }
187
188 $data['billing_email'] = $wcOrder->get_billing_email();
189 $this->subscribeOnCheckout($orderId, $data);
190 }
191
192 public function subscribeOnCheckout($orderId, $data) {
193 $this->triggerAutomateWooOptin();
194 if (empty($data['billing_email'])) {
195 // no email in posted order data
196 return null;
197 }
198
199 $subscriber = $this->subscribersRepository->findOneBy(
200 ['email' => $data['billing_email'], 'isWoocommerceUser' => 1]
201 );
202
203 if (!$subscriber) {
204 // no subscriber: WooCommerce sync didn't work
205 return null;
206 }
207
208 $checkoutOptin = !empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME]);
209 $trackingConsent = !empty($_POST[self::CHECKOUT_TRACKING_CONSENT_INPUT_NAME]);
210
211 return $this->handleSubscriberOptin($subscriber, $checkoutOptin, $trackingConsent);
212 }
213
214 /**
215 * Subscribe a subscriber.
216 *
217 * @param SubscriberEntity $subscriber Subscriber object
218 * @param bool $shouldSubscribe Whether the subscriber should be subscribed
219 * @param bool $trackingConsent Whether the separate tracking-consent box was ticked
220 */
221 public function handleSubscriberOptin(SubscriberEntity $subscriber, bool $shouldSubscribe, bool $trackingConsent = false): bool {
222 // Recorded before the opt-in branch, and independently of it: consenting to
223 // tracking and subscribing are two separate decisions, so a customer who
224 // declines the newsletter can still allow tracking and vice versa.
225 $this->applyTrackingConsent($subscriber, $trackingConsent);
226
227 $wcSegment = $this->segmentsRepository->getWooCommerceSegment();
228
229 $segmentIds = (array)$this->settings->get(self::OPTIN_SEGMENTS_SETTING_NAME, []);
230 $moreSegmentsToSubscribe = [];
231 if (!empty($segmentIds)) {
232 $moreSegmentsToSubscribe = $this->segmentsRepository->findByIds($segmentIds);
233 }
234 $signupConfirmation = $this->settings->get('signup_confirmation');
235
236 if ($shouldSubscribe) {
237 $subscriber->setSource(Source::WOOCOMMERCE_CHECKOUT);
238
239 if (
240 ($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED)
241 || ((bool)$signupConfirmation['enabled'] === false)
242 ) {
243 $this->subscribe($subscriber);
244 } else {
245 $this->requireSubscriptionConfirmation($subscriber);
246 }
247
248 $this->subscriberSegmentRepository->subscribeToSegments($subscriber, array_merge([$wcSegment], $moreSegmentsToSubscribe));
249
250 return true;
251 } else {
252 return false;
253 }
254 }
255
256 /**
257 * Checkout always acts on a subscriber row that already exists (the
258 * WooCommerce customer sync creates it), so an unticked box leaves an earlier
259 * choice alone rather than revoking it. Persisted here because this path
260 * writes the entity itself instead of going through SubscriberSaveController.
261 */
262 private function applyTrackingConsent(SubscriberEntity $subscriber, bool $granted): void {
263 $method = SubscriberEntity::TRACKING_CONSENT_METHOD_WOOCOMMERCE_CHECKOUT;
264 $before = $subscriber->getTrackingConsent();
265
266 $this->trackingConsentCapture->applyToSubscriber(
267 $subscriber,
268 $granted,
269 $method,
270 $this->trackingConsentCapture->getCopy($method),
271 false
272 );
273
274 if ($subscriber->getTrackingConsent() !== $before) {
275 $this->subscribersRepository->persist($subscriber);
276 $this->subscribersRepository->flush();
277 }
278 }
279
280 public function hideAutomateWooOptinCheckbox(): void {
281 if (!$this->wp->isPluginActive('automatewoo/automatewoo.php')) {
282 return;
283 }
284 // Hide AutomateWoo checkout opt-in so we won't end up with two opt-ins
285 $this->wp->removeAction(
286 'woocommerce_checkout_after_terms_and_conditions',
287 ['AutomateWoo\Frontend', 'output_checkout_optin_checkbox']
288 );
289 }
290
291 private function triggerAutomateWooOptin(): void {
292 if (
293 !$this->wp->isPluginActive('automatewoo/automatewoo.php')
294 || empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME])
295 ) {
296 return;
297 }
298 // Emulate checkout opt-in triggering for AutomateWoo
299 $_POST['automatewoo_optin'] = 'On';
300 }
301
302 private function subscribe(SubscriberEntity $subscriber) {
303 $subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
304 if (empty($subscriber->getConfirmedIp()) && empty($subscriber->getConfirmedAt())) {
305 $subscriber->setConfirmedIp(Helpers::getIP());
306 $subscriber->setConfirmedAt(new Carbon());
307 }
308
309 $this->subscribersRepository->persist($subscriber);
310 $this->subscribersRepository->flush();
311 }
312
313 private function requireSubscriptionConfirmation(SubscriberEntity $subscriber) {
314 $subscriber->setStatus(SubscriberEntity::STATUS_UNCONFIRMED);
315 $this->subscribersRepository->persist($subscriber);
316 $this->subscribersRepository->flush();
317
318 try {
319 // Per-list confirmation settings are not resolved here because this path
320 // subscribes to the WooCommerce Customers segment (TYPE_WC_USERS),
321 // which does not support custom confirmation overrides.
322 $this->confirmationEmailMailer->sendConfirmationEmailOnce($subscriber, null, null, true);
323 } catch (\Exception $e) {
324 // ignore errors
325 }
326 }
327 }
328