PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
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 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 / Segments / RestApi / Endpoints / SegmentsBulkActionEndpoint.php
mailpoet / lib / Segments / RestApi / Endpoints Last commit date
AbstractSegmentsListingEndpoint.php 1 month ago DynamicSegmentsBulkActionEndpoint.php 3 months ago DynamicSegmentsListingEndpoint.php 1 month ago SegmentRequestValidationTrait.php 3 months ago SegmentsBulkActionEndpoint.php 3 months ago SegmentsEndpoint.php 3 months ago SegmentsListingEndpoint.php 1 month ago index.php 3 months ago
SegmentsBulkActionEndpoint.php
321 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Segments\RestApi\Endpoints;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\API\REST\ApiException;
9 use MailPoet\API\REST\Request;
10 use MailPoet\API\REST\Response;
11 use MailPoet\Entities\SegmentEntity;
12 use MailPoet\Form\FormsRepository;
13 use MailPoet\Listing\Handler as ListingHandler;
14 use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
15 use MailPoet\Segments\SegmentListingRepository;
16 use MailPoet\Segments\SegmentsRepository;
17 use MailPoet\Subscribers\SubscribersRepository;
18 use MailPoet\Validator\Builder;
19
20 class SegmentsBulkActionEndpoint extends SegmentsEndpoint {
21 private const ACTION_TRASH = 'trash';
22 private const ACTION_RESTORE = 'restore';
23 private const ACTION_DELETE = 'delete';
24 private const ACTION_EMPTY_TRASH = 'empty_trash';
25
26 private const SUPPORTED_ACTIONS = [
27 self::ACTION_TRASH,
28 self::ACTION_RESTORE,
29 self::ACTION_DELETE,
30 self::ACTION_EMPTY_TRASH,
31 ];
32
33 /** @var ListingHandler */
34 private $listingHandler;
35
36 /** @var SegmentListingRepository */
37 private $segmentListingRepository;
38
39 /** @var SegmentsRepository */
40 private $segmentsRepository;
41
42 /** @var NewsletterSegmentRepository */
43 private $newsletterSegmentRepository;
44
45 /** @var FormsRepository */
46 private $formsRepository;
47
48 /** @var SubscribersRepository */
49 private $subscribersRepository;
50
51 public function __construct(
52 ListingHandler $listingHandler,
53 SegmentListingRepository $segmentListingRepository,
54 SegmentsRepository $segmentsRepository,
55 NewsletterSegmentRepository $newsletterSegmentRepository,
56 FormsRepository $formsRepository,
57 SubscribersRepository $subscribersRepository
58 ) {
59 $this->listingHandler = $listingHandler;
60 $this->segmentListingRepository = $segmentListingRepository;
61 $this->segmentsRepository = $segmentsRepository;
62 $this->newsletterSegmentRepository = $newsletterSegmentRepository;
63 $this->formsRepository = $formsRepository;
64 $this->subscribersRepository = $subscribersRepository;
65 }
66
67 public function handle(Request $request): Response {
68 $action = $this->validateAction($request);
69 $this->validateListingParams($request);
70 $ids = $this->getSelectedIds($request, $action);
71 $this->validateTypeBoundaries($ids);
72 if ($action === self::ACTION_DELETE) {
73 $this->validateDeletionSelection($ids);
74 }
75
76 $result = [
77 'updated' => 0,
78 'deleted' => 0,
79 'skipped' => 0,
80 'errors' => [],
81 ];
82
83 if ($action === self::ACTION_TRASH) {
84 $this->trashSegments($ids, $result);
85 } elseif ($action === self::ACTION_RESTORE) {
86 $this->restoreSegments($ids, $result);
87 } else {
88 $this->deleteSegments($ids, $result);
89 }
90
91 return new Response($result);
92 }
93
94 public static function getRequestSchema(): array {
95 return [
96 'action' => Builder::string()->required(),
97 'ids' => Builder::array(Builder::integer()),
98 'select_all' => Builder::boolean(),
99 'group' => Builder::string(),
100 'page' => Builder::integer(),
101 'per_page' => Builder::integer(),
102 'orderby' => Builder::string(),
103 'order' => Builder::string(),
104 'sort_by' => Builder::string(),
105 'sort_order' => Builder::string(),
106 ];
107 }
108
109 private function validateAction(Request $request): string {
110 $action = is_string($request->getParam('action')) ? (string)$request->getParam('action') : '';
111 if (!in_array($action, self::SUPPORTED_ACTIONS, true)) {
112 throw new ApiException(
113 sprintf(
114 // translators: %s is the list of supported bulk actions.
115 __('Unsupported bulk action. Allowed values are: %s.', 'mailpoet'),
116 implode(', ', self::SUPPORTED_ACTIONS)
117 ),
118 400,
119 'mailpoet_segments_invalid_bulk_action'
120 );
121 }
122 return $action;
123 }
124
125 private function validateListingParams(Request $request): void {
126 $this->validateGroup(is_string($request->getParam('group')) ? (string)$request->getParam('group') : null);
127 $orderParam = $request->getParam('order') ?? $request->getParam('sort_order');
128 $this->validateOrder(is_string($orderParam) ? (string)$orderParam : null, 'asc');
129 $this->validatePage($request->getParam('page'));
130 $this->validatePerPage($request->getParam('per_page'), 20);
131
132 $orderbyParam = $request->getParam('orderby') ?? $request->getParam('sort_by');
133 $orderby = is_string($orderbyParam) && $orderbyParam !== ''
134 ? (string)$orderbyParam
135 : 'name';
136 $allowedSortFields = ['name', 'created_at', 'updated_at', 'average_engagement_score'];
137 if (!in_array($orderby, $allowedSortFields, true)) {
138 throw new ApiException(
139 sprintf(
140 // translators: %s is the list of supported sort fields.
141 __('Unsupported sort field. Allowed values are: %s.', 'mailpoet'),
142 implode(', ', $allowedSortFields)
143 ),
144 400,
145 'mailpoet_segments_invalid_orderby'
146 );
147 }
148 }
149
150 /**
151 * @return int[]
152 */
153 private function getSelectedIds(Request $request, string $action): array {
154 if ($action === self::ACTION_EMPTY_TRASH) {
155 return $this->getAllMatchingIds('trash');
156 }
157
158 if ($request->getParam('select_all') === true) {
159 $group = $this->validateGroup(is_string($request->getParam('group')) ? (string)$request->getParam('group') : null);
160 return $this->getAllMatchingIds($group);
161 }
162
163 return $this->validateIds($request->getParam('ids'));
164 }
165
166 /**
167 * @return int[]
168 */
169 private function getAllMatchingIds(string $group): array {
170 $definition = $this->listingHandler->getListingDefinition([
171 'group' => $group,
172 'sort_by' => 'name',
173 'sort_order' => 'asc',
174 'params' => ['lists'],
175 ]);
176 $ids = $this->segmentListingRepository->getActionableIds($definition);
177 return array_map('intval', $ids);
178 }
179
180 /**
181 * @param int[] $ids
182 * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
183 */
184 private function trashSegments(array $ids, array &$result): void {
185 $newsletterBlockers = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
186 $formBlockers = $this->formsRepository->getNamesOfFormsForSegments();
187
188 foreach ($ids as $id) {
189 $segment = $this->segmentsRepository->findOneById($id);
190 if (!$segment instanceof SegmentEntity) {
191 $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
192 continue;
193 }
194 if (!in_array($segment->getType(), [SegmentEntity::TYPE_DEFAULT, SegmentEntity::TYPE_WP_USERS], true)) {
195 $this->skip($result, $id, __('This list cannot be moved to trash.', 'mailpoet'));
196 continue;
197 }
198 if (isset($newsletterBlockers[$id])) {
199 $this->skip($result, $id, str_replace(
200 '%1$s',
201 "'" . join("', '", $newsletterBlockers[$id]) . "'",
202 // translators: %1$s is a comma-separated list of emails for which the segment is used.
203 _x('List cannot be deleted because it’s used for %1$s email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet')
204 ));
205 continue;
206 }
207 if (isset($formBlockers[$id])) {
208 $this->skip($result, $id, str_replace(
209 '%1$s',
210 "'" . join("', '", $formBlockers[$id]) . "'",
211 // translators: %1$s is a comma-separated list of forms for which the segment is used.
212 _nx(
213 'List cannot be deleted because it’s used for %1$s form',
214 'List cannot be deleted because it’s used for %1$s forms',
215 count($formBlockers[$id]),
216 'Alert shown when trying to delete segment, when it is assigned to a form.',
217 'mailpoet'
218 )
219 ));
220 continue;
221 }
222 if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
223 $subscribers = $this->subscribersRepository->findExclusiveSubscribersBySegment((int)$segment->getId());
224 $subscriberIds = array_map(static function ($subscriber): int {
225 return (int)$subscriber->getId();
226 }, $subscribers);
227 $this->subscribersRepository->bulkTrash($subscriberIds);
228 }
229 $result['updated'] += $this->segmentsRepository->doTrash([$id], $segment->getType());
230 }
231 }
232
233 /**
234 * @param int[] $ids
235 * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
236 */
237 private function restoreSegments(array $ids, array &$result): void {
238 foreach ($ids as $id) {
239 $segment = $this->segmentsRepository->findOneById($id);
240 if (!$segment instanceof SegmentEntity) {
241 $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
242 continue;
243 }
244 if (!in_array($segment->getType(), [SegmentEntity::TYPE_DEFAULT, SegmentEntity::TYPE_WP_USERS], true)) {
245 $this->skip($result, $id, __('This list cannot be restored.', 'mailpoet'));
246 continue;
247 }
248 if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
249 $subscribers = $this->subscribersRepository->findBySegment((int)$segment->getId());
250 $subscriberIds = array_map(static function ($subscriber): int {
251 return (int)$subscriber->getId();
252 }, $subscribers);
253 $this->subscribersRepository->bulkRestore($subscriberIds);
254 }
255 $result['updated'] += $this->segmentsRepository->bulkRestore([$id], $segment->getType());
256 }
257 }
258
259 /**
260 * @param int[] $ids
261 * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
262 */
263 private function deleteSegments(array $ids, array &$result): void {
264 foreach ($ids as $id) {
265 $segment = $this->segmentsRepository->findOneById($id);
266 if (!$segment instanceof SegmentEntity) {
267 $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
268 continue;
269 }
270 if ($segment->getType() !== SegmentEntity::TYPE_DEFAULT) {
271 $this->skip($result, $id, __('This list cannot be deleted.', 'mailpoet'));
272 continue;
273 }
274 $result['deleted'] += $this->segmentsRepository->bulkDelete([$id]);
275 }
276 }
277
278 /**
279 * @param int[] $ids
280 */
281 private function validateTypeBoundaries(array $ids): void {
282 foreach ($ids as $id) {
283 $segment = $this->segmentsRepository->findOneById($id);
284 if ($segment instanceof SegmentEntity && $segment->getType() === SegmentEntity::TYPE_DYNAMIC) {
285 throw new ApiException(
286 __('This endpoint only supports lists.', 'mailpoet'),
287 400,
288 'mailpoet_segments_invalid_type'
289 );
290 }
291 }
292 }
293
294 /**
295 * @param int[] $ids
296 */
297 private function validateDeletionSelection(array $ids): void {
298 foreach ($ids as $id) {
299 $segment = $this->segmentsRepository->findOneById($id);
300 if ($segment instanceof SegmentEntity && $segment->getDeletedAt() === null) {
301 throw new ApiException(
302 __('Only lists in the trash can be permanently deleted.', 'mailpoet'),
303 400,
304 'mailpoet_segments_delete_requires_trash'
305 );
306 }
307 }
308 }
309
310 /**
311 * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
312 */
313 private function skip(array &$result, ?int $id, string $message): void {
314 $result['skipped']++;
315 $result['errors'][] = [
316 'id' => $id,
317 'message' => $message,
318 ];
319 }
320 }
321