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 / Newsletter / NewsletterSaveController.php
mailpoet / lib / Newsletter Last commit date
Editor 2 months ago Embed 1 month ago Links 2 months ago Listing 9 months ago Options 2 years ago Preview 1 month ago Renderer 4 days ago RestApi 1 week ago Scheduler 4 days ago Segment 1 year ago Sending 1 week ago Sharing 1 month ago Shortcodes 3 days ago Statistics 1 week ago ViewInBrowser 1 month ago ApiDataSanitizer.php 3 years ago AutomatedLatestContent.php 2 months ago AutomaticEmailsRepository.php 3 years ago BlockPostQuery.php 2 months ago BulkActionController.php 1 month ago BulkActionException.php 1 month ago DynamicProducts.php 11 months ago NewsletterCoupon.php 2 months ago NewsletterDeleteController.php 2 years ago NewsletterHtmlSanitizer.php 2 years ago NewsletterPostsRepository.php 2 years ago NewsletterResendController.php 4 days ago NewsletterSaveController.php 1 month ago NewsletterValidator.php 1 year ago NewslettersRepository.php 2 weeks ago StatusController.php 1 month ago Url.php 2 months ago index.php 3 years ago
NewsletterSaveController.php
558 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
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\EmailEditor\Integrations\MailPoet\EmailEditor;
10 use MailPoet\Entities\NewsletterEntity;
11 use MailPoet\Entities\NewsletterOptionEntity;
12 use MailPoet\Entities\NewsletterOptionFieldEntity;
13 use MailPoet\Entities\NewsletterSegmentEntity;
14 use MailPoet\Entities\ScheduledTaskEntity;
15 use MailPoet\Entities\SegmentEntity;
16 use MailPoet\Entities\WpPostEntity;
17 use MailPoet\InvalidStateException;
18 use MailPoet\Newsletter\Options\NewsletterOptionFieldsRepository;
19 use MailPoet\Newsletter\Options\NewsletterOptionsRepository;
20 use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
21 use MailPoet\Newsletter\Scheduler\Scheduler;
22 use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
23 use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
24 use MailPoet\Newsletter\Sending\TimeZoneCampaignScheduler;
25 use MailPoet\Newsletter\Sharing\ShareVisibility;
26 use MailPoet\NewsletterTemplates\NewsletterTemplatesRepository;
27 use MailPoet\NotFoundException;
28 use MailPoet\Services\AuthorizedEmailsController;
29 use MailPoet\Settings\SettingsController;
30 use MailPoet\UnexpectedValueException;
31 use MailPoet\Util\Security;
32 use MailPoet\WP\Emoji;
33 use MailPoet\WP\Functions as WPFunctions;
34 use MailPoetVendor\Carbon\Carbon;
35 use MailPoetVendor\Doctrine\ORM\EntityManager;
36
37 class NewsletterSaveController {
38 /** @var AuthorizedEmailsController */
39 private $authorizedEmailsController;
40
41 /** @var Emoji */
42 private $emoji;
43
44 /** @var EntityManager */
45 private $entityManager;
46
47 /** @var NewslettersRepository */
48 private $newslettersRepository;
49
50 /** @var NewsletterOptionsRepository */
51 private $newsletterOptionsRepository;
52
53 /** @var NewsletterOptionFieldsRepository */
54 private $newsletterOptionFieldsRepository;
55
56 /** @var NewsletterSegmentRepository */
57 private $newsletterSegmentRepository;
58
59 /** @var NewsletterTemplatesRepository */
60 private $newsletterTemplatesRepository;
61
62 /** @var PostNotificationScheduler */
63 private $postNotificationScheduler;
64
65 /** @var ScheduledTasksRepository */
66 private $scheduledTasksRepository;
67
68 /** @var SettingsController */
69 private $settings;
70
71 /** @var Security */
72 private $security;
73
74 /** @var WPFunctions */
75 private $wp;
76
77 /** @var ApiDataSanitizer */
78 private $dataSanitizer;
79
80 /** @var Scheduler */
81 private $scheduler;
82
83 /*** @var NewsletterCoupon */
84 private $newsletterCoupon;
85
86 /** @var TimeZoneCampaignScheduler */
87 private $timeZoneCampaignScheduler;
88
89 /** @var ShareVisibility */
90 private $shareVisibility;
91
92 public function __construct(
93 AuthorizedEmailsController $authorizedEmailsController,
94 Emoji $emoji,
95 EntityManager $entityManager,
96 NewslettersRepository $newslettersRepository,
97 NewsletterOptionsRepository $newsletterOptionsRepository,
98 NewsletterOptionFieldsRepository $newsletterOptionFieldsRepository,
99 NewsletterSegmentRepository $newsletterSegmentRepository,
100 NewsletterTemplatesRepository $newsletterTemplatesRepository,
101 PostNotificationScheduler $postNotificationScheduler,
102 ScheduledTasksRepository $scheduledTasksRepository,
103 SettingsController $settings,
104 Security $security,
105 WPFunctions $wp,
106 ApiDataSanitizer $dataSanitizer,
107 Scheduler $scheduler,
108 NewsletterCoupon $newsletterCoupon,
109 TimeZoneCampaignScheduler $timeZoneCampaignScheduler,
110 ShareVisibility $shareVisibility
111 ) {
112 $this->authorizedEmailsController = $authorizedEmailsController;
113 $this->emoji = $emoji;
114 $this->entityManager = $entityManager;
115 $this->newslettersRepository = $newslettersRepository;
116 $this->newsletterOptionsRepository = $newsletterOptionsRepository;
117 $this->newsletterOptionFieldsRepository = $newsletterOptionFieldsRepository;
118 $this->newsletterSegmentRepository = $newsletterSegmentRepository;
119 $this->newsletterTemplatesRepository = $newsletterTemplatesRepository;
120 $this->postNotificationScheduler = $postNotificationScheduler;
121 $this->scheduledTasksRepository = $scheduledTasksRepository;
122 $this->settings = $settings;
123 $this->security = $security;
124 $this->wp = $wp;
125 $this->dataSanitizer = $dataSanitizer;
126 $this->scheduler = $scheduler;
127 $this->newsletterCoupon = $newsletterCoupon;
128 $this->timeZoneCampaignScheduler = $timeZoneCampaignScheduler;
129 $this->shareVisibility = $shareVisibility;
130 }
131
132 public function save(array $data = []): NewsletterEntity {
133 if (!empty($data['template_id'])) {
134 $template = $this->newsletterTemplatesRepository->findOneById($data['template_id']);
135 if ($template) {
136 $data['body'] = json_encode($template->getBody());
137 }
138 }
139
140 if (!empty($data['body'])) {
141 $newslettersTableName = $this->newslettersRepository->getTableName();
142 $body = $this->emoji->encodeForUTF8Column($newslettersTableName, 'body', $data['body']);
143 $decodedBody = json_decode($body, true);
144 $body = $this->dataSanitizer->sanitizeBody(is_array($decodedBody) ? $decodedBody : []);
145 $data['body'] = json_encode($body);
146 }
147
148 $newsletter = isset($data['id']) ? $this->getNewsletter($data) : $this->createNewsletter($data);
149 $data = $this->sanitizeAutomationEmailData($data, $newsletter);
150 $oldSenderAddress = $newsletter->getSenderAddress();
151
152 $this->updateNewsletter($newsletter, $data);
153 $this->newslettersRepository->flush();
154 if (!empty($data['segments'])) {
155 $this->updateSegments($newsletter, $data['segments']);
156 }
157 if (!empty($data['options'])) {
158 $this->updateOptions($newsletter, $data['options']);
159 }
160
161 // save default sender if needed
162 if (!$this->settings->get('sender') && !empty($data['sender_address']) && !empty($data['sender_name'])) {
163 $this->settings->set('sender', [
164 'address' => $data['sender_address'],
165 'name' => $data['sender_name'],
166 ]);
167 }
168
169 $this->rescheduleIfNeeded($newsletter);
170 $this->updateQueue($newsletter, $data['options'] ?? []);
171 $this->authorizedEmailsController->onNewsletterSenderAddressUpdate($newsletter, $oldSenderAddress);
172 if ($this->isNewEditor($data)) {
173 $this->ensureWpPost($newsletter);
174 }
175 return $newsletter;
176 }
177
178 public function updateShareVisibility(NewsletterEntity $newsletter, string $visibility): NewsletterEntity {
179 $this->updateOptions($newsletter, [
180 NewsletterOptionFieldEntity::NAME_SHARE_VISIBILITY => $visibility,
181 ]);
182 return $newsletter;
183 }
184
185 private function isNewEditor(array $data): bool {
186 if (!isset($data['new_editor'])) {
187 return false;
188 }
189
190 $value = $data['new_editor'];
191
192 if (is_bool($value)) return $value;
193 if (is_int($value)) return $value === 1;
194 if (is_string($value)) {
195 $norm = strtolower(trim($value));
196 if (in_array($norm, ['1', 'true', 'yes', 'on'], true)) return true;
197 if (in_array($norm, ['0', 'false', 'no', 'off', ''], true)) return false;
198 }
199 return (bool)$value;
200 }
201
202 private function sanitizeAutomationEmailData(array $data, NewsletterEntity $newsletter): array {
203 if ($newsletter->getType() !== NewsletterEntity::TYPE_AUTOMATION) {
204 return $data;
205 }
206 $data['segments'] = [];
207 return $data;
208 }
209
210 public function duplicate(NewsletterEntity $newsletter): NewsletterEntity {
211 $duplicate = clone $newsletter;
212
213 // reset timestamps
214 $createdAt = Carbon::now()->millisecond(0);
215 $duplicate->setCreatedAt($createdAt);
216 $duplicate->setUpdatedAt($createdAt);
217 $duplicate->setDeletedAt(null);
218
219 // translators: %s is the subject of the mail which has been copied.
220 $duplicate->setSubject(sprintf(__('Copy of %s', 'mailpoet'), $newsletter->getSubject()));
221 // generate new unsubscribe token
222 $duplicate->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($duplicate));
223 // reset status
224 $duplicate->setStatus(NewsletterEntity::STATUS_DRAFT);
225 // reset hash
226 $duplicate->setHash(Security::generateHash());
227 // reset sent at date
228 $duplicate->setSentAt(null);
229
230 $body = $duplicate->getBody();
231 if ($body) {
232 $duplicate->setBody($this->newsletterCoupon->cleanupBodySensitiveData($body));
233 }
234 $this->newslettersRepository->persist($duplicate);
235 $this->newslettersRepository->flush();
236
237 // duplicate wp post data
238 $post = !is_null($newsletter->getWpPostId()) ? $this->wp->getPost($newsletter->getWpPostId()) : null;
239 if ($post instanceof \WP_Post) {
240 // Automation emails use 'private' status to prevent them from appearing in public queries
241 $isAutomation = $duplicate->getType() === NewsletterEntity::TYPE_AUTOMATION;
242 $postStatus = $isAutomation ? 'private' : NewsletterEntity::STATUS_DRAFT;
243 $postData = [
244 'post_status' => $postStatus,
245 'post_author' => $this->wp->getCurrentUserId(),
246 'post_content' => $post->post_content, // @phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
247 'post_type' => $post->post_type, // @phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
248 // translators: %s is the campaign name of the mail which has been copied.
249 'post_title' => sprintf(__('Copy of %s', 'mailpoet'), $post->post_title), // @phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
250 ];
251
252 // Set meta flag for automation emails to prevent other plugins from creating duplicate newsletters
253 if ($isAutomation) {
254 $postData['meta_input'] = ['_mailpoet_is_automation_email' => '1'];
255 }
256
257 $newPostId = $this->wp->wpInsertPost($postData);
258 // Post meta duplication
259 $originalPostMeta = $this->wp->getPostMeta($post->ID);
260 foreach ($originalPostMeta as $key => $values) {
261 foreach ($values as $value) {
262 // Unserialize the value if it was serialized to avoid invalid data format
263 if (is_string($value) && is_serialized($value)) {
264 $value = unserialize($value);
265 }
266 update_post_meta($newPostId, $key, $value);
267 }
268 }
269
270 $duplicate->setWpPost($this->entityManager->getReference(WpPostEntity::class, $newPostId));
271 }
272
273 // create relationships between duplicate and segments
274 foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
275 $segment = $newsletterSegment->getSegment();
276 if (!$segment) {
277 continue;
278 }
279 $duplicateSegment = new NewsletterSegmentEntity($duplicate, $segment);
280 $duplicate->getNewsletterSegments()->add($duplicateSegment);
281 $this->newsletterSegmentRepository->persist($duplicateSegment);
282 }
283
284 // duplicate options
285 $ignoredOptions = [
286 NewsletterOptionFieldEntity::NAME_IS_SCHEDULED,
287 NewsletterOptionFieldEntity::NAME_SCHEDULED_AT,
288 NewsletterOptionFieldEntity::NAME_EXCLUDE_FROM_ARCHIVE,
289 ];
290 foreach ($newsletter->getOptions() as $newsletterOption) {
291 $optionField = $newsletterOption->getOptionField();
292 if (!$optionField) {
293 continue;
294 }
295 if (in_array($optionField->getName(), $ignoredOptions, true)) {
296 continue;
297 }
298 $duplicateOption = new NewsletterOptionEntity($duplicate, $optionField);
299 $duplicateOption->setValue($newsletterOption->getValue());
300 $duplicate->getOptions()->add($duplicateOption);
301 $this->newsletterOptionsRepository->persist($duplicateOption);
302 }
303 $this->newslettersRepository->flush();
304
305 return $duplicate;
306 }
307
308 private function getNewsletter(array $data): NewsletterEntity {
309 if (!isset($data['id'])) {
310 throw new UnexpectedValueException('Missing newsletter ID in data');
311 }
312
313 $newsletter = $this->newslettersRepository->findOneById((int)$data['id']);
314 if (!$newsletter) {
315 throw new NotFoundException('Newsletter not found');
316 }
317 return $newsletter;
318 }
319
320 private function createNewsletter(array $data): NewsletterEntity {
321 $newsletter = new NewsletterEntity();
322 $newsletter->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($newsletter));
323 $newsletter->setHash(Security::generateHash());
324 // set default sender based on settings
325 if (empty($data['sender'])) {
326 $sender = $this->settings->get('sender', []);
327 $data['sender_name'] = $sender['name'] ?? '';
328 $data['sender_address'] = $sender['address'] ?? '';
329 }
330
331 // set default reply_to based on settings
332 if (empty($data['reply_to'])) {
333 $replyTo = $this->settings->get('reply_to', []);
334 $data['reply_to_name'] = $replyTo['name'] ?? '';
335 $data['reply_to_address'] = $replyTo['address'] ?? '';
336 }
337
338 $this->updateNewsletter($newsletter, $data);
339 $this->newslettersRepository->persist($newsletter);
340 return $newsletter;
341 }
342
343 private function updateNewsletter(NewsletterEntity $newsletter, array $data) {
344 if (array_key_exists('type', $data)) {
345 $newsletter->setType($data['type']);
346 }
347
348 if (array_key_exists('subject', $data)) {
349 $newsletter->setSubject($data['subject']);
350 }
351
352 if (array_key_exists('preheader', $data)) {
353 $newsletter->setPreheader($data['preheader']);
354 }
355
356 if (array_key_exists('body', $data)) {
357 $decodedBody = json_decode($data['body'], true);
358 $newsletter->setBody(is_array($decodedBody) ? $decodedBody : null);
359 }
360
361 if (array_key_exists('ga_campaign', $data)) {
362 $newsletter->setGaCampaign($data['ga_campaign']);
363 }
364
365 if (array_key_exists('sender_name', $data)) {
366 $newsletter->setSenderName($data['sender_name'] ?? '');
367 }
368
369 if (array_key_exists('sender_address', $data)) {
370 $newsletter->setSenderAddress($data['sender_address'] ?? '');
371 }
372
373 if (array_key_exists('reply_to_name', $data)) {
374 $newsletter->setReplyToName($data['reply_to_name'] ?? '');
375 }
376
377 if (array_key_exists('reply_to_address', $data)) {
378 $newsletter->setReplyToAddress($data['reply_to_address'] ?? '');
379 }
380
381 if ($newsletter->getStatus() === NewsletterEntity::STATUS_CORRUPT) {
382 $newsletter->setStatus($newsletter->canBeSetActive() ? NewsletterEntity::STATUS_ACTIVE : NewsletterEntity::STATUS_SENDING);
383 }
384 }
385
386 private function updateSegments(NewsletterEntity $newsletter, array $segments) {
387 $newsletterSegments = [];
388 foreach ($segments as $segmentData) {
389 if (!is_array($segmentData) || !isset($segmentData['id']) || !is_numeric($segmentData['id'])) {
390 continue;
391 }
392
393 $segment = $this->entityManager->getReference(SegmentEntity::class, (int)$segmentData['id']);
394 if (!$segment) {
395 continue;
396 }
397
398 $newsletterSegment = $this->newsletterSegmentRepository->findOneBy([
399 'newsletter' => $newsletter,
400 'segment' => $segment,
401 ]);
402
403 if (!$newsletterSegment) {
404 $newsletterSegment = new NewsletterSegmentEntity($newsletter, $segment);
405 $this->entityManager->persist($newsletterSegment);
406 }
407
408 if (!$newsletter->getNewsletterSegments()->contains($newsletterSegment)) {
409 $newsletter->getNewsletterSegments()->add($newsletterSegment);
410 }
411 $newsletterSegments[] = $newsletterSegment;
412 }
413
414 // on Doctrine < 2.6, when using orphan removal, we need to remove items manually instead of replacing the
415 // whole collection (see https://github.com/doctrine/orm/commit/1587aac4ff6b0753ddd5f8b8d4558b6b40096057)
416 foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
417 if (!in_array($newsletterSegment, $newsletterSegments, true)) {
418 $newsletter->getNewsletterSegments()->removeElement($newsletterSegment); // triggers orphan removal
419 }
420 }
421
422 $this->entityManager->flush();
423 }
424
425 private function updateOptions(NewsletterEntity $newsletter, array $options) {
426 $optionFields = $this->newsletterOptionFieldsRepository->findBy(['newsletterType' => $newsletter->getType()]);
427 foreach ($optionFields as $optionField) {
428 $name = $optionField->getName();
429 if (!isset($options[$name])) {
430 continue;
431 }
432
433 $option = $this->newsletterOptionsRepository->findOneBy([
434 'newsletter' => $newsletter,
435 'optionField' => $optionField,
436 ]);
437
438 if (!$option) {
439 $option = new NewsletterOptionEntity($newsletter, $optionField);
440 $this->newsletterOptionsRepository->persist($option);
441 }
442 $value = $options[$name];
443 if ($name === NewsletterOptionFieldEntity::NAME_SHARE_VISIBILITY) {
444 $value = $this->shareVisibility->sanitize((string)$value);
445 }
446 $option->setValue($value);
447
448 if (!$newsletter->getOptions()->contains($option)) {
449 $newsletter->getOptions()->add($option);
450 }
451 }
452
453 $this->entityManager->flush();
454 }
455
456 private function rescheduleIfNeeded(NewsletterEntity $newsletter) {
457 if ($newsletter->getType() !== NewsletterEntity::TYPE_NOTIFICATION) {
458 return;
459 }
460
461 // generate the new schedule from options and get the new "next run" date
462 $schedule = $this->postNotificationScheduler->processPostNotificationSchedule($newsletter);
463 $nextRunDateString = $this->scheduler->getNextRunDate($schedule);
464 $nextRunDate = $nextRunDateString ? Carbon::createFromFormat('Y-m-d H:i:s', $nextRunDateString) : null;
465 if ($nextRunDate === false) {
466 throw InvalidStateException::create()->withMessage('Invalid next run date generated');
467 }
468
469 // find previously scheduled jobs and reschedule them
470 $scheduledTasks = $this->scheduledTasksRepository->findByNewsletterAndStatus($newsletter, ScheduledTaskEntity::STATUS_SCHEDULED);
471 foreach ($scheduledTasks as $scheduledTask) {
472 $scheduledTask->setScheduledAt($nextRunDate);
473 }
474 $this->entityManager->flush();
475
476 // 'processPostNotificationSchedule' modifies newsletter options by old model - let's reload them
477 foreach ($newsletter->getOptions() as $newsletterOption) {
478 $this->entityManager->refresh($newsletterOption);
479 }
480 }
481
482 private function updateQueue(NewsletterEntity $newsletter, array $options) {
483 if ($newsletter->getType() !== NewsletterEntity::TYPE_STANDARD) {
484 return;
485 }
486
487 $queue = $newsletter->getLatestQueue();
488 if (!$queue) {
489 return;
490 }
491
492 // if newsletter was previously scheduled and is now unscheduled, set its status to DRAFT and delete associated queue record
493 if ($newsletter->getStatus() === NewsletterEntity::STATUS_SCHEDULED && isset($options['isScheduled']) && empty($options['isScheduled'])) {
494 if ($this->timeZoneCampaignScheduler->isTimeZoneQueue($queue)) {
495 $this->timeZoneCampaignScheduler->deleteScheduledCampaignQueues($newsletter);
496 $newsletter->setStatus(NewsletterEntity::STATUS_DRAFT);
497 $this->entityManager->flush();
498 return;
499 }
500 $this->entityManager->remove($queue);
501 $newsletter->setStatus(NewsletterEntity::STATUS_DRAFT);
502 } else {
503 if ($this->timeZoneCampaignScheduler->isTimeZoneQueue($queue)) {
504 if (!$this->timeZoneCampaignScheduler->canReplaceScheduledCampaign($newsletter)) {
505 throw new InvalidStateException(__('This email can no longer be edited because one or more time zone batches have already started.', 'mailpoet'));
506 }
507 foreach ($this->timeZoneCampaignScheduler->getCampaignQueues($queue) as $campaignQueue) {
508 $campaignQueue->setNewsletterRenderedSubject(null);
509 $campaignQueue->setNewsletterRenderedBody(null);
510 $this->entityManager->persist($campaignQueue);
511 }
512 $this->entityManager->flush();
513 return;
514 }
515 $queue->setNewsletterRenderedSubject(null);
516 $queue->setNewsletterRenderedBody(null);
517 $this->entityManager->persist($queue);
518
519 $newsletterQueueTask = new NewsletterQueueTask();
520 $task = $queue->getTask();
521
522 if (!$task instanceof ScheduledTaskEntity) {
523 throw new InvalidStateException('Newsletter queue has no associated scheduled task');
524 }
525
526 $newsletterQueueTask->preProcessNewsletter($newsletter, $task);
527 }
528 $this->entityManager->flush();
529 }
530
531 private function ensureWpPost(NewsletterEntity $newsletter): void {
532 if ($newsletter->getWpPostId()) {
533 return;
534 }
535
536 // Automation emails use 'private' status to prevent them from appearing in public queries
537 $isAutomation = $newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION;
538 $postStatus = $isAutomation ? 'private' : 'draft';
539
540 $postData = [
541 'post_content' => '',
542 'post_type' => EmailEditor::MAILPOET_EMAIL_POST_TYPE,
543 'post_status' => $postStatus,
544 'post_author' => $this->wp->getCurrentUserId(),
545 'post_title' => __('New Email', 'mailpoet'),
546 ];
547
548 // Set meta flag for automation emails to prevent other plugins from creating duplicate newsletters
549 if ($isAutomation) {
550 $postData['meta_input'] = ['_mailpoet_is_automation_email' => '1'];
551 }
552
553 $newPostId = $this->wp->wpInsertPost($postData);
554 $newsletter->setWpPost($this->entityManager->getReference(WpPostEntity::class, $newPostId));
555 $this->entityManager->flush();
556 }
557 }
558