Нет описания

Newsletters.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. namespace MailPoet\API\JSON\v1;
  3. if (!defined('ABSPATH')) exit;
  4. use MailPoet\API\JSON\Endpoint as APIEndpoint;
  5. use MailPoet\API\JSON\Error as APIError;
  6. use MailPoet\API\JSON\Response;
  7. use MailPoet\API\JSON\ResponseBuilders\NewslettersResponseBuilder;
  8. use MailPoet\Config\AccessControl;
  9. use MailPoet\Cron\CronHelper;
  10. use MailPoet\Doctrine\Validator\ValidationException;
  11. use MailPoet\Entities\NewsletterEntity;
  12. use MailPoet\Entities\NewsletterOptionFieldEntity;
  13. use MailPoet\Entities\SendingQueueEntity;
  14. use MailPoet\InvalidStateException;
  15. use MailPoet\Listing;
  16. use MailPoet\Newsletter\Listing\NewsletterListingRepository;
  17. use MailPoet\Newsletter\NewsletterSaveController;
  18. use MailPoet\Newsletter\NewslettersRepository;
  19. use MailPoet\Newsletter\Preview\SendPreviewController;
  20. use MailPoet\Newsletter\Preview\SendPreviewException;
  21. use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
  22. use MailPoet\Newsletter\Scheduler\Scheduler;
  23. use MailPoet\Newsletter\Url as NewsletterUrl;
  24. use MailPoet\Settings\SettingsController;
  25. use MailPoet\UnexpectedValueException;
  26. use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
  27. use MailPoet\Util\Security;
  28. use MailPoet\WP\Emoji;
  29. use MailPoet\WP\Functions as WPFunctions;
  30. use MailPoetVendor\Carbon\Carbon;
  31. class Newsletters extends APIEndpoint {
  32. /** @var Listing\Handler */
  33. private $listingHandler;
  34. /** @var WPFunctions */
  35. private $wp;
  36. /** @var SettingsController */
  37. private $settings;
  38. /** @var CronHelper */
  39. private $cronHelper;
  40. public $permissions = [
  41. 'global' => AccessControl::PERMISSION_MANAGE_EMAILS,
  42. ];
  43. /** @var NewslettersRepository */
  44. private $newslettersRepository;
  45. /** @var NewsletterListingRepository */
  46. private $newsletterListingRepository;
  47. /** @var NewslettersResponseBuilder */
  48. private $newslettersResponseBuilder;
  49. /** @var PostNotificationScheduler */
  50. private $postNotificationScheduler;
  51. /** @var Emoji */
  52. private $emoji;
  53. /** @var SubscribersFeature */
  54. private $subscribersFeature;
  55. /** @var SendPreviewController */
  56. private $sendPreviewController;
  57. /** @var NewsletterSaveController */
  58. private $newsletterSaveController;
  59. /** @var NewsletterUrl */
  60. private $newsletterUrl;
  61. public function __construct(
  62. Listing\Handler $listingHandler,
  63. WPFunctions $wp,
  64. SettingsController $settings,
  65. CronHelper $cronHelper,
  66. NewslettersRepository $newslettersRepository,
  67. NewsletterListingRepository $newsletterListingRepository,
  68. NewslettersResponseBuilder $newslettersResponseBuilder,
  69. PostNotificationScheduler $postNotificationScheduler,
  70. Emoji $emoji,
  71. SubscribersFeature $subscribersFeature,
  72. SendPreviewController $sendPreviewController,
  73. NewsletterSaveController $newsletterSaveController,
  74. NewsletterUrl $newsletterUrl
  75. ) {
  76. $this->listingHandler = $listingHandler;
  77. $this->wp = $wp;
  78. $this->settings = $settings;
  79. $this->cronHelper = $cronHelper;
  80. $this->newslettersRepository = $newslettersRepository;
  81. $this->newsletterListingRepository = $newsletterListingRepository;
  82. $this->newslettersResponseBuilder = $newslettersResponseBuilder;
  83. $this->postNotificationScheduler = $postNotificationScheduler;
  84. $this->emoji = $emoji;
  85. $this->subscribersFeature = $subscribersFeature;
  86. $this->sendPreviewController = $sendPreviewController;
  87. $this->newsletterSaveController = $newsletterSaveController;
  88. $this->newsletterUrl = $newsletterUrl;
  89. }
  90. public function get($data = []) {
  91. $newsletter = $this->getNewsletter($data);
  92. if (!$newsletter) {
  93. return $this->errorResponse([
  94. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  95. ]);
  96. }
  97. $response = $this->newslettersResponseBuilder->build($newsletter, [
  98. NewslettersResponseBuilder::RELATION_SEGMENTS,
  99. NewslettersResponseBuilder::RELATION_OPTIONS,
  100. NewslettersResponseBuilder::RELATION_QUEUE,
  101. ]);
  102. $response = $this->wp->applyFilters('mailpoet_api_newsletters_get_after', $response);
  103. return $this->successResponse($response, ['preview_url' => $this->getViewInBrowserUrl($newsletter)]);
  104. }
  105. public function getWithStats($data = []) {
  106. $newsletter = $this->getNewsletter($data);
  107. if (!$newsletter) {
  108. return $this->errorResponse([
  109. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  110. ]);
  111. }
  112. $response = $this->newslettersResponseBuilder->build($newsletter, [
  113. NewslettersResponseBuilder::RELATION_SEGMENTS,
  114. NewslettersResponseBuilder::RELATION_OPTIONS,
  115. NewslettersResponseBuilder::RELATION_QUEUE,
  116. NewslettersResponseBuilder::RELATION_TOTAL_SENT,
  117. NewslettersResponseBuilder::RELATION_STATISTICS,
  118. ]);
  119. $response = $this->wp->applyFilters('mailpoet_api_newsletters_get_after', $response);
  120. $response['preview_url'] = $this->getViewInBrowserUrl($newsletter);
  121. return $this->successResponse($response);
  122. }
  123. public function save($data = []) {
  124. $data = $this->wp->applyFilters('mailpoet_api_newsletters_save_before', $data);
  125. $newsletter = $this->newsletterSaveController->save($data);
  126. $response = $this->newslettersResponseBuilder->build($newsletter);
  127. $previewUrl = $this->getViewInBrowserUrl($newsletter);
  128. $response = $this->wp->applyFilters('mailpoet_api_newsletters_save_after', $response);
  129. return $this->successResponse($response, ['preview_url' => $previewUrl]);
  130. }
  131. public function setStatus($data = []) {
  132. $status = (isset($data['status']) ? $data['status'] : null);
  133. if (!$status) {
  134. return $this->badRequest([
  135. APIError::BAD_REQUEST => __('You need to specify a status.', 'mailpoet'),
  136. ]);
  137. }
  138. if ($status === NewsletterEntity::STATUS_ACTIVE && $this->subscribersFeature->check()) {
  139. return $this->errorResponse([
  140. APIError::FORBIDDEN => __('Subscribers limit reached.', 'mailpoet'),
  141. ], [], Response::STATUS_FORBIDDEN);
  142. }
  143. $newsletter = $this->getNewsletter($data);
  144. if ($newsletter === null) {
  145. return $this->errorResponse([
  146. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  147. ]);
  148. }
  149. $this->newslettersRepository->prefetchOptions([$newsletter]);
  150. $newsletter->setStatus($status);
  151. // if there are past due notifications, reschedule them for the next send date
  152. if ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION && $status === NewsletterEntity::STATUS_ACTIVE) {
  153. $scheduleOption = $newsletter->getOption(NewsletterOptionFieldEntity::NAME_SCHEDULE);
  154. if ($scheduleOption === null) {
  155. return $this->errorResponse([
  156. APIError::BAD_REQUEST => __('This email has incorrect state.', 'mailpoet'),
  157. ]);
  158. }
  159. $nextRunDate = Scheduler::getNextRunDate($scheduleOption->getValue());
  160. $queues = $newsletter->getQueues();
  161. foreach ($queues as $queue) {
  162. $task = $queue->getTask();
  163. if (
  164. $task &&
  165. $task->getScheduledAt() <= Carbon::createFromTimestamp($this->wp->currentTime('timestamp')) &&
  166. $task->getStatus() === SendingQueueEntity::STATUS_SCHEDULED
  167. ) {
  168. $nextRunDate = $nextRunDate ? Carbon::createFromFormat('Y-m-d H:i:s', $nextRunDate) : null;
  169. if ($nextRunDate === false) {
  170. throw InvalidStateException::create()->withMessage('Invalid next run date generated');
  171. }
  172. $task->setScheduledAt($nextRunDate);
  173. }
  174. }
  175. $this->postNotificationScheduler->createPostNotificationSendingTask($newsletter);
  176. }
  177. $this->newslettersRepository->flush();
  178. return $this->successResponse(
  179. $this->newslettersResponseBuilder->build($newsletter)
  180. );
  181. }
  182. public function restore($data = []) {
  183. $newsletter = $this->getNewsletter($data);
  184. if ($newsletter instanceof NewsletterEntity) {
  185. $this->newslettersRepository->bulkRestore([$newsletter->getId()]);
  186. $this->newslettersRepository->refresh($newsletter);
  187. return $this->successResponse(
  188. $this->newslettersResponseBuilder->build($newsletter),
  189. ['count' => 1]
  190. );
  191. } else {
  192. return $this->errorResponse([
  193. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  194. ]);
  195. }
  196. }
  197. public function trash($data = []) {
  198. $newsletter = $this->getNewsletter($data);
  199. if ($newsletter instanceof NewsletterEntity) {
  200. $this->newslettersRepository->bulkTrash([$newsletter->getId()]);
  201. $this->newslettersRepository->refresh($newsletter);
  202. return $this->successResponse(
  203. $this->newslettersResponseBuilder->build($newsletter),
  204. ['count' => 1]
  205. );
  206. } else {
  207. return $this->errorResponse([
  208. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  209. ]);
  210. }
  211. }
  212. public function delete($data = []) {
  213. $newsletter = $this->getNewsletter($data);
  214. if ($newsletter instanceof NewsletterEntity) {
  215. $this->wp->doAction('mailpoet_api_newsletters_delete_before', [$newsletter->getId()]);
  216. $this->newslettersRepository->bulkDelete([$newsletter->getId()]);
  217. $this->wp->doAction('mailpoet_api_newsletters_delete_after', [$newsletter->getId()]);
  218. return $this->successResponse(null, ['count' => 1]);
  219. } else {
  220. return $this->errorResponse([
  221. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  222. ]);
  223. }
  224. }
  225. public function duplicate($data = []) {
  226. $newsletter = $this->getNewsletter($data);
  227. if ($newsletter instanceof NewsletterEntity) {
  228. $duplicate = $this->newsletterSaveController->duplicate($newsletter);
  229. $this->wp->doAction('mailpoet_api_newsletters_duplicate_after', $newsletter, $duplicate);
  230. return $this->successResponse(
  231. $this->newslettersResponseBuilder->build($duplicate),
  232. ['count' => 1]
  233. );
  234. } else {
  235. return $this->errorResponse([
  236. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  237. ]);
  238. }
  239. }
  240. public function showPreview($data = []) {
  241. if (empty($data['body'])) {
  242. return $this->badRequest([
  243. APIError::BAD_REQUEST => __('Newsletter data is missing.', 'mailpoet'),
  244. ]);
  245. }
  246. $newsletter = $this->getNewsletter($data);
  247. if (!$newsletter) {
  248. return $this->errorResponse([
  249. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  250. ]);
  251. }
  252. $newsletter->setBody(
  253. json_decode($this->emoji->encodeForUTF8Column(MP_NEWSLETTERS_TABLE, 'body', $data['body']), true)
  254. );
  255. $this->newslettersRepository->flush();
  256. $response = $this->newslettersResponseBuilder->build($newsletter);
  257. return $this->successResponse($response, ['preview_url' => $this->getViewInBrowserUrl($newsletter)]);
  258. }
  259. public function sendPreview($data = []) {
  260. if (empty($data['subscriber'])) {
  261. return $this->badRequest([
  262. APIError::BAD_REQUEST => __('Please specify receiver information.', 'mailpoet'),
  263. ]);
  264. }
  265. $newsletter = $this->getNewsletter($data);
  266. if (!$newsletter) {
  267. return $this->errorResponse([
  268. APIError::NOT_FOUND => __('This email does not exist.', 'mailpoet'),
  269. ]);
  270. }
  271. try {
  272. $this->sendPreviewController->sendPreview($newsletter, $data['subscriber']);
  273. } catch (SendPreviewException $e) {
  274. return $this->errorResponse([APIError::BAD_REQUEST => $e->getMessage()]);
  275. } catch (\Throwable $e) {
  276. return $this->errorResponse([$e->getCode() => $e->getMessage()]);
  277. }
  278. return $this->successResponse($this->newslettersResponseBuilder->build($newsletter));
  279. }
  280. public function listing($data = []) {
  281. $definition = $this->listingHandler->getListingDefinition($data);
  282. $items = $this->newsletterListingRepository->getData($definition);
  283. $count = $this->newsletterListingRepository->getCount($definition);
  284. $filters = $this->newsletterListingRepository->getFilters($definition);
  285. $groups = $this->newsletterListingRepository->getGroups($definition);
  286. $this->fixMissingHash($items); // Fix for MAILPOET-3275. Remove after May 2021
  287. $data = [];
  288. foreach ($this->newslettersResponseBuilder->buildForListing($items) as $newsletterData) {
  289. $data[] = $this->wp->applyFilters('mailpoet_api_newsletters_listing_item', $newsletterData);
  290. }
  291. return $this->successResponse($data, [
  292. 'count' => $count,
  293. 'filters' => $filters,
  294. 'groups' => $groups,
  295. 'mta_log' => $this->settings->get('mta_log'),
  296. 'mta_method' => $this->settings->get('mta.method'),
  297. 'cron_accessible' => $this->cronHelper->isDaemonAccessible(),
  298. 'current_time' => $this->wp->currentTime('mysql'),
  299. ]);
  300. }
  301. public function bulkAction($data = []) {
  302. $definition = $this->listingHandler->getListingDefinition($data['listing']);
  303. $ids = $this->newsletterListingRepository->getActionableIds($definition);
  304. if ($data['action'] === 'trash') {
  305. $this->newslettersRepository->bulkTrash($ids);
  306. } elseif ($data['action'] === 'restore') {
  307. $this->newslettersRepository->bulkRestore($ids);
  308. } elseif ($data['action'] === 'delete') {
  309. $this->wp->doAction('mailpoet_api_newsletters_delete_before', $ids);
  310. $this->newslettersRepository->bulkDelete($ids);
  311. $this->wp->doAction('mailpoet_api_newsletters_delete_after', $ids);
  312. } else {
  313. throw UnexpectedValueException::create()
  314. ->withErrors([APIError::BAD_REQUEST => "Invalid bulk action '{$data['action']}' provided."]);
  315. }
  316. return $this->successResponse(null, ['count' => count($ids)]);
  317. }
  318. public function create($data = []) {
  319. try {
  320. $newsletter = $this->newsletterSaveController->save($data);
  321. } catch (ValidationException $exception) {
  322. return $this->badRequest(['Please specify a type.']);
  323. }
  324. $response = $this->newslettersResponseBuilder->build($newsletter);
  325. return $this->successResponse($response);
  326. }
  327. /** @return NewsletterEntity|null */
  328. private function getNewsletter(array $data) {
  329. return isset($data['id'])
  330. ? $this->newslettersRepository->findOneById((int)$data['id'])
  331. : null;
  332. }
  333. private function getViewInBrowserUrl(NewsletterEntity $newsletter): string {
  334. $this->fixMissingHash([$newsletter]); // Fix for MAILPOET-3275. Remove after May 2021
  335. $url = $this->newsletterUrl->getViewInBrowserUrl(
  336. (object)[
  337. 'id' => $newsletter->getId(),
  338. 'hash' => $newsletter->getHash(),
  339. ]
  340. );
  341. // strip protocol to avoid mix content error
  342. return preg_replace('/^https?:/i', '', $url);
  343. }
  344. /**
  345. * Some Newsletters were created without a hash due to a bug MAILPOET-3275
  346. * We can remove this fix after May 2021 since by then most users should have their data fixed
  347. * @param NewsletterEntity[] $newsletters
  348. */
  349. private function fixMissingHash(array $newsletters) {
  350. foreach ($newsletters as $newsletter) {
  351. if (!$newsletter instanceof NewsletterEntity || $newsletter->getHash() !== null) {
  352. continue;
  353. }
  354. $newsletter->setHash(Security::generateHash());
  355. $this->newslettersRepository->flush();
  356. }
  357. }
  358. }