PluginProbe
MailPoet – Newsletters, Email Marketing, and Automation / 3.89.4
MailPoet – Newsletters, Email Marketing, and Automation v3.89.4
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
537 lines 15.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 (
170 in_array($status, [
171 self::STATUS_DRAFT,
172 self::STATUS_SCHEDULED,
173 self::STATUS_SENDING,
174 self::STATUS_SENT,
175 self::STATUS_ACTIVE,
176 ])
177 ) {
178 $this->set('status', $status);
179 $this->save();
180 }
181
182 $typesWithActivation = [self::TYPE_NOTIFICATION, self::TYPE_WELCOME, self::TYPE_AUTOMATIC];
183
184 if (($status === self::STATUS_DRAFT) && in_array($this->type, $typesWithActivation)) {
185 ScheduledTask::pauseAllByNewsletter($this);
186 }
187 if (($status === self::STATUS_ACTIVE) && in_array($this->type, $typesWithActivation)) {
188 ScheduledTask::setScheduledAllByNewsletter($this);
189 }
190 return $this;
191 }
192
193 public function duplicate($data = []) {
194 $newsletterData = $this->asArray();
195
196 // remove id so that it creates a new record
197 unset($newsletterData['id']);
198
199 // merge data with newsletter data (allows override)
200 $data['unsubscribe_token'] = Security::generateUnsubscribeToken(self::class);
201 $data = array_merge($newsletterData, $data);
202
203 $duplicate = self::create();
204 $duplicate->hydrate($data);
205
206 // reset timestamps
207 $duplicate->set_expr('created_at', 'NOW()');
208 $duplicate->set_expr('updated_at', 'NOW()');
209 $duplicate->set_expr('deleted_at', 'NULL');
210
211 // reset status
212 $duplicate->set('status', self::STATUS_DRAFT);
213
214 // reset hash
215 $duplicate->set('hash', null);
216
217 // reset sent at date
218 $duplicate->set('sent_at', null);
219
220 $duplicate->save();
221
222 if ($duplicate->getErrors() === false) {
223 // create relationships between duplicate and segments
224 $segments = $this->segments()->findMany();
225
226 if (!empty($segments)) {
227 foreach ($segments as $segment) {
228 $relation = NewsletterSegment::create();
229 $relation->segmentId = $segment->id;
230 $relation->newsletterId = $duplicate->id;
231 $relation->save();
232 }
233 }
234
235 // duplicate options
236 $options = NewsletterOption::where('newsletter_id', $this->id)
237 ->findMany();
238
239 $ignoredOptionFieldIds = Helpers::flattenArray(
240 NewsletterOptionField::whereIn('name', ['isScheduled', 'scheduledAt'])
241 ->select('id')
242 ->findArray()
243 );
244
245 if (!empty($options)) {
246 foreach ($options as $option) {
247 if (in_array($option->optionFieldId, $ignoredOptionFieldIds)) {
248 continue;
249 }
250 $relation = NewsletterOption::create();
251 $relation->newsletterId = $duplicate->id;
252 $relation->optionFieldId = $option->optionFieldId;
253 $relation->value = $option->value;
254 $relation->save();
255 }
256 }
257 }
258
259 return $duplicate;
260 }
261
262 public function createNotificationHistory() {
263 $newsletterData = $this->asArray();
264
265 // remove id so that it creates a new record
266 unset($newsletterData['id']);
267
268 $data = array_merge(
269 $newsletterData,
270 [
271 'parent_id' => $this->id,
272 'type' => self::TYPE_NOTIFICATION_HISTORY,
273 'status' => self::STATUS_SENDING,
274 'unsubscribe_token' => Security::generateUnsubscribeToken(self::class),
275 ]
276 );
277
278 $notificationHistory = self::create();
279 $notificationHistory->hydrate($data);
280
281 // reset timestamps
282 $notificationHistory->set_expr('created_at', 'NOW()');
283 $notificationHistory->set_expr('updated_at', 'NOW()');
284 $notificationHistory->set_expr('deleted_at', 'NULL');
285
286 // reset hash
287 $notificationHistory->set('hash', null);
288
289 $notificationHistory->save();
290
291 if ($notificationHistory->getErrors() === false) {
292 // create relationships between notification history and segments
293 $segments = $this->segments()->findMany();
294
295 if (!empty($segments)) {
296 foreach ($segments as $segment) {
297 $relation = NewsletterSegment::create();
298 $relation->segmentId = $segment->id;
299 $relation->newsletterId = $notificationHistory->id;
300 $relation->save();
301 }
302 }
303 }
304
305 return $notificationHistory;
306 }
307
308 public function asArray() {
309 $model = parent::asArray();
310
311 if (isset($model['body'])) {
312 $model['body'] = json_decode($model['body'], true);
313 }
314 return $model;
315 }
316
317 public function withSegments($inclDeleted = false) {
318 $this->segments = $this->segments()->findArray();
319 if ($inclDeleted) {
320 $this->withDeletedSegments();
321 }
322 return $this;
323 }
324
325 public function withDeletedSegments() {
326 if (!empty($this->segments)) {
327 $segmentIds = array_column($this->segments, 'id');
328 $links = $this->segmentRelations()
329 ->whereNotIn('segment_id', $segmentIds)->findArray();
330 $deletedSegments = [];
331
332 foreach ($links as $link) {
333 $deletedSegments[] = [
334 'id' => $link['segment_id'],
335 'name' => WPFunctions::get()->__('Deleted list', 'mailpoet'),
336 ];
337 }
338 $this->segments = array_merge($this->segments, $deletedSegments);
339 }
340
341 return $this;
342 }
343
344 public function getQueue($columns = '*') {
345 return SendingTask::getByNewsletterId($this->id);
346 }
347
348 public function getBodyString(): string {
349 if (is_array($this->body)) {
350 return (string)json_encode($this->body);
351 }
352 if ($this->body === null) {
353 return '';
354 }
355 return $this->body;
356 }
357
358 public function withSendingQueue() {
359 $queue = $this->getQueue();
360 if ($queue === false) {
361 $this->queue = false;
362 } else {
363 $this->queue = $queue->asArray();
364 }
365 return $this;
366 }
367
368 public function withOptions() {
369 $options = $this->options()->findArray();
370 if (empty($options)) {
371 $this->options = [];
372 } else {
373 $this->options = array_column($options, 'value', 'name');
374 }
375 return $this;
376 }
377
378 public static function filterWithOptions($orm, $type) {
379 $orm = $orm->select(MP_NEWSLETTERS_TABLE . '.*');
380 $optionFields = NewsletterOptionField::findArray();
381 foreach ($optionFields as $optionField) {
382 if ($optionField['newsletter_type'] !== $type) {
383 continue;
384 }
385 $orm = $orm->select_expr(
386 'IFNULL(GROUP_CONCAT(CASE WHEN ' .
387 MP_NEWSLETTER_OPTION_FIELDS_TABLE . '.id=' . $optionField['id'] . ' THEN ' .
388 MP_NEWSLETTER_OPTION_TABLE . '.value END), NULL) as "' . $optionField['name'] . '"');
389 }
390 $orm = $orm
391 ->left_outer_join(
392 MP_NEWSLETTER_OPTION_TABLE,
393 [
394 MP_NEWSLETTERS_TABLE . '.id',
395 '=',
396 MP_NEWSLETTER_OPTION_TABLE . '.newsletter_id',
397 ]
398 )
399 ->left_outer_join(
400 MP_NEWSLETTER_OPTION_FIELDS_TABLE,
401 [
402 MP_NEWSLETTER_OPTION_FIELDS_TABLE . '.id',
403 '=',
404 MP_NEWSLETTER_OPTION_TABLE . '.option_field_id',
405 ]
406 )
407 ->group_by(MP_NEWSLETTERS_TABLE . '.id');
408 return $orm;
409 }
410
411 public static function filterStatus($orm, $status = false) {
412 if (
413 in_array($status, [
414 self::STATUS_DRAFT,
415 self::STATUS_SCHEDULED,
416 self::STATUS_SENDING,
417 self::STATUS_SENT,
418 self::STATUS_ACTIVE,
419 ])
420 ) {
421 $orm->where('status', $status);
422 }
423 return $orm;
424 }
425
426 public static function filterType($orm, $type = false, $group = false) {
427 if (
428 in_array($type, [
429 self::TYPE_STANDARD,
430 self::TYPE_WELCOME,
431 self::TYPE_AUTOMATIC,
432 self::TYPE_NOTIFICATION,
433 self::TYPE_NOTIFICATION_HISTORY,
434 ])
435 ) {
436 if ($type === self::TYPE_AUTOMATIC && $group) {
437 $orm = $orm->join(
438 NewsletterOptionField::$_table,
439 [
440 'option_fields.newsletter_type', '=', self::$_table . '.type',
441 ],
442 'option_fields'
443 )
444 ->join(
445 NewsletterOption::$_table,
446 [
447 'options.newsletter_id', '=', self::$_table . '.id',
448 ],
449 'options'
450 )
451 ->whereRaw('`options`.`option_field_id` = `option_fields`.`id`')
452 ->where('options.value', $group);
453 }
454 $orm = $orm->where(self::$_table . '.type', $type);
455 }
456 return $orm;
457 }
458
459 public static function createOrUpdate($data = []) {
460 $data['unsubscribe_token'] = Security::generateUnsubscribeToken(self::class);
461 return parent::_createOrUpdate($data, false, function($data) {
462 $settings = SettingsController::getInstance();
463 // set default sender based on settings
464 if (empty($data['sender'])) {
465 $sender = $settings->get('sender', []);
466 $data['sender_name'] = (
467 !empty($sender['name'])
468 ? $sender['name']
469 : ''
470 );
471 $data['sender_address'] = (
472 !empty($sender['address'])
473 ? $sender['address']
474 : ''
475 );
476 }
477
478 // set default reply_to based on settings
479 if (empty($data['reply_to'])) {
480 $replyTo = $settings->get('reply_to', []);
481 $data['reply_to_name'] = (
482 !empty($replyTo['name'])
483 ? $replyTo['name']
484 : ''
485 );
486 $data['reply_to_address'] = (
487 !empty($replyTo['address'])
488 ? $replyTo['address']
489 : ''
490 );
491 }
492
493 return $data;
494 });
495 }
496
497 public static function getWelcomeNotificationsForSegments($segments) {
498 return NewsletterOption::tableAlias('options')
499 ->select('options.newsletter_id')
500 ->select('options.value', 'segment_id')
501 ->join(
502 self::$_table,
503 'newsletters.id = options.newsletter_id',
504 'newsletters'
505 )
506 ->join(
507 MP_NEWSLETTER_OPTION_FIELDS_TABLE,
508 'option_fields.id = options.option_field_id',
509 'option_fields'
510 )
511 ->whereNull('newsletters.deleted_at')
512 ->where('newsletters.type', 'welcome')
513 ->where('option_fields.name', 'segment')
514 ->whereIn('options.value', $segments)
515 ->findMany();
516 }
517
518 public static function getByHash($hash) {
519 return parent::where('hash', $hash)
520 ->findOne();
521 }
522
523 public function getMeta() {
524 if (!$this->meta) return;
525
526 return (Helpers::isJson($this->meta) && is_string($this->meta)) ? json_decode($this->meta, true) : $this->meta;
527 }
528
529 public static function findOneWithOptions($id) {
530 $newsletter = self::findOne($id);
531 if (!$newsletter instanceof self) {
532 return false;
533 }
534 return self::filter('filterWithOptions', $newsletter->type)->findOne($id);
535 }
536 }
537