PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
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 / Automation / Engine / Storage / AutomationStatisticsStorage.php
mailpoet / lib / Automation / Engine / Storage Last commit date
AutomationRunLogStorage.php 1 year ago AutomationRunStorage.php 2 months ago AutomationStatisticsStorage.php 1 month ago AutomationStorage.php 2 months ago index.php 3 years ago
AutomationStatisticsStorage.php
353 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Automation\Engine\Storage;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Automation\Engine\Data\Automation;
9 use MailPoet\Automation\Engine\Data\AutomationRun;
10 use MailPoet\Automation\Engine\Data\AutomationStatistics;
11
12 class AutomationStatisticsStorage {
13 /** @var string */
14 private $table;
15
16 public function __construct() {
17 global $wpdb;
18 $this->table = $wpdb->prefix . 'mailpoet_automation_runs';
19 }
20
21 /** @return AutomationStatistics[] */
22 public function getAutomationStatisticsForAutomations(Automation ...$automations): array {
23 if (empty($automations)) {
24 return [];
25 }
26 $automationIds = array_map(
27 function(Automation $automation): int {
28 return $automation->getId();
29 },
30 $automations
31 );
32
33 $data = $this->getStatistics($automationIds);
34 $statistics = [];
35 foreach ($automationIds as $id) {
36 $emailStats = $this->getEmailStatistics($id);
37 $statistics[$id] = new AutomationStatistics(
38 $id,
39 (int)($data[$id]['total'] ?? 0),
40 (int)($data[$id]['running'] ?? 0),
41 null,
42 $emailStats['sent'],
43 $emailStats['opened'],
44 $emailStats['clicked'],
45 $emailStats['orders'],
46 $emailStats['revenue']
47 );
48 }
49 return $statistics;
50 }
51
52 public function getAutomationStats(int $automationId, ?int $versionId = null, ?\DateTimeImmutable $after = null, ?\DateTimeImmutable $before = null): AutomationStatistics {
53 $data = $this->getStatistics([$automationId], $versionId, $after, $before);
54 $emailStats = $this->getEmailStatistics($automationId, $versionId, $after, $before);
55
56 return new AutomationStatistics(
57 $automationId,
58 (int)($data[$automationId]['total'] ?? 0),
59 (int)($data[$automationId]['running'] ?? 0),
60 $versionId,
61 $emailStats['sent'],
62 $emailStats['opened'],
63 $emailStats['clicked'],
64 $emailStats['orders'],
65 $emailStats['revenue']
66 );
67 }
68
69 /**
70 * @param int[] $automationIds
71 * @return array<int, array{id: int, total: int, running: int}>
72 */
73 private function getStatistics(array $automationIds, ?int $versionId = null, ?\DateTimeImmutable $after = null, ?\DateTimeImmutable $before = null): array {
74 global $wpdb;
75 $totalSubquery = $this->getStatsQuery($automationIds, $versionId, $after, $before);
76 $runningSubquery = $this->getStatsQuery($automationIds, $versionId, $after, $before, AutomationRun::STATUS_RUNNING);
77
78 $results = (array)$wpdb->get_results(
79 '
80 SELECT t.id, t.count AS total, r.count AS running
81 FROM (' . $totalSubquery /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery was already prepared. */ . ') t
82 LEFT JOIN (' . $runningSubquery /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The subquery was already prepared. */ . ') r ON t.id = r.id
83 ',
84 ARRAY_A
85 );
86
87 /** @var array{id: int, total: int, running: int} $results */
88 return array_combine(array_column($results, 'id'), $results) ?: [];
89 }
90
91 private function getStatsQuery(array $automationIds, ?int $versionId = null, ?\DateTimeImmutable $after = null, ?\DateTimeImmutable $before = null, ?string $status = null): string {
92 global $wpdb;
93
94 $versionCondition = $versionId ? 'AND version_id = %d' : '';
95 $statusCondition = $status ? 'AND status = %s' : '';
96 $dateCondition = $after !== null && $before !== null ? 'AND created_at BETWEEN %s AND %s' : '';
97
98 $coditions = "$versionCondition $statusCondition $dateCondition";
99 $query = $wpdb->prepare(
100 '
101 SELECT automation_id AS id, COUNT(*) AS count
102 FROM %i
103 WHERE automation_id IN (' . implode(',', array_fill(0, count($automationIds), '%d')) . ')
104 ' . $coditions . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The conditions use placeholders. */ '
105 GROUP BY automation_id
106 ',
107 array_merge(
108 [$this->table],
109 $automationIds,
110 $versionId ? [$versionId] : [],
111 $status ? [$status] : [],
112 $after !== null && $before !== null ? [$after->format('Y-m-d H:i:s'), $before->format('Y-m-d H:i:s')] : []
113 )
114 );
115 return strval($query);
116 }
117
118 private function getAutomationEmailIds(int $automationId, ?int $versionId = null): array {
119 global $wpdb;
120
121 $versionsTable = $wpdb->prefix . 'mailpoet_automation_versions';
122
123 if ($versionId) {
124 $query = $wpdb->prepare(
125 '
126 SELECT steps
127 FROM %i
128 WHERE automation_id = %d
129 AND id = %d
130 ',
131 [$versionsTable, $automationId, $versionId]
132 );
133 } else {
134 $query = $wpdb->prepare(
135 '
136 SELECT steps
137 FROM %i
138 WHERE automation_id = %d
139 ',
140 [$versionsTable, $automationId]
141 );
142 }
143
144 $results = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above.
145 $emailIds = [];
146
147 foreach ($results as $result) {
148 $steps = json_decode($result['steps'], true);
149 if (!is_array($steps)) {
150 continue;
151 }
152
153 foreach ($steps as $step) {
154 if (
155 is_array($step)
156 && isset($step['key'], $step['args'])
157 && $step['key'] === 'mailpoet:send-email'
158 && is_array($step['args'])
159 && isset($step['args']['email_id'])
160 && is_numeric($step['args']['email_id'])
161 ) {
162 $emailIds[] = (int)$step['args']['email_id'];
163 }
164 }
165 }
166
167 return array_unique($emailIds);
168 }
169
170 private function queryEmailStatistics(array $emailIds, ?\DateTimeImmutable $after = null, ?\DateTimeImmutable $before = null): array {
171 if (empty($emailIds)) {
172 return ['sent' => 0, 'opened' => 0, 'clicked' => 0, 'orders' => 0, 'revenue' => 0.0];
173 }
174
175 $dateCondition = '';
176 $dateParams = [];
177 if ($after && $before) {
178 $dateCondition = 'AND created_at BETWEEN %s AND %s';
179 $dateParams = [$after->format('Y-m-d H:i:s'), $before->format('Y-m-d H:i:s')];
180 } elseif ($after && $before === null) {
181 $dateCondition = 'AND created_at >= %s';
182 $dateParams = [$after->format('Y-m-d H:i:s')];
183 } elseif ($after === null && $before) {
184 $dateCondition = 'AND created_at <= %s';
185 $dateParams = [$before->format('Y-m-d H:i:s')];
186 }
187
188 $sentCounts = $this->getEmailSentCounts($emailIds, $dateCondition, $dateParams);
189 $openCounts = $this->getEmailOpenCounts($emailIds, $dateCondition, $dateParams);
190 $clickCounts = $this->getEmailClickCounts($emailIds, $dateCondition, $dateParams);
191 $revenueData = $this->getEmailRevenueCounts($emailIds, $dateCondition, $dateParams);
192
193 // Sum all the per-email results
194 $totalSent = array_sum($sentCounts);
195 $totalOpened = array_sum($openCounts);
196 $totalClicked = array_sum($clickCounts);
197 $totalOrders = array_sum(array_column($revenueData, 'orders'));
198 $totalRevenue = array_sum(array_column($revenueData, 'revenue'));
199
200 return [
201 'sent' => $totalSent,
202 'opened' => $totalOpened,
203 'clicked' => $totalClicked,
204 'orders' => $totalOrders,
205 'revenue' => $totalRevenue,
206 ];
207 }
208
209 private function getEmailSentCounts(array $emailIds, string $dateCondition, array $dateParams): array {
210 global $wpdb;
211
212 if (empty($emailIds)) {
213 return [];
214 }
215
216 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.NotPrepared -- The number of replacements is dynamic.
217 $results = $wpdb->get_results(
218 $wpdb->prepare(
219 '
220 SELECT sq.newsletter_id, SUM(sq.count_processed) as total
221 FROM %i sq
222 JOIN %i st ON sq.task_id = st.id
223 WHERE sq.newsletter_id IN (' . implode(',', array_fill(0, count($emailIds), '%d')) . ')
224 AND st.status = %s
225 ' . ($dateCondition ? str_replace('created_at', 'sq.created_at', $dateCondition) : '') . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The condition uses placeholders. */ '
226 GROUP BY sq.newsletter_id
227 ',
228 array_merge(
229 [$wpdb->prefix . 'mailpoet_sending_queues', $wpdb->prefix . 'mailpoet_scheduled_tasks'],
230 $emailIds,
231 ['completed'],
232 $dateParams
233 )
234 ),
235 ARRAY_A
236 );
237 $counts = [];
238 foreach ($results as $result) {
239 $counts[(int)$result['newsletter_id']] = (int)$result['total'];
240 }
241 return $counts;
242 }
243
244 private function getEmailOpenCounts(array $emailIds, string $dateCondition, array $dateParams): array {
245 global $wpdb;
246
247 if (empty($emailIds)) {
248 return [];
249 }
250
251 $query = $wpdb->prepare('
252 SELECT newsletter_id, COUNT(DISTINCT subscriber_id) as total
253 FROM %i
254 WHERE newsletter_id IN (' . implode(',', array_fill(0, count($emailIds), '%d')) . ')
255 AND ((user_agent_type = %d) OR (user_agent_type IS NULL))
256 ' . $dateCondition /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The date condition uses placeholders. */ . '
257 GROUP BY newsletter_id
258 ', array_merge(
259 [$wpdb->prefix . 'mailpoet_statistics_opens'],
260 $emailIds,
261 [0], // USER_AGENT_TYPE_HUMAN = 0
262 $dateParams
263 ));
264 $results = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above.
265 $counts = [];
266 foreach ($results as $result) {
267 $counts[(int)$result['newsletter_id']] = (int)$result['total'];
268 }
269 return $counts;
270 }
271
272 private function getEmailClickCounts(array $emailIds, string $dateCondition, array $dateParams): array {
273 global $wpdb;
274
275 if (empty($emailIds)) {
276 return [];
277 }
278
279 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic placeholders are used with prepare().
280 $query = $wpdb->prepare('
281 SELECT newsletter_id, COUNT(DISTINCT subscriber_id) as total
282 FROM %i
283 WHERE newsletter_id IN (' . implode(',', array_fill(0, count($emailIds), '%d')) . ')
284 AND ((user_agent_type = %d) OR (user_agent_type IS NULL))
285 ' . $dateCondition /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The date condition uses placeholders. */ . '
286 GROUP BY newsletter_id
287 ', array_merge(
288 [$wpdb->prefix . 'mailpoet_statistics_clicks'],
289 $emailIds,
290 [0], // USER_AGENT_TYPE_HUMAN = 0
291 $dateParams
292 ));
293 $results = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above.
294 $counts = [];
295 foreach ($results as $result) {
296 $counts[(int)$result['newsletter_id']] = (int)$result['total'];
297 }
298 return $counts;
299 }
300
301 private function getEmailRevenueCounts(array $emailIds, string $dateCondition, array $dateParams): array {
302 global $wpdb;
303
304 // Check if WooCommerce is active before querying
305 if (!function_exists('get_woocommerce_currency')) {
306 return [];
307 }
308
309 if (empty($emailIds)) {
310 return [];
311 }
312
313 $currency = get_woocommerce_currency();
314
315 // Use the same purchase states as overview (defaults to ['completed'] but could be filtered)
316 $purchaseStates = ['completed']; // Default value matching WCHelper::getPurchaseStates()
317 if (function_exists('apply_filters')) {
318 $purchaseStates = apply_filters('mailpoet_purchase_order_states', $purchaseStates);
319 }
320
321 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic placeholders are used with prepare().
322 $query = $wpdb->prepare('
323 SELECT newsletter_id, COUNT(id) as orders, SUM(order_price_total) as revenue
324 FROM %i
325 WHERE newsletter_id IN (' . implode(',', array_fill(0, count($emailIds), '%d')) . ')
326 AND status IN (' . implode(',', array_fill(0, count($purchaseStates), '%s')) . ')
327 AND order_currency = %s
328 ' . $dateCondition . /* phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The date condition uses placeholders. */ '
329 GROUP BY newsletter_id
330 ', array_merge(
331 [$wpdb->prefix . 'mailpoet_statistics_woocommerce_purchases'],
332 $emailIds,
333 $purchaseStates,
334 [$currency],
335 $dateParams
336 ));
337 $results = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above.
338 $revenueData = [];
339 foreach ($results as $result) {
340 $revenueData[(int)$result['newsletter_id']] = [
341 'orders' => (int)$result['orders'],
342 'revenue' => (float)$result['revenue'],
343 ];
344 }
345 return $revenueData;
346 }
347
348 private function getEmailStatistics(int $automationId, ?int $versionId = null, ?\DateTimeImmutable $after = null, ?\DateTimeImmutable $before = null): array {
349 $emailIds = $this->getAutomationEmailIds($automationId, $versionId);
350 return $this->queryEmailStatistics($emailIds, $after, $before);
351 }
352 }
353