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 / Models / Newsletter.php

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

567 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MailPoet\Models;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\DI\ContainerWrapper;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\Newsletter\NewslettersRepository;
11 use MailPoet\Settings\SettingsController;
12 use MailPoet\Tasks\Sending as SendingTask;
13 use MailPoet\Util\Helpers;
14 use MailPoet\Util\Security;
15 use MailPoet\WP\Functions as WPFunctions;
16
17 /**
18 * @property int $id
19 * @property int $parentId
20 * @property string $type
21 * @property object|array|bool $queue
22 * @property string $hash
23 * @property string $senderName
24 * @property string $senderAddress
25 * @property string $replyToName
26 * @property string $replyToAddress
27 * @property string $status
28 * @property string|object $meta
29 * @property array $options
30 * @property bool|array $statistics
31 * @property string $sentAt
32 * @property string $deletedAt
33 * @property int $totalSent
34 * @property int $totalScheduled
35 * @property array $segments
36 * @property string $subject
37 * @property string $preheader
38 * @property string|array|null $body
39 * @property string|null $schedule
40 * @property bool|null $isScheduled
41 * @property string|null $scheduledAt
42 * @property string $gaCampaign
43 * @property string $event
44 * @property string $unsubscribeToken
45 */
46
47 class Newsletter extends Model {
48 public static $_table = MP_NEWSLETTERS_TABLE; // phpcs:ignore PSR2.Classes.PropertyDeclaration
49 const TYPE_AUTOMATIC = NewsletterEntity::TYPE_AUTOMATIC;
50 const TYPE_STANDARD = NewsletterEntity::TYPE_STANDARD;
51 const TYPE_WELCOME = NewsletterEntity::TYPE_WELCOME;
52 const TYPE_NOTIFICATION = NewsletterEntity::TYPE_NOTIFICATION;
53 const TYPE_NOTIFICATION_HISTORY = NewsletterEntity::TYPE_NOTIFICATION_HISTORY;
54 const TYPE_WC_TRANSACTIONAL_EMAIL = NewsletterEntity::TYPE_WC_TRANSACTIONAL_EMAIL;
55 // standard newsletters
56 const STATUS_DRAFT = NewsletterEntity::STATUS_DRAFT;
57 const STATUS_SCHEDULED = NewsletterEntity::STATUS_SCHEDULED;
58 const STATUS_SENDING = NewsletterEntity::STATUS_SENDING;
59 const STATUS_SENT = NewsletterEntity::STATUS_SENT;
60 // automatic newsletters status
61 const STATUS_ACTIVE = NewsletterEntity::STATUS_ACTIVE;
62
63 public function __construct() {
64 parent::__construct();
65 $this->addValidations('type', [
66 'required' => WPFunctions::get()->__('Please specify a type.', 'mailpoet'),
67 ]);
68 }
69
70 public function queue() {
71 return $this->hasOne(__NAMESPACE__ . '\SendingQueue', 'newsletter_id', 'id');
72 }
73
74 public function children() {
75 return $this->hasMany(
76 __NAMESPACE__ . '\Newsletter',
77 'parent_id',
78 'id'
79 );
80 }
81
82 public function parent() {
83 return $this->hasOne(
84 __NAMESPACE__ . '\Newsletter',
85 'id',
86 'parent_id'
87 );
88 }
89
90 public function segments() {
91 return $this->hasManyThrough(
92 __NAMESPACE__ . '\Segment',
93 __NAMESPACE__ . '\NewsletterSegment',
94 'newsletter_id',
95 'segment_id'
96 );
97 }
98
99 public function segmentRelations() {
100 return $this->hasMany(
101 __NAMESPACE__ . '\NewsletterSegment',
102 'newsletter_id',
103 'id'
104 );
105 }
106
107 public function options() {
108 return $this->hasManyThrough(
109 __NAMESPACE__ . '\NewsletterOptionField',
110 __NAMESPACE__ . '\NewsletterOption',
111 'newsletter_id',
112 'option_field_id'
113 )->select_expr(MP_NEWSLETTER_OPTION_TABLE . '.value');
114 }
115
116 public function save() {
117 if (is_string($this->deletedAt) && strlen(trim($this->deletedAt)) === 0) {
118 $this->set_expr('deleted_at', 'NULL');
119 }
120
121 if (isset($this->body) && ($this->body !== false)) {
122 $this->body = $this->getBodyString();
123 $this->set(
124 'body',
125 $this->body
126 );
127 }
128
129 $this->set('hash',
130 ($this->hash)
131 ? $this->hash
132 : Security::generateHash()
133 );
134 return parent::save();
135 }
136
137 public function trash() {
138 $this->save();
139 trigger_error('Calling Newsletter::trash() is deprecated and will be removed. Use \MailPoet\Newsletter\NewslettersRepository instead.', E_USER_DEPRECATED);
140 ContainerWrapper::getInstance()->get(NewslettersRepository::class)->bulkTrash([$this->id]);
141 return $this;
142 }
143
144 public function restore() {
145 $this->save();
146 trigger_error('Calling Newsletter::restore() is deprecated and will be removed. Use \MailPoet\Newsletter\NewslettersRepository instead.', E_USER_DEPRECATED);
147 ContainerWrapper::getInstance()->get(NewslettersRepository::class)->bulkRestore([$this->id]);
148 return $this;
149 }
150
151 public function delete() {
152 trigger_error('Calling Newsletter::delete() is deprecated and will be removed. Use \MailPoet\Newsletter\NewslettersRepository instead.', E_USER_DEPRECATED);
153 ContainerWrapper::getInstance()->get(NewslettersRepository::class)->bulkDelete([$this->id]);
154 return null;
155 }
156
157 public function setStatus($status = null) {
158 if ($status === self::STATUS_ACTIVE) {
159 if (!$this->body || empty(json_decode($this->getBodyString()))) {
160 $this->setError(
161 Helpers::replaceLinkTags(
162 __('This is an empty email without any content and it cannot be sent. Please update [link]the email[/link].'),
163 'admin.php?page=mailpoet-newsletter-editor&id=' . $this->id
164 )
165 );
166 return $this;
167 }
168 }
169 if (in_array($status, [
170 self::STATUS_DRAFT,
171 self::STATUS_SCHEDULED,
172 self::STATUS_SENDING,
173 self::STATUS_SENT,
174 self::STATUS_ACTIVE,
175 ])) {
176 $this->set('status', $status);
177 $this->save();
178 }
179
180 $typesWithActivation = [self::TYPE_NOTIFICATION, self::TYPE_WELCOME, self::TYPE_AUTOMATIC];
181
182 if (($status === self::STATUS_DRAFT) && in_array($this->type, $typesWithActivation)) {
183 ScheduledTask::pauseAllByNewsletter($this);
184 }
185 if (($status === self::STATUS_ACTIVE) && in_array($this->type, $typesWithActivation)) {
186 ScheduledTask::setScheduledAllByNewsletter($this);
187 }
188 return $this;
189 }
190
191 public function duplicate($data = []) {
192 $newsletterData = $this->asArray();
193
194 // remove id so that it creates a new record
195 unset($newsletterData['id']);
196
197 // merge data with newsletter data (allows override)
198 $data['unsubscribe_token'] = Security::generateUnsubscribeToken(self::class);
199 $data = array_merge($newsletterData, $data);
200
201 $duplicate = self::create();
202 $duplicate->hydrate($data);
203
204 // reset timestamps
205 $duplicate->set_expr('created_at', 'NOW()');
206 $duplicate->set_expr('updated_at', 'NOW()');
207 $duplicate->set_expr('deleted_at', 'NULL');
208
209 // reset status
210 $duplicate->set('status', self::STATUS_DRAFT);
211
212 // reset hash
213 $duplicate->set('hash', null);
214
215 // reset sent at date
216 $duplicate->set('sent_at', null);
217
218 $duplicate->save();
219
220 if ($duplicate->getErrors() === false) {
221 // create relationships between duplicate and segments
222 $segments = $this->segments()->findMany();
223
224 if (!empty($segments)) {
225 foreach ($segments as $segment) {
226 $relation = NewsletterSegment::create();
227 $relation->segmentId = $segment->id;
228 $relation->newsletterId = $duplicate->id;
229 $relation->save();
230 }
231 }
232
233 // duplicate options
234 $options = NewsletterOption::where('newsletter_id', $this->id)
235 ->findMany();
236
237 $ignoredOptionFieldIds = Helpers::flattenArray(
238 NewsletterOptionField::whereIn('name', ['isScheduled', 'scheduledAt'])
239 ->select('id')
240 ->findArray()
241 );
242
243 if (!empty($options)) {
244 foreach ($options as $option) {
245 if (in_array($option->optionFieldId, $ignoredOptionFieldIds)) {
246 continue;
247 }
248 $relation = NewsletterOption::create();
249 $relation->newsletterId = $duplicate->id;
250 $relation->optionFieldId = $option->optionFieldId;
251 $relation->value = $option->value;
252 $relation->save();
253 }
254 }
255 }
256
257 return $duplicate;
258 }
259
260 public function createNotificationHistory() {
261 $newsletterData = $this->asArray();
262
263 // remove id so that it creates a new record
264 unset($newsletterData['id']);
265
266 $data = array_merge(
267 $newsletterData,
268 [
269 'parent_id' => $this->id,
270 'type' => self::TYPE_NOTIFICATION_HISTORY,
271 'status' => self::STATUS_SENDING,
272 'unsubscribe_token' => Security::generateUnsubscribeToken(self::class),
273 ]
274 );
275
276 $notificationHistory = self::create();
277 $notificationHistory->hydrate($data);
278
279 // reset timestamps
280 $notificationHistory->set_expr('created_at', 'NOW()');
281 $notificationHistory->set_expr('updated_at', 'NOW()');
282 $notificationHistory->set_expr('deleted_at', 'NULL');
283
284 // reset hash
285 $notificationHistory->set('hash', null);
286
287 $notificationHistory->save();
288
289 if ($notificationHistory->getErrors() === false) {
290 // create relationships between notification history and segments
291 $segments = $this->segments()->findMany();
292
293 if (!empty($segments)) {
294 foreach ($segments as $segment) {
295 $relation = NewsletterSegment::create();
296 $relation->segmentId = $segment->id;
297 $relation->newsletterId = $notificationHistory->id;
298 $relation->save();
299 }
300 }
301 }
302
303 return $notificationHistory;
304 }
305
306 public function asArray() {
307 $model = parent::asArray();
308
309 if (isset($model['body'])) {
310 $model['body'] = json_decode($model['body'], true);
311 }
312 return $model;
313 }
314
315 public function withSegments($inclDeleted = false) {
316 $this->segments = $this->segments()->findArray();
317 if ($inclDeleted) {
318 $this->withDeletedSegments();
319 }
320 return $this;
321 }
322
323 public function withDeletedSegments() {
324 if (!empty($this->segments)) {
325 $segmentIds = array_column($this->segments, 'id');
326 $links = $this->segmentRelations()
327 ->whereNotIn('segment_id', $segmentIds)->findArray();
328 $deletedSegments = [];
329
330 foreach ($links as $link) {
331 $deletedSegments[] = [
332 'id' => $link['segment_id'],
333 'name' => WPFunctions::get()->__('Deleted list', 'mailpoet'),
334 ];
335 }
336 $this->segments = array_merge($this->segments, $deletedSegments);
337 }
338
339 return $this;
340 }
341
342 public function getQueue($columns = '*') {
343 return SendingTask::getByNewsletterId($this->id);
344 }
345
346 public function getBodyString(): string {
347 if (is_array($this->body)) {
348 return (string)json_encode($this->body);
349 }
350 if ($this->body === null) {
351 return '';
352 }
353 return $this->body;
354 }
355
356 public function withSendingQueue() {
357 $queue = $this->getQueue();
358 if ($queue === false) {
359 $this->queue = false;
360 } else {
361 $this->queue = $queue->asArray();
362 }
363 return $this;
364 }
365
366 public function withOptions() {
367 $options = $this->options()->findArray();
368 if (empty($options)) {
369 $this->options = [];
370 } else {
371 $this->options = array_column($options, 'value', 'name');
372 }
373 return $this;
374 }
375
376 public static function filterWithOptions($orm, $type) {
377 $orm = $orm->select(MP_NEWSLETTERS_TABLE . '.*');
378 $optionFields = NewsletterOptionField::findArray();
379 foreach ($optionFields as $optionField) {
380 if ($optionField['newsletter_type'] !== $type) {
381 continue;
382 }
383 $orm = $orm->select_expr(
384 'IFNULL(GROUP_CONCAT(CASE WHEN ' .
385 MP_NEWSLETTER_OPTION_FIELDS_TABLE . '.id=' . $optionField['id'] . ' THEN ' .
386 MP_NEWSLETTER_OPTION_TABLE . '.value END), NULL) as "' . $optionField['name'] . '"');
387 }
388 $orm = $orm
389 ->left_outer_join(
390 MP_NEWSLETTER_OPTION_TABLE,
391 [
392 MP_NEWSLETTERS_TABLE . '.id',
393 '=',
394 MP_NEWSLETTER_OPTION_TABLE . '.newsletter_id',
395 ]
396 )
397 ->left_outer_join(
398 MP_NEWSLETTER_OPTION_FIELDS_TABLE,
399 [
400 MP_NEWSLETTER_OPTION_FIELDS_TABLE . '.id',
401 '=',
402 MP_NEWSLETTER_OPTION_TABLE . '.option_field_id',
403 ]
404 )
405 ->group_by(MP_NEWSLETTERS_TABLE . '.id');
406 return $orm;
407 }
408
409 public static function filterStatus($orm, $status = false) {
410 if (in_array($status, [
411 self::STATUS_DRAFT,
412 self::STATUS_SCHEDULED,
413 self::STATUS_SENDING,
414 self::STATUS_SENT,
415 self::STATUS_ACTIVE,
416 ])) {
417 $orm->where('status', $status);
418 }
419 return $orm;
420 }
421
422 public static function filterType($orm, $type = false, $group = false) {
423 if (in_array($type, [
424 self::TYPE_STANDARD,
425 self::TYPE_WELCOME,
426 self::TYPE_AUTOMATIC,
427 self::TYPE_NOTIFICATION,
428 self::TYPE_NOTIFICATION_HISTORY,
429 ])) {
430 if ($type === self::TYPE_AUTOMATIC && $group) {
431 $orm = $orm->join(
432 NewsletterOptionField::$_table,
433 [
434 'option_fields.newsletter_type', '=', self::$_table . '.type',
435 ],
436 'option_fields'
437 )
438 ->join(
439 NewsletterOption::$_table,
440 [
441 'options.newsletter_id', '=', self::$_table . '.id',
442 ],
443 'options'
444 )
445 ->whereRaw('`options`.`option_field_id` = `option_fields`.`id`')
446 ->where('options.value', $group);
447 }
448 $orm = $orm->where(self::$_table . '.type', $type);
449 }
450 return $orm;
451 }
452
453 public static function createOrUpdate($data = []) {
454 $data['unsubscribe_token'] = Security::generateUnsubscribeToken(self::class);
455 return parent::_createOrUpdate($data, false, function($data) {
456 $settings = SettingsController::getInstance();
457 // set default sender based on settings
458 if (empty($data['sender'])) {
459 $sender = $settings->get('sender', []);
460 $data['sender_name'] = (
461 !empty($sender['name'])
462 ? $sender['name']
463 : ''
464 );
465 $data['sender_address'] = (
466 !empty($sender['address'])
467 ? $sender['address']
468 : ''
469 );
470 }
471
472 // set default reply_to based on settings
473 if (empty($data['reply_to'])) {
474 $replyTo = $settings->get('reply_to', []);
475 $data['reply_to_name'] = (
476 !empty($replyTo['name'])
477 ? $replyTo['name']
478 : ''
479 );
480 $data['reply_to_address'] = (
481 !empty($replyTo['address'])
482 ? $replyTo['address']
483 : ''
484 );
485 }
486
487 return $data;
488 });
489 }
490
491 public static function getWelcomeNotificationsForSegments($segments) {
492 return NewsletterOption::tableAlias('options')
493 ->select('options.newsletter_id')
494 ->select('options.value', 'segment_id')
495 ->join(
496 self::$_table,
497 'newsletters.id = options.newsletter_id',
498 'newsletters'
499 )
500 ->join(
501 MP_NEWSLETTER_OPTION_FIELDS_TABLE,
502 'option_fields.id = options.option_field_id',
503 'option_fields'
504 )
505 ->whereNull('newsletters.deleted_at')
506 ->where('newsletters.type', 'welcome')
507 ->where('option_fields.name', 'segment')
508 ->whereIn('options.value', $segments)
509 ->findMany();
510 }
511
512 public static function getArchives($segmentIds = []) {
513 $orm = self::tableAlias('newsletters')
514 ->distinct()->select('newsletters.*')
515 ->select('newsletter_rendered_subject')
516 ->select('task_id')
517 ->whereIn('newsletters.type', [
518 self::TYPE_STANDARD,
519 self::TYPE_NOTIFICATION_HISTORY,
520 ])
521 ->join(
522 MP_SENDING_QUEUES_TABLE,
523 'queues.newsletter_id = newsletters.id',
524 'queues'
525 )
526 ->join(
527 MP_SCHEDULED_TASKS_TABLE,
528 'queues.task_id = tasks.id',
529 'tasks'
530 )
531 ->where('tasks.status', SendingQueue::STATUS_COMPLETED)
532 ->whereNull('newsletters.deleted_at')
533 ->select('tasks.processed_at')
534 ->orderByDesc('tasks.processed_at')
535 ->orderByAsc('task_id');
536
537 if (!empty($segmentIds)) {
538 $orm->join(
539 MP_NEWSLETTER_SEGMENT_TABLE,
540 'newsletter_segments.newsletter_id = newsletters.id',
541 'newsletter_segments'
542 )
543 ->whereIn('newsletter_segments.segment_id', $segmentIds);
544 }
545 return $orm->findMany();
546 }
547
548 public static function getByHash($hash) {
549 return parent::where('hash', $hash)
550 ->findOne();
551 }
552
553 public function getMeta() {
554 if (!$this->meta) return;
555
556 return (Helpers::isJson($this->meta) && is_string($this->meta)) ? json_decode($this->meta, true) : $this->meta;
557 }
558
559 public static function findOneWithOptions($id) {
560 $newsletter = self::findOne($id);
561 if (!$newsletter instanceof self) {
562 return false;
563 }
564 return self::filter('filterWithOptions', $newsletter->type)->findOne($id);
565 }
566 }
567