This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,26 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoetVendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
class API {
/**
* @param string $version
* @return \MailPoet\API\MP\v1\API
* @throws \Exception
*/
public static function MP($version) {
/** @var class-string<\MailPoet\API\MP\v1\API> $apiClass */
$apiClass = sprintf('%s\MP\%s\API', __NAMESPACE__, $version);
try {
return ContainerWrapper::getInstance()->get($apiClass);
} catch (ServiceNotFoundException $e) {
throw new \Exception(__('Invalid API version.', 'mailpoet'));
}
}
}
@@ -0,0 +1,313 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
use MailPoet\Captcha\CaptchaConstants;
use MailPoet\Config\AccessControl;
use MailPoet\Exception;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Tracy\ApiPanel\ApiPanel;
use MailPoet\Tracy\DIPanel\DIPanel;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Psr\Container\ContainerInterface;
use Throwable;
use Tracy\Debugger;
use Tracy\ILogger;
class API {
private $requestApiVersion;
private $requestEndpoint;
private $requestMethod;
private $requestToken;
private $requestType;
private $requestEndpointClass;
private $requestData = [];
private $endpointNamespaces = [];
private $availableApiVersions = [
'v1',
];
/** @var ContainerInterface */
private $container;
/** @var AccessControl */
private $accessControl;
/** @var ErrorHandler */
private $errorHandler;
/** @var WPFunctions */
private $wp;
/** @var SettingsController */
private $settings;
/** @var LoggerFactory */
private $loggerFactory;
const CURRENT_VERSION = 'v1';
public function __construct(
ContainerInterface $container,
AccessControl $accessControl,
ErrorHandler $errorHandler,
SettingsController $settings,
LoggerFactory $loggerFactory,
WPFunctions $wp
) {
$this->container = $container;
$this->accessControl = $accessControl;
$this->errorHandler = $errorHandler;
$this->settings = $settings;
$this->wp = $wp;
foreach ($this->availableApiVersions as $availableApiVersion) {
$this->addEndpointNamespace(
sprintf('%s\%s', __NAMESPACE__, $availableApiVersion),
$availableApiVersion
);
}
$this->loggerFactory = $loggerFactory;
}
public function init() {
// admin security token and API version
WPFunctions::get()->addAction(
'admin_head',
[$this, 'setTokenAndAPIVersion']
);
// ajax (logged in users)
WPFunctions::get()->addAction(
'wp_ajax_mailpoet',
[$this, 'setupAjax']
);
// ajax (logged out users)
WPFunctions::get()->addAction(
'wp_ajax_nopriv_mailpoet',
[$this, 'setupAjax']
);
// nonce refreshing via heartbeats
WPFunctions::get()->addAction(
'wp_refresh_nonces',
[$this, 'addTokenToHeartbeatResponse']
);
}
public function setupAjax() {
$this->wp->doAction('mailpoet_api_setup', [$this]);
if (isset($_POST['api_version'])) {
$this->setRequestData($_POST, Endpoint::TYPE_POST);
} else {
$this->setRequestData($_GET, Endpoint::TYPE_GET);
}
$ignoreToken = (
$this->settings->get('captcha.type') != CaptchaConstants::TYPE_DISABLED &&
$this->requestEndpoint === 'subscribers' &&
$this->requestMethod === 'subscribe'
) || (
$this->requestEndpoint === 'captcha'
);
if (!$ignoreToken && $this->wp->wpVerifyNonce($this->requestToken, 'mailpoet_token') === false) {
$errorMessage = __("Sorry, but we couldn't connect to the MailPoet server. Please refresh the web page and try again.", 'mailpoet');
$errorResponse = $this->createErrorResponse(Error::UNAUTHORIZED, $errorMessage, Response::STATUS_UNAUTHORIZED);
return $errorResponse->send();
}
$response = $this->processRoute();
$response->send();
}
public function setRequestData($data, $requestType) {
$this->requestApiVersion = (!empty($data['api_version']) && is_string($data['api_version'])) ? $data['api_version'] : false;
$this->requestEndpoint = (isset($data['endpoint']) && is_string($data['endpoint']))
? Helpers::underscoreToCamelCase(trim($data['endpoint']))
: null;
// JS part of /wp-admin/customize.php does not like a 'method' field in a form widget
$methodParamName = isset($data['mailpoet_method']) ? 'mailpoet_method' : 'method';
$this->requestMethod = (isset($data[$methodParamName]) && is_string($data[$methodParamName]))
? Helpers::underscoreToCamelCase(trim($data[$methodParamName]))
: null;
$this->requestType = $requestType;
$this->requestToken = (isset($data['token']) && is_string($data['token']))
? trim($data['token'])
: null;
if (!$this->requestEndpoint || !$this->requestMethod || !$this->requestApiVersion) {
$errorMessage = __('Invalid API request.', 'mailpoet');
$errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
return $errorResponse;
} else if (!empty($this->endpointNamespaces[$this->requestApiVersion])) {
foreach ($this->endpointNamespaces[$this->requestApiVersion] as $namespace) {
$endpointClass = sprintf(
'%s\%s',
$namespace,
ucfirst($this->requestEndpoint)
);
if ($this->container->has($endpointClass)) {
$this->requestEndpointClass = $endpointClass;
break;
}
}
$this->requestData = isset($data['data'])
? WPFunctions::get()->stripslashesDeep($data['data'])
: [];
// remove reserved keywords from data
if (is_array($this->requestData) && !empty($this->requestData)) {
// filter out reserved keywords from data
$reservedKeywords = [
'token',
'endpoint',
'method',
'api_version',
'mailpoet_method', // alias of 'method'
'mailpoet_redirect',
];
$this->requestData = array_diff_key(
$this->requestData,
array_flip($reservedKeywords)
);
}
}
}
public function processRoute() {
try {
if (
empty($this->requestEndpointClass) ||
!$this->container->has($this->requestEndpointClass)
) {
throw new \Exception(__('Invalid API endpoint.', 'mailpoet'));
}
$endpoint = $this->container->get($this->requestEndpointClass);
if (!method_exists($endpoint, $this->requestMethod)) {
throw new \Exception(__('Invalid API endpoint method.', 'mailpoet'));
}
if (!$endpoint->isMethodAllowed($this->requestMethod, $this->requestType)) {
throw new \Exception(__('HTTP request method not allowed.', 'mailpoet'));
}
if (
class_exists(Debugger::class)
&& class_exists(DIPanel::class)
&& class_exists(ApiPanel::class)
) {
ApiPanel::init($endpoint, $this->requestMethod, $this->requestData);
DIPanel::init();
}
// check the accessibility of the requested endpoint's action
// by default, an endpoint's action is considered "private"
if (!$this->validatePermissions($this->requestMethod, $endpoint->permissions)) {
$errorMessage = __('You do not have the required permissions.', 'mailpoet');
$errorResponse = $this->createErrorResponse(Error::FORBIDDEN, $errorMessage, Response::STATUS_FORBIDDEN);
return $errorResponse;
}
$response = $endpoint->{$this->requestMethod}($this->requestData);
return $response;
} catch (Exception $e) {
$this->logError($e);
return $this->errorHandler->convertToResponse($e);
} catch (Throwable $e) {
if (class_exists(Debugger::class) && Debugger::$logDirectory) {
Debugger::log($e, ILogger::EXCEPTION);
}
$this->logError($e);
$errorMessage = $e->getMessage();
$errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
return $errorResponse;
}
}
public function validatePermissions($requestMethod, $permissions) {
// validate method permission if defined, otherwise validate global permission
return(!empty($permissions['methods'][$requestMethod])) ?
$this->accessControl->validatePermission($permissions['methods'][$requestMethod]) :
$this->accessControl->validatePermission($permissions['global']);
}
public function setTokenAndAPIVersion() {
echo sprintf(
'<script type="text/javascript">' .
'var mailpoet_token = "%s";' .
'var mailpoet_api_version = "%s";' .
'</script>',
esc_js($this->wp->wpCreateNonce('mailpoet_token')),
esc_js(self::CURRENT_VERSION)
);
}
public function addTokenToHeartbeatResponse($response) {
$response['mailpoet_token'] = $this->wp->wpCreateNonce('mailpoet_token');
return $response;
}
public function addEndpointNamespace($namespace, $version) {
if (!empty($this->endpointNamespaces[$version][$namespace])) return;
$this->endpointNamespaces[$version][] = $namespace;
}
public function getEndpointNamespaces() {
return $this->endpointNamespaces;
}
public function getRequestedEndpointClass() {
return $this->requestEndpointClass;
}
public function getRequestedAPIVersion() {
return $this->requestApiVersion;
}
public function createErrorResponse($errorType, $errorMessage, $responseStatus) {
$errorMessages = [
$errorType => $errorMessage,
];
if ($errorType === Error::BAD_REQUEST) {
$mpReinstallErrorMessage = __('The plugin has encountered an unexpected error. Please reload the page. If that does not help, [link]re-install the MailPoet Plugin.[/link]', 'mailpoet');
$mpReinstallErrorMessage = Helpers::replaceLinkTags(
$mpReinstallErrorMessage,
'https://kb.mailpoet.com/article/258-re-installing-updating-the-plugin-via-ftp',
['target' => '_blank']
);
$errorMessages[Error::REINSTALL_PLUGIN] = $mpReinstallErrorMessage;
}
$errorResponse = new ErrorResponse(
$errorMessages,
[],
$responseStatus
);
return $errorResponse;
}
private function logError(Throwable $e): void {
// logging to the php log
if (function_exists('error_log')) {
error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
}
// logging to the MailPoet table
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_API)->warning($e->getMessage(), [
'requestMethod' => $this->requestMethod,
'requestEndpoint' => $this->requestEndpoint,
'exceptionMessage' => $e->getMessage(),
'exceptionTrace' => $e->getTrace(),
]);
}
}
@@ -0,0 +1,60 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\v1\RedirectResponse;
use MailPoet\Config\AccessControl;
abstract class Endpoint {
const TYPE_POST = 'POST';
const TYPE_GET = 'GET';
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
'methods' => [],
];
protected static $getMethods = [];
public function successResponse(
$data = [], $meta = [], $status = Response::STATUS_OK
) {
return new SuccessResponse($data, $meta, $status);
}
public function errorResponse(
$errors = [], $meta = [], $status = Response::STATUS_NOT_FOUND
) {
if (empty($errors)) {
$errors = [
Error::UNKNOWN => __('An unknown error occurred.', 'mailpoet'),
];
}
return new ErrorResponse($errors, $meta, $status);
}
public function badRequest($errors = [], $meta = []) {
if (empty($errors)) {
$errors = [
Error::BAD_REQUEST => __('Invalid request parameters', 'mailpoet'),
];
}
return new ErrorResponse($errors, $meta, Response::STATUS_BAD_REQUEST);
}
public function redirectResponse($url) {
return new RedirectResponse($url);
}
public function isMethodAllowed($name, $type) {
// Block GET requests on POST endpoints, but allow POST requests on GET endpoints (some plugins
// change REQUEST_METHOD to POST on GET requests, which caused them to be blocked)
if ($type === self::TYPE_GET && !in_array($name, static::$getMethods)) {
return false;
}
return true;
}
}
@@ -0,0 +1,19 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
final class Error {
const UNKNOWN = 'unknown';
const BAD_REQUEST = 'bad_request';
const UNAUTHORIZED = 'unauthorized';
const FORBIDDEN = 'forbidden';
const NOT_FOUND = 'not_found';
const REINSTALL_PLUGIN = 'reinstall_plugin';
private function __construct() {
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
use MailPoet\Exception;
use MailPoet\HttpAwareException;
class ErrorHandler {
/** @var string[] */
private $defaultErrors = [];
public function convertToResponse(\Throwable $e): ErrorResponse {
$this->defaultErrors[Error::UNKNOWN] = __('An unknown error occurred.', 'mailpoet');
if ($e instanceof Exception) {
$errors = $e->getErrors() ?: $this->defaultErrors;
$statusCode = $e instanceof HttpAwareException ? $e->getHttpStatusCode() : Response::STATUS_UNKNOWN;
return new ErrorResponse($errors, [], $statusCode);
}
return new ErrorResponse($this->defaultErrors, [], Response::STATUS_UNKNOWN);
}
}
@@ -0,0 +1,36 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
class ErrorResponse extends Response {
public $errors;
public function __construct(
$errors = [],
$meta = [],
$status = self::STATUS_NOT_FOUND
) {
parent::__construct($status, $meta);
$this->errors = $this->formatErrors($errors);
}
public function getData() {
return (empty($this->errors)) ? null : ['errors' => $this->errors];
}
public function formatErrors($errors = []) {
return array_map(function($error, $message) {
// sanitize SQL error
if (preg_match('/^SQLSTATE/i', $message)) {
$message = __('An unknown error occurred.', 'mailpoet');
}
return [
'error' => $error,
'message' => $message,
];
}, array_keys($errors), array_values($errors));
}
}
@@ -0,0 +1,55 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
use MailPoet\WP\Functions as WPFunctions;
abstract class Response {
const STATUS_OK = 200;
const REDIRECT = 302;
const STATUS_BAD_REQUEST = 400;
const STATUS_UNAUTHORIZED = 401;
const STATUS_FORBIDDEN = 403;
const STATUS_NOT_FOUND = 404;
const STATUS_CONFLICT = 409;
const STATUS_UNKNOWN = 500;
public $status;
public $meta;
public $location;
public function __construct($status, $meta = [], $location = null) { // phpcs:ignore
$this->status = $status;
$this->meta = $meta;
$this->location = $location;
}
public function send() {
if ($this->status === self::REDIRECT && $this->location) {
header("Location: " . $this->location, true, $this->status);
exit;
}
WPFunctions::get()->statusHeader($this->status);
$data = $this->getData();
$response = [];
if (!empty($this->meta)) {
$response['meta'] = $this->meta;
}
if ($data === null) {
$data = [];
}
$response = array_merge($response, $data);
@header('Content-Type: application/json; charset=' . get_option('blog_charset'));
echo wp_json_encode($response);
die();
}
public abstract function getData();
}
@@ -0,0 +1,33 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\CustomFieldEntity;
class CustomFieldsResponseBuilder {
/**
* @param CustomFieldEntity[] $customFields
* @return array
*/
public function buildBatch(array $customFields) {
return array_map([$this, 'build'], $customFields);
}
/**
* @param CustomFieldEntity $customField
* @return array
*/
public function build(CustomFieldEntity $customField) {
return [
'id' => $customField->getId(),
'name' => $customField->getName(),
'type' => $customField->getType(),
'params' => $customField->getParams(),
'created_at' => ($createdAt = $customField->getCreatedAt()) ? $createdAt->format('Y-m-d H:i:s') : null,
'updated_at' => $customField->getUpdatedAt()->format('Y-m-d H:i:s'),
];
}
}
@@ -0,0 +1,120 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Segments\SegmentDependencyValidator;
use MailPoet\Segments\SegmentSubscribersRepository;
use MailPoet\Subscribers\SubscribersCountsController;
use MailPoet\WP\Functions;
class DynamicSegmentsResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
/** @var SegmentsResponseBuilder */
private $segmentsResponseBuilder;
/** @var Functions */
private $wp;
/** @var SegmentSubscribersRepository */
private $segmentSubscriberRepository;
/** @var SegmentDependencyValidator */
private $segmentDependencyValidator;
/** @var SubscribersCountsController */
private $subscribersCountsController;
public function __construct(
Functions $wp,
SegmentSubscribersRepository $segmentSubscriberRepository,
SegmentsResponseBuilder $segmentsResponseBuilder,
SegmentDependencyValidator $segmentDependencyValidator,
SubscribersCountsController $subscribersCountsController
) {
$this->segmentsResponseBuilder = $segmentsResponseBuilder;
$this->segmentSubscriberRepository = $segmentSubscriberRepository;
$this->wp = $wp;
$this->segmentDependencyValidator = $segmentDependencyValidator;
$this->subscribersCountsController = $subscribersCountsController;
}
public function build(SegmentEntity $segmentEntity) {
$data = $this->segmentsResponseBuilder->build($segmentEntity);
$data = $this->addMissingPluginProperties($segmentEntity, $data);
$dynamicFilters = $segmentEntity->getDynamicFilters();
$filters = [];
foreach ($dynamicFilters as $dynamicFilter) {
$filter = $dynamicFilter->getFilterData()->getData();
$filter['id'] = $dynamicFilter->getId();
$filter['segmentType'] = $dynamicFilter->getFilterData()->getFilterType(); // We need to add filterType with key segmentType due to BC
$filter['action'] = $dynamicFilter->getFilterData()->getAction();
if (isset($filter['country_code']) && !is_array($filter['country_code'])) {
// Convert to multiple values filter
$filter['country_code'] = [$filter['country_code']];
}
if (isset($filter['wordpressRole']) && !is_array($filter['wordpressRole'])) {
// new filters are always array, they support multiple values, the old didn't convert old filters to new format
$filter['wordpressRole'] = [$filter['wordpressRole']];
}
if (($filter['segmentType'] === DynamicSegmentFilterData::TYPE_EMAIL) && isset($filter['newsletter_id'])) {
$filter['newsletter_id'] = intval($filter['newsletter_id']);
}
$filters[] = $filter;
}
$data['filters'] = $filters;
return $data;
}
public function buildForListing(array $segments): array {
$data = [];
foreach ($segments as $segment) {
$data[] = $this->buildListingItem($segment);
}
return $data;
}
private function addMissingPluginProperties(SegmentEntity $segment, array $data): array {
$missingPlugins = $this->segmentDependencyValidator->getMissingPluginsBySegment($segment);
if ($missingPlugins) {
$missingPlugin = reset($missingPlugins);
$data['is_plugin_missing'] = true;
$missingPluginMessage = $this->segmentDependencyValidator->getCustomErrorMessage($missingPlugin);
if ($missingPluginMessage) {
$data['missing_plugin_message'] = $missingPluginMessage;
} else {
$data['missing_plugin_message']['message'] =
sprintf(
// translators: %s is the name of the missing plugin.
__('Activate the %s plugin to see the number of subscribers and enable the editing of this segment.', 'mailpoet'),
$missingPlugin
);
}
} else {
$data['is_plugin_missing'] = false;
$data['missing_plugin_message'] = null;
}
return $data;
}
private function buildListingItem(SegmentEntity $segment): array {
$data = $this->segmentsResponseBuilder->build($segment);
$data = $this->addMissingPluginProperties($segment, $data);
$data['subscribers_url'] = $this->wp->adminUrl(
'admin.php?page=mailpoet-subscribers#/filter[segment=' . $segment->getId() . ']'
);
$segmentStatisticsCount = $this->subscribersCountsController->getSegmentStatisticsCount($segment);
$data['count_all'] = $segmentStatisticsCount['all'];
$data['count_subscribed'] = $segmentStatisticsCount[SubscriberEntity::STATUS_SUBSCRIBED];
return $data;
}
}
@@ -0,0 +1,54 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\FormEntity;
use MailPoet\Statistics\StatisticsFormsRepository;
class FormsResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
/** @var StatisticsFormsRepository */
private $statisticsFormsRepository;
public function __construct(
StatisticsFormsRepository $statisticsFormsRepository
) {
$this->statisticsFormsRepository = $statisticsFormsRepository;
}
public function build(FormEntity $form) {
return [
'id' => (string)$form->getId(), // (string) for BC
'name' => $form->getName(),
'status' => $form->getStatus(),
'body' => $form->getBody(),
'settings' => $form->getSettings(),
'styles' => $form->getStyles(),
'created_at' => ($createdAt = $form->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $form->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $form->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
];
}
public function buildForListing(array $forms) {
$data = [];
foreach ($forms as $form) {
$form = $this->build($form);
$form['signups'] = $this->statisticsFormsRepository->getTotalSignups($form['id']);
$form['segments'] = (
!empty($form['settings']['segments'])
? $form['settings']['segments']
: []
);
$data[] = $form;
}
return $data;
}
}
@@ -0,0 +1,44 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterTemplateEntity;
class NewsletterTemplatesResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
public function build(NewsletterTemplateEntity $template): array {
return [
'id' => $template->getId(),
'categories' => $template->getCategories(),
'thumbnail' => $template->getThumbnail(),
'name' => $template->getName(),
'readonly' => $template->getReadonly(),
'body' => $template->getBody(),
'created_at' => ($createdAt = $template->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $template->getUpdatedAt()->format(self::DATE_FORMAT),
'newsletter_id' => ($newsletter = $template->getNewsletter()) ? $newsletter->getId() : null,
];
}
/**
* @param NewsletterTemplateEntity[] $newsletterTemplates
* @return mixed[]
*/
public function buildForListing(array $newsletterTemplates): array {
$data = [];
foreach ($newsletterTemplates as $template) {
$data[] = [
'id' => $template->getId(),
'categories' => $template->getCategories(),
'thumbnail' => $template->getThumbnail(),
'name' => $template->getName(),
'readonly' => $template->getReadonly(),
];
}
return $data;
}
}
@@ -0,0 +1,328 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Logging\LogRepository;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatistics;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class NewslettersResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
const RELATION_QUEUE = 'queue';
const RELATION_SEGMENTS = 'segments';
const RELATION_OPTIONS = 'options';
const RELATION_TOTAL_SENT = 'total_sent';
const RELATION_CHILDREN_COUNT = 'children_count';
const RELATION_SCHEDULED = 'scheduled';
const RELATION_STATISTICS = 'statistics';
/** @var NewsletterStatisticsRepository */
private $newslettersStatsRepository;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var EntityManager */
private $entityManager;
/** @var NewsletterUrl */
private $newsletterUrl;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/*** @var LogRepository */
private $logRepository;
public function __construct(
EntityManager $entityManager,
NewslettersRepository $newslettersRepository,
NewsletterStatisticsRepository $newslettersStatsRepository,
NewsletterUrl $newsletterUrl,
SendingQueuesRepository $sendingQueuesRepository,
LogRepository $logRepository
) {
$this->newslettersStatsRepository = $newslettersStatsRepository;
$this->newslettersRepository = $newslettersRepository;
$this->entityManager = $entityManager;
$this->newsletterUrl = $newsletterUrl;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->logRepository = $logRepository;
}
public function build(NewsletterEntity $newsletter, $relations = []) {
$data = [
'id' => (string)$newsletter->getId(), // (string) for BC
'hash' => $newsletter->getHash(),
'subject' => $newsletter->getSubject(),
'type' => $newsletter->getType(),
'sender_address' => $newsletter->getSenderAddress(),
'sender_name' => $newsletter->getSenderName(),
'status' => $newsletter->getStatus(),
'reply_to_address' => $newsletter->getReplyToAddress(),
'reply_to_name' => $newsletter->getReplyToName(),
'preheader' => $newsletter->getPreheader(),
'body' => $newsletter->getBody(),
'sent_at' => ($sentAt = $newsletter->getSentAt()) ? $sentAt->format(self::DATE_FORMAT) : null,
'created_at' => ($createdAt = $newsletter->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $newsletter->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $newsletter->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
'parent_id' => ($parent = $newsletter->getParent()) ? $parent->getId() : null,
'unsubscribe_token' => $newsletter->getUnsubscribeToken(),
'ga_campaign' => $newsletter->getGaCampaign(),
'wp_post_id' => $newsletter->getWpPostId(),
'campaign_name' => $newsletter->getCampaignName(),
];
foreach ($relations as $relation) {
if ($relation === self::RELATION_QUEUE) {
$data['queue'] = ($queue = $newsletter->getLatestQueue()) ? $this->buildQueue($queue) : false; // false for BC
}
if ($relation === self::RELATION_SEGMENTS) {
$data['segments'] = $this->buildSegments($newsletter);
}
if ($relation === self::RELATION_OPTIONS) {
$data['options'] = $this->buildOptions($newsletter);
}
if ($relation === self::RELATION_TOTAL_SENT) {
$data['total_sent'] = $this->newslettersStatsRepository->getTotalSentCount($newsletter);
}
if ($relation === self::RELATION_CHILDREN_COUNT) {
$data['children_count'] = $this->newslettersStatsRepository->getChildrenCount($newsletter);
}
if ($relation === self::RELATION_SCHEDULED) {
$data['total_scheduled'] = $this->sendingQueuesRepository->countAllToProcessByNewsletter(
$newsletter
);
}
if ($relation === self::RELATION_STATISTICS) {
$data['statistics'] = $this->newslettersStatsRepository->getStatistics($newsletter)->asArray();
}
}
return $data;
}
private function processPersonalizationTags(?string $content): ?string {
if (is_null($content) || strlen($content) === 0) {
return $content;
}
if (strpos($content, '<!--') === false) {
// we don't need to parse anything if there are no personalization tags
return $content;
}
if (!class_exists('\MailPoet\EmailEditor\Engine\PersonalizationTags\HTML_Tag_Processor')) {
// editor is not active, we cannot process personalization tags
return $content;
}
$content_processor = new \MailPoet\EmailEditor\Engine\PersonalizationTags\HTML_Tag_Processor($content);
while ($content_processor->next_token()) {
$type = $content_processor->get_token_type();
if ($type === '#comment') {
$token = $content_processor->get_modifiable_text();
$content_processor->replace_token($token);
}
}
$content_processor->flush_updates();
return $content_processor->get_updated_html();
}
public function buildForListing(array $newsletters): array {
$statistics = $this->newslettersStatsRepository->getBatchStatistics($newsletters);
$latestQueues = $this->getBatchLatestQueuesWithTasks($newsletters);
$this->newslettersRepository->prefetchOptions($newsletters);
$this->newslettersRepository->prefetchSegments($newsletters);
$data = [];
foreach ($newsletters as $newsletter) {
$id = $newsletter->getId();
$data[] = $this->buildListingItem($newsletter, $statistics[$id] ?? null, $latestQueues[$id] ?? null);
}
return $data;
}
/**
* @param NewsletterEntity $newsletter
* @param NewsletterStatistics|null $statistics
* @param SendingQueueEntity|null $latestQueue
* @return array<string, mixed>
*/
private function buildListingItem(NewsletterEntity $newsletter, NewsletterStatistics $statistics = null, SendingQueueEntity $latestQueue = null): array {
$couponBlockLogs = array_map(function ($item) {
return "Coupon block: $item";
}, $this->logRepository->getRawMessagesForNewsletter($newsletter, LoggerFactory::TOPIC_COUPONS));
$data = [
'id' => (string)$newsletter->getId(), // (string) for BC
'hash' => $newsletter->getHash(),
'subject' => $this->processPersonalizationTags($newsletter->getSubject()),
'type' => $newsletter->getType(),
'status' => $newsletter->getStatus(),
'sent_at' => ($sentAt = $newsletter->getSentAt()) ? $sentAt->format(self::DATE_FORMAT) : null,
'updated_at' => $newsletter->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $newsletter->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
'segments' => [],
'queue' => false,
'wp_post_id' => $newsletter->getWpPostId(),
'statistics' => ($statistics && $newsletter->getType() !== NewsletterEntity::TYPE_NOTIFICATION)
? $statistics->asArray()
: false,
'preview_url' => $this->newsletterUrl->getViewInBrowserUrl(
$newsletter,
null,
in_array($newsletter->getStatus(), [NewsletterEntity::STATUS_SENT, NewsletterEntity::STATUS_SENDING], true)
? $latestQueue
: null
),
'logs' => $couponBlockLogs,
'campaign_name' => $newsletter->getCampaignName(),
];
if ($newsletter->getType() === NewsletterEntity::TYPE_STANDARD) {
$data['segments'] = $this->buildSegments($newsletter);
$data['queue'] = $latestQueue ? $this->buildQueue($latestQueue) : false; // false for BC
$data['options'] = $this->buildOptions($newsletter);
} elseif (in_array($newsletter->getType(), [NewsletterEntity::TYPE_WELCOME, NewsletterEntity::TYPE_AUTOMATIC], true)) {
$data['segments'] = [];
$data['options'] = $this->buildOptions($newsletter);
$data['total_sent'] = $statistics ? $statistics->getTotalSentCount() : 0;
$data['total_scheduled'] = $this->sendingQueuesRepository->countAllToProcessByNewsletter(
$newsletter
);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION) {
$data['segments'] = $this->buildSegments($newsletter);
$data['children_count'] = $this->newslettersStatsRepository->getChildrenCount($newsletter);
$data['options'] = $this->buildOptions($newsletter);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY) {
$data['segments'] = $this->buildSegments($newsletter);
$data['queue'] = $latestQueue ? $this->buildQueue($latestQueue) : false; // false for BC
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_RE_ENGAGEMENT) {
$data['segments'] = $this->buildSegments($newsletter);
$data['options'] = $this->buildOptions($newsletter);
$data['total_sent'] = $statistics ? $statistics->getTotalSentCount() : 0;
}
return $data;
}
private function buildSegments(NewsletterEntity $newsletter) {
$output = [];
foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
$segment = $newsletterSegment->getSegment();
if (!$segment || $segment->getDeletedAt()) {
continue;
}
$output[] = $this->buildSegment($segment);
}
return $output;
}
private function buildOptions(NewsletterEntity $newsletter) {
$output = [];
foreach ($newsletter->getOptions() as $option) {
$optionField = $option->getOptionField();
if (!$optionField) {
continue;
}
$output[$optionField->getName()] = $option->getValue();
}
// convert 'afterTimeNumber' string to integer
if (isset($output['afterTimeNumber']) && is_numeric($output['afterTimeNumber'])) {
$output['afterTimeNumber'] = (int)$output['afterTimeNumber'];
}
return $output;
}
private function buildSegment(SegmentEntity $segment) {
$filters = $segment->getType() === SegmentEntity::TYPE_DYNAMIC ? $segment->getDynamicFilters()->toArray() : [];
return [
'id' => (string)$segment->getId(), // (string) for BC
'name' => $segment->getName(),
'type' => $segment->getType(),
'filters' => array_map(function(DynamicSegmentFilterEntity $filter) {
return [
'action' => $filter->getFilterData()->getAction(),
'type' => $filter->getFilterData()->getFilterType(),
];
}, $filters),
'description' => $segment->getDescription(),
'created_at' => ($createdAt = $segment->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $segment->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $segment->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
];
}
private function buildQueue(SendingQueueEntity $queue) {
$task = $queue->getTask();
if ($task === null) {
return null;
}
return [
'id' => (string)$queue->getId(), // (string) for BC
'type' => $task->getType(),
'status' => $task->getStatus(),
'priority' => (string)$task->getPriority(), // (string) for BC
'scheduled_at' => ($scheduledAt = $task->getScheduledAt()) ? $scheduledAt->format(self::DATE_FORMAT) : null,
'processed_at' => ($processedAt = $task->getProcessedAt()) ? $processedAt->format(self::DATE_FORMAT) : null,
'created_at' => ($createdAt = $queue->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $queue->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $queue->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
'meta' => $queue->getMeta(),
'task_id' => (string)$task->getId(), // (string) for BC
'newsletter_id' => ($newsletter = $queue->getNewsletter()) ? (string)$newsletter->getId() : null, // (string) for BC
'newsletter_rendered_subject' => $this->processPersonalizationTags($queue->getNewsletterRenderedSubject()),
'count_total' => (string)$queue->getCountTotal(), // (string) for BC
'count_processed' => (string)$queue->getCountProcessed(), // (string) for BC
'count_to_process' => (string)$queue->getCountToProcess(), // (string) for BC
];
}
private function getBatchLatestQueuesWithTasks(array $newsletters): array {
// this implements the same logic as NewsletterEntity::getLatestQueue() but for a batch of $newsletters
$subqueryQueryBuilder = $this->entityManager->createQueryBuilder();
$subquery = $subqueryQueryBuilder
->select('MAX(subSq.id) AS maxId')
->from(SendingQueueEntity::class, 'subSq')
->where('subSq.newsletter IN (:newsletters)')
->setParameter('newsletters', $newsletters)
->groupBy('subSq.newsletter')
->getQuery();
$latestQueueIds = array_column($subquery->getResult(), 'maxId');
if (empty($latestQueueIds)) {
return [];
}
$queryBuilder = $this->entityManager->createQueryBuilder();
$results = $queryBuilder
->select('PARTIAL sq.{id, createdAt, updatedAt, deletedAt, meta, newsletterRenderedSubject, countTotal, countProcessed, countToProcess}')
->addSelect('PARTIAL t.{id, type, status, priority, scheduledAt, processedAt}')
->addSelect('IDENTITY(sq.newsletter)')
->from(SendingQueueEntity::class, 'sq')
->join('sq.task', 't')
->where('sq.id IN (:sub)')
->setParameter('sub', $latestQueueIds)
->getQuery()
->getResult();
$latestQueues = [];
foreach ($results as $result) {
$latestQueues[(int)$result[1]] = $result[0];
}
return $latestQueues;
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
class ScheduledTaskSubscriberResponseBuilder {
public function build(ScheduledTaskSubscriberEntity $scheduledSubscriber) {
$subscriber = $scheduledSubscriber->getSubscriber();
$task = $scheduledSubscriber->getTask();
return [
'processed' => $scheduledSubscriber->getProcessed(),
'failed' => $scheduledSubscriber->getFailed(),
'error' => $scheduledSubscriber->getError(),
'taskId' => $task ? $task->getId() : null,
'email' => $subscriber ? $subscriber->getEmail() : null,
'subscriberId' => $subscriber ? $subscriber->getId() : null,
'firstName' => $subscriber ? $subscriber->getFirstName() : null,
'lastName' => $subscriber ? $subscriber->getLastName() : null,
];
}
public function buildForListing(array $scheduledSubscribers) {
$data = [];
foreach ($scheduledSubscribers as $scheduledSubscriber) {
$data[] = $this->build($scheduledSubscriber);
}
return $data;
}
}
@@ -0,0 +1,61 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Subscribers\SubscribersCountsController;
use MailPoet\WP\Functions;
class SegmentsResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
/** @var Functions */
private $wp;
/** @var SubscribersCountsController */
private $subscribersCountsController;
public function __construct(
Functions $wp,
SubscribersCountsController $subscribersCountsController
) {
$this->wp = $wp;
$this->subscribersCountsController = $subscribersCountsController;
}
public function build(SegmentEntity $segment): array {
return [
'id' => (string)$segment->getId(), // (string) for BC
'name' => $segment->getName(),
'type' => $segment->getType(),
'description' => $segment->getDescription(),
'created_at' => ($createdAt = $segment->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $segment->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $segment->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
'average_engagement_score' => $segment->getAverageEngagementScore(),
'filters_connect' => $segment->getFiltersConnectOperator(),
'showInManageSubscriptionPage' => (int)$segment->getDisplayInManageSubscriptionPage(),
];
}
public function buildForListing(array $segments): array {
$data = [];
foreach ($segments as $segment) {
$data[] = $this->buildListingItem($segment);
}
return $data;
}
private function buildListingItem(SegmentEntity $segment): array {
$data = $this->build($segment);
$data['subscribers_count'] = $this->subscribersCountsController->getSegmentStatisticsCount($segment);
$data['subscribers_url'] = $this->wp->adminUrl(
'admin.php?page=mailpoet-subscribers#/filter[segment=' . $segment->getId() . ']'
);
return $data;
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
class SendingQueuesResponseBuilder {
public function build(SendingQueueEntity $sendingQueue): array {
if (!$sendingQueue->getTask() instanceof ScheduledTaskEntity) {
throw new \RuntimeException('Invalid state. SendingQueue has no ScheduledTask associated.');
}
return [
'id' => $sendingQueue->getId(),
'type' => $sendingQueue->getTask()->getType(),
'status' => $sendingQueue->getTask()->getStatus(),
'priority' => $sendingQueue->getTask()->getPriority(),
'scheduled_at' => $this->getFormattedDateOrNull($sendingQueue->getTask()->getScheduledAt()),
'processed_at' => $this->getFormattedDateOrNull($sendingQueue->getTask()->getProcessedAt()),
'created_at' => $this->getFormattedDateOrNull($sendingQueue->getTask()->getCreatedAt()),
'updated_at' => $this->getFormattedDateOrNull($sendingQueue->getTask()->getUpdatedAt()),
'deleted_at' => $this->getFormattedDateOrNull($sendingQueue->getTask()->getDeletedAt()),
'in_progress' => $sendingQueue->getTask()->getInProgress(),
'reschedule_count' => $sendingQueue->getTask()->getRescheduleCount(),
'meta' => $sendingQueue->getMeta(),
'task_id' => $sendingQueue->getTask()->getId(),
'newsletter_id' => ($sendingQueue->getNewsletter() instanceof NewsletterEntity) ? $sendingQueue->getNewsletter()->getId() : null,
'newsletter_rendered_body' => $sendingQueue->getNewsletterRenderedBody(),
'newsletter_rendered_subject' => $sendingQueue->getNewsletterRenderedSubject(),
'count_total' => $sendingQueue->getCountTotal(),
'count_processed' => $sendingQueue->getCountProcessed(),
'count_to_process' => $sendingQueue->getCountToProcess(),
];
}
private function getFormattedDateOrNull(?\DateTimeInterface $date): ?string {
return $date ? $date->format('Y-m-d H:i:s') : null;
}
}
@@ -0,0 +1,199 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\ResponseBuilders;
if (!defined('ABSPATH')) exit;
use MailPoet\CustomFields\CustomFieldsRepository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberCustomFieldEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Statistics\StatisticsUnsubscribesRepository;
use MailPoet\Subscribers\SubscriberCustomFieldRepository;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class SubscribersResponseBuilder {
const DATE_FORMAT = 'Y-m-d H:i:s';
/** @var StatisticsUnsubscribesRepository */
private $statisticsUnsubscribesRepository;
/** @var CustomFieldsRepository */
private $customFieldsRepository;
/** @var SubscriberCustomFieldRepository */
private $subscriberCustomFieldRepository;
/** @var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager,
CustomFieldsRepository $customFieldsRepository,
SubscriberCustomFieldRepository $subscriberCustomFieldRepository,
StatisticsUnsubscribesRepository $statisticsUnsubscribesRepository
) {
$this->statisticsUnsubscribesRepository = $statisticsUnsubscribesRepository;
$this->customFieldsRepository = $customFieldsRepository;
$this->subscriberCustomFieldRepository = $subscriberCustomFieldRepository;
$this->entityManager = $entityManager;
}
public function buildForListing(array $subscribers): array {
$this->prefetchRelations($subscribers);
$data = [];
foreach ($subscribers as $subscriber) {
$data[] = $this->buildListingItem($subscriber);
}
return $data;
}
private function buildListingItem(SubscriberEntity $subscriber): array {
return [
'id' => (string)$subscriber->getId(), // (string) for BC
'email' => $subscriber->getEmail(),
'first_name' => $subscriber->getFirstName(),
'last_name' => $subscriber->getLastName(),
'subscriptions' => $this->buildSubscriptions($subscriber),
'status' => $subscriber->getStatus(),
'count_confirmations' => $subscriber->getConfirmationsCount(),
'wp_user_id' => $subscriber->getWpUserId(),
'is_woocommerce_user' => $subscriber->getIsWoocommerceUser(),
'created_at' => ($createdAt = $subscriber->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'last_subscribed_at' => ($lastSubscribedAt = $subscriber->getLastSubscribedAt()) ? $lastSubscribedAt->format(self::DATE_FORMAT) : null,
'engagement_score' => $subscriber->getEngagementScore(),
'tags' => $this->buildTags($subscriber),
];
}
public function build(SubscriberEntity $subscriberEntity): array {
$data = [
'id' => (string)$subscriberEntity->getId(),
'wp_user_id' => $subscriberEntity->getWpUserId(),
'is_woocommerce_user' => $subscriberEntity->getIsWoocommerceUser(),
'subscriptions' => $this->buildSubscriptions($subscriberEntity),
'unsubscribes' => $this->buildUnsubscribes($subscriberEntity),
'status' => $subscriberEntity->getStatus(),
'last_name' => $subscriberEntity->getLastName(),
'first_name' => $subscriberEntity->getFirstName(),
'email' => $subscriberEntity->getEmail(),
'created_at' => ($createdAt = $subscriberEntity->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => ($updatedAt = $subscriberEntity->getUpdatedAt()) ? $updatedAt->format(self::DATE_FORMAT) : null,
'deleted_at' => ($deletedAt = $subscriberEntity->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
'subscribed_ip' => $subscriberEntity->getSubscribedIp(),
'confirmed_ip' => $subscriberEntity->getConfirmedIp(),
'confirmed_at' => ($confirmedAt = $subscriberEntity->getConfirmedAt()) ? $confirmedAt->format(self::DATE_FORMAT) : null,
'last_subscribed_at' => ($lastSubscribedAt = $subscriberEntity->getLastSubscribedAt()) ? $lastSubscribedAt->format(self::DATE_FORMAT) : null,
'unconfirmed_data' => $subscriberEntity->getUnconfirmedData(),
'source' => $subscriberEntity->getSource(),
'count_confirmations' => $subscriberEntity->getConfirmationsCount(),
'unsubscribe_token' => $subscriberEntity->getUnsubscribeToken(),
'link_token' => $subscriberEntity->getLinkToken(),
'tags' => $this->buildTags($subscriberEntity),
];
return $this->buildCustomFields($subscriberEntity, $data);
}
private function buildSubscriptions(SubscriberEntity $subscriberEntity): array {
$result = [];
foreach ($subscriberEntity->getSubscriberSegments() as $subscriberSegment) {
$segment = $subscriberSegment->getSegment();
if ($segment instanceof SegmentEntity) {
$result[] = [
'id' => $subscriberSegment->getId(),
'subscriber_id' => (string)$subscriberEntity->getId(),
'created_at' => ($createdAt = $subscriberSegment->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'segment_id' => (string)$segment->getId(),
'status' => $subscriberSegment->getStatus(),
'updated_at' => $subscriberSegment->getUpdatedAt()->format(self::DATE_FORMAT),
];
}
}
return $result;
}
private function buildUnsubscribes(SubscriberEntity $subscriberEntity): array {
$unsubscribes = $this->statisticsUnsubscribesRepository->findBy([
'subscriber' => $subscriberEntity,
], [
'createdAt' => 'desc',
]);
$result = [];
foreach ($unsubscribes as $unsubscribe) {
$mapped = [
'source' => $unsubscribe->getSource(),
'meta' => $unsubscribe->getMeta(),
'createdAt' => $unsubscribe->getCreatedAt(),
];
$newsletter = $unsubscribe->getNewsletter();
if ($newsletter instanceof NewsletterEntity) {
$mapped['newsletterId'] = $newsletter->getId();
$mapped['newsletterSubject'] = $newsletter->getSubject();
}
$result[] = $mapped;
}
return $result;
}
private function buildCustomFields(SubscriberEntity $subscriberEntity, array $data): array {
$customFields = $this->customFieldsRepository->findAll();
foreach ($customFields as $customField) {
$subscriberCustomField = $this->subscriberCustomFieldRepository->findOneBy(
['subscriber' => $subscriberEntity, 'customField' => $customField]
);
if ($subscriberCustomField instanceof SubscriberCustomFieldEntity) {
$data['cf_' . $customField->getId()] = $subscriberCustomField->getValue();
}
}
return $data;
}
private function buildTags(SubscriberEntity $subscriber): array {
$result = [];
foreach ($subscriber->getSubscriberTags() as $subscriberTag) {
$tag = $subscriberTag->getTag();
if (!$tag) {
continue;
}
$result[] = [
'id' => $subscriberTag->getId(),
'subscriber_id' => (string)$subscriber->getId(),
'tag_id' => (string)$tag->getId(),
'created_at' => ($createdAt = $subscriberTag->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $subscriberTag->getUpdatedAt()->format(self::DATE_FORMAT),
'name' => $tag->getName(),
];
}
return $result;
}
/**
* @param SubscriberEntity[] $subscribers
*/
private function prefetchRelations(array $subscribers): void {
// Prefetch subscriptions
$this->entityManager->createQueryBuilder()
->select('PARTIAL s.{id}, ssg, sg')
->from(SubscriberEntity::class, 's')
->leftJoin('s.subscriberSegments', 'ssg')
->leftJoin('ssg.segment', 'sg')
->where('s.id IN (:subscribers)')
->setParameter('subscribers', $subscribers)
->getQuery()
->getResult();
// Prefetch tags
$this->entityManager->createQueryBuilder()
->select('PARTIAL s.{id}, st, t')
->from(SubscriberEntity::class, 's')
->leftJoin('s.subscriberTags', 'st')
->leftJoin('st.tag', 't')
->where('s.id IN (:subscribers)')
->setParameter('subscribers', $subscribers)
->getQuery()
->getResult();
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,27 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON;
if (!defined('ABSPATH')) exit;
class SuccessResponse extends Response {
public $data;
public function __construct(
$data = [],
$meta = [],
$status = self::STATUS_OK
) {
parent::__construct($status, $meta);
$this->data = $data;
}
public function getData() {
if ($this->data === null) return [];
return [
'data' => $this->data,
];
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,30 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\Analytics\Reporter;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\AccessControl;
class Analytics extends APIEndpoint {
/** @var Reporter */
private $reporter;
public $permissions = [
'global' => AccessControl::NO_ACCESS_RESTRICTION,
];
public function __construct(
Reporter $reporter
) {
$this->reporter = $reporter;
}
public function getTrackingData() {
return $this->successResponse($this->reporter->getTrackingData());
}
}
@@ -0,0 +1,135 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\Config\AccessControl;
use MailPoet\Newsletter\AutomatedLatestContent as ALC;
use MailPoet\Newsletter\BlockPostQuery;
use MailPoet\Util\APIPermissionHelper;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Posts as WPPosts;
class AutomatedLatestContent extends APIEndpoint {
/** @var ALC */
public $ALC;
/*** @var WPFunctions */
private $wp;
/*** @var APIPermissionHelper */
private $permissionHelper;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
public function __construct(
ALC $alc,
APIPermissionHelper $permissionHelper,
WPFunctions $wp
) {
$this->ALC = $alc;
$this->wp = $wp;
$this->permissionHelper = $permissionHelper;
}
public function getPostTypes() {
$postTypes = array_map(function($postType) {
return [
'name' => $postType->name,
'label' => $postType->label,
];
}, WPPosts::getTypes([], 'objects'));
return $this->successResponse(
array_filter($postTypes)
);
}
public function getTaxonomies($data = []) {
$postType = (isset($data['postType'])) ? $data['postType'] : 'post';
$allTaxonomies = WPFunctions::get()->getObjectTaxonomies($postType, 'objects');
$taxonomiesWithLabel = array_filter($allTaxonomies, function($taxonomy) {
return $taxonomy->label;
});
return $this->successResponse($taxonomiesWithLabel);
}
public function getTerms($data = []) {
$taxonomies = (isset($data['taxonomies'])) ? $data['taxonomies'] : [];
$search = (isset($data['search'])) ? $data['search'] : '';
$limit = (isset($data['limit'])) ? (int)$data['limit'] : 100;
$page = (isset($data['page'])) ? (int)$data['page'] : 1;
$args = [
'taxonomy' => $taxonomies,
'hide_empty' => false,
'search' => $search,
'number' => $limit,
'offset' => $limit * ($page - 1),
'orderby' => 'name',
'order' => 'ASC',
];
$args = (array)$this->wp->applyFilters('mailpoet_search_terms_args', $args);
$terms = WPFunctions::get()->getTerms($args);
return $this->successResponse(array_values($terms));
}
/**
* Fetches posts for Posts static block
*/
public function getPosts(array $data = []): SuccessResponse {
return $this->successResponse(
$this->getPermittedPosts($this->ALC->getPosts(new BlockPostQuery(['args' => $data, 'dynamic' => false])))
);
}
/**
* Fetches products for Abandoned Cart Content dynamic block
*/
public function getTransformedPosts(array $data = []): SuccessResponse {
$posts = $this->getPermittedPosts($this->ALC->getPosts(new BlockPostQuery([
'args' => $data,
// If the request is for Posts or Products block then we are fetching data for a static block
'dynamic' => !(isset($data['type']) && in_array($data['type'], ["posts", "products"])),
])));
return $this->successResponse(
$this->ALC->transformPosts($data, $posts)
);
}
/**
* Fetches different post types for ALC dynamic block
*/
public function getBulkTransformedPosts(array $data = []): SuccessResponse {
$usedPosts = [];
$renderedPosts = [];
foreach ($data['blocks'] as $block) {
$query = new BlockPostQuery(['args' => $block, 'postsToExclude' => $usedPosts]);
$posts = $this->getPermittedPosts($this->ALC->getPosts($query));
$renderedPosts[] = $this->ALC->transformPosts($block, $posts);
foreach ($posts as $post) {
$usedPosts[] = $post->ID;
}
}
return $this->successResponse($renderedPosts);
}
/**
* @param \WP_Post[] $posts
* @return \WP_Post[]
*/
private function getPermittedPosts($posts) {
return array_filter($posts, function ($post) {
return $this->permissionHelper->checkReadPermission($post);
});
}
}
@@ -0,0 +1,90 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\AutomaticEmails\AutomaticEmails as AutomaticEmailsController;
use MailPoet\Config\AccessControl;
use MailPoet\WP\Functions as WPFunctions;
class AutomaticEmails extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS,
];
/** @var AutomaticEmailsController */
private $automaticEmails;
/** @var WPFunctions */
private $wp;
public function __construct(
AutomaticEmailsController $automaticEmails,
WPFunctions $wp
) {
$this->automaticEmails = $automaticEmails;
$this->wp = $wp;
}
public function getEventOptions($data) {
$query = (!empty($data['query'])) ? $data['query'] : null;
$filter = (!empty($data['filter'])) ? $data['filter'] : null;
$emailSlug = (!empty($data['email_slug'])) ? $data['email_slug'] : null;
$eventSlug = (!empty($data['event_slug'])) ? $data['event_slug'] : null;
if (!$query || !$filter || !$emailSlug || !$eventSlug) {
return $this->errorResponse(
[
APIError::BAD_REQUEST => __('Improperly formatted request.', 'mailpoet'),
]
);
}
$event = $this->automaticEmails->getAutomaticEmailEventBySlug($emailSlug, $eventSlug);
$eventFilter = (!empty($event['options']['remoteQueryFilter'])) ? $event['options']['remoteQueryFilter'] : null;
return ($eventFilter === $filter && WPFunctions::get()->hasFilter($eventFilter)) ?
$this->successResponse($this->wp->applyFilters($eventFilter, $query)) :
$this->errorResponse(
[
APIError::BAD_REQUEST => __('Automatic email event filter does not exist.', 'mailpoet'),
]
);
}
public function getEventShortcodes($data) {
$emailSlug = (!empty($data['email_slug'])) ? $data['email_slug'] : null;
$eventSlug = (!empty($data['event_slug'])) ? $data['event_slug'] : null;
if (!$emailSlug || !$eventSlug) {
return $this->errorResponse(
[
APIError::BAD_REQUEST => __('Improperly formatted request.', 'mailpoet'),
]
);
}
$automaticEmail = $this->automaticEmails->getAutomaticEmailBySlug($emailSlug);
$event = $this->automaticEmails->getAutomaticEmailEventBySlug($emailSlug, $eventSlug);
if (!$event) {
return $this->errorResponse(
[
APIError::BAD_REQUEST => __('Automatic email event does not exist.', 'mailpoet'),
]
);
}
$eventShortcodes = (!empty($event['shortcodes']) && is_array($event['shortcodes'])) ?
[
$automaticEmail['title'] => $event['shortcodes'],
] :
null;
return $this->successResponse($eventShortcodes);
}
}
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Captcha\CaptchaSession;
use MailPoet\Captcha\CaptchaUrlFactory;
use MailPoet\Config\AccessControl;
class Captcha extends APIEndpoint {
private CaptchaSession $captchaSession;
private CaptchaUrlFactory $urlFactory;
public $permissions = [
'global' => AccessControl::NO_ACCESS_RESTRICTION,
];
public function __construct(
CaptchaSession $captchaSession,
CaptchaUrlFactory $urlFactory
) {
$this->captchaSession = $captchaSession;
$this->urlFactory = $urlFactory;
}
public function render(array $data = []) {
$sessionId = $this->captchaSession->generateSessionId();
$data = array_merge($data, ['captcha_session_id' => $sessionId]);
$captchaUrl = $this->urlFactory->getCaptchaUrl($data);
return $this->redirectResponse($captchaUrl);
}
}
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\Config\AccessControl;
use MailPoet\WooCommerce\Helper;
use MailPoet\WP\Functions as WPFunctions;
class Coupons extends APIEndpoint {
public const DEFAULT_PAGE_SIZE = 100;
/** @var Helper */
public $helper;
/*** @var WPFunctions */
private $wp;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
public function __construct(
WPFunctions $wp,
Helper $helper
) {
$this->wp = $wp;
$this->helper = $helper;
}
public function getCoupons(array $data = []): SuccessResponse {
$pageSize = $data['page_size'] ?? self::DEFAULT_PAGE_SIZE;
$pageNumber = $data['page_number'] ?? 1;
$discountType = $data['discount_type'] ?? null;
$search = $data['search'] ?? null;
$includeCouponIds = $data['include_coupon_ids'] ?? [];
return $this->successResponse(
$this->formatCoupons($this->helper->getCouponList(
(int)$pageSize,
(int)$pageNumber,
$discountType,
$search,
$includeCouponIds
))
);
}
/**
* @param array $couponPosts
* @return array
*/
private function formatCoupons(array $couponPosts): array {
return array_map(function (\WP_Post $post): array {
$discountType = $this->wp->getPostMeta($post->ID, 'discount_type', true);
return [
'id' => $post->ID,
'text' => $post->post_title, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'excerpt' => $post->post_excerpt, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'discountType' => $discountType,
];
}, $couponPosts);
}
}
@@ -0,0 +1,83 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\CustomFieldsResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\CustomFields\CustomFieldsRepository;
use MailPoet\Entities\CustomFieldEntity;
use MailPoet\Form\ApiDataSanitizer;
class CustomFields extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_FORMS,
];
/** @var CustomFieldsRepository */
private $customFieldsRepository;
/** @var CustomFieldsResponseBuilder */
private $customFieldsResponseBuilder;
/** @var ApiDataSanitizer */
private $dataSanitizer;
public function __construct(
CustomFieldsRepository $customFieldsRepository,
CustomFieldsResponseBuilder $customFieldsResponseBuilder,
ApiDataSanitizer $dataSanitizer
) {
$this->customFieldsRepository = $customFieldsRepository;
$this->customFieldsResponseBuilder = $customFieldsResponseBuilder;
$this->dataSanitizer = $dataSanitizer;
}
public function getAll() {
$collection = $this->customFieldsRepository->findBy([], ['createdAt' => 'asc']);
return $this->successResponse($this->customFieldsResponseBuilder->buildBatch($collection));
}
public function delete($data = []) {
$id = (isset($data['id']) ? (int)$data['id'] : null);
$customField = $this->customFieldsRepository->findOneById($id);
if ($customField instanceof CustomFieldEntity) {
$this->customFieldsRepository->remove($customField);
$this->customFieldsRepository->flush();
return $this->successResponse($this->customFieldsResponseBuilder->build($customField));
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This custom field does not exist.', 'mailpoet'),
]);
}
}
public function save($data = []) {
try {
$data = $this->dataSanitizer->sanitizeBlock($data);
$customField = $this->customFieldsRepository->createOrUpdate($data);
$customField = $this->customFieldsRepository->findOneById($customField->getId());
if(!$customField instanceof CustomFieldEntity) return $this->errorResponse();
return $this->successResponse($this->customFieldsResponseBuilder->build($customField));
} catch (\Exception $e) {
return $this->errorResponse($errors = [], $meta = [], $status = Response::STATUS_BAD_REQUEST);
}
}
public function get($data = []) {
$id = (isset($data['id']) ? (int)$data['id'] : null);
$customField = $this->customFieldsRepository->findOneById($id);
if ($customField instanceof CustomFieldEntity) {
return $this->successResponse($this->customFieldsResponseBuilder->build($customField));
}
return $this->errorResponse([
APIError::NOT_FOUND => __('This custom field does not exist.', 'mailpoet'),
]);
}
}
@@ -0,0 +1,317 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\DynamicSegmentsResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\ConflictException;
use MailPoet\Doctrine\Validator\ValidationException;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Listing\Handler;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Segments\DynamicSegments\DynamicSegmentsListingRepository;
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\Segments\DynamicSegments\FilterDataMapper;
use MailPoet\Segments\DynamicSegments\SegmentSaveController;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\SegmentSubscribersRepository;
use MailPoet\UnexpectedValueException;
use Throwable;
class DynamicSegments extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS,
];
/** @var Handler */
private $listingHandler;
/** @var DynamicSegmentsListingRepository */
private $dynamicSegmentsListingRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var DynamicSegmentsResponseBuilder */
private $segmentsResponseBuilder;
/** @var SegmentSaveController */
private $saveController;
/** @var SegmentSubscribersRepository */
private $segmentSubscribersRepository;
/** @var FilterDataMapper */
private $filterDataMapper;
/** @var NewsletterSegmentRepository */
private $newsletterSegmentRepository;
public function __construct(
Handler $handler,
DynamicSegmentsListingRepository $dynamicSegmentsListingRepository,
DynamicSegmentsResponseBuilder $segmentsResponseBuilder,
SegmentsRepository $segmentsRepository,
SegmentSubscribersRepository $segmentSubscribersRepository,
FilterDataMapper $filterDataMapper,
SegmentSaveController $saveController,
NewsletterSegmentRepository $newsletterSegmentRepository
) {
$this->listingHandler = $handler;
$this->dynamicSegmentsListingRepository = $dynamicSegmentsListingRepository;
$this->segmentsResponseBuilder = $segmentsResponseBuilder;
$this->segmentsRepository = $segmentsRepository;
$this->saveController = $saveController;
$this->segmentSubscribersRepository = $segmentSubscribersRepository;
$this->filterDataMapper = $filterDataMapper;
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
}
public function get($data = []) {
if (isset($data['id'])) {
$id = (int)$data['id'];
} else {
return $this->errorResponse([
Error::BAD_REQUEST => __('Missing mandatory argument `id`.', 'mailpoet'),
]);
}
$segment = $this->segmentsRepository->findOneById($id);
if (!$segment instanceof SegmentEntity) {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
return $this->successResponse($this->segmentsResponseBuilder->build($segment));
}
public function getCount($data = []) {
try {
$filterData = $this->filterDataMapper->map($data);
$count = $this->segmentSubscribersRepository->getDynamicSubscribersCount($filterData);
return $this->successResponse([
'count' => $count,
]);
} catch (InvalidFilterException $e) {
return $this->errorResponse([
Error::BAD_REQUEST => $this->getErrorString($e),
], [], Response::STATUS_BAD_REQUEST);
}
}
public function save($data) {
try {
$data['name'] = isset($data['name']) ? sanitize_text_field($data['name']) : '';
$data['description'] = isset($data['description']) ? sanitize_textarea_field($data['description']) : '';
$segment = $this->saveController->save($data);
return $this->successResponse($this->segmentsResponseBuilder->build($segment));
} catch (InvalidFilterException $e) {
return $this->errorResponse([
Error::BAD_REQUEST => $this->getErrorString($e),
], [], Response::STATUS_BAD_REQUEST);
} catch (ConflictException $e) {
return $this->badRequest([
Error::BAD_REQUEST => __('Another record already exists. Please specify a different "name".', 'mailpoet'),
]);
} catch (ValidationException $exception) {
return $this->badRequest([
Error::BAD_REQUEST => __('Please specify a name.', 'mailpoet'),
]);
}
}
public function duplicate($data = []) {
$segment = $this->getSegment($data);
if ($segment instanceof SegmentEntity) {
try {
$duplicate = $this->saveController->duplicate($segment);
} catch (Throwable $e) {
return $this->errorResponse([
// translators: %s is the error message
Error::UNKNOWN => sprintf(__('Duplicating of segment failed: %s', 'mailpoet'), $e->getMessage()),
], [], Response::STATUS_UNKNOWN);
}
return $this->successResponse(
$this->segmentsResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
}
private function getErrorString(InvalidFilterException $e) {
switch ($e->getCode()) {
case InvalidFilterException::MISSING_TYPE:
return __('The segment type is missing.', 'mailpoet');
case InvalidFilterException::INVALID_TYPE:
return __('The segment type is unknown.', 'mailpoet');
case InvalidFilterException::MISSING_ROLE:
return __('Please select a user role.', 'mailpoet');
case InvalidFilterException::MISSING_ACTION:
case InvalidFilterException::INVALID_EMAIL_ACTION:
return __('Please select an email action.', 'mailpoet');
case InvalidFilterException::MISSING_NEWSLETTER_ID:
return __('Please select an email.', 'mailpoet');
case InvalidFilterException::MISSING_PRODUCT_ID:
return __('Please select a product.', 'mailpoet');
case InvalidFilterException::MISSING_COUNTRY:
return __('Please select a country.', 'mailpoet');
case InvalidFilterException::MISSING_CATEGORY_ID:
return __('Please select a category.', 'mailpoet');
case InvalidFilterException::MISSING_VALUE:
return __('Please fill all required values.', 'mailpoet');
case InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS:
return __('Please select a type for the comparison, a number of orders and a number of days.', 'mailpoet');
case InvalidFilterException::MISSING_TOTAL_SPENT_FIELDS:
case InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS:
case InvalidFilterException::MISSING_AVERAGE_SPENT_FIELDS:
return __('Please select a type for the comparison, an amount and a number of days.', 'mailpoet');
case InvalidFilterException::MISSING_FILTER:
return __('Please add at least one condition for filtering.', 'mailpoet');
case InvalidFilterException::MISSING_OPERATOR:
return __('Please select a type for the comparison.', 'mailpoet');
default:
return __('An error occurred while saving data.', 'mailpoet');
}
}
public function trash($data = []) {
if (!isset($data['id'])) {
return $this->errorResponse([
Error::BAD_REQUEST => __('Missing mandatory argument `id`.', 'mailpoet'),
]);
}
$segment = $this->getSegment($data);
if ($segment === null) {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
$activelyUsedErrors = $this->getErrorMessagesForSegmentsUsedInActiveNewsletters([$segment->getId()]);
if (count($activelyUsedErrors) > 0) {
return $this->badRequest($activelyUsedErrors);
}
$this->segmentsRepository->bulkTrash([$segment->getId()], SegmentEntity::TYPE_DYNAMIC);
return $this->successResponse(
$this->segmentsResponseBuilder->build($segment),
['count' => 1]
);
}
public function getErrorMessagesForSegmentsUsedInActiveNewsletters(array $segmentIds): array {
$errors = [];
$activelyUsedNewslettersSubjects = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($segmentIds);
foreach ($segmentIds as $segmentId) {
if (isset($activelyUsedNewslettersSubjects[$segmentId])) {
$segment = $this->getSegment(['id' => $segmentId]);
if ($segment) {
$errors[] = sprintf(
// translators: %1$s is the name of the segment, %2$s is a comma-seperated list of emails for which the segment is used.
_x('Segment \'%1$s\' cannot be deleted because its used for \'%2$s\' email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet'),
$segment->getName(),
join("', '", $activelyUsedNewslettersSubjects[$segmentId])
);
}
}
}
return $errors;
}
public function restore($data = []) {
if (!isset($data['id'])) {
return $this->errorResponse([
Error::BAD_REQUEST => __('Missing mandatory argument `id`.', 'mailpoet'),
]);
}
$segment = $this->getSegment($data);
if ($segment === null) {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
$this->segmentsRepository->bulkRestore([$segment->getId()], SegmentEntity::TYPE_DYNAMIC);
return $this->successResponse(
$this->segmentsResponseBuilder->build($segment),
['count' => 1]
);
}
public function delete($data = []) {
if (!isset($data['id'])) {
return $this->errorResponse([
Error::BAD_REQUEST => __('Missing mandatory argument `id`.', 'mailpoet'),
]);
}
$segment = $this->getSegment($data);
if ($segment === null) {
return $this->errorResponse([
Error::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
$this->segmentsRepository->bulkDelete([$segment->getId()], SegmentEntity::TYPE_DYNAMIC);
return $this->successResponse(null, ['count' => 1]);
}
public function listing($data = []) {
$data['params'] = $data['params'] ?? ['segments']; // Dummy param to apply constraints properly
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->dynamicSegmentsListingRepository->getData($definition);
$count = $this->dynamicSegmentsListingRepository->getCount($definition);
$filters = $this->dynamicSegmentsListingRepository->getFilters($definition);
$groups = $this->dynamicSegmentsListingRepository->getGroups($definition);
$segments = $this->segmentsResponseBuilder->buildForListing($items);
return $this->successResponse($segments, [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
]);
}
public function bulkAction($data = []) {
$definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->dynamicSegmentsListingRepository->getActionableIds($definition);
$meta = [];
if ($data['action'] === 'trash') {
$errors = $this->getErrorMessagesForSegmentsUsedInActiveNewsletters($ids);
if (count($errors) > 0) {
$meta['errors'] = $errors;
}
$meta['count'] = $this->segmentsRepository->bulkTrash($ids, SegmentEntity::TYPE_DYNAMIC);
} elseif ($data['action'] === 'restore') {
$meta['count'] = $this->segmentsRepository->bulkRestore($ids, SegmentEntity::TYPE_DYNAMIC);
} elseif ($data['action'] === 'delete') {
$meta['count'] = $this->segmentsRepository->bulkDelete($ids, SegmentEntity::TYPE_DYNAMIC);
} else {
throw UnexpectedValueException::create()
->withErrors([Error::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
}
return $this->successResponse(null, $meta);
}
private function getSegment(array $data): ?SegmentEntity {
return isset($data['id'])
? $this->segmentsRepository->findOneById((int)$data['id'])
: null;
}
}
@@ -0,0 +1,53 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\AccessControl;
use MailPoet\Features\FeatureFlagsController;
use MailPoet\Features\FeaturesController;
class FeatureFlags extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_FEATURES,
];
/** @var FeaturesController */
private $featuresController;
/** @var FeatureFlagsController */
private $featureFlagsController;
public function __construct(
FeaturesController $featuresController,
FeatureFlagsController $featureFlags
) {
$this->featuresController = $featuresController;
$this->featureFlagsController = $featureFlags;
}
public function getAll() {
$featureFlags = $this->featureFlagsController->getAll();
return $this->successResponse($featureFlags);
}
public function set(array $flags) {
foreach ($flags as $name => $value) {
if (!$this->featuresController->exists($name)) {
return $this->badRequest([
APIError::BAD_REQUEST => "Feature '$name' does not exist'",
]);
}
}
foreach ($flags as $name => $value) {
$this->featureFlagsController->set($name, (bool)$value);
}
return $this->successResponse([]);
}
}
@@ -0,0 +1,361 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use Exception;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\FormsResponseBuilder;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\Config\AccessControl;
use MailPoet\Entities\FormEntity;
use MailPoet\Form\ApiDataSanitizer;
use MailPoet\Form\DisplayFormInWPContent;
use MailPoet\Form\FormSaveController;
use MailPoet\Form\FormsRepository;
use MailPoet\Form\Listing\FormListingRepository;
use MailPoet\Form\PreviewPage;
use MailPoet\Form\Templates\TemplateRepository;
use MailPoet\Listing;
use MailPoet\Settings\UserFlagsController;
use MailPoet\Tags\TagRepository;
use MailPoet\UnexpectedValueException;
use MailPoet\WP\Emoji;
use MailPoet\WP\Functions as WPFunctions;
class Forms extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_FORMS,
];
/** @var Listing\Handler */
private $listingHandler;
/** @var UserFlagsController */
private $userFlags;
/** @var FormsResponseBuilder */
private $formsResponseBuilder;
/** @var WPFunctions */
private $wp;
/** @var FormsRepository */
private $formsRepository;
/** @var TemplateRepository */
private $templateRepository;
/** @var FormListingRepository */
private $formListingRepository;
/** @var Emoji */
private $emoji;
/** @var ApiDataSanitizer */
private $dataSanitizer;
/** @var TagRepository */
private $tagRepository;
/** @var FormSaveController */
private $formSaveController;
public function __construct(
Listing\Handler $listingHandler,
UserFlagsController $userFlags,
FormsRepository $formsRepository,
TemplateRepository $templateRepository,
FormListingRepository $formListingRepository,
FormsResponseBuilder $formsResponseBuilder,
WPFunctions $wp,
Emoji $emoji,
ApiDataSanitizer $dataSanitizer,
TagRepository $tagRepository,
FormSaveController $formSaveController
) {
$this->listingHandler = $listingHandler;
$this->userFlags = $userFlags;
$this->wp = $wp;
$this->formsRepository = $formsRepository;
$this->templateRepository = $templateRepository;
$this->formListingRepository = $formListingRepository;
$this->formsResponseBuilder = $formsResponseBuilder;
$this->emoji = $emoji;
$this->dataSanitizer = $dataSanitizer;
$this->tagRepository = $tagRepository;
$this->formSaveController = $formSaveController;
}
public function get($data = []) {
$id = (isset($data['id']) ? (int)$data['id'] : false);
$form = $this->formsRepository->findOneById($id);
if ($form instanceof FormEntity) {
return $this->successResponse($this->formsResponseBuilder->build($form));
}
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
public function setStatus($data = []) {
$status = (isset($data['status']) ? $data['status'] : null);
if (!$status) {
return $this->badRequest([
APIError::BAD_REQUEST => __('You need to specify a status.', 'mailpoet'),
]);
}
$id = (isset($data['id'])) ? (int)$data['id'] : false;
$form = $this->formsRepository->findOneById($id);
if (!$form instanceof FormEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
if (!in_array($status, [FormEntity::STATUS_ENABLED, FormEntity::STATUS_DISABLED])) {
return $this->badRequest([
APIError::BAD_REQUEST =>
sprintf(
// translators: %1$s is a comma-seperated list of allowed values, %2$s the status the user specified.
__('Invalid status. Allowed values are (%1$s), you specified %2$s', 'mailpoet'),
join(', ', [FormEntity::STATUS_ENABLED, FormEntity::STATUS_DISABLED]),
$status
),
]);
}
$form->setStatus($status);
$this->formsRepository->flush();
if ($status === FormEntity::STATUS_ENABLED) {
$this->wp->deleteTransient(DisplayFormInWPContent::NO_FORM_TRANSIENT_KEY);
}
$form = $this->formsRepository->findOneById($id);
if (!$form instanceof FormEntity) return $this->errorResponse();
return $this->successResponse(
$form->toArray()
);
}
public function listing($data = []) {
$data['sort_order'] = $data['sort_order'] ?? 'desc';
$data['sort_by'] = $data['sort_by'] ?? 'updatedAt';
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->formListingRepository->getData($definition);
$count = $this->formListingRepository->getCount($definition);
$filters = $this->formListingRepository->getFilters($definition);
$groups = $this->formListingRepository->getGroups($definition);
return $this->successResponse($this->formsResponseBuilder->buildForListing($items), [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
]);
}
public function previewEditor($data = []) {
// We want to allow preview for unsaved forms
$formId = $data['id'] ?? 0;
$this->wp->setTransient(PreviewPage::PREVIEW_DATA_TRANSIENT_PREFIX . $formId, $data, PreviewPage::PREVIEW_DATA_EXPIRATION);
return $this->successResponse();
}
public function saveEditor($data = []) {
$formId = (isset($data['id']) ? (int)$data['id'] : 0);
$initialForm = $this->getFormTemplateData(TemplateRepository::INITIAL_FORM_TEMPLATE);
$name = ($data['name'] ?? __('New form', 'mailpoet'));
$body = ($data['body'] ?? $initialForm['body']);
$body = $this->dataSanitizer->sanitizeBody($body);
$settings = ($data['settings'] ?? $initialForm['settings']);
$styles = ($data['styles'] ?? $initialForm['styles']);
$status = ($data['status'] ?? FormEntity::STATUS_ENABLED);
// check if the form is used as a widget
$isWidget = false;
$widgets = $this->wp->getOption('widget_mailpoet_form');
if (!empty($widgets)) {
foreach ($widgets as $widget) {
if (isset($widget['form']) && (int)$widget['form'] === $formId) {
$isWidget = true;
break;
}
}
}
// Reset no form cache
$this->wp->deleteTransient(DisplayFormInWPContent::NO_FORM_TRANSIENT_KEY);
// check if the user gets to pick his own lists
// or if it's selected by the admin
$formEntity = new FormEntity($name);
$formEntity->setBody($body);
$listSelection = $formEntity->getSegmentBlocksSegmentIds();
// check list selection
if (count($listSelection)) {
$settings['segments_selected_by'] = 'user';
$settings['segments'] = $listSelection;
} else {
$settings['segments_selected_by'] = 'admin';
}
// check tags and create them if they don't exist
if (isset($settings['tags'])) {
$this->createTagsIfDoNotExist($settings['tags']);
}
// Check Custom HTML block permissions
$customHtmlBlocks = $formEntity->getBlocksByTypes([FormEntity::HTML_BLOCK_TYPE]);
if (count($customHtmlBlocks) && !$this->wp->currentUserCan('administrator')) {
return $this->errorResponse([
Error::FORBIDDEN => __('Only administrator can edit forms containing Custom HTML block.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
if ($body !== null) {
$body = $this->emoji->sanitizeEmojisInFormBody($body);
}
$form = $this->getForm($data);
if (!$form instanceof FormEntity) {
$form = new FormEntity($name);
}
$form->setName($name);
$form->setBody($body);
$form->setSettings($settings);
$form->setStyles($styles);
$form->setStatus($status);
$this->formsRepository->persist($form);
try {
$this->formsRepository->flush();
} catch (\Exception $e) {
return $this->badRequest();
}
if (isset($data['editor_version']) && $data['editor_version'] === "2") {
$this->userFlags->set('display_new_form_editor_nps_survey', true);
}
$form = $this->getForm(['id' => $form->getId()]);
if(!$form instanceof FormEntity) return $this->errorResponse();
return $this->successResponse(
$this->formsResponseBuilder->build($form),
['is_widget' => $isWidget]
);
}
public function restore($data = []) {
$form = $this->getForm($data);
if ($form instanceof FormEntity) {
$this->formsRepository->restore($form);
return $this->successResponse(
$form->toArray(),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
}
public function trash($data = []) {
$form = $this->getForm($data);
if ($form instanceof FormEntity) {
$this->formsRepository->trash($form);
return $this->successResponse(
$form->toArray(),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
}
public function delete($data = []) {
$form = $this->getForm($data);
if ($form instanceof FormEntity) {
$this->formsRepository->delete($form);
return $this->successResponse(null, ['count' => 1]);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
}
public function duplicate($data = []) {
$form = $this->getForm($data);
if ($form instanceof FormEntity) {
try {
$duplicate = $this->formSaveController->duplicate($form);
} catch (Exception $e) {
return $this->errorResponse([
APIError::UNKNOWN => __('Duplicating form failed.', 'mailpoet'),
], [], Response::STATUS_UNKNOWN);
}
return $this->successResponse(
$this->formsResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This form does not exist.', 'mailpoet'),
]);
}
}
public function bulkAction($data = []): SuccessResponse {
$definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->formListingRepository->getActionableIds($definition);
if ($data['action'] === 'trash') {
$this->formsRepository->bulkTrash($ids);
} elseif ($data['action'] === 'restore') {
$this->formsRepository->bulkRestore($ids);
} elseif ($data['action'] === 'delete') {
$this->formsRepository->bulkDelete($ids);
} else {
throw UnexpectedValueException::create()
->withErrors([APIError::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
}
return $this->successResponse(null, ['count' => count($ids)]);
}
private function getForm(array $data): ?FormEntity {
return isset($data['id'])
? $this->formsRepository->findOneById((int)$data['id'])
: null;
}
private function getFormTemplateData(string $templateId): array {
$formTemplate = $this->templateRepository->getFormTemplate($templateId);
$form = $formTemplate->toFormEntity();
return $form->toArray();
}
private function createTagsIfDoNotExist(array $tagNames): void {
foreach ($tagNames as $tagName) {
$this->tagRepository->createOrUpdate(['name' => $tagName]);
}
}
}
@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\Config\AccessControl;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Util\DataInconsistency\DataInconsistencyController;
class Help extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_HELP,
];
private ScheduledTasksRepository $scheduledTasksRepository;
private DataInconsistencyController $dataInconsistencyController;
public function __construct(
ScheduledTasksRepository $scheduledTasksRepository,
DataInconsistencyController $dataInconsistencyController
) {
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->dataInconsistencyController = $dataInconsistencyController;
}
public function cancelTask($data): Response {
try {
$this->validateTaskId($data);
$task = $this->scheduledTasksRepository->findOneById($data['id']);
if (!$task instanceof ScheduledTaskEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Task not found.', 'mailpoet'),
]);
}
$this->scheduledTasksRepository->cancelTask($task);
return $this->successResponse();
} catch (\Exception $e) {
return $this->badRequest([ApiError::BAD_REQUEST => $e->getMessage()]);
}
}
public function rescheduleTask($data): Response {
try {
$this->validateTaskId($data);
$task = $this->scheduledTasksRepository->findOneById($data['id']);
if (!$task instanceof ScheduledTaskEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Task not found.', 'mailpoet'),
]);
}
$this->scheduledTasksRepository->rescheduleTask($task);
return $this->successResponse();
} catch (\Exception $e) {
return $this->badRequest([ApiError::BAD_REQUEST => $e->getMessage()]);
}
}
public function getInconsistentDataStatus(): Response {
return $this->successResponse($this->dataInconsistencyController->getInconsistentDataStatus());
}
public function fixInconsistentData($data): Response {
try {
$this->dataInconsistencyController->fixInconsistentData($data['inconsistency'] ?? '');
} catch (\Exception $e) {
return $this->badRequest([ApiError::BAD_REQUEST => $e->getMessage()]);
}
return $this->successResponse($this->dataInconsistencyController->getInconsistentDataStatus());
}
private function validateTaskId($data): void {
$isValid = isset($data['id']) && is_numeric($data['id']);
if (!$isValid) {
throw new \Exception(__('Invalid or missing parameter `id`.', 'mailpoet'));
}
}
}
@@ -0,0 +1,184 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\ResponseBuilders\SegmentsResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\ConflictException;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\Cron\Workers\WooCommerceSync;
use MailPoet\CustomFields\CustomFieldsRepository;
use MailPoet\Doctrine\Validator\ValidationException;
use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
use MailPoet\Segments\SegmentSaveController;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\WP;
use MailPoet\Services\Validator;
use MailPoet\Subscribers\ImportExport\Export\Export;
use MailPoet\Subscribers\ImportExport\Import\Import;
use MailPoet\Subscribers\ImportExport\Import\MailChimp;
use MailPoet\Subscribers\ImportExport\ImportExportRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tags\TagRepository;
class ImportExport extends APIEndpoint {
/** @var WP */
private $wpSegment;
/** @var CustomFieldsRepository */
private $customFieldsRepository;
/** @var ImportExportRepository */
private $importExportRepository;
/** @var NewsletterOptionsRepository */
private $newsletterOptionsRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscribersRepository */
private $subscriberRepository;
/** @var SegmentSaveController */
private $segmentSavecontroller;
/** @var SegmentsResponseBuilder */
private $segmentsResponseBuilder;
/** @var TagRepository */
private $tagRepository;
/** @var Validator */
private $validator;
/** @var CronWorkerScheduler */
private $cronWorkerScheduler;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SUBSCRIBERS,
];
public function __construct(
WP $wpSegment,
CustomFieldsRepository $customFieldsRepository,
ImportExportRepository $importExportRepository,
NewsletterOptionsRepository $newsletterOptionsRepository,
SegmentsRepository $segmentsRepository,
SegmentSaveController $segmentSavecontroller,
SegmentsResponseBuilder $segmentsResponseBuilder,
CronWorkerScheduler $cronWorkerScheduler,
SubscribersRepository $subscribersRepository,
TagRepository $tagRepository,
Validator $validator
) {
$this->wpSegment = $wpSegment;
$this->customFieldsRepository = $customFieldsRepository;
$this->importExportRepository = $importExportRepository;
$this->newsletterOptionsRepository = $newsletterOptionsRepository;
$this->segmentsRepository = $segmentsRepository;
$this->subscriberRepository = $subscribersRepository;
$this->segmentSavecontroller = $segmentSavecontroller;
$this->cronWorkerScheduler = $cronWorkerScheduler;
$this->segmentsResponseBuilder = $segmentsResponseBuilder;
$this->tagRepository = $tagRepository;
$this->validator = $validator;
}
public function getMailChimpLists($data) {
try {
$mailChimp = new MailChimp($data['api_key']);
$lists = $mailChimp->getLists();
return $this->successResponse($lists);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
public function getMailChimpSubscribers($data) {
try {
$mailChimp = new MailChimp($data['api_key']);
$subscribers = $mailChimp->getSubscribers($data['lists']);
return $this->successResponse($subscribers);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
public function addSegment($data) {
try {
$data['name'] = isset($data['name']) ? sanitize_text_field($data['name']) : '';
$data['description'] = isset($data['description']) ? sanitize_textarea_field($data['description']) : '';
$segment = $this->segmentSavecontroller->save($data);
$response = $this->segmentsResponseBuilder->build($segment);
return $this->successResponse($response);
} catch (ValidationException $exception) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Please specify a name.', 'mailpoet'),
]);
} catch (ConflictException $exception) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Another record already exists. Please specify a different "name".', 'mailpoet'),
]);
}
}
public function processImport($data) {
try {
$import = new Import(
$this->wpSegment,
$this->customFieldsRepository,
$this->importExportRepository,
$this->newsletterOptionsRepository,
$this->subscriberRepository,
$this->tagRepository,
$this->validator,
json_decode($data, true)
);
$process = $import->process();
return $this->successResponse($process);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
public function processExport($data) {
try {
$export = new Export(
$this->customFieldsRepository,
$this->importExportRepository,
$this->segmentsRepository,
json_decode($data, true)
);
$process = $export->process();
return $this->successResponse($process);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
public function setupWooCommerceInitialImport() {
try {
$this->cronWorkerScheduler->scheduleImmediatelyIfNotRunning(WooCommerceSync::TASK_TYPE);
return $this->successResponse();
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
}
@@ -0,0 +1,100 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\AccessControl;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\AuthorizedSenderDomainController;
use MailPoet\Settings\SettingsController;
class Mailer extends APIEndpoint {
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
/** @var SettingsController */
private $settings;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var MailerFactory */
private $mailerFactory;
/** @var AuthorizedSenderDomainController */
private $senderDomainController;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
public function __construct(
AuthorizedEmailsController $authorizedEmailsController,
SettingsController $settings,
MailerFactory $mailerFactory,
MetaInfo $mailerMetaInfo,
AuthorizedSenderDomainController $senderDomainController
) {
$this->authorizedEmailsController = $authorizedEmailsController;
$this->settings = $settings;
$this->mailerFactory = $mailerFactory;
$this->mailerMetaInfo = $mailerMetaInfo;
$this->senderDomainController = $senderDomainController;
}
public function send($data = []) {
try {
$mailer = $this->mailerFactory->buildMailer(
$data['mailer'] ?? null,
$data['sender'] ?? null,
$data['reply_to'] ?? null
);
// report this as 'sending_test' in metadata since this endpoint is only used to test sending methods for now
$extraParams = [
'meta' => $this->mailerMetaInfo->getSendingTestMetaInfo(),
];
$result = $mailer->send($data['newsletter'], $data['subscriber'], $extraParams);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
if ($result['response'] === false) {
$error = sprintf(
// translators: %s is the error message.
__('The email could not be sent: %s', 'mailpoet'),
$result['error']->getMessage()
);
return $this->errorResponse([APIError::BAD_REQUEST => $error]);
} else {
return $this->successResponse(null);
}
}
public function resumeSending() {
if ($this->settings->get(AuthorizedEmailsController::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING)) {
$this->authorizedEmailsController->checkAuthorizedEmailAddresses();
}
MailerLog::resumeSending();
return $this->successResponse(null);
}
public function getAuthorizedEmailAddresses() {
$authorizedEmails = $this->authorizedEmailsController->getAuthorizedEmailAddresses();
return $this->successResponse($authorizedEmails);
}
public function getVerifiedSenderDomains() {
$verifiedDomains = $this->senderDomainController->getVerifiedSenderDomains();
return $this->successResponse($verifiedDomains);
}
}
@@ -0,0 +1,38 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
class NewsletterLinks extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS,
];
/** @var NewsletterLinkRepository */
private $newsletterLinkRepository;
public function __construct(
NewsletterLinkRepository $newsletterLinkRepository
) {
$this->newsletterLinkRepository = $newsletterLinkRepository;
}
public function get($data = []) {
$links = $this->newsletterLinkRepository->findBy(['newsletter' => $data['newsletterId']]);
$response = [];
foreach ($links as $link) {
$response[] = [
'id' => $link->getId(),
'url' => $link->getUrl(),
];
}
return $this->successResponse($response);
}
}
@@ -0,0 +1,125 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\ResponseBuilders\NewsletterTemplatesResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\Newsletter\ApiDataSanitizer;
use MailPoet\Newsletter\NewsletterCoupon;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\NewsletterTemplates\NewsletterTemplatesRepository;
use MailPoet\NewsletterTemplates\ThumbnailSaver;
class NewsletterTemplates extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
protected static $getMethods = [
'getAll',
];
/** @var NewsletterTemplatesRepository */
private $newsletterTemplatesRepository;
/** @var NewsletterTemplatesResponseBuilder */
private $newsletterTemplatesResponseBuilder;
/** @var ThumbnailSaver */
private $thumbnailImageSaver;
/** @var ApiDataSanitizer */
private $apiDataSanitizer;
/** @var NewslettersRepository */
private $newsletterRepository;
/*** @var NewsletterCoupon */
private $newsletterCoupon;
public function __construct(
NewsletterTemplatesRepository $newsletterTemplatesRepository,
NewsletterTemplatesResponseBuilder $newsletterTemplatesResponseBuilder,
ThumbnailSaver $thumbnailImageSaver,
ApiDataSanitizer $apiDataSanitizer,
NewslettersRepository $newsletterRepository,
NewsletterCoupon $newsletterCoupon
) {
$this->newsletterTemplatesRepository = $newsletterTemplatesRepository;
$this->newsletterTemplatesResponseBuilder = $newsletterTemplatesResponseBuilder;
$this->thumbnailImageSaver = $thumbnailImageSaver;
$this->apiDataSanitizer = $apiDataSanitizer;
$this->newsletterRepository = $newsletterRepository;
$this->newsletterCoupon = $newsletterCoupon;
}
public function get($data = []) {
$template = isset($data['id'])
? $this->newsletterTemplatesRepository->findOneById((int)$data['id'])
: null;
if (!$template) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This template does not exist.', 'mailpoet'),
]);
}
$data = $this->newsletterTemplatesResponseBuilder->build($template);
return $this->successResponse($data);
}
public function getAll() {
$templates = $this->newsletterTemplatesRepository->findAllForListing();
$data = $this->newsletterTemplatesResponseBuilder->buildForListing($templates);
return $this->successResponse($data);
}
public function save($data = []) {
ignore_user_abort(true);
// Do not save templates for emails created via Gutenberg editor
$newsletterId = isset($data['newsletter_id']) ? (int)$data['newsletter_id'] : null;
if ($newsletterId) {
$newsletter = $this->newsletterRepository->findOneById($newsletterId);
if ($newsletter && $newsletter->getWpPostId() !== null) {
return $this->successResponse($data);
}
}
if (!empty($data['body'])) {
$body = $this->apiDataSanitizer->sanitizeBody(json_decode($data['body'], true));
$body = $this->newsletterCoupon->cleanupBodySensitiveData($body);
$data['body'] = json_encode($body);
}
try {
$template = $this->newsletterTemplatesRepository->createOrUpdate($data);
$template = $this->thumbnailImageSaver->ensureTemplateThumbnailFile($template);
if (!empty($data['categories']) && $data['categories'] === NewsletterTemplatesRepository::RECENTLY_SENT_CATEGORIES) {
$this->newsletterTemplatesRepository->cleanRecentlySent();
}
$data = $this->newsletterTemplatesResponseBuilder->build($template);
return $this->successResponse($data);
} catch (\Throwable $e) {
return $this->errorResponse();
}
}
public function delete($data = []) {
$template = isset($data['id'])
? $this->newsletterTemplatesRepository->findOneById((int)$data['id'])
: null;
if (!$template) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This template does not exist.', 'mailpoet'),
]);
}
$this->newsletterTemplatesRepository->remove($template);
$this->newsletterTemplatesRepository->flush();
return $this->successResponse(null, ['count' => 1]);
}
}
@@ -0,0 +1,438 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\NewslettersResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\CronHelper;
use MailPoet\Doctrine\Validator\ValidationException;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\InvalidStateException;
use MailPoet\Listing;
use MailPoet\Newsletter\Listing\NewsletterListingRepository;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewsletterSaveController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\NewsletterValidator;
use MailPoet\Newsletter\Preview\SendPreviewController;
use MailPoet\Newsletter\Preview\SendPreviewException;
use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
use MailPoet\Newsletter\Scheduler\Scheduler;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Settings\SettingsController;
use MailPoet\UnexpectedValueException;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Emoji;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class Newsletters extends APIEndpoint {
/** @var Listing\Handler */
private $listingHandler;
/** @var WPFunctions */
private $wp;
/** @var SettingsController */
private $settings;
/** @var CronHelper */
private $cronHelper;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterListingRepository */
private $newsletterListingRepository;
/** @var NewslettersResponseBuilder */
private $newslettersResponseBuilder;
/** @var PostNotificationScheduler */
private $postNotificationScheduler;
/** @var Emoji */
private $emoji;
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var SendPreviewController */
private $sendPreviewController;
/** @var NewsletterSaveController */
private $newsletterSaveController;
private NewsletterDeleteController $newsletterDeleteController;
/** @var NewsletterUrl */
private $newsletterUrl;
/** @var NewsletterValidator */
private $newsletterValidator;
/** @var Scheduler */
private $scheduler;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
public function __construct(
Listing\Handler $listingHandler,
WPFunctions $wp,
SettingsController $settings,
CronHelper $cronHelper,
NewslettersRepository $newslettersRepository,
NewsletterListingRepository $newsletterListingRepository,
NewslettersResponseBuilder $newslettersResponseBuilder,
PostNotificationScheduler $postNotificationScheduler,
SubscribersFeature $subscribersFeature,
Emoji $emoji,
SendPreviewController $sendPreviewController,
NewsletterSaveController $newsletterSaveController,
NewsletterDeleteController $newsletterDeleteController,
NewsletterUrl $newsletterUrl,
Scheduler $scheduler,
NewsletterValidator $newsletterValidator,
AuthorizedEmailsController $authorizedEmailsController
) {
$this->listingHandler = $listingHandler;
$this->wp = $wp;
$this->settings = $settings;
$this->cronHelper = $cronHelper;
$this->newslettersRepository = $newslettersRepository;
$this->newsletterListingRepository = $newsletterListingRepository;
$this->newslettersResponseBuilder = $newslettersResponseBuilder;
$this->postNotificationScheduler = $postNotificationScheduler;
$this->subscribersFeature = $subscribersFeature;
$this->emoji = $emoji;
$this->sendPreviewController = $sendPreviewController;
$this->newsletterSaveController = $newsletterSaveController;
$this->newsletterDeleteController = $newsletterDeleteController;
$this->newsletterUrl = $newsletterUrl;
$this->scheduler = $scheduler;
$this->newsletterValidator = $newsletterValidator;
$this->authorizedEmailsController = $authorizedEmailsController;
}
public function get($data = []) {
$newsletter = $this->getNewsletter($data);
if (!$newsletter) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
$response = $this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
]);
$response = $this->wp->applyFilters('mailpoet_api_newsletters_get_after', $response);
return $this->successResponse($response, ['preview_url' => $this->getViewInBrowserUrl($newsletter)]);
}
public function getWithStats($data = []) {
$newsletter = $this->getNewsletter($data);
if (!$newsletter) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
$response = $this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
NewslettersResponseBuilder::RELATION_OPTIONS,
NewslettersResponseBuilder::RELATION_QUEUE,
NewslettersResponseBuilder::RELATION_TOTAL_SENT,
NewslettersResponseBuilder::RELATION_STATISTICS,
]);
$response = $this->wp->applyFilters('mailpoet_api_newsletters_get_after', $response);
$response['preview_url'] = $this->getViewInBrowserUrl($newsletter);
return $this->successResponse($response);
}
public function save($data = []) {
$data = $this->wp->applyFilters('mailpoet_api_newsletters_save_before', $data);
$newsletter = $this->newsletterSaveController->save($data);
$response = $this->newslettersResponseBuilder->build($newsletter, [
NewslettersResponseBuilder::RELATION_SEGMENTS,
]);
$previewUrl = $this->getViewInBrowserUrl($newsletter);
$response = $this->wp->applyFilters('mailpoet_api_newsletters_save_after', $response);
return $this->successResponse($response, ['preview_url' => $previewUrl]);
}
public function setStatus($data = []) {
$status = (isset($data['status']) ? $data['status'] : null);
if (!$status) {
return $this->badRequest([
APIError::BAD_REQUEST => __('You need to specify a status.', 'mailpoet'),
]);
}
if ($status === NewsletterEntity::STATUS_ACTIVE && $this->subscribersFeature->check()) {
return $this->errorResponse([
APIError::FORBIDDEN => __('Subscribers limit reached.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
$newsletter = $this->getNewsletter($data);
if ($newsletter === null) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
if ($status === NewsletterEntity::STATUS_ACTIVE && !$this->authorizedEmailsController->isSenderAddressValid($newsletter)) {
return $this->errorResponse([
APIError::FORBIDDEN => __('The sender address is not an authorized sender domain.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
if ($status === NewsletterEntity::STATUS_ACTIVE) {
$validationError = $this->newsletterValidator->validate($newsletter);
if ($validationError !== null) {
return $this->errorResponse([APIError::FORBIDDEN => $validationError], [], Response::STATUS_FORBIDDEN);
}
}
$this->newslettersRepository->prefetchOptions([$newsletter]);
$newsletter->setStatus($status);
// if there are paused tasks unpause them
if ($newsletter->getStatus() === NewsletterEntity::STATUS_ACTIVE) {
$queues = $newsletter->getUnfinishedQueues();
foreach ($queues as $queue) {
$task = $queue->getTask();
if ($task && $task->getStatus() === ScheduledTaskEntity::STATUS_PAUSED) {
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
}
}
}
// if there are past due notifications, reschedule them for the next send date
if ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION && $status === NewsletterEntity::STATUS_ACTIVE) {
$scheduleOption = $newsletter->getOption(NewsletterOptionFieldEntity::NAME_SCHEDULE);
if ($scheduleOption === null) {
return $this->errorResponse([
APIError::BAD_REQUEST => __('This email has incorrect state.', 'mailpoet'),
]);
}
$nextRunDate = $this->scheduler->getNextRunDate($scheduleOption->getValue());
$queues = $newsletter->getQueues();
foreach ($queues as $queue) {
$task = $queue->getTask();
if (
$task &&
$task->getScheduledAt() <= Carbon::now()->millisecond(0) &&
$task->getStatus() === SendingQueueEntity::STATUS_SCHEDULED
) {
$nextRunDate = $nextRunDate ? Carbon::createFromFormat('Y-m-d H:i:s', $nextRunDate) : null;
if ($nextRunDate === false) {
throw InvalidStateException::create()->withMessage('Invalid next run date generated');
}
$task->setScheduledAt($nextRunDate);
}
}
$this->postNotificationScheduler->createPostNotificationSendingTask($newsletter);
}
$this->newslettersRepository->flush();
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter)
);
}
public function restore($data = []) {
$newsletter = $this->getNewsletter($data);
if ($newsletter instanceof NewsletterEntity) {
$this->newslettersRepository->bulkRestore([$newsletter->getId()]);
$this->newslettersRepository->refresh($newsletter);
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
}
public function trash($data = []) {
$newsletter = $this->getNewsletter($data);
if ($newsletter instanceof NewsletterEntity) {
$this->newslettersRepository->bulkTrash([$newsletter->getId()]);
$this->newslettersRepository->refresh($newsletter);
return $this->successResponse(
$this->newslettersResponseBuilder->build($newsletter),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
}
public function delete($data = []) {
$newsletter = $this->getNewsletter($data);
if ($newsletter instanceof NewsletterEntity) {
$this->wp->doAction('mailpoet_api_newsletters_delete_before', [$newsletter->getId()]);
$this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]);
$this->wp->doAction('mailpoet_api_newsletters_delete_after', [$newsletter->getId()]);
return $this->successResponse(null, ['count' => 1]);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
}
public function duplicate($data = []) {
$newsletter = $this->getNewsletter($data);
if ($newsletter instanceof NewsletterEntity) {
$duplicate = $this->newsletterSaveController->duplicate($newsletter);
$this->wp->doAction('mailpoet_api_newsletters_duplicate_after', $newsletter, $duplicate);
return $this->successResponse(
$this->newslettersResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
}
public function showPreview($data = []) {
if (empty($data['body'])) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Newsletter data is missing.', 'mailpoet'),
]);
}
$newsletter = $this->getNewsletter($data);
if (!$newsletter) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
$newslettersTableName = $this->newslettersRepository->getTableName();
$newsletter->setBody(
json_decode($this->emoji->encodeForUTF8Column($newslettersTableName, 'body', $data['body']), true)
);
$this->newslettersRepository->flush();
$response = $this->newslettersResponseBuilder->build($newsletter);
return $this->successResponse($response, ['preview_url' => $this->getViewInBrowserUrl($newsletter)]);
}
public function sendPreview($data = []) {
if (empty($data['subscriber'])) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Please specify receiver information.', 'mailpoet'),
]);
}
$newsletter = $this->getNewsletter($data);
if (!$newsletter) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
]);
}
try {
$this->sendPreviewController->sendPreview($newsletter, $data['subscriber']);
} catch (SendPreviewException $e) {
return $this->errorResponse([APIError::BAD_REQUEST => $e->getMessage()]);
} catch (\Throwable $e) {
return $this->errorResponse([$e->getCode() => $e->getMessage()]);
}
return $this->successResponse($this->newslettersResponseBuilder->build($newsletter));
}
public function listing($data = []) {
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->newsletterListingRepository->getData($definition);
$count = $this->newsletterListingRepository->getCount($definition);
$filters = $this->newsletterListingRepository->getFilters($definition);
$groups = $this->newsletterListingRepository->getGroups($definition);
$data = [];
foreach ($this->newslettersResponseBuilder->buildForListing($items) as $newsletterData) {
$data[] = $this->wp->applyFilters('mailpoet_api_newsletters_listing_item', $newsletterData);
}
return $this->successResponse($data, [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
'mta_log' => $this->settings->get('mta_log'),
'mta_method' => $this->settings->get('mta.method'),
'cron_accessible' => $this->cronHelper->isDaemonAccessible(),
'current_time' => $this->wp->currentTime('mysql'),
]);
}
public function bulkAction($data = []) {
$definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->newsletterListingRepository->getActionableIds($definition);
if ($data['action'] === 'trash') {
$this->newslettersRepository->bulkTrash($ids);
} elseif ($data['action'] === 'restore') {
$this->newslettersRepository->bulkRestore($ids);
} elseif ($data['action'] === 'delete') {
$this->wp->doAction('mailpoet_api_newsletters_delete_before', $ids);
$this->newsletterDeleteController->bulkDelete($ids);
$this->wp->doAction('mailpoet_api_newsletters_delete_after', $ids);
} else {
throw UnexpectedValueException::create()
->withErrors([APIError::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
}
return $this->successResponse(null, ['count' => count($ids)]);
}
public function create($data = []) {
try {
$newsletter = $this->newsletterSaveController->save($data);
} catch (ValidationException $exception) {
return $this->badRequest(['Please specify a type.']);
}
$response = $this->newslettersResponseBuilder->build($newsletter);
return $this->successResponse($response);
}
/** @return NewsletterEntity|null */
private function getNewsletter(array $data) {
return isset($data['id'])
? $this->newslettersRepository->findOneById((int)$data['id'])
: null;
}
private function getViewInBrowserUrl(NewsletterEntity $newsletter): string {
$url = $this->newsletterUrl->getViewInBrowserUrl($newsletter);
// strip protocol to avoid mix content error
return preg_replace('/^https?:/i', '', $url);
}
}
@@ -0,0 +1,98 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\AccessControl;
use MailPoet\Config\ServicesChecker;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WPCOM\DotcomHelperFunctions;
use WP_Error;
class Premium extends APIEndpoint {
const PREMIUM_PLUGIN_SLUG = 'mailpoet-premium';
const PREMIUM_PLUGIN_PATH = 'mailpoet-premium/mailpoet-premium.php';
// This is the path to the managed plugin on Dotcom platform. It is relative to WP_PLUGIN_DIR.
const DOTCOM_SYMLINK_PATH = '../../../../wordpress/plugins/mailpoet-premium/latest';
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
];
/** @var ServicesChecker */
private $servicesChecker;
/** @var WPFunctions */
private $wp;
/** @var DotcomHelperFunctions */
private $dotcomHelperFunctions;
public function __construct(
ServicesChecker $servicesChecker,
WPFunctions $wp,
DotcomHelperFunctions $dotcomHelperFunctions
) {
$this->servicesChecker = $servicesChecker;
$this->wp = $wp;
$this->dotcomHelperFunctions = $dotcomHelperFunctions;
}
public function installPlugin() {
$premiumKeyValid = $this->servicesChecker->isPremiumKeyValid(false);
if (!$premiumKeyValid) {
return $this->error(__('Premium key is not valid.', 'mailpoet'));
}
$pluginInfo = $this->wp->pluginsApi('plugin_information', [
'slug' => self::PREMIUM_PLUGIN_SLUG,
]);
if (!$pluginInfo || $pluginInfo instanceof WP_Error) {
return $this->error(__('Error when installing MailPoet Premium plugin.', 'mailpoet'));
}
$pluginInfo = (array)$pluginInfo;
// If we are in Dotcom platform, we try to symlink the plugin instead of downloading it
try {
if ($this->dotcomHelperFunctions->isDotcom()) {
$result = symlink(self::DOTCOM_SYMLINK_PATH, WP_PLUGIN_DIR . '/' . self::PREMIUM_PLUGIN_SLUG);
if ($result === true) {
return $this->successResponse();
}
}
} catch (\Exception $e) {
// Do nothing and continue with a regular installation
}
$result = $this->wp->installPlugin($pluginInfo['download_link']);
if ($result !== true) {
return $this->error(__('Error when installing MailPoet Premium plugin.', 'mailpoet'));
}
return $this->successResponse();
}
public function activatePlugin() {
$premiumKeyValid = $this->servicesChecker->isPremiumKeyValid(false);
if (!$premiumKeyValid) {
return $this->error(__('Premium key is not valid.', 'mailpoet'));
}
$result = $this->wp->activatePlugin(self::PREMIUM_PLUGIN_PATH);
if ($result !== null) {
return $this->error(__('Error when activating MailPoet Premium plugin.', 'mailpoet'));
}
return $this->successResponse();
}
private function error($message) {
return $this->badRequest([
APIError::BAD_REQUEST => $message,
]);
}
}
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Response;
class RedirectResponse extends Response {
public function __construct($location) { // phpcs:ignore
parent::__construct(self::REDIRECT, [], $location);
}
public function getData() {
return [];
}
}
@@ -0,0 +1,337 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use Exception;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\SegmentsResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\ConflictException;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\Cron\Workers\WooCommerceSync;
use MailPoet\Doctrine\Validator\ValidationException;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Form\FormsRepository;
use MailPoet\Listing;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Segments\SegmentListingRepository;
use MailPoet\Segments\SegmentSaveController;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\SegmentSubscribersRepository;
use MailPoet\Segments\WooCommerce;
use MailPoet\Segments\WP;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\UnexpectedValueException;
class Segments extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS,
];
/** @var Listing\Handler */
private $listingHandler;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SegmentsResponseBuilder */
private $segmentsResponseBuilder;
/** @var SegmentSaveController */
private $segmentSavecontroller;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WooCommerce */
private $wooCommerceSync;
/** @var WP */
private $wpSegment;
/** @var SegmentListingRepository */
private $segmentListingRepository;
/** @var NewsletterSegmentRepository */
private $newsletterSegmentRepository;
/** @var CronWorkerScheduler */
private $cronWorkerScheduler;
/** @var FormsRepository */
private $formsRepository;
/** @var SegmentSubscribersRepository */
private $segmentSubscribersRepository;
public function __construct(
Listing\Handler $listingHandler,
SegmentsRepository $segmentsRepository,
SegmentListingRepository $segmentListingRepository,
SegmentsResponseBuilder $segmentsResponseBuilder,
SegmentSaveController $segmentSavecontroller,
SegmentSubscribersRepository $segmentSubscribersRepository,
SubscribersRepository $subscribersRepository,
WooCommerce $wooCommerce,
WP $wpSegment,
NewsletterSegmentRepository $newsletterSegmentRepository,
CronWorkerScheduler $cronWorkerScheduler,
FormsRepository $formsRepository
) {
$this->listingHandler = $listingHandler;
$this->wooCommerceSync = $wooCommerce;
$this->segmentsRepository = $segmentsRepository;
$this->segmentsResponseBuilder = $segmentsResponseBuilder;
$this->segmentSavecontroller = $segmentSavecontroller;
$this->subscribersRepository = $subscribersRepository;
$this->wpSegment = $wpSegment;
$this->segmentListingRepository = $segmentListingRepository;
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
$this->cronWorkerScheduler = $cronWorkerScheduler;
$this->formsRepository = $formsRepository;
$this->segmentSubscribersRepository = $segmentSubscribersRepository;
}
public function get($data = []) {
$id = (isset($data['id']) ? (int)$data['id'] : false);
$segment = $this->segmentsRepository->findOneById($id);
if ($segment instanceof SegmentEntity) {
return $this->successResponse($this->segmentsResponseBuilder->build($segment));
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This list does not exist.', 'mailpoet'),
]);
}
}
public function listing($data = []) {
$data['params'] = $data['params'] ?? ['lists']; // Dummy param to apply constraints properly
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->segmentListingRepository->getData($definition);
$count = $this->segmentListingRepository->getCount($definition);
$filters = $this->segmentListingRepository->getFilters($definition);
$groups = $this->segmentListingRepository->getGroups($definition);
$segments = $this->segmentsResponseBuilder->buildForListing($items);
return $this->successResponse($segments, [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
]);
}
public function save($data = []) {
try {
$data['name'] = isset($data['name']) ? sanitize_text_field($data['name']) : '';
$data['description'] = isset($data['description']) ? sanitize_textarea_field($data['description']) : '';
$segment = $this->segmentSavecontroller->save($data);
} catch (ValidationException $exception) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Please specify a name.', 'mailpoet'),
]);
} catch (ConflictException $exception) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Another record already exists. Please specify a different "name".', 'mailpoet'),
]);
}
$response = $this->segmentsResponseBuilder->build($segment);
return $this->successResponse($response);
}
public function restore($data = []) {
$segment = $this->getSegment($data);
if ($segment instanceof SegmentEntity) {
if (!$this->isTrashOrRestoreAllowed($segment)) {
return $this->errorResponse([
APIError::FORBIDDEN => __('This list cannot be moved to trash.', 'mailpoet'),
]);
}
// When the segment is of type WP_USERS we want to restore all its subscribers
if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
$subscribers = $this->subscribersRepository->findBySegment((int)$segment->getId());
$subscriberIds = array_map(function (SubscriberEntity $subscriberEntity): int {
return (int)$subscriberEntity->getId();
}, $subscribers);
$this->subscribersRepository->bulkRestore($subscriberIds);
}
$this->segmentsRepository->bulkRestore([$segment->getId()], $segment->getType());
$this->segmentsRepository->refresh($segment);
return $this->successResponse(
$this->segmentsResponseBuilder->build($segment),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This list does not exist.', 'mailpoet'),
]);
}
}
public function trash($data = []) {
$segment = $this->getSegment($data);
if (!$segment instanceof SegmentEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This list does not exist.', 'mailpoet'),
]);
}
if (!$this->isTrashOrRestoreAllowed($segment)) {
return $this->errorResponse([
APIError::FORBIDDEN => __('This list cannot be moved to trash.', 'mailpoet'),
]);
}
$activelyUsedNewslettersSubjects = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments([$segment->getId()]);
if (isset($activelyUsedNewslettersSubjects[$segment->getId()])) {
return $this->badRequest([
APIError::BAD_REQUEST => str_replace(
'%1$s',
"'" . join("', '", $activelyUsedNewslettersSubjects[$segment->getId()]) . "'",
// translators: %1$s is a comma-seperated list of emails for which the segment is used.
_x('List cannot be deleted because its used for %1$s email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet')
),
]);
}
$activelyUsedFormNames = $this->formsRepository->getNamesOfFormsForSegments();
if (isset($activelyUsedFormNames[$segment->getId()])) {
return $this->badRequest([
APIError::BAD_REQUEST => str_replace(
'%1$s',
"'" . join("', '", $activelyUsedFormNames[$segment->getId()]) . "'",
// translators: %1$s is a comma-seperated list of forms for which the segment is used.
_nx(
'List cannot be deleted because its used for %1$s form',
'List cannot be deleted because its used for %1$s forms',
count($activelyUsedFormNames[$segment->getId()]),
'Alert shown when trying to delete segment, when it is assigned to a form.',
'mailpoet'
)
),
]);
}
// When the segment is of type WP_USERS we want to trash all subscribers who aren't subscribed in another list
if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
$subscribers = $this->subscribersRepository->findExclusiveSubscribersBySegment((int)$segment->getId());
$subscriberIds = array_map(function (SubscriberEntity $subscriberEntity): int {
return (int)$subscriberEntity->getId();
}, $subscribers);
$this->subscribersRepository->bulkTrash($subscriberIds);
}
$this->segmentsRepository->doTrash([$segment->getId()], $segment->getType());
$this->segmentsRepository->refresh($segment);
return $this->successResponse(
$this->segmentsResponseBuilder->build($segment),
['count' => 1]
);
}
public function delete($data = []) {
$segment = $this->getSegment($data);
if ($segment instanceof SegmentEntity) {
$this->segmentsRepository->bulkDelete([$segment->getId()]);
return $this->successResponse(null, ['count' => 1]);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This list does not exist.', 'mailpoet'),
]);
}
}
public function duplicate($data = []) {
$segment = $this->getSegment($data);
if ($segment instanceof SegmentEntity) {
try {
$duplicate = $this->segmentSavecontroller->duplicate($segment);
} catch (Exception $e) {
return $this->errorResponse([
APIError::UNKNOWN => __('Duplicating of segment failed.', 'mailpoet'),
], [], Response::STATUS_UNKNOWN);
}
return $this->successResponse(
$this->segmentsResponseBuilder->build($duplicate),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This list does not exist.', 'mailpoet'),
]);
}
}
public function synchronize($data) {
try {
if ($data['type'] === SegmentEntity::TYPE_WC_USERS) {
$this->cronWorkerScheduler->scheduleImmediatelyIfNotRunning(WooCommerceSync::TASK_TYPE);
} else {
$this->wpSegment->synchronizeUsers();
}
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
return $this->successResponse(null);
}
public function bulkAction($data = []) {
$definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->segmentListingRepository->getActionableIds($definition);
$count = 0;
if ($data['action'] === 'trash') {
$count = $this->segmentsRepository->bulkTrash($ids);
} elseif ($data['action'] === 'restore') {
$count = $this->segmentsRepository->bulkRestore($ids);
} elseif ($data['action'] === 'delete') {
$count = $this->segmentsRepository->bulkDelete($ids);
} else {
throw UnexpectedValueException::create()
->withErrors([APIError::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
}
return $this->successResponse(null, ['count' => $count]);
}
public function subscriberCount($data = []) {
$segmentIds = $data['segmentIds'] ?? [];
if (empty($segmentIds)) {
return $this->errorResponse([
APIError::BAD_REQUEST => __('No segment IDs provided.', 'mailpoet'),
]);
}
$filterSegmentId = $data['filterSegmentId'] ?? null;
$status = $data['status'] ?? SubscriberEntity::STATUS_SUBSCRIBED;
$response['count'] = $this->segmentSubscribersRepository->getSubscribersCountBySegmentIds($segmentIds, $status, $filterSegmentId);
return $this->successResponse($response);
}
private function isTrashOrRestoreAllowed(SegmentEntity $segment): bool {
$allowedSegmentTypes = [
SegmentEntity::TYPE_DEFAULT,
SegmentEntity::TYPE_WP_USERS,
];
if (in_array($segment->getType(), $allowedSegmentTypes, true)) {
return true;
}
return false;
}
private function getSegment(array $data): ?SegmentEntity {
return isset($data['id'])
? $this->segmentsRepository->findOneById((int)$data['id'])
: null;
}
}
@@ -0,0 +1,286 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\SendingQueuesResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\ActionScheduler\Actions\DaemonTrigger;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\CronTrigger;
use MailPoet\Cron\Triggers\WordPress;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\NewsletterValidator;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoetVendor\Carbon\Carbon;
class SendingQueue extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var SubscribersFinder */
private $subscribersFinder;
/** @var NewslettersRepository */
private $newsletterRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var MailerFactory */
private $mailerFactory;
/** @var NewsletterValidator */
private $newsletterValidator;
/** @var SettingsController */
private $settings;
/** @var DaemonTrigger */
private $actionSchedulerDaemonTriggerAction;
/** @var SendingQueuesResponseBuilder */
private $sendingQueuesResponseBuilder;
/** @var CronHelper */
private $cronHelper;
public function __construct(
SubscribersFeature $subscribersFeature,
NewslettersRepository $newsletterRepository,
SendingQueuesRepository $sendingQueuesRepository,
SubscribersFinder $subscribersFinder,
ScheduledTasksRepository $scheduledTasksRepository,
MailerFactory $mailerFactory,
SettingsController $settings,
DaemonTrigger $actionSchedulerDaemonTriggerAction,
NewsletterValidator $newsletterValidator,
SendingQueuesResponseBuilder $sendingQueuesResponseBuilder,
CronHelper $cronHelper
) {
$this->subscribersFeature = $subscribersFeature;
$this->subscribersFinder = $subscribersFinder;
$this->newsletterRepository = $newsletterRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->mailerFactory = $mailerFactory;
$this->settings = $settings;
$this->actionSchedulerDaemonTriggerAction = $actionSchedulerDaemonTriggerAction;
$this->newsletterValidator = $newsletterValidator;
$this->sendingQueuesResponseBuilder = $sendingQueuesResponseBuilder;
$this->cronHelper = $cronHelper;
}
public function add($data = []) {
if ($this->subscribersFeature->check()) {
return $this->errorResponse([
APIError::FORBIDDEN => __('Subscribers limit reached.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
$newsletterId = (isset($data['newsletter_id'])
? (int)$data['newsletter_id']
: false
);
// check that the newsletter exists
$newsletter = $this->newsletterRepository->findOneById($newsletterId);
$this->newsletterRepository->prefetchOptions([$newsletter]);
if (!$newsletter instanceof NewsletterEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This newsletter does not exist.', 'mailpoet'),
]);
}
$validationError = $this->newsletterValidator->validate($newsletter);
if ($validationError) {
return $this->errorResponse([
APIError::BAD_REQUEST => $validationError,
]);
}
try {
// check that the sending method has been configured properly by verifying that default mailer can be build
$this->mailerFactory->getDefaultMailer();
$sendingQueue = $this->sendingQueuesRepository->findOneByNewsletterAndTaskStatus($newsletter, null);
if ($sendingQueue instanceof SendingQueueEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This newsletter is already being sent.', 'mailpoet'),
]);
}
$sendingQueue = $this->sendingQueuesRepository->findOneByNewsletterAndTaskStatus($newsletter, ScheduledTaskEntity::STATUS_SCHEDULED);
if (is_null($sendingQueue)) {
$scheduledTask = new ScheduledTaskEntity();
$scheduledTask->setType(SendingQueueWorker::TASK_TYPE);
$sendingQueue = new SendingQueueEntity();
$sendingQueue->setNewsletter($newsletter);
$sendingQueue->setTask($scheduledTask);
$this->sendingQueuesRepository->persist($sendingQueue);
$this->newsletterRepository->refresh($newsletter);
} else {
$scheduledTask = $sendingQueue->getTask();
}
if (!$scheduledTask instanceof ScheduledTaskEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Unable to find scheduled task associated with this newsletter.', 'mailpoet'),
]);
}
$scheduledTask->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
$this->scheduledTasksRepository->persist($scheduledTask);
$this->scheduledTasksRepository->flush();
WordPress::resetRunInterval();
if ((bool)$newsletter->getOptionValue('isScheduled')) {
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SCHEDULED);
// set scheduled task status
$scheduledTask->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$scheduledTask->setScheduledAt(new Carbon($newsletter->getOptionValue('scheduledAt')));
} else {
$segments = $newsletter->getSegmentIds();
$this->scheduledTasksRepository->refresh($scheduledTask);
$this->subscribersFinder->addSubscribersToTaskFromSegments($scheduledTask, $segments, $newsletter->getFilterSegmentId());
$subscribersCount = $scheduledTask->getSubscribers()->count();
if (!$subscribersCount) {
return $this->errorResponse([
APIError::UNKNOWN => __('There are no subscribers in that list!', 'mailpoet'),
]);
}
$this->sendingQueuesRepository->updateCounts($sendingQueue);
$scheduledTask->setStatus(null);
$scheduledTask->setScheduledAt(null);
// set newsletter status
$newsletter->setStatus(NewsletterEntity::STATUS_SENDING);
}
$this->scheduledTasksRepository->persist($scheduledTask);
$this->newsletterRepository->flush();
$this->triggerSending($newsletter);
return $this->successResponse(
($newsletter->getLatestQueue() instanceof SendingQueueEntity) ? $this->sendingQueuesResponseBuilder->build($newsletter->getLatestQueue()) : null
);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
public function pause($data = []) {
$newsletterId = (isset($data['newsletter_id'])
? (int)$data['newsletter_id']
: false
);
$newsletter = $this->newsletterRepository->findOneById($newsletterId);
if ($newsletter instanceof NewsletterEntity) {
$queue = $newsletter->getLastUpdatedQueue();
if (!$queue instanceof SendingQueueEntity) {
return $this->errorResponse([
APIError::UNKNOWN => __('This newsletter has not been sent yet.', 'mailpoet'),
]);
} else {
$this->sendingQueuesRepository->pause($queue);
return $this->successResponse();
}
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This newsletter does not exist.', 'mailpoet'),
]);
}
}
public function resume($data = []) {
if ($this->subscribersFeature->check()) {
return $this->errorResponse([
APIError::FORBIDDEN => __('Subscribers limit reached.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
$newsletterId = (isset($data['newsletter_id'])
? (int)$data['newsletter_id']
: false
);
$newsletter = $this->newsletterRepository->findOneById($newsletterId);
if ($newsletter instanceof NewsletterEntity) {
$queue = $newsletter->getLastUpdatedQueue();
if (!$queue instanceof SendingQueueEntity) {
return $this->errorResponse([
APIError::UNKNOWN => __('This newsletter has not been sent yet.', 'mailpoet'),
]);
} else {
$this->sendingQueuesRepository->resume($queue);
$this->triggerSending($newsletter);
return $this->successResponse();
}
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This newsletter does not exist.', 'mailpoet'),
]);
}
}
public function pingCron() {
try {
$cronPingResponse = $this->cronHelper->pingDaemon();
} catch (\Exception $e) {
return $this->errorResponse([
APIError::UNKNOWN => $e->getMessage(),
]);
}
if (!$this->cronHelper->validatePingResponse($cronPingResponse)) {
return $this->errorResponse([
APIError::UNKNOWN => $cronPingResponse,
]);
}
return $this->successResponse();
}
/**
* In case the newsletter was switched to sending trigger the background job immediately.
* This is done so that user immediately sees that email is sending and doesn't have to wait on WP Cron to start it.
*/
private function triggerSending(NewsletterEntity $newsletter): void {
if (
$newsletter->getStatus() === NewsletterEntity::STATUS_SENDING
&& $this->settings->get('cron_trigger.method') === CronTrigger::METHOD_ACTION_SCHEDULER
) {
$this->actionSchedulerDaemonTriggerAction->process();
}
}
}
@@ -0,0 +1,148 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\ScheduledTaskSubscriberResponseBuilder;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\CronHelper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Listing;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersListingRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
class SendingTaskSubscribers extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
/** @var Listing\Handler */
private $listingHandler;
/** @var SettingsController */
private $settings;
/** @var CronHelper */
private $cronHelper;
/** @var WPFunctions */
private $wp;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var ScheduledTaskSubscribersListingRepository */
private $taskSubscribersListingRepository;
/** @var ScheduledTaskSubscriberResponseBuilder */
private $scheduledTaskSubscriberResponseBuilder;
public function __construct(
Listing\Handler $listingHandler,
SettingsController $settings,
CronHelper $cronHelper,
SendingQueuesRepository $sendingQueuesRepository,
ScheduledTaskSubscribersListingRepository $taskSubscribersListingRepository,
ScheduledTaskSubscriberResponseBuilder $scheduledTaskSubscriberResponseBuilder,
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
WPFunctions $wp
) {
$this->listingHandler = $listingHandler;
$this->settings = $settings;
$this->cronHelper = $cronHelper;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->taskSubscribersListingRepository = $taskSubscribersListingRepository;
$this->scheduledTaskSubscriberResponseBuilder = $scheduledTaskSubscriberResponseBuilder;
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
$this->wp = $wp;
}
public function listing($data = []) {
$newsletterId = !empty($data['params']['id']) ? (int)$data['params']['id'] : false;
if (empty($newsletterId)) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Newsletter not found!', 'mailpoet'),
]);
}
$tasksIds = $this->sendingQueuesRepository->getTaskIdsByNewsletterId($newsletterId);
if (empty($tasksIds)) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email has not been sent yet.', 'mailpoet'),
]);
}
$data['params']['task_ids'] = $tasksIds;
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->taskSubscribersListingRepository->getData($definition);
$groups = $this->taskSubscribersListingRepository->getGroups($definition);
$filters = $this->taskSubscribersListingRepository->getFilters($definition);
$count = $this->taskSubscribersListingRepository->getCount($definition);
return $this->successResponse($this->scheduledTaskSubscriberResponseBuilder->buildForListing($items), [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
'mta_log' => $this->settings->get('mta_log'),
'mta_method' => $this->settings->get('mta.method'),
'cron_accessible' => $this->cronHelper->isDaemonAccessible(),
'current_time' => $this->wp->currentTime('mysql'),
]);
}
public function resend($data = []) {
$taskId = !empty($data['taskId']) ? (int)$data['taskId'] : 0;
$subscriberId = !empty($data['subscriberId']) ? (int)$data['subscriberId'] : 0;
$taskSubscriber = $this->scheduledTaskSubscribersRepository->findOneBy([
'task' => $taskId,
'subscriber' => $subscriberId,
'failed' => 1,
]);
$sendingQueue = $this->sendingQueuesRepository->findOneBy(['task' => $taskId]);
if (
!$taskSubscriber
|| !$taskSubscriber->getTask()
|| !$sendingQueue
) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Failed sending task not found!', 'mailpoet'),
]);
}
$newsletter = $sendingQueue->getNewsletter();
if (!$newsletter) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Newsletter not found!', 'mailpoet'),
]);
}
if ($newsletter->canBeSetActive() && $newsletter->getStatus() !== NewsletterEntity::STATUS_ACTIVE) {
return $this->errorResponse([
// translators: This error occurs when resending a failed email message to a recipient and the associated email definition (e.g., a welcome email, an automation email) is inactive.
APIError::BAD_REQUEST => __('Failed to resend! The email is not active. Please activate it first.', 'mailpoet'),
], [], Response::STATUS_BAD_REQUEST);
}
$taskSubscriber->resetToUnprocessed();
$taskSubscriber->getTask()->setStatus(null);
if (!$newsletter->canBeSetActive()) {
$newsletter->setStatus(NewsletterEntity::STATUS_SENDING);
}
// Each repository flushes all changes
$this->scheduledTaskSubscribersRepository->flush();
return $this->successResponse([]);
}
}
@@ -0,0 +1,358 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\Analytics\Analytics as AnalyticsHelper;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Response;
use MailPoet\Config\AccessControl;
use MailPoet\Config\Installer;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck;
use MailPoet\Mailer\MailerLog;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\AuthorizedSenderDomainController;
use MailPoet\Services\Bridge;
use MailPoet\Services\CongratulatoryMssEmailController;
use MailPoet\Services\SubscribersCountReporter;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\WP\DateTime;
use MailPoet\WP\Functions as WPFunctions;
class Services extends APIEndpoint {
/** @var Bridge */
private $bridge;
/** @var SettingsController */
private $settings;
/** @var AnalyticsHelper */
private $analytics;
/** @var DateTime */
public $dateTime;
/** @var SendingServiceKeyCheck */
private $mssWorker;
/** @var PremiumKeyCheck */
private $premiumWorker;
/** @var ServicesChecker */
private $servicesChecker;
/** @var CongratulatoryMssEmailController */
private $congratulatoryMssEmailController;
/** @var WPFunctions */
private $wp;
/** @var AuthorizedSenderDomainController */
private $senderDomainController;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
/** @var SubscribersCountReporter */
private $subscribersCountReporter;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
'methods' => ['pingBridge' => AccessControl::PERMISSION_ACCESS_PLUGIN_ADMIN],
];
public function __construct(
Bridge $bridge,
SettingsController $settings,
AnalyticsHelper $analytics,
SendingServiceKeyCheck $mssWorker,
PremiumKeyCheck $premiumWorker,
ServicesChecker $servicesChecker,
SubscribersCountReporter $subscribersCountReporter,
CongratulatoryMssEmailController $congratulatoryMssEmailController,
WPFunctions $wp,
AuthorizedSenderDomainController $senderDomainController,
AuthorizedEmailsController $authorizedEmailsController
) {
$this->bridge = $bridge;
$this->settings = $settings;
$this->analytics = $analytics;
$this->mssWorker = $mssWorker;
$this->premiumWorker = $premiumWorker;
$this->dateTime = new DateTime();
$this->servicesChecker = $servicesChecker;
$this->subscribersCountReporter = $subscribersCountReporter;
$this->congratulatoryMssEmailController = $congratulatoryMssEmailController;
$this->wp = $wp;
$this->senderDomainController = $senderDomainController;
$this->authorizedEmailsController = $authorizedEmailsController;
}
public function checkMSSKey($data = []) {
$key = isset($data['key']) ? trim($data['key']) : null;
if (!$key) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet'),
]);
}
$wasPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
try {
$result = $this->bridge->checkMSSKey($key);
$this->bridge->storeMSSKeyAndState($key, $result);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
// pause sending when key is pending approval, resume when not pending anymore
$isPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
if (!$wasPendingApproval && $isPendingApproval) {
MailerLog::pauseSending(MailerLog::getMailerLog());
} elseif ($wasPendingApproval && !$isPendingApproval) {
MailerLog::resumeSending();
}
$state = !empty($result['state']) ? $result['state'] : null;
$successMessage = null;
if ($state == Bridge::KEY_VALID) {
$successMessage = __('Your MailPoet Sending Service key has been successfully validated', 'mailpoet');
} else if ($state == Bridge::KEY_VALID_UNDERPRIVILEGED) {
$successMessage = __('Your Premium key has been successfully validated, but is not valid for MailPoet Sending Service', 'mailpoet');
} elseif ($state == Bridge::KEY_EXPIRING) {
$successMessage = sprintf(
// translators: %s is the expiration date.
__('Your MailPoet Sending Service key expires on %s!', 'mailpoet'),
$this->dateTime->formatDate(strtotime($result['data']['expire_at']))
);
}
if (!empty($result['data']['public_id'])) {
$this->analytics->setPublicId($result['data']['public_id']);
}
if ($successMessage) {
return $this->successResponse(['message' => $successMessage, 'state' => $state, 'result' => $result]);
}
switch ($state) {
case Bridge::KEY_INVALID:
$error = __('Your key is not valid for the MailPoet Sending Service', 'mailpoet');
break;
case Bridge::KEY_ALREADY_USED:
$error = __('Your MailPoet Sending Service key is already <a>used on another site</a>', 'mailpoet'); // we will use createInterpolateElement to replace <a> element
break;
default:
$code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
// translators: %s is the error message.
$errorMessage = __('Error validating MailPoet Sending Service key, please try again later (%s).', 'mailpoet');
// If site runs on localhost
if (1 === preg_match("/^(http|https)\:\/\/(localhost|127\.0\.0\.1)/", $this->wp->siteUrl())) {
$errorMessage .= ' ' . __("Note that it doesn't work on localhost.", 'mailpoet');
}
$error = sprintf(
$errorMessage,
$this->getErrorDescriptionByCode($code)
);
break;
}
return $this->errorResponse([APIError::BAD_REQUEST => $error]);
}
public function checkPremiumKey($data = []) {
$key = isset($data['key']) ? trim($data['key']) : null;
if (!$key) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet'),
]);
}
try {
$result = $this->bridge->checkPremiumKey($key);
$this->bridge->storePremiumKeyAndState($key, $result);
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
$state = !empty($result['state']) ? $result['state'] : null;
$successMessage = null;
if ($state == Bridge::KEY_VALID) {
$successMessage = __('Your Premium key has been successfully validated', 'mailpoet');
} else if ($state == Bridge::KEY_VALID_UNDERPRIVILEGED) {
$successMessage = __('Your Premium key has been successfully validated, but is not valid for MailPoet Sending Service', 'mailpoet');
} elseif ($state == Bridge::KEY_EXPIRING) {
$successMessage = sprintf(
// translators: %s is the expiration date.
__('Your Premium key expires on %s', 'mailpoet'),
$this->dateTime->formatDate(strtotime($result['data']['expire_at']))
);
}
if (!empty($result['data']['public_id'])) {
$this->analytics->setPublicId($result['data']['public_id']);
}
if ($successMessage) {
return $this->successResponse(
['message' => $successMessage, 'state' => $state, 'result' => $result],
Installer::getPremiumStatus()
);
}
switch ($state) {
case Bridge::KEY_INVALID:
$error = __('Your key is not valid for MailPoet Premium', 'mailpoet');
break;
case Bridge::KEY_ALREADY_USED:
$error = __('Your Premium key is already <a>used on another site</a>', 'mailpoet'); // we will use createInterpolateElement to replace <a> element
break;
default:
$code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
$error = sprintf(
// translators: %s is the error message.
__('Error validating Premium key, please try again later (%s)', 'mailpoet'),
$this->getErrorDescriptionByCode($code)
);
break;
}
return $this->errorResponse(
[APIError::BAD_REQUEST => $error],
['code' => $result['code'] ?? null]
);
}
public function recheckKeys() {
// Report subscribers count before rechecking keys so that shop can lift access restrictions in case
// user deleted subscribers and no longer exceeds the limit.
$key = $this->servicesChecker->getValidAccountKey();
if ($key) {
$this->subscribersCountReporter->report($key);
}
$this->mssWorker->init();
$mssCheck = $this->mssWorker->checkKey();
$this->premiumWorker->init();
$premiumCheck = $this->premiumWorker->checkKey();
// continue sending when it is paused and states are valid
$mailerLog = MailerLog::getMailerLog();
if (
(isset($mailerLog['status']) && $mailerLog['status'] === MailerLog::STATUS_PAUSED)
&& (isset($mssCheck['state']) && $mssCheck['state'] === Bridge::KEY_VALID)
&& (isset($premiumCheck['state']) && $premiumCheck['state'] === Bridge::PREMIUM_KEY_VALID)
) {
MailerLog::resumeSending();
}
return $this->successResponse();
}
public function sendCongratulatoryMssEmail() {
if (!Bridge::isMPSendingServiceEnabled()) {
return $this->createBadRequest(__('MailPoet Sending Service is not active.', 'mailpoet'));
}
$fromEmail = $this->settings->get('sender.address');
if (!$fromEmail) {
return $this->createBadRequest(__('Sender email address is not set.', 'mailpoet'));
}
$verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache();
$emailDomain = Helpers::extractEmailDomain($fromEmail);
if (!$this->isItemInArray($emailDomain, $verifiedDomains)) {
$authorizedEmails = $this->authorizedEmailsController->getAuthorizedEmailAddresses();
if (!$authorizedEmails) {
return $this->createBadRequest(__('No FROM email addresses are authorized.', 'mailpoet'));
}
if (!$this->isItemInArray($fromEmail, $authorizedEmails)) {
// translators: %s is the email address, which is not authorized.
return $this->createBadRequest(sprintf(__("Sender email address '%s' is not authorized.", 'mailpoet'), $fromEmail));
}
}
try {
// congratulatory email is sent to the current FROM address (authorized at this point)
$this->congratulatoryMssEmailController->sendCongratulatoryEmail($fromEmail);
} catch (\Throwable $e) {
return $this->errorResponse([
APIError::UNKNOWN => __('Sending of congratulatory email failed.', 'mailpoet'),
], [], Response::STATUS_UNKNOWN);
}
return $this->successResponse([
'email_address' => $fromEmail,
]);
}
public function pingBridge() {
$response = $this->bridge->pingBridge();
if ($this->wp->isWpError($response)) {
/** @var \WP_Error $response */
$errorDesc = $this->getErrorDescriptionByCode(Bridge::CHECK_ERROR_UNKNOWN);
return $this->errorResponse([
APIError::UNKNOWN => "{$errorDesc}: {$response->get_error_message()}",
]);
}
if (!$this->bridge->validateBridgePingResponse($response)) {
$code = $this->wp->wpRemoteRetrieveResponseCode($response) ?: Bridge::CHECK_ERROR_UNKNOWN;
return $this->errorResponse([
APIError::UNKNOWN => $this->getErrorDescriptionByCode($code),
]);
}
return $this->successResponse();
}
public function refreshMSSKeyStatus() {
$key = $this->settings->get('mta.mailpoet_api_key');
return $this->checkMSSKey(['key' => $key]);
}
public function refreshPremiumKeyStatus() {
$key = $this->settings->get('premium.premium_key');
return $this->checkPremiumKey(['key' => $key]);
}
private function isItemInArray($item, $array): bool {
return in_array($item, $array, true);
}
private function getErrorDescriptionByCode($code) {
switch ($code) {
case Bridge::CHECK_ERROR_UNAVAILABLE:
$text = __('Service unavailable', 'mailpoet');
break;
case Bridge::CHECK_ERROR_UNKNOWN:
$text = __('Contact your hosting support to check the connection between your host and https://bridge.mailpoet.com', 'mailpoet');
break;
default:
// translators: %s is the code.
$text = sprintf(_x('code: %s', 'Error code (inside parentheses)', 'mailpoet'), $code);
break;
}
return $text;
}
private function createBadRequest(string $message) {
return $this->badRequest([
APIError::BAD_REQUEST => $message,
]);
}
}
@@ -0,0 +1,499 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\ErrorResponse;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\Config\AccessControl;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\Workers\SubscribersEngagementScore;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Form\FormMessageController;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerLog;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\AuthorizedSenderDomainController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsChangeHandler;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\StatisticsOpensRepository;
use MailPoet\Subscribers\ConfirmationEmailCustomizer;
use MailPoet\Subscribers\SubscribersCountsController;
use MailPoet\Util\Notices\DisabledMailFunctionNotice;
use MailPoet\WooCommerce\TransactionalEmails;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class Settings extends APIEndpoint {
/** @var SettingsController */
private $settings;
/** @var Bridge */
private $bridge;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
/** @var AuthorizedSenderDomainController */
private $senderDomainController;
/** @var TransactionalEmails */
private $wcTransactionalEmails;
/** @var ServicesChecker */
private $servicesChecker;
/** @var EntityManager */
private $entityManager;
/** @var StatisticsOpensRepository */
private $statisticsOpensRepository;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var FormMessageController */
private $messageController;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscribersCountsController */
private $subscribersCountsController;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
];
/** @var NewslettersRepository */
private $newsletterRepository;
/** @var TrackingConfig */
private $trackingConfig;
/** @var SettingsChangeHandler */
private $settingsChangeHandler;
/** @var ConfirmationEmailCustomizer */
private $confirmationEmailCustomizer;
public function __construct(
SettingsController $settings,
Bridge $bridge,
AuthorizedEmailsController $authorizedEmailsController,
AuthorizedSenderDomainController $senderDomainController,
TransactionalEmails $wcTransactionalEmails,
EntityManager $entityManager,
NewslettersRepository $newslettersRepository,
StatisticsOpensRepository $statisticsOpensRepository,
ScheduledTasksRepository $scheduledTasksRepository,
FormMessageController $messageController,
ServicesChecker $servicesChecker,
SegmentsRepository $segmentsRepository,
SettingsChangeHandler $settingsChangeHandler,
SubscribersCountsController $subscribersCountsController,
TrackingConfig $trackingConfig,
ConfirmationEmailCustomizer $confirmationEmailCustomizer
) {
$this->settings = $settings;
$this->bridge = $bridge;
$this->authorizedEmailsController = $authorizedEmailsController;
$this->senderDomainController = $senderDomainController;
$this->wcTransactionalEmails = $wcTransactionalEmails;
$this->servicesChecker = $servicesChecker;
$this->entityManager = $entityManager;
$this->newsletterRepository = $newslettersRepository;
$this->statisticsOpensRepository = $statisticsOpensRepository;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->messageController = $messageController;
$this->segmentsRepository = $segmentsRepository;
$this->settingsChangeHandler = $settingsChangeHandler;
$this->subscribersCountsController = $subscribersCountsController;
$this->trackingConfig = $trackingConfig;
$this->confirmationEmailCustomizer = $confirmationEmailCustomizer;
}
public function get() {
return $this->successResponse($this->settings->getAll());
}
public function set($settings = []) {
if (empty($settings)) {
return $this->badRequest(
[
APIError::BAD_REQUEST =>
__('You have not specified any settings to be saved.', 'mailpoet'),
]
);
} else {
$oldSettings = $this->settings->getAll();
$meta = [];
$signupConfirmation = $this->settings->get('signup_confirmation.enabled');
foreach ($settings as $name => $value) {
$this->settings->set($name, $value);
}
$this->onSettingsChange($oldSettings, $this->settings->getAll());
// when pending approval, leave this to cron / Key Activation tab logic
if (!$this->servicesChecker->isMailPoetAPIKeyPendingApproval()) {
$this->settingsChangeHandler->updateApiKeyState($settings);
}
$meta = $this->authorizedEmailsController->onSettingsSave($settings);
if ($signupConfirmation !== $this->settings->get('signup_confirmation.enabled')) {
$this->messageController->updateSuccessMessages();
}
// Tracking and re-engagement Emails
$meta['showNotice'] = false;
if ($oldSettings['tracking'] !== $this->settings->get('tracking')) {
try {
$meta = $this->updateReEngagementEmailStatus($this->settings->get('tracking'));
} catch (\Exception $e) {
return $this->badRequest([
APIError::UNKNOWN => $e->getMessage()]);
}
}
return $this->successResponse($this->settings->getAll(), $meta);
}
}
public function delete(string $settingName): Response {
if (empty($settingName)) {
return $this->badRequest(
[
APIError::BAD_REQUEST =>
__('You have not specified any setting to be deleted.', 'mailpoet'),
]
);
}
$setting = $this->settings->get($settingName);
if (is_null($setting)) {
return $this->badRequest(
[
APIError::BAD_REQUEST =>
__('Setting doesn\'t exist.', 'mailpoet'),
]
);
}
$this->settings->delete($settingName);
return $this->successResponse();
}
public function recalculateSubscribersScore() {
$this->statisticsOpensRepository->resetSubscribersScoreCalculation();
$this->statisticsOpensRepository->resetSegmentsScoreCalculation();
$task = $this->scheduledTasksRepository->findOneBy([
'type' => SubscribersEngagementScore::TASK_TYPE,
'status' => ScheduledTaskEntity::STATUS_SCHEDULED,
]);
if (!$task) {
$task = new ScheduledTaskEntity();
$task->setType(SubscribersEngagementScore::TASK_TYPE);
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
}
$task->setScheduledAt(Carbon::now()->millisecond(0));
$this->entityManager->persist($task);
$this->entityManager->flush();
return $this->successResponse();
}
public function setAuthorizedFromAddress($data = []) {
$address = $data['address'] ?? null;
if (!$address) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No email address specified.', 'mailpoet'),
]);
}
$address = trim($address);
try {
$this->authorizedEmailsController->setFromEmailAddress($address);
} catch (\InvalidArgumentException $e) {
return $this->badRequest([
APIError::UNAUTHORIZED => __('Cant use this email yet! Please authorize it first.', 'mailpoet'),
]);
}
if (!$this->servicesChecker->isMailPoetAPIKeyPendingApproval()) {
MailerLog::resumeSending();
}
return $this->successResponse();
}
/**
* Create POST request to Bridge endpoint to add email to user email authorization list
*/
public function authorizeSenderEmailAddress($data = []) {
$emailAddress = $data['email'] ?? null;
if (!$emailAddress) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No email address specified.', 'mailpoet'),
]);
}
$emailAddress = trim($emailAddress);
try {
$response = $this->authorizedEmailsController->createAuthorizedEmailAddress($emailAddress);
} catch (\InvalidArgumentException $e) {
if (
$e->getMessage() === AuthorizedEmailsController::AUTHORIZED_EMAIL_ERROR_ALREADY_AUTHORIZED ||
$e->getMessage() === AuthorizedEmailsController::AUTHORIZED_EMAIL_ERROR_PENDING_CONFIRMATION
) {
// return true if the email is already authorized or pending confirmation
$response = ['status' => true];
} else {
return $this->badRequest([
APIError::BAD_REQUEST => $e->getMessage(),
]);
}
}
return $this->successResponse($response);
}
public function confirmSenderEmailAddressIsAuthorized($data = []) {
$emailAddress = $data['email'] ?? null;
if (!$emailAddress) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No email address specified.', 'mailpoet'),
]);
}
$emailAddress = trim($emailAddress);
$response = ['isAuthorized' => $this->authorizedEmailsController->isEmailAddressAuthorized($emailAddress)];
return $this->successResponse($response);
}
public function getAuthorizedSenderDomains($data = []) {
$domain = $data['domain'] ?? null;
if (!$domain) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No sender domain specified.', 'mailpoet'),
]);
}
$domain = strtolower(trim($domain));
$records = $this->bridge->getAuthorizedSenderDomains($domain);
return $this->successResponse($records);
}
public function createAuthorizedSenderDomain($data = []) {
$domain = $data['domain'] ?? null;
if (!$domain) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No sender domain specified.', 'mailpoet'),
]);
}
$domain = strtolower(trim($domain));
try {
$response = $this->senderDomainController->createAuthorizedSenderDomain($domain);
} catch (\InvalidArgumentException $e) {
if (
$e->getMessage() === AuthorizedSenderDomainController::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED
) {
// domain already created
$response = $this->senderDomainController->getDomainRecords($domain);
} else {
return $this->badRequest([
APIError::BAD_REQUEST => $e->getMessage(),
]);
}
}
return $this->successResponse($response);
}
public function verifyAuthorizedSenderDomain($data = []) {
$domain = $data['domain'] ?? null;
if (!$domain) {
return $this->badRequest([
APIError::BAD_REQUEST => __('No sender domain specified.', 'mailpoet'),
]);
}
$domain = strtolower(trim($domain));
try {
$response = $this->senderDomainController->verifyAuthorizedSenderDomain($domain);
} catch (\InvalidArgumentException $e) {
if (
$e->getMessage() === AuthorizedSenderDomainController::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_VERIFIED
) {
// domain already verified, we have to wrap this in the format returned by the api
$response = ['ok' => true, 'dns' => $this->senderDomainController->getDomainRecords($domain)];
} else {
return $this->badRequest([
APIError::BAD_REQUEST => $e->getMessage(),
]);
}
}
if (!$response['ok']) {
// sender domain verification error. probably an improper setup
return $this->badRequest([
APIError::BAD_REQUEST => $response['message'] ?? __('Sender domain verification failed.', 'mailpoet'),
], $response);
}
return $this->successResponse($response);
}
private function onSettingsChange($oldSettings, $newSettings) {
// Recalculate inactive subscribers
$oldInactivationInterval = $oldSettings['deactivate_subscriber_after_inactive_days'];
$newInactivationInterval = $newSettings['deactivate_subscriber_after_inactive_days'];
if ($oldInactivationInterval !== $newInactivationInterval) {
$this->settingsChangeHandler->onInactiveSubscribersIntervalChange();
}
$oldSendingMethod = $oldSettings['mta_group'];
$newSendingMethod = $newSettings['mta_group'];
if (($oldSendingMethod !== $newSendingMethod) && ($newSendingMethod === 'mailpoet')) {
$this->settingsChangeHandler->onMSSActivate($newSettings);
}
if (($oldSendingMethod !== $newSendingMethod)) {
$sendingMethodSet = $newSettings['mta']['method'] ?? null;
if ($sendingMethodSet === 'PHPMail') {
// check for valid mail function
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, true);
} else {
// when the user switch to a new sending method
// do not display the DisabledMailFunctionNotice
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
$this->settings->set(DisabledMailFunctionNotice::DISABLED_MAIL_FUNCTION_CHECK, false); // do not display notice
}
}
// Sync WooCommerce Customers list
$oldSubscribeOldWoocommerceCustomers = isset($oldSettings['mailpoet_subscribe_old_woocommerce_customers']['enabled'])
? $oldSettings['mailpoet_subscribe_old_woocommerce_customers']['enabled']
: '0';
$newSubscribeOldWoocommerceCustomers = isset($newSettings['mailpoet_subscribe_old_woocommerce_customers']['enabled'])
? $newSettings['mailpoet_subscribe_old_woocommerce_customers']['enabled']
: '0';
if ($oldSubscribeOldWoocommerceCustomers !== $newSubscribeOldWoocommerceCustomers) {
$this->settingsChangeHandler->onSubscribeOldWoocommerceCustomersChange();
}
if (!empty($newSettings['woocommerce']['use_mailpoet_editor'])) {
$this->wcTransactionalEmails->init();
}
if (!empty($newSettings['signup_confirmation']['use_mailpoet_editor'])) {
$this->confirmationEmailCustomizer->init();
}
}
public function recalculateSubscribersCountsCache() {
$segments = $this->segmentsRepository->findAll();
foreach ($segments as $segment) {
$this->subscribersCountsController->recalculateSegmentStatisticsCache($segment);
}
$this->subscribersCountsController->recalculateSubscribersWithoutSegmentStatisticsCache();
// remove redundancies from cache
$this->subscribersCountsController->removeRedundancyFromStatisticsCache();
return $this->successResponse();
}
/**
* @throws \Exception
*/
public function updateReEngagementEmailStatus($newTracking): array {
if (!empty($newTracking['level']) && $this->trackingConfig->isEmailTrackingEnabled($newTracking['level'])) {
return $this->reactivateReEngagementEmails();
}
try {
return $this->deactivateReEngagementEmails();
} catch (\Exception $e) {
throw new \Exception(
sprintf(
// translators: %s is the error message.
__('Unable to deactivate re-engagement emails: %s', 'mailpoet'),
$e->getMessage()
)
);
}
}
/**
* @throws \Exception
*/
public function deactivateReEngagementEmails(): array {
$reEngagementEmails = $this->newsletterRepository->findActiveByTypes(([NewsletterEntity::TYPE_RE_ENGAGEMENT]));
if (!$reEngagementEmails) {
return [
'showNotice' => false,
'action' => 'deactivate',
];
}
foreach ($reEngagementEmails as $reEngagementEmail) {
$reEngagementEmail->setStatus(NewsletterEntity::STATUS_DRAFT);
$this->entityManager->persist($reEngagementEmail);
$this->entityManager->flush();
}
return [
'showNotice' => true,
'action' => 'deactivate',
];
}
public function reactivateReEngagementEmails(): array {
$draftReEngagementEmails = $this->newsletterRepository->findDraftByTypes(([NewsletterEntity::TYPE_RE_ENGAGEMENT]));
return [
'showNotice' => !!$draftReEngagementEmails,
'action' => 'reactivate',
];
}
/**
* Prepares the settings to set up MSS with the given key and calls the set method.
*
* @param string $apiKey
* @return ErrorResponse|SuccessResponse
*/
public function setKeyAndSetupMss(string $apiKey) {
$new_settings = [
'mta_group' => 'mailpoet',
'mta' => [
'method' => Mailer::METHOD_MAILPOET,
'mailpoet_api_key' => $apiKey,
],
'signup_confirmation' => [
'enabled' => '1',
],
'premium.premium_key' => $apiKey,
];
return $this->set($new_settings);
}
}
@@ -0,0 +1,51 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\AccessControl;
use MailPoet\Config\Activator;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
class Setup extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
];
/** @var WPFunctions */
private $wp;
/** @var Activator */
private $activator;
/** @var SettingsController */
private $settings;
public function __construct(
WPFunctions $wp,
Activator $activator,
SettingsController $settings
) {
$this->wp = $wp;
$this->activator = $activator;
$this->settings = $settings;
}
public function reset() {
try {
$this->activator->deactivate();
$this->settings->resetCache();
$this->activator->activate();
$this->wp->doAction('mailpoet_setup_reset');
return $this->successResponse();
} catch (\Exception $e) {
return $this->errorResponse([
$e->getCode() => $e->getMessage(),
]);
}
}
}
@@ -0,0 +1,107 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\AccessControl;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\Statistics\SubscriberStatistics;
use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WooCommerce\Helper;
use MailPoetVendor\Carbon\Carbon;
class SubscriberStats extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SUBSCRIBERS,
];
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SubscriberStatisticsRepository */
private $subscribersStatisticsRepository;
/** @var Helper */
private $wooCommerceHelper;
public function __construct(
SubscribersRepository $subscribersRepository,
SubscriberStatisticsRepository $subscribersStatisticsRepository,
Helper $wooCommerceHelper
) {
$this->subscribersRepository = $subscribersRepository;
$this->subscribersStatisticsRepository = $subscribersStatisticsRepository;
$this->wooCommerceHelper = $wooCommerceHelper;
}
public function get($data) {
$subscriber = isset($data['subscriber_id'])
? $this->subscribersRepository->findOneById((int)$data['subscriber_id'])
: null;
if (!$subscriber instanceof SubscriberEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
$response = [
'email' => $subscriber->getEmail(),
'engagement_score' => $subscriber->getEngagementScore(),
'is_woo_active' => $this->wooCommerceHelper->isWooCommerceActive(),
];
$statsMapper = function(SubscriberStatistics $statistics, string $timeframe) {
return [
'timeframe' => $timeframe,
'total_sent' => $statistics->getTotalSentCount(),
'open' => $statistics->getOpenCount(),
'machine_open' => $statistics->getMachineOpenCount(),
'click' => $statistics->getClickCount(),
'woocommerce' => $statistics->getWooCommerceRevenue() ? $statistics->getWooCommerceRevenue()->asArray() : null,
];
};
$lifetimeStats = $this->subscribersStatisticsRepository->getStatistics($subscriber);
$oneYearStats = $this->subscribersStatisticsRepository->getStatistics($subscriber, Carbon::now()->subYear());
$thirtyDaysStats = $this->subscribersStatisticsRepository->getStatistics($subscriber, Carbon::now()->subDays(30));
$response['periodic_stats'] = [
// translators: table header meaning 30 days
$statsMapper($thirtyDaysStats, __('30(d)', 'mailpoet')),
// translators: table header meaning 12 months
$statsMapper($oneYearStats, __('12(m)', 'mailpoet')),
$statsMapper($lifetimeStats, __('Lifetime', 'mailpoet')),
];
$dateFormat = 'Y-m-d H:i:s';
$lastEngagement = $subscriber->getLastEngagementAt();
if ($lastEngagement instanceof \DateTimeInterface) {
$response['last_engagement'] = $lastEngagement->format($dateFormat);
}
$lastClick = $subscriber->getLastClickAt();
if ($lastClick instanceof \DateTimeInterface) {
$response['last_click'] = $lastClick->format($dateFormat);
}
$lastOpen = $subscriber->getLastOpenAt();
if ($lastOpen instanceof \DateTimeInterface) {
$response['last_open'] = $lastOpen->format($dateFormat);
}
$lastPageView = $subscriber->getLastPageViewAt();
if ($lastPageView instanceof \DateTimeInterface) {
$response['last_page_view'] = $lastPageView->format($dateFormat);
}
$lastPurchase = $subscriber->getLastPurchaseAt();
if ($lastPurchase instanceof \DateTimeInterface) {
$response['last_purchase'] = $lastPurchase->format($dateFormat);
}
$lastSending = $subscriber->getLastSendingAt();
if ($lastSending instanceof \DateTimeInterface) {
$response['last_sending'] = $lastSending->format($dateFormat);
}
return $this->successResponse($response);
}
}
@@ -0,0 +1,350 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\ErrorResponse;
use MailPoet\API\JSON\Response;
use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\Config\AccessControl;
use MailPoet\ConflictException;
use MailPoet\Doctrine\Validator\ValidationException;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\TagEntity;
use MailPoet\Exception;
use MailPoet\Listing;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\ConfirmationEmailMailer;
use MailPoet\Subscribers\SubscriberListingRepository;
use MailPoet\Subscribers\SubscriberSaveController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Subscribers\SubscriberSubscribeController;
use MailPoet\Tags\TagRepository;
use MailPoet\UnexpectedValueException;
use MailPoet\Util\Helpers;
class Subscribers extends APIEndpoint {
const SUBSCRIPTION_LIMIT_COOLDOWN = 60;
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_SUBSCRIBERS,
'methods' => ['subscribe' => AccessControl::NO_ACCESS_RESTRICTION],
];
/** @var Listing\Handler */
private $listingHandler;
/** @var ConfirmationEmailMailer; */
private $confirmationEmailMailer;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SubscribersResponseBuilder */
private $subscribersResponseBuilder;
/** @var SubscriberListingRepository */
private $subscriberListingRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var TagRepository */
private $tagRepository;
/** @var SubscriberSaveController */
private $saveController;
/** @var SubscriberSubscribeController */
private $subscribeController;
/** @var SettingsController */
private $settings;
public function __construct(
Listing\Handler $listingHandler,
ConfirmationEmailMailer $confirmationEmailMailer,
SubscribersRepository $subscribersRepository,
SubscribersResponseBuilder $subscribersResponseBuilder,
SubscriberListingRepository $subscriberListingRepository,
SegmentsRepository $segmentsRepository,
TagRepository $tagRepository,
SubscriberSaveController $saveController,
SubscriberSubscribeController $subscribeController,
SettingsController $settings
) {
$this->listingHandler = $listingHandler;
$this->confirmationEmailMailer = $confirmationEmailMailer;
$this->subscribersRepository = $subscribersRepository;
$this->subscribersResponseBuilder = $subscribersResponseBuilder;
$this->subscriberListingRepository = $subscriberListingRepository;
$this->segmentsRepository = $segmentsRepository;
$this->tagRepository = $tagRepository;
$this->saveController = $saveController;
$this->subscribeController = $subscribeController;
$this->settings = $settings;
}
public function get($data = []) {
$subscriber = $this->getSubscriber($data);
if (!$subscriber instanceof SubscriberEntity) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
$result = $this->subscribersResponseBuilder->build($subscriber);
return $this->successResponse($result);
}
public function listing($data = []) {
$definition = $this->listingHandler->getListingDefinition($data);
$items = $this->subscriberListingRepository->getData($definition);
$count = $this->subscriberListingRepository->getCount($definition);
$filters = $this->subscriberListingRepository->getFilters($definition);
$groups = $this->subscriberListingRepository->getGroups($definition);
$subscribers = $this->subscribersResponseBuilder->buildForListing($items);
if ($data['filter']['segment'] ?? false) {
foreach ($subscribers as $key => $subscriber) {
$subscribers[$key] = $this->preferUnsubscribedStatusFromSegment($subscriber, $data['filter']['segment']);
}
}
return $this->successResponse($subscribers, [
'count' => $count,
'filters' => $filters,
'groups' => $groups,
]);
}
private function preferUnsubscribedStatusFromSegment(array $subscriber, $segmentId) {
$segmentStatus = $this->findSegmentStatus($subscriber, $segmentId);
if ($segmentStatus === SubscriberEntity::STATUS_UNSUBSCRIBED) {
$subscriber['status'] = $segmentStatus;
}
return $subscriber;
}
private function findSegmentStatus(array $subscriber, $segmentId) {
foreach ($subscriber['subscriptions'] as $segment) {
if ($segment['segment_id'] === $segmentId) {
return $segment['status'];
}
}
}
public function subscribe($data = []) {
try {
$meta = $this->subscribeController->subscribe($data);
} catch (Exception $exception) {
return $this->badRequest([$exception->getMessage()]);
}
if (!empty($meta['error'])) {
$errorMessage = $meta['error'];
unset($meta['error']);
return $this->badRequest([APIError::BAD_REQUEST => $errorMessage], $meta);
}
return $this->successResponse(
[],
$meta
);
}
/**
* @param array $data
* @return ErrorResponse|SuccessResponse
* @throws \Exception
*/
public function save(array $data = []) {
try {
$subscriber = $this->saveController->save($data);
} catch (ValidationException $validationException) {
return $this->badRequest([$this->getErrorMessage($validationException)]);
} catch (ConflictException $conflictException) {
return $this->badRequest([
APIError::BAD_REQUEST => $conflictException->getMessage(),
]);
};
return $this->successResponse(
$this->subscribersResponseBuilder->build($subscriber)
);
}
public function restore($data = []) {
$subscriber = $this->getSubscriber($data);
if ($subscriber instanceof SubscriberEntity) {
$this->subscribersRepository->bulkRestore([$subscriber->getId()]);
$this->subscribersRepository->refresh($subscriber);
return $this->successResponse(
$this->subscribersResponseBuilder->build($subscriber),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
}
public function trash($data = []) {
$subscriber = $this->getSubscriber($data);
if ($subscriber instanceof SubscriberEntity) {
$this->subscribersRepository->bulkTrash([$subscriber->getId()]);
$this->subscribersRepository->refresh($subscriber);
return $this->successResponse(
$this->subscribersResponseBuilder->build($subscriber),
['count' => 1]
);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
}
public function delete($data = []) {
$subscriber = $this->getSubscriber($data);
if ($subscriber instanceof SubscriberEntity) {
$count = $this->subscribersRepository->bulkDelete([$subscriber->getId()]);
return $this->successResponse(null, ['count' => $count]);
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
}
public function sendConfirmationEmail($data = []) {
if (!(bool)$this->settings->get('signup_confirmation.enabled', true)) {
$errorMessage = __('Sign-up confirmation is disabled in your [link]MailPoet settings[/link]. Please enable it to resend confirmation emails or update your subscribers status manually.', 'mailpoet');
$errorMessage = Helpers::replaceLinkTags($errorMessage, 'admin.php?page=mailpoet-settings#/signup');
return $this->errorResponse([APIError::BAD_REQUEST => $errorMessage], [], Response::STATUS_BAD_REQUEST);
}
$id = (isset($data['id']) ? (int)$data['id'] : false);
$subscriber = $this->subscribersRepository->findOneById($id);
if ($subscriber instanceof SubscriberEntity) {
try {
if ($this->confirmationEmailMailer->sendConfirmationEmail($subscriber)) {
return $this->successResponse();
} else {
return $this->errorResponse([
APIError::UNKNOWN => __('There was a problem with your sending method. Please check if your sending method is properly configured.', 'mailpoet'),
]);
}
} catch (\Exception $e) {
return $this->errorResponse([
APIError::UNKNOWN => __('There was a problem with your sending method. Please check if your sending method is properly configured.', 'mailpoet'),
]);
}
} else {
return $this->errorResponse([
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]);
}
}
public function bulkAction($data = []) {
$definition = $this->listingHandler->getListingDefinition($data['listing']);
$ids = $this->subscriberListingRepository->getActionableIds($definition);
$count = 0;
$segment = null;
if (isset($data['segment_id'])) {
$segment = $this->getSegment($data);
if (!$segment) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This segment does not exist.', 'mailpoet'),
]);
}
}
$tag = null;
if (isset($data['tag_id'])) {
$tag = $this->getTag($data);
if (!$tag) {
return $this->errorResponse([
APIError::NOT_FOUND => __('This tag does not exist.', 'mailpoet'),
]);
}
}
if ($data['action'] === 'trash') {
$count = $this->subscribersRepository->bulkTrash($ids);
} elseif ($data['action'] === 'restore') {
$count = $this->subscribersRepository->bulkRestore($ids);
} elseif ($data['action'] === 'delete') {
$count = $this->subscribersRepository->bulkDelete($ids);
} elseif ($data['action'] === 'removeFromAllLists') {
$count = $this->subscribersRepository->bulkRemoveFromAllSegments($ids);
} elseif ($data['action'] === 'removeFromList' && $segment instanceof SegmentEntity) {
$count = $this->subscribersRepository->bulkRemoveFromSegment($segment, $ids);
} elseif ($data['action'] === 'addToList' && $segment instanceof SegmentEntity) {
$count = $this->subscribersRepository->bulkAddToSegment($segment, $ids);
} elseif ($data['action'] === 'moveToList' && $segment instanceof SegmentEntity) {
$count = $this->subscribersRepository->bulkMoveToSegment($segment, $ids);
} elseif ($data['action'] === 'unsubscribe') {
$count = $this->subscribersRepository->bulkUnsubscribe($ids);
} elseif ($data['action'] === 'addTag' && $tag instanceof TagEntity) {
$count = $this->subscribersRepository->bulkAddTag($tag, $ids);
} elseif ($data['action'] === 'removeTag' && $tag instanceof TagEntity) {
$count = $this->subscribersRepository->bulkRemoveTag($tag, $ids);
} else {
throw UnexpectedValueException::create()
->withErrors([APIError::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
}
$meta = [
'count' => $count,
];
if ($segment) {
$meta['segment'] = $segment->getName();
}
if ($tag) {
$meta['tag'] = $tag->getName();
}
return $this->successResponse(null, $meta);
}
/**
* @param array $data
* @return SubscriberEntity|null
*/
private function getSubscriber($data) {
return isset($data['id'])
? $this->subscribersRepository->findOneById((int)$data['id'])
: null;
}
private function getSegment(array $data): ?SegmentEntity {
return isset($data['segment_id'])
? $this->segmentsRepository->findOneById((int)$data['segment_id'])
: null;
}
private function getTag(array $data): ?TagEntity {
return isset($data['tag_id'])
? $this->tagRepository->findOneById((int)$data['tag_id'])
: null;
}
private function getErrorMessage(ValidationException $exception): string {
$exceptionMessage = $exception->getMessage();
if (strpos($exceptionMessage, 'This value should not be blank.') !== false) {
return __('Please enter your email address', 'mailpoet');
} elseif (strpos($exceptionMessage, 'This value is not a valid email address.') !== false) {
return __('Your email address is invalid!', 'mailpoet');
}
return __('Unexpected error.', 'mailpoet');
}
}
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Entities\TagEntity;
use MailPoet\Tags\TagRepository;
class Tags extends APIEndpoint {
private $repository;
public function __construct(
TagRepository $repository
) {
$this->repository = $repository;
}
public function create($data = []) {
if (!isset($data['name'])) {
return $this->badRequest([
APIError::BAD_REQUEST => __('A tag needs to have a name.', 'mailpoet'),
]);
}
$data['name'] = sanitize_text_field(wp_unslash($data['name']));
$data['description'] = isset($data['description']) ? sanitize_text_field(wp_unslash($data['description'])) : '';
return $this->successResponse(
$this->mapTagEntity($this->repository->createOrUpdate($data))
);
}
public function listing() {
return $this->successResponse(
array_map(
[$this, 'mapTagEntity'],
$this->repository->findAll()
)
);
}
private function mapTagEntity(TagEntity $tag): array {
return [
'id' => $tag->getId(),
'name' => $tag->getName(),
'description' => $tag->getDescription(),
'created_at' => $tag->getCreatedAt(),
'updated_at' => $tag->getUpdatedAt(),
];
}
}
@@ -0,0 +1,43 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\AccessControl;
use MailPoet\Settings\UserFlagsController;
class UserFlags extends APIEndpoint {
/** @var UserFlagsController */
private $userFlags;
public $permissions = [
'global' => AccessControl::ALL_ROLES_ACCESS,
];
public function __construct(
UserFlagsController $userFlags
) {
$this->userFlags = $userFlags;
}
public function set(array $flags = []) {
if (empty($flags)) {
return $this->badRequest(
[
APIError::BAD_REQUEST =>
__('You have not specified any user flags to be saved.', 'mailpoet'),
]
);
} else {
foreach ($flags as $name => $value) {
$this->userFlags->set($name, $value);
}
return $this->successResponse([]);
}
}
}
@@ -0,0 +1,38 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\JSON\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\AccessControl;
use MailPoet\WP\Functions as WPFunctions;
class WoocommerceSettings extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
private $allowedSettings = [
'woocommerce_email_base_color',
];
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function set($data = []) {
foreach ($data as $option => $value) {
if (in_array($option, $this->allowedSettings)) {
$this->wp->updateOption($option, $value);
}
}
return $this->successResponse([]);
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,121 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\MP\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Changelog;
/**
* API used by other plugins
* Do not add bodies of methods into this class. Use other classes. See CustomFields or Subscribers.
* This class is under refactor, and we are going to move most of the remaining implementations from here.
*/
class API {
/** @var CustomFields */
private $customFields;
/** @var Segments */
private $segments;
/** @var Subscribers */
private $subscribers;
/** @var Changelog */
private $changelog;
public function __construct(
CustomFields $customFields,
Segments $segments,
Subscribers $subscribers,
Changelog $changelog
) {
$this->customFields = $customFields;
$this->segments = $segments;
$this->subscribers = $subscribers;
$this->changelog = $changelog;
}
public function getSubscriberFields() {
return $this->customFields->getSubscriberFields();
}
public function addSubscriberField(array $data = []) {
try {
return $this->customFields->addSubscriberField($data);
} catch (\InvalidArgumentException $e) {
throw new APIException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* @throws APIException
*/
public function subscribeToList($subscriberId, $listId, $options = []): array {
return $this->subscribeToLists($subscriberId, [$listId], $options);
}
/**
* @throws APIException
*/
public function subscribeToLists($subscriberId, array $listIds, $options = []) {
return $this->subscribers->subscribeToLists($subscriberId, $listIds, $options);
}
public function unsubscribeFromList($subscriberId, $listId) {
return $this->unsubscribeFromLists($subscriberId, [$listId]);
}
public function unsubscribeFromLists($subscriberId, array $listIds) {
return $this->subscribers->unsubscribeFromLists($subscriberId, $listIds);
}
public function unsubscribe($subscriberIdOrEmail) {
return $this->subscribers->unsubscribe($subscriberIdOrEmail);
}
public function getLists(): array {
return $this->segments->getAll();
}
public function addSubscriber(array $subscriber, $listIds = [], $options = []): array {
return $this->subscribers->addSubscriber($subscriber, $listIds, $options);
}
public function updateSubscriber($subscriberIdOrEmail, array $subscriber): array {
return $this->subscribers->updateSubscriber($subscriberIdOrEmail, $subscriber);
}
public function addList(array $list) {
return $this->segments->addList($list);
}
public function deleteList(string $listId): bool {
return $this->segments->deleteList($listId);
}
public function updateList(array $list): array {
return $this->segments->updateList($list);
}
public function getSubscriber($subscriberEmail) {
return $this->subscribers->getSubscriber($subscriberEmail);
}
public function getSubscribers(array $filter = [], int $limit = 50, int $offset = 0): array {
return $this->subscribers->getSubscribers($filter, $limit, $offset);
}
public function getSubscribersCount(array $filter = []): int {
return $this->subscribers->getSubscribersCount($filter);
}
public function isSetupComplete() {
return !(
$this->changelog->shouldShowWelcomeWizard()
|| $this->changelog->shouldShowWooCommerceListImportPage()
|| $this->changelog->shouldShowRevenueTrackingPermissionPage()
);
}
}
@@ -0,0 +1,31 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\MP\v1;
if (!defined('ABSPATH')) exit;
class APIException extends \Exception {
const FAILED_TO_SAVE_SUBSCRIBER_FIELD = 1;
const SEGMENT_REQUIRED = 3;
const SUBSCRIBER_NOT_EXISTS = 4;
const LIST_NOT_EXISTS = 5;
const SUBSCRIBING_TO_WP_LIST_NOT_ALLOWED = 6;
const SUBSCRIBING_TO_WC_LIST_NOT_ALLOWED = 7;
const SUBSCRIBING_TO_LIST_NOT_ALLOWED = 8;
const CONFIRMATION_FAILED_TO_SEND = 10;
const EMAIL_ADDRESS_REQUIRED = 11;
const SUBSCRIBER_EXISTS = 12;
const FAILED_TO_SAVE_SUBSCRIBER = 13;
const LIST_NAME_REQUIRED = 14;
const LIST_EXISTS = 15;
const FAILED_TO_SAVE_LIST = 16;
const WELCOME_FAILED_TO_SEND = 17;
const LIST_ID_REQUIRED = 18;
const FAILED_TO_UPDATE_LIST = 19;
const LIST_USED_IN_EMAIL = 20;
const LIST_USED_IN_FORM = 21;
const FAILED_TO_DELETE_LIST = 22;
const LIST_TYPE_IS_NOT_SUPPORTED = 23;
const SUBSCRIBER_ALREADY_UNSUBSCRIBED = 24;
}
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\MP\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\CustomFields\ApiDataSanitizer;
use MailPoet\CustomFields\CustomFieldsRepository;
class CustomFields {
/** @var ApiDataSanitizer */
private $customFieldsDataSanitizer;
/** @var CustomFieldsRepository */
private $customFieldsRepository;
public function __construct(
ApiDataSanitizer $customFieldsDataSanitizer,
CustomFieldsRepository $customFieldsRepository
) {
$this->customFieldsDataSanitizer = $customFieldsDataSanitizer;
$this->customFieldsRepository = $customFieldsRepository;
}
public function getSubscriberFields(): array {
$data = [
[
'id' => 'email',
'name' => __('Email', 'mailpoet'),
'type' => 'text',
'params' => [
'required' => '1',
],
],
[
'id' => 'first_name',
'name' => __('First name', 'mailpoet'),
'type' => 'text',
'params' => [
'required' => '',
],
],
[
'id' => 'last_name',
'name' => __('Last name', 'mailpoet'),
'type' => 'text',
'params' => [
'required' => '',
],
],
];
$customFields = $this->customFieldsRepository->findAll();
foreach ($customFields as $customField) {
$result = [
'id' => 'cf_' . $customField->getId(),
'name' => $customField->getName(),
'type' => $customField->getType(),
'params' => $customField->getParams(),
];
$data[] = $result;
}
return $data;
}
public function addSubscriberField(array $data = []): array {
try {
$customField = $this->customFieldsRepository->createOrUpdate($this->customFieldsDataSanitizer->sanitize($data));
} catch (\Exception $e) {
throw new APIException('Failed to save a new subscriber field ' . $e->getMessage(), APIException::FAILED_TO_SAVE_SUBSCRIBER_FIELD);
}
return [
'id' => 'cf_' . $customField->getId(),
'name' => $customField->getName(),
'type' => $customField->getType(),
'params' => $customField->getParams(),
];
}
}
@@ -0,0 +1,208 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\MP\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Form\FormsRepository;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Segments\SegmentsRepository;
class Segments {
private const DATE_FORMAT = 'Y-m-d H:i:s';
/** @var NewsletterSegmentRepository */
private $newsletterSegmentRepository;
/** @var FormsRepository */
private $formsRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
public function __construct (
NewsletterSegmentRepository $newsletterSegmentRepository,
FormsRepository $formsRepository,
SegmentsRepository $segmentsRepository
) {
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
$this->formsRepository = $formsRepository;
$this->segmentsRepository = $segmentsRepository;
}
public function getAll(): array {
$segments = $this->segmentsRepository->findBy(['type' => SegmentEntity::TYPE_DEFAULT], ['id' => 'asc']);
$result = [];
foreach ($segments as $segment) {
$result[] = $this->buildItem($segment);
}
return $result;
}
public function addList(array $data): array {
$this->validateSegmentName($data);
try {
$name = isset($data['name']) ? sanitize_text_field($data['name']) : '';
$description = isset($data['description']) ? sanitize_textarea_field($data['description']) : '';
$segment = $this->segmentsRepository->createOrUpdate($name, $description);
} catch (\Exception $e) {
throw new APIException(
__('The list couldnt be created in the database', 'mailpoet'),
APIException::FAILED_TO_SAVE_LIST
);
}
return $this->buildItem($segment);
}
public function updateList(array $data): array {
// firstly validation on list id
$this->validateSegmentId((string)($data['id'] ?? ''));
// secondly validation on list name
$this->validateSegmentName($data);
// update is supported only for default segment type
$this->validateSegmentType((string)$data['id']);
$name = isset($data['name']) ? sanitize_text_field($data['name']) : '';
$description = isset($data['description']) ? sanitize_textarea_field($data['description']) : '';
try {
$segment = $this->segmentsRepository->createOrUpdate(
$name,
$description,
SegmentEntity::TYPE_DEFAULT,
[],
(int)$data['id']
);
} catch (\Exception $e) {
throw new APIException(
__('The list couldnt be updated in the database', 'mailpoet'),
APIException::FAILED_TO_UPDATE_LIST
);
}
return $this->buildItem($segment);
}
public function deleteList(string $listId): bool {
$this->validateSegmentId($listId);
// delete is supported only for default segment type
$this->validateSegmentType($listId);
$activelyUsedNewslettersSubjects = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments([$listId]);
if (isset($activelyUsedNewslettersSubjects[$listId])) {
throw new APIException(
str_replace(
'%1$s',
"'" . join("', '", $activelyUsedNewslettersSubjects[$listId]) . "'",
// translators: %1$s is a comma-seperated list of emails for which the segment is used.
_x('List cannot be deleted because its used for %1$s email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet')
),
APIException::LIST_USED_IN_EMAIL
);
}
$activelyUsedFormNames = $this->formsRepository->getNamesOfFormsForSegments();
if (isset($activelyUsedFormNames[$listId])) {
throw new APIException(
str_replace(
'%1$s',
"'" . join("', '", $activelyUsedFormNames[$listId]) . "'",
// translators: %1$s is a comma-seperated list of forms for which the segment is used.
_nx(
'List cannot be deleted because its used for %1$s form',
'List cannot be deleted because its used for %1$s forms',
count($activelyUsedFormNames[$listId]),
'Alert shown when trying to delete segment, when it is assigned to a form.',
'mailpoet'
)
),
APIException::LIST_USED_IN_FORM
);
}
try {
$this->segmentsRepository->bulkDelete([$listId]);
return true;
} catch (\Exception $e) {
throw new APIException(
__('The list couldnt be deleted from the database', 'mailpoet'),
APIException::FAILED_TO_DELETE_LIST
);
}
}
private function validateSegmentId(string $segmentId): void {
if (empty($segmentId)) {
throw new APIException(
__('List id is required.', 'mailpoet'),
APIException::LIST_ID_REQUIRED
);
}
if (!$this->segmentsRepository->findOneById($segmentId)) {
throw new APIException(
__('The list does not exist.', 'mailpoet'),
APIException::LIST_NOT_EXISTS
);
}
}
/**
* Throws an exception when the segment's name is invalid
* @return void
*/
private function validateSegmentName(array $data): void {
if (empty($data['name'])) {
throw new APIException(
__('List name is required.', 'mailpoet'),
APIException::LIST_NAME_REQUIRED
);
}
$segmentId = isset($data['id']) ? (int)$data['id'] : null;
if (!$this->segmentsRepository->isNameUnique($data['name'], $segmentId)) {
throw new APIException(
__('This list already exists.', 'mailpoet'),
APIException::LIST_EXISTS
);
}
}
private function validateSegmentType(string $segmentId): void {
$segment = $this->segmentsRepository->findOneById($segmentId);
if ($segment && $segment->getType() !== SegmentEntity::TYPE_DEFAULT) {
throw new APIException(
str_replace(
'%1$s',
"'" . $segment->getType() . "'",
// translators: %1$s is an invalid segment type.
__('List of the type %1$s is not supported for this action.', 'mailpoet')
),
APIException::LIST_TYPE_IS_NOT_SUPPORTED
);
}
}
/**
* @param SegmentEntity $segment
* @return array
*/
private function buildItem(SegmentEntity $segment): array {
return [
'id' => (string)$segment->getId(), // (string) for BC
'name' => $segment->getName(),
'type' => $segment->getType(),
'description' => $segment->getDescription(),
'created_at' => ($createdAt = $segment->getCreatedAt()) ? $createdAt->format(self::DATE_FORMAT) : null,
'updated_at' => $segment->getUpdatedAt()->format(self::DATE_FORMAT),
'deleted_at' => ($deletedAt = $segment->getDeletedAt()) ? $deletedAt->format(self::DATE_FORMAT) : null,
];
}
}
@@ -0,0 +1,512 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\API\MP\v1;
if (!defined('ABSPATH')) exit;
use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\Track\Unsubscribes;
use MailPoet\Subscribers\ConfirmationEmailMailer;
use MailPoet\Subscribers\NewSubscriberNotificationMailer;
use MailPoet\Subscribers\RequiredCustomFieldValidator;
use MailPoet\Subscribers\Source;
use MailPoet\Subscribers\SubscriberListingRepository;
use MailPoet\Subscribers\SubscriberSaveController;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class Subscribers {
const CONTEXT_SUBSCRIBE = 'subscribe';
const CONTEXT_UNSUBSCRIBE = 'unsubscribe';
/** @var SettingsController */
private $settings;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscriberSegmentRepository */
private $subscribersSegmentRepository;
/** @var ConfirmationEmailMailer */
private $confirmationEmailMailer;
/** @var WelcomeScheduler */
private $welcomeScheduler;
/** @var SubscribersResponseBuilder */
private $subscribersResponseBuilder;
/** @var NewSubscriberNotificationMailer */
private $newSubscriberNotificationMailer;
/** @var SubscriberSaveController */
private $subscriberSaveController;
/** @var RequiredCustomFieldValidator */
private $requiredCustomFieldsValidator;
/** @var WPFunctions */
private $wp;
/** @var SubscriberListingRepository */
private $subscriberListingRepository;
/** @var Unsubscribes */
private $unsubscribesTracker;
public function __construct (
ConfirmationEmailMailer $confirmationEmailMailer,
NewSubscriberNotificationMailer $newSubscriberNotificationMailer,
SegmentsRepository $segmentsRepository,
SettingsController $settings,
SubscriberSegmentRepository $subscriberSegmentRepository,
SubscribersRepository $subscribersRepository,
SubscriberSaveController $subscriberSaveController,
SubscribersResponseBuilder $subscribersResponseBuilder,
WelcomeScheduler $welcomeScheduler,
RequiredCustomFieldValidator $requiredCustomFieldsValidator,
SubscriberListingRepository $subscriberListingRepository,
WPFunctions $wp,
Unsubscribes $unsubscribesTracker
) {
$this->confirmationEmailMailer = $confirmationEmailMailer;
$this->newSubscriberNotificationMailer = $newSubscriberNotificationMailer;
$this->segmentsRepository = $segmentsRepository;
$this->settings = $settings;
$this->subscribersSegmentRepository = $subscriberSegmentRepository;
$this->subscribersRepository = $subscribersRepository;
$this->subscriberSaveController = $subscriberSaveController;
$this->subscribersResponseBuilder = $subscribersResponseBuilder;
$this->welcomeScheduler = $welcomeScheduler;
$this->requiredCustomFieldsValidator = $requiredCustomFieldsValidator;
$this->wp = $wp;
$this->subscriberListingRepository = $subscriberListingRepository;
$this->unsubscribesTracker = $unsubscribesTracker;
}
public function getSubscriber($subscriberIdOrEmail): array {
$subscriber = $this->findSubscriber($subscriberIdOrEmail);
return $this->subscribersResponseBuilder->build($subscriber);
}
public function addSubscriber(array $data, array $listIds = [], array $options = []): array {
$sendConfirmationEmail = !(isset($options['send_confirmation_email']) && $options['send_confirmation_email'] === false);
$scheduleWelcomeEmail = !(isset($options['schedule_welcome_email']) && $options['schedule_welcome_email'] === false);
$skipSubscriberNotification = (isset($options['skip_subscriber_notification']) && $options['skip_subscriber_notification'] === true);
// throw exception when subscriber email is missing
if (empty($data['email'])) {
throw new APIException(
__('Subscriber email address is required.', 'mailpoet'),
APIException::EMAIL_ADDRESS_REQUIRED
);
}
// throw exception when subscriber already exists
if ($this->subscribersRepository->findOneBy(['email' => $data['email']])) {
throw new APIException(
__('This subscriber already exists.', 'mailpoet'),
APIException::SUBSCRIBER_EXISTS
);
}
[$defaultFields, $customFields] = $this->extractCustomFieldsFromFromSubscriberData($data);
$this->requiredCustomFieldsValidator->validate($customFields);
// filter out all incoming data that we don't want to change, like status ...
$defaultFields = array_intersect_key($defaultFields, array_flip(['email', 'first_name', 'last_name', 'subscribed_ip']));
if (empty($defaultFields['subscribed_ip'])) {
$defaultFields['subscribed_ip'] = Helpers::getIP();
}
$defaultFields['source'] = Source::API;
try {
$subscriberEntity = $this->subscriberSaveController->createOrUpdate($defaultFields, null);
} catch (\Exception $e) {
throw new APIException(
// translators: %s is an error message.
sprintf(__('Failed to add subscriber: %s', 'mailpoet'), $e->getMessage()),
APIException::FAILED_TO_SAVE_SUBSCRIBER
);
}
try {
$this->subscriberSaveController->updateCustomFields($customFields, $subscriberEntity);
} catch (\Exception $e) {
throw new APIException(
// translators: %s is an error message
sprintf(__('Failed to save subscriber custom fields: %s', 'mailpoet'), $e->getMessage()),
APIException::FAILED_TO_SAVE_SUBSCRIBER
);
}
// subscribe to segments and optionally: 1) send confirmation email, 2) schedule welcome email(s)
if (!empty($listIds)) {
$this->subscribeToLists($subscriberEntity->getId(), $listIds, [
'send_confirmation_email' => $sendConfirmationEmail,
'schedule_welcome_email' => $scheduleWelcomeEmail,
'skip_subscriber_notification' => $skipSubscriberNotification,
]);
}
return $this->subscribersResponseBuilder->build($subscriberEntity);
}
public function updateSubscriber($subscriberIdOrEmail, array $data): array {
$this->checkSubscriberParam($subscriberIdOrEmail);
$subscriber = $this->findSubscriber($subscriberIdOrEmail);
[$defaultFields, $customFields] = $this->extractCustomFieldsFromFromSubscriberData($data);
$this->requiredCustomFieldsValidator->validate($customFields);
// filter out all incoming data that we don't want to change, like status ...
$defaultFields = array_intersect_key($defaultFields, array_flip(['email', 'first_name', 'last_name', 'subscribed_ip']));
if ($subscriber->getWpUserId() !== null) {
unset($defaultFields['email']);
unset($defaultFields['first_name']);
unset($defaultFields['last_name']);
};
if (empty($defaultFields['subscribed_ip'])) {
$defaultFields['subscribed_ip'] = Helpers::getIP();
}
$defaultFields['source'] = Source::API;
try {
$subscriberEntity = $this->subscriberSaveController->createOrUpdate($defaultFields, $subscriber);
} catch (\Exception $e) {
throw new APIException(
// translators: %s is an error message.
sprintf(__('Failed to update subscriber: %s', 'mailpoet'), $e->getMessage()),
APIException::FAILED_TO_SAVE_SUBSCRIBER
);
}
try {
$this->subscriberSaveController->updateCustomFields($customFields, $subscriberEntity);
} catch (\Exception $e) {
throw new APIException(
// translators: %s is an error message
sprintf(__('Failed to save subscriber custom fields: %s', 'mailpoet'), $e->getMessage()),
APIException::FAILED_TO_SAVE_SUBSCRIBER
);
}
return $this->subscribersResponseBuilder->build($subscriberEntity);
}
/**
* @throws APIException
*/
public function subscribeToLists(
$subscriberId,
array $listIds,
array $options = []
): array {
$scheduleWelcomeEmail = !((isset($options['schedule_welcome_email']) && $options['schedule_welcome_email'] === false));
$sendConfirmationEmail = !((isset($options['send_confirmation_email']) && $options['send_confirmation_email'] === false));
$skipSubscriberNotification = isset($options['skip_subscriber_notification']) && $options['skip_subscriber_notification'] === true;
$signupConfirmationEnabled = (bool)$this->settings->get('signup_confirmation.enabled');
$this->checkSubscriberAndListParams($subscriberId, $listIds);
$subscriber = $this->findSubscriber($subscriberId);
$foundSegments = $this->getAndValidateSegments($listIds, self::CONTEXT_SUBSCRIBE);
// restore trashed subscriber
if ($subscriber->getDeletedAt()) {
$subscriber->setDeletedAt(null);
}
$this->subscribersSegmentRepository->subscribeToSegments($subscriber, $foundSegments);
// set status depending on signup confirmation setting
if ($subscriber->getStatus() !== SubscriberEntity::STATUS_SUBSCRIBED) {
if ($signupConfirmationEnabled === true) {
$subscriber->setStatus(SubscriberEntity::STATUS_UNCONFIRMED);
} else {
$subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
}
try {
$this->subscribersRepository->flush();
} catch (\Exception $e) {
throw new APIException(
// translators: %s is the error message
sprintf(__('Failed to save a status of a subscriber : %s', 'mailpoet'), $e->getMessage()),
APIException::FAILED_TO_SAVE_SUBSCRIBER
);
}
// when global status changes to subscribed, fire subscribed hook for all subscribed segments
/** @var SubscriberEntity $subscriber - From some reason PHPStan evaluates $subscriber->getStatus() as mixed */
if ($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {
$subscriberSegments = $subscriber->getSubscriberSegments();
foreach ($subscriberSegments as $subscriberSegment) {
if ($subscriberSegment->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {
$this->wp->doAction('mailpoet_segment_subscribed', $subscriberSegment);
}
}
}
}
// schedule welcome email
$foundSegmentsIds = array_map(
function(SegmentEntity $segment) {
return $segment->getId();
},
$foundSegments
);
if ($scheduleWelcomeEmail && $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {
$this->_scheduleWelcomeNotification($subscriber, $foundSegmentsIds);
}
// send confirmation email
if ($sendConfirmationEmail) {
$this->_sendConfirmationEmail($subscriber);
}
if (!$skipSubscriberNotification && ($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED)) {
$this->newSubscriberNotificationMailer->send($subscriber, $this->segmentsRepository->findBy(['id' => $foundSegmentsIds]));
}
$this->subscribersRepository->refresh($subscriber);
return $this->subscribersResponseBuilder->build($subscriber);
}
public function unsubscribe($subscriberIdOrEmail): array {
$this->checkSubscriberParam($subscriberIdOrEmail);
$subscriber = $this->findSubscriber($subscriberIdOrEmail);
if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNSUBSCRIBED) {
throw new APIException(__('This subscriber is already unsubscribed.', 'mailpoet'), APIException::SUBSCRIBER_ALREADY_UNSUBSCRIBED);
}
$this->unsubscribesTracker->track(
(int)$subscriber->getId(),
StatisticsUnsubscribeEntity::SOURCE_MP_API
);
$subscriber->setStatus(SubscriberEntity::STATUS_UNSUBSCRIBED);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
$this->subscribersSegmentRepository->unsubscribeFromSegments($subscriber);
return $this->subscribersResponseBuilder->build($subscriber);
}
public function unsubscribeFromLists($subscriberIdOrEmail, array $listIds): array {
$this->checkSubscriberAndListParams($subscriberIdOrEmail, $listIds);
$subscriber = $this->findSubscriber($subscriberIdOrEmail);
$foundSegments = $this->getAndValidateSegments($listIds, self::CONTEXT_UNSUBSCRIBE);
$this->subscribersSegmentRepository->unsubscribeFromSegments($subscriber, $foundSegments);
return $this->subscribersResponseBuilder->build($subscriber);
}
public function getSubscribers(array $filter, int $limit, int $offset): array {
$listingDefinition = $this->buildListingDefinition($filter, $limit, $offset);
$subscribers = $this->subscriberListingRepository->getData($listingDefinition);
$result = [];
foreach ($subscribers as $subscriber) {
$result[] = $this->subscribersResponseBuilder->build($subscriber);
}
return $result;
}
public function getSubscribersCount(array $filter): int {
$listingDefinition = $this->buildListingDefinition($filter);
return $this->subscriberListingRepository->getCount($listingDefinition);
}
/**
* @param array $filter {
* Filters to retrieve subscribers.
*
* @type string $status One of values: subscribed, unconfirmed, unsubscribed, inactive, bounced
* @type int $listId id of a list or dynamic segment
* @type \DateTime|int $minUpdatedAt DateTime object or timestamp of last update of subscriber.
* }
*/
private function buildListingDefinition(array $filter, int $limit = 50, int $offset = 0): ListingDefinition {
$group = isset($filter['status']) && is_string($filter['status']) ? $filter['status'] : null;
$listingFilters = [];
// Set filtering by listId
if (isset($filter['listId']) && is_int($filter['listId'])) {
$listingFilters['segment'] = $filter['listId'];
}
// Set filtering by minimal updatedAt
if (isset($filter['minUpdatedAt'])) {
if ($filter['minUpdatedAt'] instanceof \DateTime) {
$listingFilters['minUpdatedAt'] = $filter['minUpdatedAt'];
} elseif (is_int($filter['minUpdatedAt'])) {
$listingFilters['minUpdatedAt'] = Carbon::createFromTimestamp($filter['minUpdatedAt']);
}
}
return new ListingDefinition($group, $listingFilters, null, [], 'id', 'asc', $offset, $limit);
}
/**
* @throws APIException
*/
protected function _scheduleWelcomeNotification(SubscriberEntity $subscriber, array $segments) {
try {
$this->welcomeScheduler->scheduleSubscriberWelcomeNotification($subscriber->getId(), $segments);
} catch (\Throwable $e) {
throw new APIException(
// translators: %s is an error message
sprintf(__('Subscriber added, but welcome email failed to send: %s', 'mailpoet'), $e->getMessage()),
APIException::WELCOME_FAILED_TO_SEND
);
}
}
/**
* @throws APIException
*/
protected function _sendConfirmationEmail(SubscriberEntity $subscriberEntity) {
try {
$this->confirmationEmailMailer->sendConfirmationEmailOnce($subscriberEntity);
} catch (\Exception $e) {
throw new APIException(
// translators: %s is the error message
sprintf(__('Subscriber added to lists, but confirmation email failed to send: %s', 'mailpoet'), strtolower($e->getMessage())),
APIException::CONFIRMATION_FAILED_TO_SEND
);
}
}
/**
* @throws APIException
*/
private function checkSubscriberAndListParams($subscriberIdOrEmail, array $listIds): void {
if (empty($listIds)) {
throw new APIException(__('At least one segment ID is required.', 'mailpoet'), APIException::SEGMENT_REQUIRED);
}
$this->checkSubscriberParam($subscriberIdOrEmail);
}
/**
* @throws APIException
*/
private function checkSubscriberParam($subscriberIdOrEmail): void {
if (empty($subscriberIdOrEmail)) {
throw new APIException(__('A subscriber is required.', 'mailpoet'), APIException::SUBSCRIBER_NOT_EXISTS);
}
}
/**
* @throws APIException
*/
private function findSubscriber($subscriberIdOrEmail): SubscriberEntity {
// throw exception when subscriber does not exist
$subscriber = null;
if (is_int($subscriberIdOrEmail) || (string)(int)$subscriberIdOrEmail === $subscriberIdOrEmail) {
$subscriber = $this->subscribersRepository->findOneById($subscriberIdOrEmail);
} else if (strlen(trim($subscriberIdOrEmail)) > 0) {
$subscriber = $this->subscribersRepository->findOneBy(['email' => $subscriberIdOrEmail]);
}
if (!$subscriber) {
throw new APIException(__('This subscriber does not exist.', 'mailpoet'), APIException::SUBSCRIBER_NOT_EXISTS);
}
return $subscriber;
}
/**
* @return SegmentEntity[]
* @throws APIException
*/
private function getAndValidateSegments(array $listIds, string $context): array {
// throw exception when none of the segments exist
$foundSegments = $this->segmentsRepository->findBy(['id' => $listIds]);
if (!$foundSegments) {
$exception = _n('This list does not exist.', 'These lists do not exist.', count($listIds), 'mailpoet');
throw new APIException($exception, APIException::LIST_NOT_EXISTS);
}
// throw exception when trying to subscribe to WP Users or WooCommerce Customers segments
$foundSegmentsIds = [];
foreach ($foundSegments as $foundSegment) {
if ($foundSegment->getType() === SegmentEntity::TYPE_WP_USERS) {
if ($context === self::CONTEXT_SUBSCRIBE) {
// translators: %d is the ID of the segment
$message = __("Can't subscribe to a WordPress Users list with ID '%d'.", 'mailpoet');
} else {
// translators: %d is the ID of the segment
$message = __("Can't unsubscribe from a WordPress Users list with ID '%d'.", 'mailpoet');
}
throw new APIException(sprintf($message, $foundSegment->getId()), APIException::SUBSCRIBING_TO_WP_LIST_NOT_ALLOWED);
}
if ($foundSegment->getType() === SegmentEntity::TYPE_WC_USERS) {
if ($context === self::CONTEXT_SUBSCRIBE) {
// translators: %d is the ID of the segment
$message = __("Can't subscribe to a WooCommerce Customers list with ID '%d'.", 'mailpoet');
} else {
// translators: %d is the ID of the segment
$message = __("Can't unsubscribe from a WooCommerce Customers list with ID '%d'.", 'mailpoet');
}
throw new APIException(sprintf($message, $foundSegment->getId()), APIException::SUBSCRIBING_TO_WC_LIST_NOT_ALLOWED);
}
if ($foundSegment->getType() !== SegmentEntity::TYPE_DEFAULT) {
if ($context === self::CONTEXT_SUBSCRIBE) {
// translators: %d is the ID of the segment
$message = __("Can't subscribe to a list with ID '%d'.", 'mailpoet');
} else {
// translators: %d is the ID of the segment
$message = __("Can't unsubscribe from a list with ID '%d'.", 'mailpoet');
}
throw new APIException(sprintf($message, $foundSegment->getId()), APIException::SUBSCRIBING_TO_LIST_NOT_ALLOWED);
}
$foundSegmentsIds[] = $foundSegment->getId();
}
// throw an exception when one or more segments do not exist
if (count($foundSegmentsIds) !== count($listIds)) {
$missingIds = array_values(array_diff($listIds, $foundSegmentsIds));
$exception = sprintf(
// translators: %s is the count of lists
_n("List with ID '%s' does not exist.", "Lists with IDs '%s' do not exist.", count($missingIds), 'mailpoet'),
implode(', ', $missingIds)
);
throw new APIException(sprintf($exception, implode(', ', $missingIds)), APIException::LIST_NOT_EXISTS);
}
return $foundSegments;
}
/**
* Splits subscriber data into two arrays with basic data (index 0) and custom fields data (index 1)
* @return array<int, array>
*/
private function extractCustomFieldsFromFromSubscriberData($data): array {
$customFields = [];
foreach ($data as $key => $value) {
if (strpos($key, 'cf_') === 0) {
$customFields[$key] = $value;
unset($data[$key]);
}
}
return [$data, $customFields];
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,105 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
use MailPoet\Validator\Schema;
use MailPoet\WP\Functions as WPFunctions;
use Throwable;
use WP_REST_Request;
class API {
public const REST_API_INIT_ACTION = 'mailpoet/rest-api/init';
private const PREFIX = 'mailpoet/v1';
private const WP_REST_API_INIT_ACTION = 'rest_api_init';
/** @var EndpointContainer */
private $endpointContainer;
/** @var WPFunctions */
private $wp;
public function __construct(
EndpointContainer $endpointContainer,
WPFunctions $wp
) {
$this->endpointContainer = $endpointContainer;
$this->wp = $wp;
}
public function init(): void {
$this->wp->addAction(self::WP_REST_API_INIT_ACTION, function () {
$this->wp->doAction(self::REST_API_INIT_ACTION, [$this]);
});
}
public function registerGetRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'GET');
}
public function registerPostRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'POST');
}
public function registerPutRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'PUT');
}
public function registerPatchRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'PATCH');
}
public function registerDeleteRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'DELETE');
}
protected function registerRoute(string $route, string $endpointClass, string $method): void {
$schema = array_map(function (Schema $field) {
return $field->toArray();
}, $endpointClass::getRequestSchema());
$this->wp->registerRestRoute(self::PREFIX, $route, [
'methods' => $method,
'callback' => function (WP_REST_Request $wpRequest) use ($endpointClass, $schema) {
try {
$endpoint = $this->endpointContainer->get($endpointClass);
$wpRequest = $this->sanitizeUnknownParams($wpRequest, $schema);
$request = new Request($wpRequest);
return $endpoint->handle($request);
} catch (Throwable $e) {
return $this->convertToErrorResponse($e);
}
},
'permission_callback' => function () use ($endpointClass) {
$endpoint = $this->endpointContainer->get($endpointClass);
return $endpoint->checkPermissions(); // nosemgrep: scanner.php.wp.security.rest-route.permission-callback.incorrect-return
},
'args' => $schema,
]);
}
private function convertToErrorResponse(Throwable $e): ErrorResponse {
$response = $e instanceof Exception
? new ErrorResponse($e->getStatusCode(), $e->getMessage(), $e->getErrorCode(), $e->getErrors())
: new ErrorResponse(500, __('An unknown error occurred.', 'mailpoet'), 'mailpoet_automation_unknown_error');
if ($response->get_status() >= 500 && function_exists('error_log')) {
error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
}
return $response;
}
private function sanitizeUnknownParams(WP_REST_Request $wpRequest, array $args): WP_REST_Request {
// Remove all params that are not declared in the schema, so we use just the validated ones.
// Note that this doesn't work recursively for object properties as it is harder to solve
// with features like oneOf, anyOf, additional properties, or pattern properties.
$extraParams = array_diff(array_keys($wpRequest->get_params()), array_keys($args));
foreach ($extraParams as $extraParam) {
unset($wpRequest[(string)$extraParam]);
}
return $wpRequest;
}
}
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
use MailPoet\Validator\Schema;
abstract class Endpoint {
abstract public function handle(Request $request): Response;
public function checkPermissions(): bool {
return current_user_can('admin');
}
/** @return array<string, Schema> */
public static function getRequestSchema(): array {
return [];
}
}
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
use MailPoet\InvalidStateException;
use MailPoetVendor\Psr\Container\ContainerInterface;
class EndpointContainer {
/** @var ContainerInterface */
private $container;
public function __construct(
ContainerInterface $container
) {
$this->container = $container;
}
public function get(string $class): Endpoint {
$endpoint = $this->container->get($class);
if (!$endpoint instanceof Endpoint) {
throw new InvalidStateException(sprintf("Class '%s' doesn't implement '%s'", $class, Endpoint::class));
}
return $endpoint;
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
class ErrorResponse extends Response {
public function __construct(
int $status,
string $message,
string $code,
array $errors = []
) {
parent::__construct(null, $status);
$this->set_data([
'code' => $code,
'message' => $message,
'data' => [
'status' => $status,
'errors' => $errors,
],
]);
}
}
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
interface Exception {
public function getStatusCode(): int;
public function getErrorCode(): string;
public function getErrors(): array;
}
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
use WP_REST_Request;
class Request {
/** @var WP_REST_Request */
private $wpRequest;
public function __construct(
WP_REST_Request $wpRequest
) {
$this->wpRequest = $wpRequest;
}
public function getHeader(string $key): ?string {
return $this->wpRequest->get_header($key);
}
public function getParams(): array {
return $this->wpRequest->get_params();
}
/** @return mixed */
public function getParam(string $name) {
return $this->wpRequest->get_param($name);
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
if (!defined('ABSPATH')) exit;
use WP_REST_Response;
class Response extends WP_REST_Response {
public function __construct(
array $data = null,
int $status = 200
) {
parent::__construct(['data' => $data], $status);
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1 @@
<?php