<?php
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\Track\Unsubscribes;
use MailPoet\Subscribers\ConfirmationEmailMailer;
use MailPoet\Subscribers\Source;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class Subscription {
const CHECKOUT_OPTIN_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin';
const CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin_present';
const OPTIN_ENABLED_SETTING_NAME = 'woocommerce.optin_on_checkout.enabled';
const OPTIN_SEGMENTS_SETTING_NAME = 'woocommerce.optin_on_checkout.segments';
const OPTIN_MESSAGE_SETTING_NAME = 'woocommerce.optin_on_checkout.message';
private $allowedHtml = [
'input' => [
'type' => true,
'name' => true,
'id' => true,
'class' => true,
'value' => true,
'checked' => true,
],
'span' => [
'class' => true,
],
'label' => [
'class' => true,
'data-automation-id' => true,
'for' => true,
],
'p' => [
'class' => true,
'id' => true,
'data-priority' => true,
],
];
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
/** @var Helper */
private $wcHelper;
/** @var ConfirmationEmailMailer */
private $confirmationEmailMailer;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var Unsubscribes */
private $unsubscribesTracker;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscriberSegmentRepository */
private $subscriberSegmentRepository;
public function __construct(
SettingsController $settings,
ConfirmationEmailMailer $confirmationEmailMailer,
WPFunctions $wp,
Helper $wcHelper,
SubscribersRepository $subscribersRepository,
Unsubscribes $unsubscribesTracker,
SegmentsRepository $segmentsRepository,
SubscriberSegmentRepository $subscriberSegmentRepository
) {
$this->settings = $settings;
$this->wp = $wp;
$this->wcHelper = $wcHelper;
$this->confirmationEmailMailer = $confirmationEmailMailer;
$this->subscribersRepository = $subscribersRepository;
$this->unsubscribesTracker = $unsubscribesTracker;
$this->segmentsRepository = $segmentsRepository;
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
}
public function extendWooCommerceCheckoutForm() {
$inputName = self::CHECKOUT_OPTIN_INPUT_NAME;
$checked = $this->isCurrentUserSubscribed();
if (!empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME])) {
$checked = true;
}
$labelString = $this->settings->get(self::OPTIN_MESSAGE_SETTING_NAME);
$template = (string)$this->wp->applyFilters(
'mailpoet_woocommerce_checkout_optin_template',
wp_kses(
$this->getSubscriptionField($inputName, $checked, $labelString),
$this->allowedHtml
),
$inputName,
$checked,
$labelString
);
// The template has been sanitized above and can be considered safe.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPressDotOrg.sniffs.OutputEscaping.UnescapedOutputParameter
echo $template;
if ($template) {
$field = $this->getSubscriptionPresenceCheckField();
echo wp_kses($field, $this->allowedHtml);
}
}
private function getSubscriptionField($inputName, $checked, $labelString) {
return $this->wcHelper->woocommerceFormField(
$this->wp->escAttr($inputName),
[
'type' => 'checkbox',
'label' => $this->wp->escHtml($labelString),
'input_class' => ['woocommerce-form__input', 'woocommerce-form__input-checkbox', 'input-checkbox'],
'label_class' => ['woocommerce-form__label', 'woocommerce-form__label-for-checkbox', 'checkbox'],
'custom_attributes' => ['data-automation-id' => 'woo-commerce-subscription-opt-in'],
'return' => true,
],
$checked ? '1' : '0'
);
}
private function getSubscriptionPresenceCheckField() {
$field = $this->wcHelper->woocommerceFormField(
self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
[
'type' => 'hidden',
'return' => true,
],
1
);
if ($field) {
return $field;
}
// Workaround for older WooCommerce versions (below 4.6.0) that don't support hidden fields
// We can remove it after we drop support of older WooCommerce
$field = $this->wcHelper->woocommerceFormField(
self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
[
'type' => 'text',
'return' => true,
],
1
);
return str_replace('type="text"', 'type="hidden"', $field);
}
public function isCurrentUserSubscribed() {
$subscriber = $this->subscribersRepository->getCurrentWPUser();
if (!$subscriber instanceof SubscriberEntity) {
return false;
}
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy(
['subscriber' => $subscriber->getId(), 'segment' => $wcSegment->getId()]
);
return $subscriberSegment instanceof SubscriberSegmentEntity
&& $subscriberSegment->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED;
}
public function subscribeOnOrderPay($orderId) {
$wcOrder = $this->wcHelper->wcGetOrder($orderId);
if (!$wcOrder instanceof \WC_Order) {
return null;
}
$data['billing_email'] = $wcOrder->get_billing_email();
$this->subscribeOnCheckout($orderId, $data);
}
public function subscribeOnCheckout($orderId, $data) {
if (empty($data['billing_email'])) {
// no email in posted order data
return null;
}
$subscriber = $this->subscribersRepository->findOneBy(
['email' => $data['billing_email'], 'isWoocommerceUser' => 1]
);
if (!$subscriber) {
// no subscriber: WooCommerce sync didn't work
return null;
}
$checkoutOptinEnabled = (bool)$this->settings->get(self::OPTIN_ENABLED_SETTING_NAME);
$checkoutOptin = !empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME]);
return $this->handleSubscriberOptin($subscriber, $checkoutOptinEnabled, $checkoutOptin);
}
/**
* Subscribe or unsubscribe a subscriber.
*
* @param SubscriberEntity $subscriber Subscriber object
* @param bool $checkoutOptinEnabled
* @param bool $checkoutOptin
*/
public function handleSubscriberOptin(SubscriberEntity $subscriber, bool $checkoutOptinEnabled, bool $checkoutOptin): bool {
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
$segmentIds = (array)$this->settings->get(self::OPTIN_SEGMENTS_SETTING_NAME, []);
$moreSegmentsToSubscribe = [];
if (!empty($segmentIds)) {
$moreSegmentsToSubscribe = $this->segmentsRepository->findBy(['id' => $segmentIds]);
}
$signupConfirmation = $this->settings->get('signup_confirmation');
if (!$checkoutOptin) {
// Opt-in is disabled or checkbox is unchecked
$this->subscriberSegmentRepository->unsubscribeFromSegments($subscriber, [$wcSegment]);
// Unsubscribe from configured segment only when opt-in is enabled
if ($checkoutOptinEnabled && $moreSegmentsToSubscribe) {
$this->subscriberSegmentRepository->unsubscribeFromSegments($subscriber, $moreSegmentsToSubscribe);
}
// Update global status only in case the opt-in is enabled
if ($checkoutOptinEnabled) {
$this->updateSubscriberStatus($subscriber);
}
return false;
}
$subscriber->setSource(Source::WOOCOMMERCE_CHECKOUT);
if (
($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED)
|| ((bool)$signupConfirmation['enabled'] === false)
) {
$this->subscribe($subscriber);
} else {
$this->requireSubscriptionConfirmation($subscriber);
}
$this->subscriberSegmentRepository->subscribeToSegments($subscriber, array_merge([$wcSegment], $moreSegmentsToSubscribe));
return true;
}
private function subscribe(SubscriberEntity $subscriber) {
$subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
if (empty($subscriber->getConfirmedIp()) && empty($subscriber->getConfirmedAt())) {
$subscriber->setConfirmedIp(Helpers::getIP());
$subscriber->setConfirmedAt(new Carbon());
}
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
}
private function requireSubscriptionConfirmation(SubscriberEntity $subscriber) {
$subscriber->setStatus(SubscriberEntity::STATUS_UNCONFIRMED);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
try {
$this->confirmationEmailMailer->sendConfirmationEmailOnce($subscriber);
} catch (\Exception $e) {
// ignore errors
}
}
private function updateSubscriberStatus(SubscriberEntity $subscriber) {
$segmentsCount = $subscriber->getSubscriberSegments(SubscriberEntity::STATUS_SUBSCRIBED)->count();
if (!$segmentsCount) {
$subscriber->setStatus(SubscriberEntity::STATUS_UNSUBSCRIBED);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
$this->unsubscribesTracker->track((int)$subscriber->getId(), StatisticsUnsubscribeEntity::SOURCE_ORDER_CHECKOUT);
}
}
}