PluginProbe
MailPoet – Newsletters, Email Marketing, and Automation / 3.59.2
MailPoet – Newsletters, Email Marketing, and Automation v3.59.2
5.38.0 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 All 542 releases
mailpoet / lib / Newsletter / NewsletterSaveController.php

NewsletterSaveController.php in MailPoet – Newsletters, Email Marketing, and Automation 3.59.2, at lib/Newsletter/NewsletterSaveController.php

396 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MailPoet\Newsletter;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterQueueTask;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\Entities\NewsletterOptionEntity;
11 use MailPoet\Entities\NewsletterOptionFieldEntity;
12 use MailPoet\Entities\NewsletterSegmentEntity;
13 use MailPoet\Entities\ScheduledTaskEntity;
14 use MailPoet\Entities\SegmentEntity;
15 use MailPoet\InvalidStateException;
16 use MailPoet\Models\Newsletter;
17 use MailPoet\Newsletter\Options\NewsletterOptionFieldsRepository;
18 use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
19 use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
20 use MailPoet\Newsletter\Scheduler\Scheduler;
21 use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
22 use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
23 use MailPoet\NewsletterTemplates\NewsletterTemplatesRepository;
24 use MailPoet\NotFoundException;
25 use MailPoet\Services\AuthorizedEmailsController;
26 use MailPoet\Settings\SettingsController;
27 use MailPoet\UnexpectedValueException;
28 use MailPoet\Util\Security;
29 use MailPoet\WP\Emoji;
30 use MailPoet\WP\Functions as WPFunctions;
31 use MailPoetVendor\Carbon\Carbon;
32 use MailPoetVendor\Doctrine\ORM\EntityManager;
33
34 class NewsletterSaveController {
35 /** @var AuthorizedEmailsController */
36 private $authorizedEmailsController;
37
38 /** @var Emoji */
39 private $emoji;
40
41 /** @var EntityManager */
42 private $entityManager;
43
44 /** @var NewslettersRepository */
45 private $newslettersRepository;
46
47 /** @var NewsletterOptionsRepository */
48 private $newsletterOptionsRepository;
49
50 /** @var NewsletterOptionFieldsRepository */
51 private $newsletterOptionFieldsRepository;
52
53 /** @var NewsletterSegmentRepository */
54 private $newsletterSegmentRepository;
55
56 /** @var NewsletterTemplatesRepository */
57 private $newsletterTemplatesRepository;
58
59 /** @var PostNotificationScheduler */
60 private $postNotificationScheduler;
61
62 /** @var ScheduledTasksRepository */
63 private $scheduledTasksRepository;
64
65 /** @var SettingsController */
66 private $settings;
67
68 /** @var Security */
69 private $security;
70
71 /** @var WPFunctions */
72 private $wp;
73
74 public function __construct(
75 AuthorizedEmailsController $authorizedEmailsController,
76 Emoji $emoji,
77 EntityManager $entityManager,
78 NewslettersRepository $newslettersRepository,
79 NewsletterOptionsRepository $newsletterOptionsRepository,
80 NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository,
81 NewsletterSegmentRepository $newsletterSegmentRepository,
82 NewsletterTemplatesRepository $newsletterTemplatesRepository,
83 PostNotificationScheduler $postNotificationScheduler,
84 ScheduledTasksRepository $scheduledTasksRepository,
85 SettingsController $settings,
86 Security $security,
87 WPFunctions $wp
88 ) {
89 $this->authorizedEmailsController = $authorizedEmailsController;
90 $this->emoji = $emoji;
91 $this->entityManager = $entityManager;
92 $this->newslettersRepository = $newslettersRepository;
93 $this->newsletterOptionsRepository = $newsletterOptionsRepository;
94 $this->newsletterOptionFieldsRepository = $newsletterOptionFieldsRepository;
95 $this->newsletterSegmentRepository = $newsletterSegmentRepository;
96 $this->newsletterTemplatesRepository = $newsletterTemplatesRepository;
97 $this->postNotificationScheduler = $postNotificationScheduler;
98 $this->scheduledTasksRepository = $scheduledTasksRepository;
99 $this->settings = $settings;
100 $this->security = $security;
101 $this->wp = $wp;
102 }
103
104 public function save(array $data = []): NewsletterEntity {
105 if (!empty($data['template_id'])) {
106 $template = $this->newsletterTemplatesRepository->findOneById($data['template_id']);
107 if ($template) {
108 $data['body'] = json_encode($template->getBody());
109 }
110 }
111
112 if (!empty($data['body'])) {
113 $data['body'] = $this->emoji->encodeForUTF8Column(MP_NEWSLETTERS_TABLE, 'body', $data['body']);
114 }
115
116 $newsletter = isset($data['id']) ? $this->getNewsletter($data) : $this->createNewsletter($data);
117 $oldSenderAddress = $newsletter->getSenderAddress();
118
119 $this->updateNewsletter($newsletter, $data);
120 $this->newslettersRepository->flush();
121 if (!empty($data['segments'])) {
122 $this->updateSegments($newsletter, $data['segments']);
123 }
124 if (!empty($data['options'])) {
125 $this->updateOptions($newsletter, $data['options']);
126 }
127
128 // fetch model with updated options (for back compatibility)
129 $newsletterModel = Newsletter::filter('filterWithOptions', $newsletter->getType())->findOne($newsletter->getId());
130 if (!$newsletterModel) {
131 throw new InvalidStateException();
132 }
133
134 // save default sender if needed
135 if (!$this->settings->get('sender') && !empty($data['sender_address']) && !empty($data['sender_name'])) {
136 $this->settings->set('sender', [
137 'address' => $data['sender_address'],
138 'name' => $data['sender_name'],
139 ]);
140 }
141
142 $this->rescheduleIfNeeded($newsletter, $newsletterModel);
143 $this->updateQueue($newsletter, $newsletterModel, $data['options'] ?? []);
144 $this->authorizedEmailsController->onNewsletterSenderAddressUpdate($newsletter, $oldSenderAddress);
145 return $newsletter;
146 }
147
148 public function duplicate(NewsletterEntity $newsletter): NewsletterEntity {
149 $duplicate = clone $newsletter;
150
151 // reset timestamps
152 $createdAt = Carbon::createFromTimestamp($this->wp->currentTime('timestamp'));
153 $duplicate->setCreatedAt($createdAt);
154 $duplicate->setUpdatedAt($createdAt);
155 $duplicate->setDeletedAt(null);
156
157 $duplicate->setSubject(sprintf(__('Copy of %s', 'mailpoet'), $newsletter->getSubject()));
158 // generate new unsubscribe token
159 $duplicate->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($duplicate));
160 // reset status
161 $duplicate->setStatus(NewsletterEntity::STATUS_DRAFT);
162 // reset hash
163 $duplicate->setHash(Security::generateHash());
164 // reset sent at date
165 $duplicate->setSentAt(null);
166
167 $this->newslettersRepository->persist($duplicate);
168 $this->newslettersRepository->flush();
169
170 // create relationships between duplicate and segments
171 foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
172 $segment = $newsletterSegment->getSegment();
173 if (!$segment) {
174 continue;
175 }
176 $duplicateSegment = new NewsletterSegmentEntity($duplicate, $segment);
177 $duplicate->getNewsletterSegments()->add($duplicateSegment);
178 $this->newsletterSegmentRepository->persist($duplicateSegment);
179 }
180
181 // duplicate options
182 $ignoredOptions = [
183 NewsletterOptionFieldEntity::NAME_IS_SCHEDULED,
184 NewsletterOptionFieldEntity::NAME_SCHEDULED_AT,
185 ];
186 foreach ($newsletter->getOptions() as $newsletterOption) {
187 $optionField = $newsletterOption->getOptionField();
188 if (!$optionField) {
189 continue;
190 }
191 if (in_array($optionField->getName(), $ignoredOptions, true)) {
192 continue;
193 }
194 $duplicateOption = new NewsletterOptionEntity($duplicate, $optionField);
195 $duplicateOption->setValue($newsletterOption->getValue());
196 $duplicate->getOptions()->add($duplicateOption);
197 $this->newsletterOptionsRepository->persist($duplicateOption);
198 }
199 $this->newslettersRepository->flush();
200
201 return $duplicate;
202 }
203
204 private function getNewsletter(array $data): NewsletterEntity {
205 if (!isset($data['id'])) {
206 throw new UnexpectedValueException();
207 }
208
209 $newsletter = $this->newslettersRepository->findOneById((int)$data['id']);
210 if (!$newsletter) {
211 throw new NotFoundException();
212 }
213 return $newsletter;
214 }
215
216 private function createNewsletter(array $data): NewsletterEntity {
217 $newsletter = new NewsletterEntity();
218 $newsletter->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($newsletter));
219 $newsletter->setHash(Security::generateHash());
220 // set default sender based on settings
221 if (empty($data['sender'])) {
222 $sender = $this->settings->get('sender', []);
223 $data['sender_name'] = $sender['name'] ?? '';
224 $data['sender_address'] = $sender['address'] ?? '';
225 }
226
227 // set default reply_to based on settings
228 if (empty($data['reply_to'])) {
229 $replyTo = $this->settings->get('reply_to', []);
230 $data['reply_to_name'] = $replyTo['name'] ?? '';
231 $data['reply_to_address'] = $replyTo['address'] ?? '';
232 }
233
234 $this->updateNewsletter($newsletter, $data);
235 $this->newslettersRepository->persist($newsletter);
236 return $newsletter;
237 }
238
239 private function updateNewsletter(NewsletterEntity $newsletter, array $data) {
240 if (array_key_exists('type', $data)) {
241 $newsletter->setType($data['type']);
242 }
243
244 if (array_key_exists('subject', $data)) {
245 $newsletter->setSubject($data['subject']);
246 }
247
248 if (array_key_exists('preheader', $data)) {
249 $newsletter->setPreheader($data['preheader']);
250 }
251
252 if (array_key_exists('body', $data)) {
253 $newsletter->setBody(json_decode($data['body'], true));
254 }
255
256 if (array_key_exists('ga_campaign', $data)) {
257 $newsletter->setGaCampaign($data['ga_campaign']);
258 }
259
260 if (array_key_exists('sender_name', $data)) {
261 $newsletter->setSenderName($data['sender_name'] ?? '');
262 }
263
264 if (array_key_exists('sender_address', $data)) {
265 $newsletter->setSenderAddress($data['sender_address'] ?? '');
266 }
267
268 if (array_key_exists('reply_to_name', $data)) {
269 $newsletter->setReplyToName($data['reply_to_name'] ?? '');
270 }
271
272 if (array_key_exists('reply_to_address', $data)) {
273 $newsletter->setReplyToAddress($data['reply_to_address'] ?? '');
274 }
275 }
276
277 private function updateSegments(NewsletterEntity $newsletter, array $segments) {
278 $newsletterSegments = [];
279 foreach ($segments as $segmentData) {
280 if (!is_array($segmentData) || !isset($segmentData['id'])) {
281 continue;
282 }
283
284 $segment = $this->entityManager->getReference(SegmentEntity::class, (int)$segmentData['id']);
285 if (!$segment) {
286 continue;
287 }
288
289 $newsletterSegment = $this->newsletterSegmentRepository->findOneBy([
290 'newsletter' => $newsletter,
291 'segment' => $segment,
292 ]);
293
294 if (!$newsletterSegment) {
295 $newsletterSegment = new NewsletterSegmentEntity($newsletter, $segment);
296 $this->entityManager->persist($newsletterSegment);
297 }
298
299 if (!$newsletter->getNewsletterSegments()->contains($newsletterSegment)) {
300 $newsletter->getNewsletterSegments()->add($newsletterSegment);
301 }
302 $newsletterSegments[] = $newsletterSegment;
303 }
304
305 // on Doctrine < 2.6, when using orphan removal, we need to remove items manually instead of replacing the
306 // whole collection (see https://github.com/doctrine/orm/commit/1587aac4ff6b0753ddd5f8b8d4558b6b40096057)
307 foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
308 if (!in_array($newsletterSegment, $newsletterSegments, true)) {
309 $newsletter->getNewsletterSegments()->removeElement($newsletterSegment); // triggers orphan removal
310 }
311 }
312
313 $this->entityManager->flush();
314 }
315
316 private function updateOptions(NewsletterEntity $newsletter, array $options) {
317 $optionFields = $this->newsletterOptionFieldsRepository->findBy(['newsletterType' => $newsletter->getType()]);
318 foreach ($optionFields as $optionField) {
319 if (!isset($options[$optionField->getName()])) {
320 continue;
321 }
322
323 $option = $this->newsletterOptionsRepository->findOneBy([
324 'newsletter' => $newsletter,
325 'optionField' => $optionField,
326 ]);
327
328 if (!$option) {
329 $option = new NewsletterOptionEntity($newsletter, $optionField);
330 $this->newsletterOptionsRepository->persist($option);
331 }
332 $option->setValue($options[$optionField->getName()]);
333
334 if (!$newsletter->getOptions()->contains($option)) {
335 $newsletter->getOptions()->add($option);
336 }
337 }
338
339 $this->entityManager->flush();
340 }
341
342 private function rescheduleIfNeeded(NewsletterEntity $newsletter, Newsletter $newsletterModel) {
343 if ($newsletter->getType() !== NewsletterEntity::TYPE_NOTIFICATION) {
344 return;
345 }
346
347 // generate the new schedule from options and get the new "next run" date
348 $schedule = $this->postNotificationScheduler->processPostNotificationSchedule($newsletter);
349 $nextRunDateString = Scheduler::getNextRunDate($schedule);
350 $nextRunDate = $nextRunDateString ? Carbon::createFromFormat('Y-m-d H:i:s', $nextRunDateString) : null;
351 if ($nextRunDate === false) {
352 throw InvalidStateException::create()->withMessage('Invalid next run date generated');
353 }
354
355 // find previously scheduled jobs and reschedule them
356 $scheduledTasks = $this->scheduledTasksRepository->findByNewsletterAndStatus($newsletter, ScheduledTaskEntity::STATUS_SCHEDULED);
357 foreach ($scheduledTasks as $scheduledTask) {
358 $scheduledTask->setScheduledAt($nextRunDate);
359 }
360 $this->entityManager->flush();
361
362 // 'processPostNotificationSchedule' modifies newsletter options by old model - let's reload them
363 foreach ($newsletter->getOptions() as $newsletterOption) {
364 $this->entityManager->refresh($newsletterOption);
365 }
366 }
367
368 private function updateQueue(NewsletterEntity $newsletter, Newsletter $newsletterModel, array $options) {
369 if ($newsletter->getType() !== NewsletterEntity::TYPE_STANDARD) {
370 return;
371 }
372
373 $queue = $newsletter->getLatestQueue();
374 if (!$queue) {
375 return;
376 }
377
378 // if newsletter was previously scheduled and is now unscheduled, set its status to DRAFT and delete associated queue record
379 if ($newsletter->getStatus() === NewsletterEntity::STATUS_SCHEDULED && isset($options['isScheduled']) && empty($options['isScheduled'])) {
380 $this->entityManager->remove($queue);
381 $newsletter->setStatus(NewsletterEntity::STATUS_DRAFT);
382 } else {
383 $queueModel = $newsletterModel->getQueue();
384 $queueModel->newsletterRenderedSubject = null;
385 $queueModel->newsletterRenderedBody = null;
386
387 $newsletterQueueTask = new NewsletterQueueTask();
388 $newsletterQueueTask->preProcessNewsletter($newsletterModel, $queueModel);
389
390 // 'preProcessNewsletter' modifies queue by old model - let's reload it
391 $this->entityManager->refresh($queue);
392 }
393 $this->entityManager->flush();
394 }
395 }
396