diff --git a/lib/API/JSON/v1/Subscribers.php b/lib/API/JSON/v1/Subscribers.php index a4e45f1f55..7c9557c2fd 100644 --- a/lib/API/JSON/v1/Subscribers.php +++ b/lib/API/JSON/v1/Subscribers.php @@ -4,31 +4,20 @@ namespace MailPoet\API\JSON\v1; use MailPoet\API\JSON\Endpoint as APIEndpoint; use MailPoet\API\JSON\Error as APIError; -use MailPoet\API\JSON\Response as APIResponse; use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder; use MailPoet\Config\AccessControl; use MailPoet\Doctrine\Validator\ValidationException; -use MailPoet\Entities\FormEntity; use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SubscriberEntity; -use MailPoet\Form\FormsRepository; -use MailPoet\Form\Util\FieldNameObfuscator; +use MailPoet\Exception; use MailPoet\Listing; -use MailPoet\Models\Form; -use MailPoet\Models\StatisticsForms; use MailPoet\Models\Subscriber; use MailPoet\Segments\SegmentsRepository; -use MailPoet\Settings\SettingsController; use MailPoet\Subscribers\ConfirmationEmailMailer; -use MailPoet\Subscribers\RequiredCustomFieldValidator; -use MailPoet\Subscribers\SubscriberActions; use MailPoet\Subscribers\SubscriberListingRepository; use MailPoet\Subscribers\SubscriberSaveController; use MailPoet\Subscribers\SubscribersRepository; -use MailPoet\Subscription\Captcha; -use MailPoet\Subscription\CaptchaSession; -use MailPoet\Subscription\SubscriptionUrlFactory; -use MailPoet\Subscription\Throttling as SubscriptionThrottling; +use MailPoet\Subscribers\SubscriberSubscribeController; use MailPoet\UnexpectedValueException; use MailPoet\WP\Functions as WPFunctions; @@ -40,33 +29,12 @@ class Subscribers extends APIEndpoint { 'methods' => ['subscribe' => AccessControl::NO_ACCESS_RESTRICTION], ]; - /** @var SubscriberActions */ - private $subscriberActions; - - /** @var RequiredCustomFieldValidator */ - private $requiredCustomFieldValidator; - /** @var Listing\Handler */ private $listingHandler; - /** @var Captcha */ - private $subscriptionCaptcha; - - /** @var SettingsController */ - private $settings; - - /** @var CaptchaSession */ - private $captchaSession; - /** @var ConfirmationEmailMailer; */ private $confirmationEmailMailer; - /** @var SubscriptionUrlFactory */ - private $subscriptionUrlFactory; - - /** @var FieldNameObfuscator */ - private $fieldNameObfuscator; - /** @var SubscribersRepository */ private $subscribersRepository; @@ -79,44 +47,30 @@ class Subscribers extends APIEndpoint { /** @var SegmentsRepository */ private $segmentsRepository; - /** @var FormsRepository */ - private $formsRepository; - /** @var SubscriberSaveController */ private $saveController; + /** @var SubscriberSubscribeController */ + private $subscribeController; + public function __construct( - SubscriberActions $subscriberActions, - RequiredCustomFieldValidator $requiredCustomFieldValidator, Listing\Handler $listingHandler, - Captcha $subscriptionCaptcha, - SettingsController $settings, - CaptchaSession $captchaSession, ConfirmationEmailMailer $confirmationEmailMailer, - SubscriptionUrlFactory $subscriptionUrlFactory, SubscribersRepository $subscribersRepository, SubscribersResponseBuilder $subscribersResponseBuilder, SubscriberListingRepository $subscriberListingRepository, SegmentsRepository $segmentsRepository, - FieldNameObfuscator $fieldNameObfuscator, - FormsRepository $formsRepository, - SubscriberSaveController $saveController + SubscriberSaveController $saveController, + SubscriberSubscribeController $subscribeController ) { - $this->subscriberActions = $subscriberActions; - $this->requiredCustomFieldValidator = $requiredCustomFieldValidator; $this->listingHandler = $listingHandler; - $this->subscriptionCaptcha = $subscriptionCaptcha; - $this->settings = $settings; - $this->captchaSession = $captchaSession; $this->confirmationEmailMailer = $confirmationEmailMailer; - $this->subscriptionUrlFactory = $subscriptionUrlFactory; - $this->fieldNameObfuscator = $fieldNameObfuscator; $this->subscribersRepository = $subscribersRepository; $this->subscribersResponseBuilder = $subscribersResponseBuilder; $this->subscriberListingRepository = $subscriberListingRepository; $this->segmentsRepository = $segmentsRepository; - $this->formsRepository = $formsRepository; $this->saveController = $saveController; + $this->subscribeController = $subscribeController; } public function get($data = []) { @@ -167,180 +121,22 @@ class Subscribers extends APIEndpoint { } public function subscribe($data = []) { - $formId = (isset($data['form_id']) ? (int)$data['form_id'] : false); - $form = Form::findOne($formId); - $formEntity = $this->formsRepository->findOneById($formId); - unset($data['form_id']); - - if (!$form instanceof Form || !$formEntity instanceof FormEntity) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please specify a valid form ID.', 'mailpoet'), - ]); - } - if (!empty($data['email'])) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please leave the first field empty.', 'mailpoet'), - ]); - } - - $captchaSettings = $this->settings->get('captcha'); - - if (!empty($captchaSettings['type']) - && $captchaSettings['type'] === Captcha::TYPE_BUILTIN - ) { - $captchaSessionId = isset($data['captcha_session_id']) ? $data['captcha_session_id'] : null; - $this->captchaSession->init($captchaSessionId); - if (!isset($data['captcha'])) { - // Save form data to session - $this->captchaSession->setFormData(array_merge($data, ['form_id' => $formId])); - } elseif ($this->captchaSession->getFormData()) { - // Restore form data from session - $data = array_merge($this->captchaSession->getFormData(), ['captcha' => $data['captcha']]); - } - // Otherwise use the post data - } - - $data = $this->deobfuscateFormPayload($data); - try { - $this->requiredCustomFieldValidator->validate($data, $form); - } catch (\Exception $e) { - return $this->badRequest([APIError::BAD_REQUEST => $e->getMessage()]); + $meta = $this->subscribeController->subscribe($data); + } catch (Exception $exception) { + return $this->badRequest([$exception->getMessage()]); } - $segmentIds = (!empty($data['segments']) - ? (array)$data['segments'] - : [] + if (!empty($meta['error'])) { + $errorMessage = $meta['error']; + unset($meta['error']); + return $this->badRequest([APIError::BAD_REQUEST => $errorMessage], $meta); + } + + return $this->successResponse( + [], + $meta ); - $segmentIds = $this->getSegmentsForSubscription($formEntity, $segmentIds); - unset($data['segments']); - - if (empty($segmentIds)) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please select a list.', 'mailpoet'), - ]); - } - - $captchaValidationResult = $this->validateCaptcha($captchaSettings, $data); - if ($captchaValidationResult instanceof APIResponse) { - return $captchaValidationResult; - } - - // only accept fields defined in the form - $formFields = $form->getFieldList(); - $data = array_intersect_key($data, array_flip($formFields)); - - // make sure we don't allow too many subscriptions with the same ip address - $timeout = SubscriptionThrottling::throttle(); - - if ($timeout > 0) { - $timeToWait = SubscriptionThrottling::secondsToTimeString($timeout); - $meta = []; - $meta['refresh_captcha'] = true; - return $this->badRequest([ - APIError::BAD_REQUEST => sprintf(WPFunctions::get()->__('You need to wait %s before subscribing again.', 'mailpoet'), $timeToWait), - ], $meta); - } - - $subscriber = $this->subscriberActions->subscribe($data, $segmentIds); - $errors = $subscriber->getErrors(); - - if ($errors !== false) { - return $this->badRequest($errors); - } else { - if (!empty($captchaSettings['type']) && $captchaSettings['type'] === Captcha::TYPE_BUILTIN) { - // Captcha has been verified, invalidate the session vars - $this->captchaSession->reset(); - } - - $meta = []; - - if ($form !== false) { - // record form statistics - StatisticsForms::record($form->id, $subscriber->id); - - $form = $form->asArray(); - - if (!empty($form['settings']['on_success'])) { - if ($form['settings']['on_success'] === 'page') { - // redirect to a page on a success, pass the page url in the meta - $meta['redirect_url'] = WPFunctions::get()->getPermalink($form['settings']['success_page']); - } else if ($form['settings']['on_success'] === 'url') { - $meta['redirect_url'] = $form['settings']['success_url']; - } - } - } - - return $this->successResponse( - [], - $meta - ); - } - } - - private function deobfuscateFormPayload($data) { - return $this->fieldNameObfuscator->deobfuscateFormPayload($data); - } - - private function validateCaptcha($captchaSettings, $data) { - if (empty($captchaSettings['type'])) { - return true; - } - - $isBuiltinCaptchaRequired = false; - if ($captchaSettings['type'] === Captcha::TYPE_BUILTIN) { - $isBuiltinCaptchaRequired = $this->subscriptionCaptcha->isRequired(isset($data['email']) ? $data['email'] : ''); - if ($isBuiltinCaptchaRequired && empty($data['captcha'])) { - $meta = []; - $meta['redirect_url'] = $this->subscriptionUrlFactory->getCaptchaUrl($this->captchaSession->getId()); - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please fill in the CAPTCHA.', 'mailpoet'), - ], $meta); - } - } - - if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA && empty($data['recaptcha'])) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please check the CAPTCHA.', 'mailpoet'), - ]); - } - - if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA) { - $res = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha']; - $res = WPFunctions::get()->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [ - 'body' => [ - 'secret' => $captchaSettings['recaptcha_secret_token'], - 'response' => $res, - ], - ]); - if (is_wp_error($res)) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'), - ]); - } - $res = json_decode(wp_remote_retrieve_body($res)); - if (empty($res->success)) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'), - ]); - } - } elseif ($captchaSettings['type'] === Captcha::TYPE_BUILTIN && $isBuiltinCaptchaRequired) { - $captchaHash = $this->captchaSession->getCaptchaHash(); - if (empty($captchaHash)) { - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('Please regenerate the CAPTCHA.', 'mailpoet'), - ]); - } elseif (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) { - $this->captchaSession->setCaptchaHash(null); - $meta = []; - $meta['refresh_captcha'] = true; - return $this->badRequest([ - APIError::BAD_REQUEST => WPFunctions::get()->__('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'), - ], $meta); - } - } - - return true; } public function save($data = []) { @@ -475,15 +271,6 @@ class Subscribers extends APIEndpoint { : null; } - private function getSegmentsForSubscription(FormEntity $formEntity, array $submittedSegmentIds = []): array { - // If form contains segment selection blocks allow only segments ids configured in those blocks - $segmentBlocksSegmentIds = $formEntity->getSegmentBlocksSegmentIds(); - if (!empty($segmentBlocksSegmentIds)) { - return array_intersect($submittedSegmentIds, $segmentBlocksSegmentIds); - } - return $formEntity->getSettingsSegmentIds(); - } - private function getErrorMessage(ValidationException $exception): string { $exceptionMessage = $exception->getMessage(); if (strpos($exceptionMessage, 'This value should not be blank.') !== false) { diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 58dd9989c3..1b30413039 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -250,6 +250,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Subscribers\SubscriberSegmentRepository::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\SubscriberCustomFieldRepository::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\SubscriberSaveController::class)->setPublic(true); + $container->autowire(\MailPoet\Subscribers\SubscriberSubscribeController::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\ImportExport\ImportExportRepository::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository::class); // Segments diff --git a/lib/Subscribers/SubscriberSubscribeController.php b/lib/Subscribers/SubscriberSubscribeController.php new file mode 100644 index 0000000000..1a50e94cd2 --- /dev/null +++ b/lib/Subscribers/SubscriberSubscribeController.php @@ -0,0 +1,236 @@ +formsRepository = $formsRepository; + $this->subscriptionCaptcha = $subscriptionCaptcha; + $this->captchaSession = $captchaSession; + $this->subscriptionUrlFactory = $subscriptionUrlFactory; + $this->requiredCustomFieldValidator = $requiredCustomFieldValidator; + $this->fieldNameObfuscator = $fieldNameObfuscator; + $this->settings = $settings; + $this->subscriberActions = $subscriberActions; + $this->wp = $wp; + $this->throttling = $throttling; + } + + public function subscribe(array $data): array { + $form = $this->getForm($data); + + if (!empty($data['email'])) { + throw new UnexpectedValueException(__('Please leave the first field empty.', 'mailpoet')); + } + + $captchaSettings = $this->settings->get('captcha'); + $data = $this->initCaptcha($captchaSettings, $form, $data); + $data = $this->deobfuscateFormPayload($data); + + try { + $this->requiredCustomFieldValidator->validate($data, $form); + } catch (\Exception $e) { + throw new UnexpectedValueException($e->getMessage()); + } + + $segmentIds = $this->getSegmentIds($form, $data); + + $meta = $this->validateCaptcha($captchaSettings, $data); + if (isset($meta['error'])) { + return $meta; + } + + // only accept fields defined in the form + $formFields = $form->getFields(); + $data = array_intersect_key($data, array_flip($formFields)); + + // make sure we don't allow too many subscriptions with the same ip address + $timeout = $this->throttling->throttle(); + + if ($timeout > 0) { + $timeToWait = $this->throttling->secondsToTimeString($timeout); + $meta['refresh_captcha'] = true; + $meta['error'] = sprintf(__('You need to wait %s before subscribing again.', 'mailpoet'), $timeToWait); + return $meta; + } + + $subscriber = $this->subscriberActions->subscribe($data, $segmentIds); + + if (!empty($captchaSettings['type']) && $captchaSettings['type'] === Captcha::TYPE_BUILTIN) { + // Captcha has been verified, invalidate the session vars + $this->captchaSession->reset(); + } + + // record form statistics + StatisticsForms::record($form->getId(), $subscriber->getId()); + + $formSettings = $form->getSettings(); + if (!empty($formSettings['on_success'])) { + if ($formSettings['on_success'] === 'page') { + // redirect to a page on a success, pass the page url in the meta + $meta['redirect_url'] = $this->wp->getPermalink($formSettings['success_page']); + } else if ($formSettings['on_success'] === 'url') { + $meta['redirect_url'] = $formSettings['success_url']; + } + } + + return $meta; + } + + private function deobfuscateFormPayload($data): array { + return $this->fieldNameObfuscator->deobfuscateFormPayload($data); + } + + private function initCaptcha(?array $captchaSettings, FormEntity $form, array $data): array { + if (!$captchaSettings || !isset($captchaSettings['type'])) { + return $data; + } + + if ($captchaSettings['type'] === Captcha::TYPE_BUILTIN) { + $captchaSessionId = isset($data['captcha_session_id']) ? $data['captcha_session_id'] : null; + $this->captchaSession->init($captchaSessionId); + if (!isset($data['captcha'])) { + // Save form data to session + $this->captchaSession->setFormData(array_merge($data, ['form_id' => $form->getId()])); + } elseif ($this->captchaSession->getFormData()) { + // Restore form data from session + $data = array_merge($this->captchaSession->getFormData(), ['captcha' => $data['captcha']]); + } + // Otherwise use the post data + } + return $data; + } + + private function validateCaptcha($captchaSettings, $data): array { + if (empty($captchaSettings['type'])) { + return []; + } + + $meta = []; + $isBuiltinCaptchaRequired = false; + if ($captchaSettings['type'] === Captcha::TYPE_BUILTIN) { + $isBuiltinCaptchaRequired = $this->subscriptionCaptcha->isRequired(isset($data['email']) ? $data['email'] : ''); + if ($isBuiltinCaptchaRequired && empty($data['captcha'])) { + $meta['redirect_url'] = $this->subscriptionUrlFactory->getCaptchaUrl($this->captchaSession->getId()); + $meta['error'] = __('Please fill in the CAPTCHA.', 'mailpoet'); + return $meta; + } + } + + if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA && empty($data['recaptcha'])) { + return ['error' => __('Please check the CAPTCHA.', 'mailpoet')]; + } + + if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA) { + $response = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha']; + $response = $this->wp->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [ + 'body' => [ + 'secret' => $captchaSettings['recaptcha_secret_token'], + 'response' => $response, + ], + ]); + if (is_wp_error($response)) { + return ['error' => __('Error while validating the CAPTCHA.', 'mailpoet')]; + } + $response = json_decode(wp_remote_retrieve_body($response)); + if (empty($response->success)) { + return ['error' => __('Error while validating the CAPTCHA.', 'mailpoet')]; + } + + } elseif ($captchaSettings['type'] === Captcha::TYPE_BUILTIN && $isBuiltinCaptchaRequired) { + $captchaHash = $this->captchaSession->getCaptchaHash(); + if (empty($captchaHash)) { + $meta['error'] = __('Please regenerate the CAPTCHA.', 'mailpoet'); + } elseif (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) { + $this->captchaSession->setCaptchaHash(null); + $meta['refresh_captcha'] = true; + $meta['error'] = __('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'); + } + } + + return $meta; + } + + private function getSegmentIds(FormEntity $form, array $data): array { + $segmentIds = !empty($data['segments']) ? (array)$data['segments'] : []; + + // If form contains segment selection blocks allow only segments ids configured in those blocks + $segmentBlocksSegmentIds = $form->getSegmentBlocksSegmentIds(); + if (!empty($segmentBlocksSegmentIds)) { + $segmentIds = array_intersect($segmentIds, $segmentBlocksSegmentIds); + } else { + $segmentIds = $form->getSettingsSegmentIds(); + } + + if (empty($segmentIds)) { + throw new UnexpectedValueException(__('Please select a list.', 'mailpoet')); + } + + return $segmentIds; + } + + private function getForm(array $data): FormEntity { + $formId = (isset($data['form_id']) ? (int)$data['form_id'] : false); + $form = $this->formsRepository->findOneById($formId); + + if (!$form) { + throw new NotFoundException(__('Please specify a valid form ID.', 'mailpoet')); + } + + return $form; + } +} diff --git a/tests/integration/API/JSON/v1/SubscribersTest.php b/tests/integration/API/JSON/v1/SubscribersTest.php index 4cc463da7c..86d1d9b0ea 100644 --- a/tests/integration/API/JSON/v1/SubscribersTest.php +++ b/tests/integration/API/JSON/v1/SubscribersTest.php @@ -19,7 +19,6 @@ use MailPoet\Entities\SendingQueueEntity; use MailPoet\Entities\SubscriberCustomFieldEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberSegmentEntity; -use MailPoet\Form\FormsRepository; use MailPoet\Form\Util\FieldNameObfuscator; use MailPoet\Listing\Handler; use MailPoet\Models\CustomField; @@ -36,16 +35,13 @@ use MailPoet\Segments\SegmentsRepository; use MailPoet\Settings\SettingsController; use MailPoet\Settings\SettingsRepository; use MailPoet\Subscribers\ConfirmationEmailMailer; -use MailPoet\Subscribers\LinkTokens; -use MailPoet\Subscribers\RequiredCustomFieldValidator; use MailPoet\Subscribers\Source; -use MailPoet\Subscribers\SubscriberActions; use MailPoet\Subscribers\SubscriberListingRepository; use MailPoet\Subscribers\SubscriberSaveController; use MailPoet\Subscribers\SubscribersRepository; +use MailPoet\Subscribers\SubscriberSubscribeController; use MailPoet\Subscription\Captcha; use MailPoet\Subscription\CaptchaSession; -use MailPoet\Subscription\SubscriptionUrlFactory; use MailPoet\Test\DataFactories\DynamicSegment; use MailPoet\UnexpectedValueException; use MailPoet\WP\Functions; @@ -94,21 +90,14 @@ class SubscribersTest extends \MailPoetTest { $this->responseBuilder = $container->get(SubscribersResponseBuilder::class); $obfuscator = new FieldNameObfuscator($wp); $this->endpoint = new Subscribers( - $container->get(SubscriberActions::class), - $container->get(RequiredCustomFieldValidator::class), $container->get(Handler::class), - $container->get(Captcha::class), - $settings, - $this->captchaSession, $container->get(ConfirmationEmailMailer::class), - new SubscriptionUrlFactory($wp, $settings, new LinkTokens), $container->get(SubscribersRepository::class), $this->responseBuilder, $container->get(SubscriberListingRepository::class), $container->get(SegmentsRepository::class), - $obfuscator, - $container->get(FormsRepository::class), - $container->get(SubscriberSaveController::class) + $container->get(SubscriberSaveController::class), + $container->get(SubscriberSubscribeController::class) ); $this->obfuscatedEmail = $obfuscator->obfuscate('email'); $this->obfuscatedSegments = $obfuscator->obfuscate('segments');