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