Нет описания

Newsletter.php 15KB

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