PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
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 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Subscribers / BulkActionController.php
mailpoet / lib / Subscribers Last commit date
ConfirmationEmailTemplate 1 month ago ImportExport 2 weeks ago RestApi 3 days ago Statistics 1 month ago BulkActionController.php 3 days ago BulkActionException.php 1 month ago BulkConfirmationEmailResender.php 2 months ago ConfirmationEmailCustomizer.php 2 months ago ConfirmationEmailMailer.php 2 months ago ConfirmationEmailResolver.php 2 months ago EngagementDataBackfiller.php 2 months ago InactiveSubscribersController.php 4 days ago LinkTokens.php 2 months ago NewSubscriberNotificationMailer.php 2 months ago RequiredCustomFieldValidator.php 2 months ago SegmentsCountRecalculator.php 2 weeks ago Source.php 2 months ago SubscriberActions.php 2 months ago SubscriberCustomFieldRepository.php 3 years ago SubscriberIPsRepository.php 2 years ago SubscriberLimitNotificationEvaluator.php 2 months ago SubscriberLimitNotificationMailer.php 2 months ago SubscriberLimitNotificationScheduler.php 2 months ago SubscriberListingRepository.php 2 weeks ago SubscriberPersonalDataEraser.php 2 months ago SubscriberSaveController.php 3 days ago SubscriberSegmentRepository.php 2 weeks ago SubscriberSubscribeController.php 2 weeks ago SubscriberTagRepository.php 4 years ago SubscribersCountsController.php 2 weeks ago SubscribersEmailCountsController.php 2 weeks ago SubscribersRepository.php 3 days ago TrackingConsentController.php 4 days ago index.php 3 years ago
BulkActionController.php
267 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Subscribers;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\SegmentEntity;
9 use MailPoet\Entities\StatisticsUnsubscribeEntity;
10 use MailPoet\Entities\SubscriberEntity;
11 use MailPoet\Entities\TagEntity;
12 use MailPoet\Listing\ListingDefinition;
13 use MailPoet\Segments\SegmentsRepository;
14 use MailPoet\Statistics\Track\Unsubscribes;
15 use MailPoet\Tags\TagRepository;
16
17 /**
18 * Centralized bulk-action dispatcher used by the REST endpoint at
19 * `mailpoet/v1/subscribers/bulk-action`. Keeping the per-action branch +
20 * reference-data resolution in one place keeps the REST endpoint a thin
21 * HTTP adapter.
22 */
23 class BulkActionController {
24 public const ACTION_TRASH = 'trash';
25 public const ACTION_RESTORE = 'restore';
26 public const ACTION_DELETE = 'delete';
27 public const ACTION_UNSUBSCRIBE = 'unsubscribe';
28 public const ACTION_MOVE_TO_LIST = 'moveToList';
29 public const ACTION_ADD_TO_LIST = 'addToList';
30 public const ACTION_REMOVE_FROM_LIST = 'removeFromList';
31 public const ACTION_REMOVE_FROM_ALL_LISTS = 'removeFromAllLists';
32 public const ACTION_ADD_TAG = 'addTag';
33 public const ACTION_REMOVE_TAG = 'removeTag';
34
35 public const SUPPORTED_ACTIONS = [
36 self::ACTION_TRASH,
37 self::ACTION_RESTORE,
38 self::ACTION_DELETE,
39 self::ACTION_UNSUBSCRIBE,
40 self::ACTION_MOVE_TO_LIST,
41 self::ACTION_ADD_TO_LIST,
42 self::ACTION_REMOVE_FROM_LIST,
43 self::ACTION_REMOVE_FROM_ALL_LISTS,
44 self::ACTION_ADD_TAG,
45 self::ACTION_REMOVE_TAG,
46 ];
47
48 private const ACTIONS_REQUIRING_SEGMENT = [
49 self::ACTION_MOVE_TO_LIST,
50 self::ACTION_ADD_TO_LIST,
51 self::ACTION_REMOVE_FROM_LIST,
52 ];
53
54 private const ACTIONS_REQUIRING_TAG = [
55 self::ACTION_ADD_TAG,
56 self::ACTION_REMOVE_TAG,
57 ];
58
59 /** @var SubscriberListingRepository */
60 private $subscriberListingRepository;
61
62 /** @var SubscribersRepository */
63 private $subscribersRepository;
64
65 /** @var SegmentsRepository */
66 private $segmentsRepository;
67
68 /** @var TagRepository */
69 private $tagRepository;
70
71 /** @var Unsubscribes */
72 private $unsubscribesTracker;
73
74 public function __construct(
75 SubscriberListingRepository $subscriberListingRepository,
76 SubscribersRepository $subscribersRepository,
77 SegmentsRepository $segmentsRepository,
78 TagRepository $tagRepository,
79 Unsubscribes $unsubscribesTracker
80 ) {
81 $this->subscriberListingRepository = $subscriberListingRepository;
82 $this->subscribersRepository = $subscribersRepository;
83 $this->segmentsRepository = $segmentsRepository;
84 $this->tagRepository = $tagRepository;
85 $this->unsubscribesTracker = $unsubscribesTracker;
86 }
87
88 /**
89 * @param array{segment_id?: int|string, tag_id?: int|string, trigger_automations?: bool} $data
90 * @return array{count: int, kept?: int, segment?: array{id: int, name: string}, tag?: array{id: int, name: string}}
91 * @throws BulkActionException
92 */
93 public function execute(string $action, ListingDefinition $definition, array $data = []): array {
94 if (!in_array($action, self::SUPPORTED_ACTIONS, true)) {
95 throw new BulkActionException(
96 // translators: %s is the offending bulk-action name.
97 sprintf(__("Invalid bulk action '%s' provided.", 'mailpoet'), $action),
98 'mailpoet_subscribers_invalid_bulk_action',
99 400
100 );
101 }
102
103 $segment = in_array($action, self::ACTIONS_REQUIRING_SEGMENT, true) || isset($data['segment_id'])
104 ? $this->resolveSegment($data)
105 : null;
106 $tag = in_array($action, self::ACTIONS_REQUIRING_TAG, true) || isset($data['tag_id'])
107 ? $this->resolveTag($data)
108 : null;
109 $skipHooks = !($data['trigger_automations'] ?? false);
110
111 $ids = $this->subscriberListingRepository->getActionableIds($definition);
112 $count = $this->dispatch($action, $ids, $segment, $tag, $skipHooks);
113
114 $result = ['count' => $count];
115 if ($action === self::ACTION_DELETE) {
116 // bulkDelete keeps subscribers linked to a WordPress user or WooCommerce
117 // customer, so the remainder of the actionable set is what was kept.
118 $result['kept'] = max(0, count($ids) - $count);
119 }
120 if ($segment instanceof SegmentEntity) {
121 $result['segment'] = ['id' => (int)$segment->getId(), 'name' => (string)$segment->getName()];
122 }
123 if ($tag instanceof TagEntity) {
124 $result['tag'] = ['id' => (int)$tag->getId(), 'name' => (string)$tag->getName()];
125 }
126 return $result;
127 }
128
129 /**
130 * @param int[] $ids
131 */
132 private function dispatch(
133 string $action,
134 array $ids,
135 ?SegmentEntity $segment,
136 ?TagEntity $tag,
137 bool $skipHooks
138 ): int {
139 switch ($action) {
140 case self::ACTION_TRASH:
141 return $this->subscribersRepository->bulkTrash($ids);
142 case self::ACTION_RESTORE:
143 return $this->subscribersRepository->bulkRestore($ids);
144 case self::ACTION_DELETE:
145 return $this->subscribersRepository->bulkDelete($ids);
146 case self::ACTION_REMOVE_FROM_ALL_LISTS:
147 return $this->subscribersRepository->bulkRemoveFromAllSegments($ids);
148 case self::ACTION_UNSUBSCRIBE:
149 $this->trackBulkUnsubscribe($ids);
150 return $this->subscribersRepository->bulkUnsubscribe($ids);
151 case self::ACTION_MOVE_TO_LIST:
152 return $this->subscribersRepository->bulkMoveToSegment(
153 $this->requireSegment($segment, $action),
154 $ids,
155 $skipHooks
156 );
157 case self::ACTION_ADD_TO_LIST:
158 return $this->subscribersRepository->bulkAddToSegment(
159 $this->requireSegment($segment, $action),
160 $ids,
161 $skipHooks
162 );
163 case self::ACTION_REMOVE_FROM_LIST:
164 return $this->subscribersRepository->bulkRemoveFromSegment($this->requireSegment($segment, $action), $ids);
165 case self::ACTION_ADD_TAG:
166 return $this->subscribersRepository->bulkAddTag($this->requireTag($tag, $action), $ids, $skipHooks);
167 case self::ACTION_REMOVE_TAG:
168 return $this->subscribersRepository->bulkRemoveTag($this->requireTag($tag, $action), $ids, $skipHooks);
169 default:
170 // Reachable only if SUPPORTED_ACTIONS and this switch drift out of sync.
171 throw new BulkActionException(
172 // translators: %s is the offending bulk-action name.
173 sprintf(__("Invalid bulk action '%s' provided.", 'mailpoet'), $action),
174 'mailpoet_subscribers_invalid_bulk_action',
175 400
176 );
177 }
178 }
179
180 /**
181 * @param array{segment_id?: int|string} $data
182 */
183 private function resolveSegment(array $data): SegmentEntity {
184 if (!isset($data['segment_id'])) {
185 throw new BulkActionException(
186 __('A list is required for this bulk action.', 'mailpoet'),
187 'mailpoet_subscribers_missing_segment',
188 400
189 );
190 }
191 $segment = $this->segmentsRepository->findOneById((int)$data['segment_id']);
192 if (!$segment instanceof SegmentEntity) {
193 throw new BulkActionException(
194 __('This list does not exist.', 'mailpoet'),
195 'mailpoet_subscribers_segment_not_found',
196 404
197 );
198 }
199 return $segment;
200 }
201
202 /**
203 * @param array{tag_id?: int|string} $data
204 */
205 private function resolveTag(array $data): TagEntity {
206 if (!isset($data['tag_id'])) {
207 throw new BulkActionException(
208 __('A tag is required for this bulk action.', 'mailpoet'),
209 'mailpoet_subscribers_missing_tag',
210 400
211 );
212 }
213 $tag = $this->tagRepository->findOneById((int)$data['tag_id']);
214 if (!$tag instanceof TagEntity) {
215 throw new BulkActionException(
216 __('This tag does not exist.', 'mailpoet'),
217 'mailpoet_subscribers_tag_not_found',
218 404
219 );
220 }
221 return $tag;
222 }
223
224 private function requireSegment(?SegmentEntity $segment, string $action): SegmentEntity {
225 if (!$segment instanceof SegmentEntity) {
226 throw new BulkActionException(
227 // translators: %s is the action name.
228 sprintf(__("The '%s' bulk action requires a list.", 'mailpoet'), $action),
229 'mailpoet_subscribers_missing_segment',
230 400
231 );
232 }
233 return $segment;
234 }
235
236 private function requireTag(?TagEntity $tag, string $action): TagEntity {
237 if (!$tag instanceof TagEntity) {
238 throw new BulkActionException(
239 // translators: %s is the action name.
240 sprintf(__("The '%s' bulk action requires a tag.", 'mailpoet'), $action),
241 'mailpoet_subscribers_missing_tag',
242 400
243 );
244 }
245 return $tag;
246 }
247
248 /**
249 * @param int[] $ids
250 */
251 private function trackBulkUnsubscribe(array $ids): void {
252 if ($ids === []) return;
253 $subscribers = $this->subscribersRepository->findBy(['id' => $ids]);
254 foreach ($subscribers as $subscriber) {
255 if (
256 $subscriber instanceof SubscriberEntity
257 && $subscriber->getStatus() !== SubscriberEntity::STATUS_UNSUBSCRIBED
258 ) {
259 $this->unsubscribesTracker->track(
260 (int)$subscriber->getId(),
261 StatisticsUnsubscribeEntity::SOURCE_ADMINISTRATOR
262 );
263 }
264 }
265 }
266 }
267