<?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\Cron\Workers\StatisticsExport as StatisticsExportWorker;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Statistics\Export\StatisticsExporter;
use MailPoet\Util\License\Features\CapabilitiesManager;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class StatisticsExport extends APIEndpoint {
public $permissions = [
'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
];
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var StatisticsExporter */
private $exporter;
/** @var LoggerFactory */
private $loggerFactory;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var CapabilitiesManager */
private $capabilitiesManager;
/** @var WPFunctions */
private $wp;
public function __construct(
NewslettersRepository $newslettersRepository,
StatisticsExporter $exporter,
LoggerFactory $loggerFactory,
ScheduledTasksRepository $scheduledTasksRepository,
CapabilitiesManager $capabilitiesManager,
WPFunctions $wp
) {
$this->newslettersRepository = $newslettersRepository;
$this->exporter = $exporter;
$this->loggerFactory = $loggerFactory;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->capabilitiesManager = $capabilitiesManager;
$this->wp = $wp;
}
public function exportCampaign($data = []) {
$newsletterId = isset($data['id']) ? (int)$data['id'] : 0;
if ($newsletterId <= 0) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Missing newsletter id.', 'mailpoet'),
]);
}
$format = $this->resolveFormat($data);
if ($format === null) {
return $this->unsupportedFormatResponse();
}
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
if (!$newsletter) {
return $this->newsletterNotFoundResponse();
}
try {
$result = $this->exporter->exportSingleAggregate($newsletter, $format);
} catch (\Throwable $e) {
if (function_exists('error_log')) {
// phpcs:disable QITStandard.PHP.DebugCode.DebugFunctionFound
error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
// phpcs:enable QITStandard.PHP.DebugCode.DebugFunctionFound
}
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_API)->warning('Campaign statistics export failed.', [
'exceptionMessage' => $e->getMessage(),
'exceptionTrace' => $e->getTraceAsString(),
]);
return $this->errorResponse([
APIError::UNKNOWN => __('Could not generate the export. Please try again.', 'mailpoet'),
], [], Response::STATUS_BAD_REQUEST);
}
return $this->successResponse($result);
}
public function exportRecipients($data = []) {
$newsletterId = isset($data['id']) ? (int)$data['id'] : 0;
if ($newsletterId <= 0) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Missing newsletter id.', 'mailpoet'),
]);
}
$format = $this->resolveFormat($data);
if ($format === null) {
return $this->unsupportedFormatResponse();
}
if ($this->isDetailedAnalyticsRestricted()) {
return $this->errorResponse([
APIError::FORBIDDEN => __('Per-recipient export requires a MailPoet plan with detailed analytics.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
if (!$newsletter) {
return $this->newsletterNotFoundResponse();
}
$task = new ScheduledTaskEntity();
$task->setType(StatisticsExportWorker::TASK_TYPE);
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$task->setScheduledAt(Carbon::now()->millisecond(0));
$task->setPriority(ScheduledTaskEntity::PRIORITY_HIGH);
$task->setMeta([
'job_type' => StatisticsExportWorker::JOB_TYPE_RECIPIENTS,
'newsletter_id' => $newsletter->getId(),
'format' => $format,
'requested_by' => (int)$this->wp->getCurrentUserId(),
]);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
return $this->successResponse($this->buildStatusPayload($task));
}
public function getStatus($data = []) {
$taskId = isset($data['task_id']) ? (int)$data['task_id'] : 0;
if ($taskId <= 0) {
return $this->badRequest([
APIError::BAD_REQUEST => __('Missing task id.', 'mailpoet'),
]);
}
if ($this->isDetailedAnalyticsRestricted()) {
return $this->errorResponse([
APIError::FORBIDDEN => __('Per-recipient export requires a MailPoet plan with detailed analytics.', 'mailpoet'),
], [], Response::STATUS_FORBIDDEN);
}
$task = $this->scheduledTasksRepository->findOneById($taskId);
if (!$task instanceof ScheduledTaskEntity || $task->getType() !== StatisticsExportWorker::TASK_TYPE) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Export task not found.', 'mailpoet'),
], [], Response::STATUS_NOT_FOUND);
}
$meta = $task->getMeta() ?? [];
$jobType = isset($meta['job_type']) ? (string)$meta['job_type'] : '';
$allowedJobTypes = [
StatisticsExportWorker::JOB_TYPE_RECIPIENTS,
StatisticsExportWorker::JOB_TYPE_BULK,
];
if (!in_array($jobType, $allowedJobTypes, true)) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Export task not found.', 'mailpoet'),
], [], Response::STATUS_NOT_FOUND);
}
$requestedBy = isset($meta['requested_by']) ? (int)$meta['requested_by'] : 0;
if ($requestedBy !== (int)$this->wp->getCurrentUserId()) {
return $this->errorResponse([
APIError::NOT_FOUND => __('Export task not found.', 'mailpoet'),
], [], Response::STATUS_NOT_FOUND);
}
return $this->successResponse($this->buildStatusPayload($task));
}
private function isDetailedAnalyticsRestricted(): bool {
$capability = $this->capabilitiesManager->getCapability('detailedAnalytics');
return $capability === null || $capability->isRestricted;
}
private function resolveFormat(array $data): ?string {
$format = isset($data['format']) && is_string($data['format'])
? strtolower($data['format'])
: StatisticsExporter::FORMAT_CSV;
if ($format !== StatisticsExporter::FORMAT_CSV && $format !== StatisticsExporter::FORMAT_XLSX) {
return null;
}
return $format;
}
private function unsupportedFormatResponse() {
return $this->badRequest([
APIError::BAD_REQUEST => __('Unsupported export format. Use csv or xlsx.', 'mailpoet'),
]);
}
private function newsletterNotFoundResponse() {
return $this->errorResponse([
APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
], [], Response::STATUS_NOT_FOUND);
}
/**
* @return array{
* taskId: int,
* status: string,
* exportFileURL?: string,
* totalExported?: int,
* error?: string,
* }
*/
private function buildStatusPayload(ScheduledTaskEntity $task): array {
$meta = $task->getMeta() ?? [];
$status = $task->getStatus() ?? ScheduledTaskEntity::VIRTUAL_STATUS_RUNNING;
$payload = [
'taskId' => (int)$task->getId(),
'status' => $status,
];
if (isset($meta['export_file_url'])) {
$payload['exportFileURL'] = (string)$meta['export_file_url'];
}
if (isset($meta['total_exported'])) {
$payload['totalExported'] = (int)$meta['total_exported'];
}
if (isset($meta['error'])) {
$payload['error'] = (string)$meta['error'];
}
return $payload;
}
}