PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
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 / Migrations / App / Migration_20240207_105912_App.php
mailpoet / lib / Migrations / App Last commit date
Migration_20221028_105818_App.php 3 years ago Migration_20230109_144830.php 3 years ago Migration_20230131_121621.php 3 years ago Migration_20230419_080000.php 3 years ago Migration_20230425_211517.php 3 years ago Migration_20230712_180341.php 3 years ago Migration_20230803_200413_App.php 2 years ago Migration_20230825_093531_App.php 1 year ago Migration_20231128_120355_App.php 1 year ago Migration_20240202_130053_App.php 2 years ago Migration_20240207_105912_App.php 1 year ago Migration_20240322_110443_App.php 2 years ago Migration_20240730_212419_App.php 2 years ago Migration_20241015_105511_App.php 1 year ago Migration_20241128_114257_App.php 1 year ago Migration_20250120_094614_App.php 1 year ago Migration_20250501_114655_App.php 11 months ago Migration_20260421_155908_App.php 3 months ago Migration_20260515_120000_App.php 3 months ago Migration_20260623_120000_App.php 1 month ago Migration_20260805_120000_App.php 1 week ago index.php 3 years ago
Migration_20240207_105912_App.php
206 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Migrations\App;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Doctrine\WPDB\Connection;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\Entities\ScheduledTaskEntity;
11 use MailPoet\Entities\ScheduledTaskSubscriberEntity;
12 use MailPoet\Entities\SendingQueueEntity;
13 use MailPoet\Entities\StatisticsNewsletterEntity;
14 use MailPoet\Migrator\AppMigration;
15 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
16
17 /**
18 * We've had a set of bugs where campaign type newsletters (see NewsletterEntity::CAMPAIGN_TYPES),
19 * such as post notifications, were getting stuck in the following state:
20 * - The newsletter was in the "sending" state.
21 * - The task failed to complete and ended up in the "invalid" state.
22 *
23 * This migration completes tasks that sent out all emails
24 * and pauses those that have unprocessed subscribers.
25 */
26 class Migration_20240207_105912_App extends AppMigration {
27 public function run(): void {
28 $this->pauseInvalidTasksWithUnprocessedSubscribers();
29 $this->completeInvalidTasksWithAllSubscribersProcessed();
30 $this->backfillMissingDataForMigratedNewsletters();
31 }
32
33 private function pauseInvalidTasksWithUnprocessedSubscribers(): void {
34 $ids = $this->entityManager->createQueryBuilder()
35 ->select('DISTINCT t.id')
36 ->from(ScheduledTaskEntity::class, 't')
37 ->join('t.subscribers', 's', 'WITH', 's.processed = :unprocessed')
38 ->join('t.sendingQueue', 'q')
39 ->join('q.newsletter', 'n')
40 ->where('t.deletedAt IS NULL')
41 ->andWhere('t.status = :invalid')
42 ->andWhere('n.deletedAt IS NULL')
43 ->andWhere('n.status = :sending')
44 ->andWhere('n.type IN (:campaignTypes)')
45 ->setParameter('unprocessed', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED)
46 ->setParameter('invalid', ScheduledTaskEntity::STATUS_INVALID)
47 ->setParameter('sending', NewsletterEntity::STATUS_SENDING)
48 ->setParameter('campaignTypes', NewsletterEntity::CAMPAIGN_TYPES)
49 ->getQuery()
50 ->getSingleColumnResult();
51
52 $this->entityManager->createQueryBuilder()
53 ->update(ScheduledTaskEntity::class, 't')
54 ->set('t.status', ':paused')
55 ->where('t.id IN (:ids)')
56 ->setParameter('paused', ScheduledTaskEntity::STATUS_PAUSED)
57 ->setParameter('ids', $ids)
58 ->getQuery()
59 ->execute();
60 }
61
62 private function completeInvalidTasksWithAllSubscribersProcessed(): void {
63 $ids = $this->entityManager->createQueryBuilder()
64 ->select('DISTINCT t.id, n.id AS nid, t.updatedAt')
65 ->from(ScheduledTaskEntity::class, 't')
66 ->leftJoin('t.subscribers', 's', 'WITH', 's.processed = :unprocessed')
67 ->join('t.sendingQueue', 'q')
68 ->join('q.newsletter', 'n')
69 ->where('t.deletedAt IS NULL')
70 ->andWhere('t.status = :invalid')
71 ->andWhere('s.task IS NULL')
72 ->andWhere('n.deletedAt IS NULL')
73 ->andWhere('n.status = :sending')
74 ->andWhere('n.type IN (:campaignTypes)')
75 ->setParameter('unprocessed', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED)
76 ->setParameter('invalid', ScheduledTaskEntity::STATUS_INVALID)
77 ->setParameter('sending', NewsletterEntity::STATUS_SENDING)
78 ->setParameter('campaignTypes', NewsletterEntity::CAMPAIGN_TYPES)
79 ->getQuery()
80 ->getSingleColumnResult();
81
82 // update sending queue counts
83 $this->entityManager->createQueryBuilder()
84 ->update(SendingQueueEntity::class, 'q')
85 ->set('q.countProcessed', 'q.countTotal')
86 ->set('q.countToProcess', 0)
87 ->where('q.task IN (:ids)')
88 ->setParameter('ids', $ids)
89 ->getQuery()
90 ->execute();
91
92 // complete the invalid tasks
93 $this->entityManager->createQueryBuilder()
94 ->update(ScheduledTaskEntity::class, 't')
95 ->set('t.status', ':completed')
96 ->where('t.id IN (:ids)')
97 ->setParameter('completed', ScheduledTaskEntity::STATUS_COMPLETED)
98 ->setParameter('ids', $ids)
99 ->getQuery()
100 ->execute();
101
102 // mark newsletters as sent, update "sentAt" (DBAL needed to be able to use JOIN)
103 $newslettersTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
104 $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
105 $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
106 $sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
107
108 // Temporarily skip the query in WP Playground.
109 // UPDATE with JOIN is not yet supported by the SQLite integration.
110 if (Connection::isSQLite()) {
111 return;
112 }
113 $this->entityManager->getConnection()->executeStatement(
114 "
115 UPDATE $newslettersTable n
116 JOIN $sendingQueuesTable q ON n.id = q.newsletter_id
117 JOIN $scheduledTasksTable t ON q.task_id = t.id
118 SET
119 n.status = :sent,
120 n.sent_at = COALESCE(
121 (
122 -- use 'updated_at' of processed subscriber with the highest ID ('MAX(subscriber_id)' can use index)
123 SELECT updated_at FROM $scheduledTaskSubscribersTable WHERE task_id = t.id AND subscriber_id = (
124 SELECT MAX(subscriber_id) FROM $scheduledTaskSubscribersTable WHERE task_id = t.id
125 )
126 ),
127 t.updated_at
128 )
129 WHERE t.id IN (:ids)
130 ",
131 ['sent' => NewsletterEntity::STATUS_SENT, 'ids' => $ids],
132 ['ids' => ArrayParameterType::INTEGER]
133 );
134 }
135
136 private function backfillMissingDataForMigratedNewsletters(): void {
137 // In https://mailpoet.atlassian.net/browse/MAILPOET-5886 we fixed missing "sent" status
138 // by https://github.com/mailpoet/mailpoet/pull/5416, but didn't backfill missing data.
139
140 // get affected newsletter IDs
141 $ids = $this->entityManager->createQueryBuilder()
142 ->select('n.id')
143 ->from(NewsletterEntity::class, 'n')
144 ->where('n.status = :sent')
145 ->andWhere('n.sentAt IS NULL')
146 ->setParameter('sent', NewsletterEntity::STATUS_SENT)
147 ->getQuery()
148 ->getSingleColumnResult();
149
150 // get missing newsletter statistics IDs
151 $data = $this->entityManager->createQueryBuilder()
152 ->select('IDENTITY(q.newsletter) AS nid, q.id AS qid, IDENTITY(s.subscriber) AS sid, s.updatedAt AS sentAt')
153 ->from(SendingQueueEntity::class, 'q')
154 ->join('q.task', 't')
155 ->join('t.subscribers', 's')
156 ->leftJoin(StatisticsNewsletterEntity::class, 'ns', 'WITH', 'ns.queue = q AND ns.subscriber = s.subscriber')
157 ->where('q.newsletter IN (:ids)')
158 ->andWhere('ns.id IS NULL')
159 ->andWhere('s.processed = :processed')
160 ->setParameter('ids', $ids)
161 ->setParameter('processed', ScheduledTaskSubscriberEntity::STATUS_PROCESSED)
162 ->getQuery()
163 ->getResult();
164
165 // insert missing newsletter statistics
166 $newsletterStatisticsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
167 foreach ($data as $row) {
168 $this->entityManager->getConnection()->executeStatement("
169 INSERT IGNORE INTO $newsletterStatisticsTable (newsletter_id, queue_id, subscriber_id, sent_at)
170 VALUES (?, ?, ?, ?)
171 ", [$row['nid'], $row['qid'], $row['sid'], $row['sentAt']->format('Y-m-d H:i:s')]);
172 }
173
174 // add missing "sentAt" (DBAL needed to be able to use JOIN)
175 $newslettersTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
176 $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
177 $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
178 $sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
179
180 // Temporarily skip the query in WP Playground.
181 // UPDATE with JOIN is not yet supported by the SQLite integration.
182 if (Connection::isSQLite()) {
183 return;
184 }
185 $this->entityManager->getConnection()->executeStatement(
186 "
187 UPDATE $newslettersTable n
188 JOIN $sendingQueuesTable q ON n.id = q.newsletter_id
189 JOIN $scheduledTasksTable t ON q.task_id = t.id
190 SET n.sent_at = COALESCE(
191 (
192 -- use 'updated_at' of processed subscriber with the highest ID ('MAX(subscriber_id)' can use index)
193 SELECT updated_at FROM $scheduledTaskSubscribersTable WHERE task_id = t.id AND subscriber_id = (
194 SELECT MAX(subscriber_id) FROM $scheduledTaskSubscribersTable WHERE task_id = t.id
195 )
196 ),
197 t.updated_at
198 )
199 WHERE q.newsletter_id IN (:ids)
200 ",
201 ['ids' => $ids],
202 ['ids' => ArrayParameterType::INTEGER]
203 );
204 }
205 }
206