<?php declare(strict_types = 1);
namespace MailPoet\Newsletter\RestApi\Endpoints;
if (!defined('ABSPATH')) exit;
use MailPoet\API\REST\ApiException;
use MailPoet\API\REST\Endpoint;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Config\AccessControl;
use MailPoet\Cron\Workers\StatisticsExport;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Listing\Handler as ListingHandler;
use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\BulkActionController;
use MailPoet\Newsletter\BulkActionException;
use MailPoet\Newsletter\Listing\NewsletterListingRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Statistics\Export\StatisticsExporter;
use MailPoet\Util\License\Features\CapabilitiesManager;
use MailPoet\Validator\Builder;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
/**
* `POST /mailpoet/v1/newsletters/bulk-action`
*
* Mirrors the legacy JSON endpoint: `trash`, `restore`, and `delete` flow
* through {@see BulkActionController}; `export_stats` is handled here because
* it is async (it queues a scheduled task) and gated behind a premium
* capability instead of the bulk dispatcher's plain list of supported actions.
*/
class NewslettersBulkActionEndpoint extends Endpoint {
public const ACTION_EXPORT_STATS = 'export_stats';
/** @var ListingHandler */
private $listingHandler;
/** @var NewsletterListingRepository */
private $newsletterListingRepository;
/** @var BulkActionController */
private $bulkActionController;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var CapabilitiesManager */
private $capabilitiesManager;
/** @var WPFunctions */
private $wp;
public function __construct(
ListingHandler $listingHandler,
NewsletterListingRepository $newsletterListingRepository,
BulkActionController $bulkActionController,
ScheduledTasksRepository $scheduledTasksRepository,
CapabilitiesManager $capabilitiesManager,
WPFunctions $wp
) {
$this->listingHandler = $listingHandler;
$this->newsletterListingRepository = $newsletterListingRepository;
$this->bulkActionController = $bulkActionController;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->capabilitiesManager = $capabilitiesManager;
$this->wp = $wp;
}
public function checkPermissions(): bool {
return $this->wp->currentUserCan(AccessControl::PERMISSION_MANAGE_EMAILS);
}
public function handle(Request $request): Response {
$actionParam = $request->getParam('action');
$action = is_string($actionParam) ? $actionParam : '';
$selectAll = $request->getParam('select_all') === true;
$selection = $this->getSelection($request);
if ($selection === [] && !$selectAll) {
throw new ApiException(
__('No newsletters selected.', 'mailpoet'),
400,
'mailpoet_newsletters_missing_selection'
);
}
$definition = $this->buildDefinition($request, $selection);
if ($action === self::ACTION_EXPORT_STATS) {
return $this->handleExportStats($request, $definition);
}
try {
$result = $this->bulkActionController->execute($action, $definition);
} catch (BulkActionException $exception) {
throw new ApiException(
$exception->getMessage(),
$exception->getStatusCode(),
$exception->getErrorCode()
);
}
return new Response([
'action' => $result['action'],
'count' => $result['count'],
]);
}
public static function getRequestSchema(): array {
return [
'action' => Builder::string()->required(),
'selection' => Builder::array(Builder::integer()),
'type' => Builder::string(),
'group' => Builder::string(),
'search' => Builder::string(),
'filter' => Builder::object(),
'parent_id' => Builder::integer(),
'select_all' => Builder::boolean(),
'format' => Builder::string(),
];
}
private function handleExportStats(Request $request, ListingDefinition $definition): Response {
$capability = $this->capabilitiesManager->getCapability('detailedAnalytics');
if ($capability === null || $capability->isRestricted) {
throw new ApiException(
__('Bulk statistics export requires a MailPoet plan with detailed analytics.', 'mailpoet'),
403,
'mailpoet_newsletters_export_forbidden'
);
}
$ids = array_values(array_map('intval', $this->newsletterListingRepository->getActionableIds($definition)));
if ($ids === []) {
throw new ApiException(
__('No newsletters selected for export.', 'mailpoet'),
400,
'mailpoet_newsletters_export_empty'
);
}
$formatParam = $request->getParam('format');
$format = is_string($formatParam) ? strtolower($formatParam) : StatisticsExporter::FORMAT_CSV;
if ($format !== StatisticsExporter::FORMAT_CSV && $format !== StatisticsExporter::FORMAT_XLSX) {
throw new ApiException(
__('Unsupported export format. Use csv or xlsx.', 'mailpoet'),
400,
'mailpoet_newsletters_export_invalid_format'
);
}
$task = new ScheduledTaskEntity();
$task->setType(StatisticsExport::TASK_TYPE);
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$task->setScheduledAt(Carbon::now()->millisecond(0));
$task->setPriority(ScheduledTaskEntity::PRIORITY_HIGH);
$task->setMeta([
'job_type' => StatisticsExport::JOB_TYPE_BULK,
'newsletter_ids' => $ids,
'format' => $format,
'requested_by' => (int)$this->wp->getCurrentUserId(),
]);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
return new Response([
'action' => self::ACTION_EXPORT_STATS,
'task_id' => (int)$task->getId(),
'count' => count($ids),
]);
}
/**
* @return int[]
*/
private function getSelection(Request $request): array {
$selection = $request->getParam('selection');
return is_array($selection) ? $this->toIntList($selection) : [];
}
/**
* @param int[] $selection
*/
private function buildDefinition(Request $request, array $selection): ListingDefinition {
$filter = $request->getParam('filter');
$searchParam = $request->getParam('search');
$groupParam = $request->getParam('group');
$typeParam = $request->getParam('type');
$parentIdParam = $request->getParam('parent_id');
$params = [];
if (is_string($typeParam) && in_array($typeParam, NewslettersListingEndpoint::SUPPORTED_TYPES, true)) {
$params['type'] = $typeParam;
}
if (is_numeric($parentIdParam)) {
$params['parentId'] = (int)$parentIdParam;
}
return $this->listingHandler->getListingDefinition([
'offset' => 0,
'limit' => 0,
'sort_by' => 'id',
'sort_order' => 'desc',
'search' => is_string($searchParam) ? $searchParam : null,
'group' => is_string($groupParam) ? $groupParam : null,
'filter' => is_array($filter) ? $filter : [],
'selection' => $selection,
'params' => $params,
]);
}
/**
* @param array<mixed> $values
* @return int[]
*/
private function toIntList(array $values): array {
$ints = [];
foreach ($values as $value) {
if (is_scalar($value) && (int)$value > 0) {
$ints[] = (int)$value;
}
}
return array_values(array_unique($ints));
}
}