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 / RestApi / Endpoints / SubscribersBulkActionEndpoint.php
mailpoet / lib / Subscribers / RestApi / Endpoints Last commit date
SubscriberConfirmationEmailEndpoint.php 2 months ago SubscribersBulkActionEndpoint.php 4 days ago SubscribersListingEndpoint.php 2 weeks ago index.php 2 months ago
SubscribersBulkActionEndpoint.php
266 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Subscribers\RestApi\Endpoints;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\API\REST\ApiException;
9 use MailPoet\API\REST\Endpoint;
10 use MailPoet\API\REST\Request;
11 use MailPoet\API\REST\Response;
12 use MailPoet\Config\AccessControl;
13 use MailPoet\Entities\SubscriberEntity;
14 use MailPoet\Listing\Handler as ListingHandler;
15 use MailPoet\Listing\ListingDefinition;
16 use MailPoet\Subscribers\BulkActionController;
17 use MailPoet\Subscribers\BulkActionException;
18 use MailPoet\Subscribers\BulkConfirmationEmailResender;
19 use MailPoet\Validator\Builder;
20 use MailPoet\WP\Functions as WPFunctions;
21
22 class SubscribersBulkActionEndpoint extends Endpoint {
23 public const ACTION_RESEND_CONFIRMATION_EMAILS = 'resendConfirmationEmails';
24
25 private const VALID_SELECT_ALL_GROUPS = [
26 'all',
27 SubscriberEntity::STATUS_SUBSCRIBED,
28 SubscriberEntity::STATUS_UNCONFIRMED,
29 SubscriberEntity::STATUS_UNSUBSCRIBED,
30 SubscriberEntity::STATUS_INACTIVE,
31 SubscriberEntity::STATUS_BOUNCED,
32 'trash',
33 ];
34
35 private const TRASH_ONLY_ACTIONS = [
36 BulkActionController::ACTION_DELETE,
37 BulkActionController::ACTION_RESTORE,
38 ];
39
40 private const NON_TRASH_ACTIONS = [
41 BulkActionController::ACTION_TRASH,
42 BulkActionController::ACTION_UNSUBSCRIBE,
43 BulkActionController::ACTION_MOVE_TO_LIST,
44 BulkActionController::ACTION_ADD_TO_LIST,
45 BulkActionController::ACTION_REMOVE_FROM_LIST,
46 BulkActionController::ACTION_REMOVE_FROM_ALL_LISTS,
47 BulkActionController::ACTION_ADD_TAG,
48 BulkActionController::ACTION_REMOVE_TAG,
49 ];
50
51 /** @var ListingHandler */
52 private $listingHandler;
53
54 /** @var BulkActionController */
55 private $bulkActionController;
56
57 /** @var BulkConfirmationEmailResender */
58 private $bulkConfirmationEmailResender;
59
60 /** @var WPFunctions */
61 private $wp;
62
63 public function __construct(
64 ListingHandler $listingHandler,
65 BulkActionController $bulkActionController,
66 BulkConfirmationEmailResender $bulkConfirmationEmailResender,
67 WPFunctions $wp
68 ) {
69 $this->listingHandler = $listingHandler;
70 $this->bulkActionController = $bulkActionController;
71 $this->bulkConfirmationEmailResender = $bulkConfirmationEmailResender;
72 $this->wp = $wp;
73 }
74
75 public function checkPermissions(): bool {
76 return $this->wp->currentUserCan(AccessControl::PERMISSION_MANAGE_SUBSCRIBERS);
77 }
78
79 public function handle(Request $request): Response {
80 $actionParam = $request->getParam('action');
81 $action = is_string($actionParam) ? $actionParam : '';
82 $definition = $this->buildDefinition($request);
83
84 if ($action === self::ACTION_RESEND_CONFIRMATION_EMAILS) {
85 return $this->handleResendConfirmation($request, $definition);
86 }
87
88 $selectAll = $request->getParam('select_all') === true;
89 $selectionParam = $request->getParam('selection');
90 $hasSelection = is_array($selectionParam) && $selectionParam !== [];
91 if (!$hasSelection && !$selectAll) {
92 throw new ApiException(
93 __('No subscribers selected.', 'mailpoet'),
94 400,
95 'mailpoet_subscribers_no_selection'
96 );
97 }
98 if ($selectAll) {
99 $this->validateSelectAllScope($action, $definition);
100 }
101
102 $data = [
103 'trigger_automations' => $request->getParam('trigger_automations') === true,
104 ];
105 $segmentIdParam = $request->getParam('segment_id');
106 if (is_numeric($segmentIdParam)) {
107 $data['segment_id'] = (int)$segmentIdParam;
108 }
109 $tagIdParam = $request->getParam('tag_id');
110 if (is_numeric($tagIdParam)) {
111 $data['tag_id'] = (int)$tagIdParam;
112 }
113
114 try {
115 $result = $this->bulkActionController->execute($action, $definition, $data);
116 } catch (BulkActionException $exception) {
117 throw new ApiException(
118 $exception->getMessage(),
119 $exception->getStatusCode(),
120 $exception->getErrorCode()
121 );
122 }
123
124 return new Response([
125 'action' => $action,
126 'count' => $result['count'],
127 'kept' => $result['kept'] ?? 0,
128 'segment' => $result['segment'] ?? null,
129 'tag' => $result['tag'] ?? null,
130 ]);
131 }
132
133 public static function getRequestSchema(): array {
134 return [
135 'action' => Builder::string()->required(),
136 'selection' => Builder::array(Builder::integer()),
137 'select_all' => Builder::boolean(),
138 'group' => Builder::string(),
139 'search' => Builder::string(),
140 'filter' => Builder::object(),
141 'segment_id' => Builder::integer(),
142 'tag_id' => Builder::integer(),
143 'trigger_automations' => Builder::boolean(),
144 ];
145 }
146
147 private function handleResendConfirmation(Request $request, ListingDefinition $definition): Response {
148 if (!$this->bulkConfirmationEmailResender->canCurrentUserResend()) {
149 throw new ApiException(
150 __('You do not have permission to resend confirmation emails.', 'mailpoet'),
151 403,
152 'mailpoet_subscribers_resend_forbidden'
153 );
154 }
155 if ($definition->getGroup() !== SubscriberEntity::STATUS_UNCONFIRMED) {
156 throw new ApiException(
157 __('Confirmation emails can be resent in bulk only from the Unconfirmed subscribers view.', 'mailpoet'),
158 400,
159 'mailpoet_subscribers_invalid_group'
160 );
161 }
162 if (!$this->bulkConfirmationEmailResender->isSignupConfirmationEnabled()) {
163 throw new ApiException(
164 $this->bulkConfirmationEmailResender->getConfirmationDisabledMessage(),
165 400,
166 'mailpoet_subscribers_confirmation_disabled'
167 );
168 }
169 $selectAll = $request->getParam('select_all') === true;
170 $selection = $request->getParam('selection');
171 $hasSelection = is_array($selection) && $selection !== [];
172 // Resend runs before the main guard, so apply the same explicit-intent
173 // rule here: without a selection and without select_all, an omitted
174 // listing selection would otherwise target every matching subscriber.
175 if (!$hasSelection && !$selectAll) {
176 throw new ApiException(
177 __('No subscribers selected.', 'mailpoet'),
178 400,
179 'mailpoet_subscribers_no_selection'
180 );
181 }
182 // BulkConfirmationEmailResender::queue() inspects $requestData['listing']
183 // to detect whether the caller provided an explicit selection (so that
184 // empty selection at the listing scope can target every matching
185 // subscriber). Rebuild that shape from the flat REST schema.
186 $listing = [
187 'group' => $request->getParam('group'),
188 'search' => $request->getParam('search'),
189 'filter' => $request->getParam('filter'),
190 ];
191 if (!$selectAll && is_array($selection)) {
192 $listing['selection'] = $this->toIntList($selection);
193 }
194 $queueResult = $this->bulkConfirmationEmailResender->queue($definition, ['listing' => $listing]);
195
196 return new Response([
197 'action' => self::ACTION_RESEND_CONFIRMATION_EMAILS,
198 'count' => $queueResult['queued_count'],
199 'segment' => null,
200 'tag' => null,
201 'queue' => $queueResult,
202 ]);
203 }
204
205 private function buildDefinition(Request $request): ListingDefinition {
206 $filter = $request->getParam('filter');
207 $selection = $request->getParam('selection');
208 $selectAll = $request->getParam('select_all') === true;
209 $searchParam = $request->getParam('search');
210 $groupParam = $request->getParam('group');
211
212 return $this->listingHandler->getListingDefinition([
213 'offset' => 0,
214 'limit' => 0,
215 'sort_by' => 'id',
216 'sort_order' => 'desc',
217 'search' => is_string($searchParam) ? $searchParam : null,
218 'group' => is_string($groupParam) ? $groupParam : null,
219 'filter' => is_array($filter) ? $filter : [],
220 'selection' => !$selectAll && is_array($selection) ? $this->toIntList($selection) : [],
221 'params' => [],
222 ]);
223 }
224
225 private function validateSelectAllScope(string $action, ListingDefinition $definition): void {
226 $group = $definition->getGroup();
227 if (!is_string($group) || !in_array($group, self::VALID_SELECT_ALL_GROUPS, true)) {
228 throw new ApiException(
229 __('Select all requires a valid subscriber view.', 'mailpoet'),
230 400,
231 'mailpoet_subscribers_invalid_select_all_group'
232 );
233 }
234
235 if (in_array($action, self::TRASH_ONLY_ACTIONS, true) && $group !== 'trash') {
236 throw new ApiException(
237 __('This bulk action can only be applied from the Trash view.', 'mailpoet'),
238 400,
239 'mailpoet_subscribers_invalid_select_all_scope'
240 );
241 }
242
243 if (in_array($action, self::NON_TRASH_ACTIONS, true) && $group === 'trash') {
244 throw new ApiException(
245 __('This bulk action cannot be applied from the Trash view.', 'mailpoet'),
246 400,
247 'mailpoet_subscribers_invalid_select_all_scope'
248 );
249 }
250 }
251
252 /**
253 * @param array<mixed> $values
254 * @return int[]
255 */
256 private function toIntList(array $values): array {
257 $ints = [];
258 foreach ($values as $value) {
259 if (is_scalar($value)) {
260 $ints[] = (int)$value;
261 }
262 }
263 return $ints;
264 }
265 }
266