PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.33.1
MailPoet – Newsletters, Email Marketing, and Automation v5.33.1
5.37.0 5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / Statistics / Export / StatisticsExporter.php
mailpoet / lib / Newsletter / Statistics / Export Last commit date
StatisticsExporter.php 3 months ago index.php 4 months ago
StatisticsExporter.php
258 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Newsletter\Statistics\Export;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\NewsletterEntity;
9 use MailPoet\Newsletter\Statistics\NewsletterStatistics;
10 use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
11 use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
12 use MailPoet\Router\Endpoints\ExportDownload;
13 use MailPoet\WP\Functions as WPFunctions;
14 use MailPoetVendor\XLSXWriter;
15
16 class StatisticsExporter {
17 public const FORMAT_CSV = 'csv';
18 public const FORMAT_XLSX = 'xlsx';
19
20 public const FILE_PREFIX = 'MailPoet_stats_export_';
21 /**
22 * Filter applied to populate per-recipient rows when exporting recipients.
23 * The free plugin ships an empty implementation; the premium plugin
24 * registers a callback that returns the per-recipient data.
25 *
26 * Filter signature: (array $rows, NewsletterEntity $newsletter): array
27 */
28 public const FILTER_RECIPIENT_ROWS = 'mailpoet_statistics_export_recipient_rows';
29
30 /** @var NewsletterStatisticsRepository */
31 private $statisticsRepository;
32
33 /** @var WPFunctions */
34 private $wp;
35
36 public function __construct(
37 NewsletterStatisticsRepository $statisticsRepository,
38 WPFunctions $wp
39 ) {
40 $this->statisticsRepository = $statisticsRepository;
41 $this->wp = $wp;
42 }
43
44 /**
45 * Export aggregate stats for a single newsletter to CSV or XLSX.
46 *
47 * @return array{exportFileURL: string, totalExported: int}
48 */
49 public function exportSingleAggregate(NewsletterEntity $newsletter, string $format): array {
50 $format = $this->normalizeFormat($format);
51 $headers = $this->getAggregateHeaders();
52 $stats = $this->statisticsRepository->getStatistics($newsletter);
53 $row = $this->buildAggregateRow($newsletter, $stats);
54
55 $this->ensureExportDirectory();
56 $file = ExportDownload::createExportFile(self::FILE_PREFIX, $format);
57 $this->writeFile($file['path'], $headers, [$row], $format);
58
59 return [
60 'exportFileURL' => $this->getExportFileUrl($file['token'], $format),
61 'totalExported' => 1,
62 ];
63 }
64
65 /**
66 * Export aggregate stats for multiple newsletters to CSV or XLSX (one
67 * row per newsletter). Used by the bulk export action on the listing.
68 *
69 * @param NewsletterEntity[] $newsletters
70 * @return array{exportFileURL: string, totalExported: int}
71 */
72 public function exportBulkAggregate(array $newsletters, string $format): array {
73 $format = $this->normalizeFormat($format);
74 $headers = $this->getAggregateHeaders();
75
76 $rows = [];
77 foreach ($newsletters as $newsletter) {
78 if (!$newsletter instanceof NewsletterEntity) {
79 continue;
80 }
81 $stats = $this->statisticsRepository->getStatistics($newsletter);
82 $rows[] = $this->buildAggregateRow($newsletter, $stats);
83 }
84
85 $this->ensureExportDirectory();
86 $file = ExportDownload::createExportFile(self::FILE_PREFIX, $format);
87 $this->writeFile($file['path'], $headers, $rows, $format);
88
89 return [
90 'exportFileURL' => $this->getExportFileUrl($file['token'], $format),
91 'totalExported' => count($rows),
92 ];
93 }
94
95 /**
96 * Export per-recipient stats for a newsletter to CSV or XLSX. Rows are
97 * populated via the FILTER_RECIPIENT_ROWS filter — the free plugin returns
98 * no rows, premium populates them.
99 *
100 * @return array{exportFileURL: string, totalExported: int}
101 */
102 public function exportRecipients(NewsletterEntity $newsletter, string $format): array {
103 $format = $this->normalizeFormat($format);
104 $headers = $this->getRecipientHeaders();
105
106 /** @var array<array<int|string|float|null>> $rows */
107 $rows = (array)$this->wp->applyFilters(self::FILTER_RECIPIENT_ROWS, [], $newsletter);
108
109 $this->ensureExportDirectory();
110 $file = ExportDownload::createExportFile(self::FILE_PREFIX, $format);
111 $this->writeFile($file['path'], $headers, $rows, $format);
112
113 return [
114 'exportFileURL' => $this->getExportFileUrl($file['token'], $format),
115 'totalExported' => count($rows),
116 ];
117 }
118
119 /**
120 * @return string[]
121 */
122 public function getRecipientHeaders(): array {
123 return [
124 __('Subscriber ID', 'mailpoet'),
125 __('Email', 'mailpoet'),
126 __('First name', 'mailpoet'),
127 __('Last name', 'mailpoet'),
128 __('Status', 'mailpoet'),
129 __('Opened', 'mailpoet'),
130 __('First open at', 'mailpoet'),
131 __('Open count', 'mailpoet'),
132 __('Machine opened', 'mailpoet'),
133 __('Clicked', 'mailpoet'),
134 __('Click count', 'mailpoet'),
135 __('Bounced', 'mailpoet'),
136 __('Unsubscribed', 'mailpoet'),
137 ];
138 }
139
140 /**
141 * @return string[]
142 */
143 public function getAggregateHeaders(): array {
144 return [
145 __('Newsletter ID', 'mailpoet'),
146 __('Subject', 'mailpoet'),
147 __('Campaign name', 'mailpoet'),
148 __('Sent at', 'mailpoet'),
149 __('Total sent', 'mailpoet'),
150 __('Unique opens', 'mailpoet'),
151 __('Machine opens', 'mailpoet'),
152 __('Unique clicks', 'mailpoet'),
153 __('Bounces', 'mailpoet'),
154 __('Unsubscribes', 'mailpoet'),
155 __('Revenue', 'mailpoet'),
156 __('Currency', 'mailpoet'),
157 __('Orders', 'mailpoet'),
158 ];
159 }
160
161 /**
162 * Build one aggregate row for a newsletter, in the same column order as
163 * `getAggregateHeaders()`. Public so it can be reused by bulk export in phase 3.
164 *
165 * @return array<int|string|float|null>
166 */
167 public function buildAggregateRow(NewsletterEntity $newsletter, NewsletterStatistics $stats): array {
168 $sentAt = $newsletter->getSentAt();
169 $revenue = $stats->getWooCommerceRevenue();
170
171 return [
172 (int)$newsletter->getId(),
173 (string)$newsletter->getSubject(),
174 (string)($newsletter->getCampaignName() ?? ''),
175 $sentAt ? $sentAt->format('Y-m-d H:i:s') : '',
176 $stats->getTotalSentCount(),
177 $stats->getOpenCount(),
178 $stats->getMachineOpenCount(),
179 $stats->getClickCount(),
180 $stats->getBounceCount(),
181 $stats->getUnsubscribeCount(),
182 $revenue instanceof WooCommerceRevenue ? $revenue->getValue() : '',
183 $revenue instanceof WooCommerceRevenue ? $revenue->getCurrency() : '',
184 $revenue instanceof WooCommerceRevenue ? $revenue->getOrdersCount() : '',
185 ];
186 }
187
188 /**
189 * @param string[] $headers
190 * @param array<array<int|string|float|null>> $rows
191 */
192 private function writeFile(string $filePath, array $headers, array $rows, string $format): void {
193 if ($format === self::FORMAT_XLSX) {
194 $this->writeXlsx($filePath, $headers, $rows);
195 return;
196 }
197 $this->writeCsv($filePath, $headers, $rows);
198 }
199
200 /**
201 * @param string[] $headers
202 * @param array<array<int|string|float|null>> $rows
203 */
204 private function writeCsv(string $filePath, array $headers, array $rows): void {
205 $handle = fopen($filePath, 'w');
206 if ($handle === false) {
207 throw new \RuntimeException('Failed opening file for export.');
208 }
209
210 // UTF-8 BOM so Excel auto-detects encoding (matches Subscribers/ImportExport/Export/Export.php)
211 fwrite($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
212 $this->writeCsvLine($handle, $headers);
213 foreach ($rows as $row) {
214 $this->writeCsvLine($handle, $row);
215 }
216 fclose($handle);
217 }
218
219 /**
220 * @param resource $handle
221 * @param array<int|string|float|null> $row
222 */
223 private function writeCsvLine($handle, array $row): void {
224 // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv -- Export handles are created under Env::$tempPath, which is MailPoet's WordPress temp directory.
225 fputcsv($handle, array_map('strval', $row), ',', '"', '');
226 }
227
228 /**
229 * @param string[] $headers
230 * @param array<array<int|string|float|null>> $rows
231 */
232 private function writeXlsx(string $filePath, array $headers, array $rows): void {
233 $writer = new XLSXWriter();
234 $sheetName = __('Statistics', 'mailpoet');
235 $writer->writeSheetHeader($sheetName, array_fill_keys($headers, 'string'));
236 foreach ($rows as $row) {
237 $writer->writeSheetRow($sheetName, $row);
238 }
239 $writer->writeToFile($filePath);
240 }
241
242 private function ensureExportDirectory(): void {
243 ExportDownload::ensureExportDirectory($this->wp);
244 }
245
246 private function getExportFileUrl(string $token, string $format): string {
247 return ExportDownload::buildStatisticsExportUrl($token, $format, $this->wp->homeUrl());
248 }
249
250 private function normalizeFormat(string $format): string {
251 $format = strtolower($format);
252 if ($format !== self::FORMAT_CSV && $format !== self::FORMAT_XLSX) {
253 throw new \InvalidArgumentException(sprintf('Unsupported export format "%s".', $format));
254 }
255 return $format;
256 }
257 }
258