Nenhuma Descrição

NewsletterSaveController.php 14KB

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