Per WP security best practices, sanitization should be handled as early as possible. So this commit move updates the calls to sanitize the segment name and description to the part of the code where the user input is first processed, instead of when the data is saved to the database. [MAILPOET-5232]
315 lines
11 KiB
PHP
315 lines
11 KiB
PHP
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||
|
||
namespace MailPoet\API\JSON\v1;
|
||
|
||
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\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;
|
||
|
||
public function __construct(
|
||
Listing\Handler $listingHandler,
|
||
SegmentsRepository $segmentsRepository,
|
||
SegmentListingRepository $segmentListingRepository,
|
||
SegmentsResponseBuilder $segmentsResponseBuilder,
|
||
SegmentSaveController $segmentSavecontroller,
|
||
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;
|
||
}
|
||
|
||
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 it’s 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 it’s used for %1$s form',
|
||
'List cannot be deleted because it’s 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]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|