PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.37.0
MailPoet – Newsletters, Email Marketing, and Automation v5.37.0
5.37.0 5.36.1 5.36.0 5.35.1 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 / API / JSON / API.php
mailpoet / lib / API / JSON Last commit date
ResponseBuilders 2 days ago v1 2 days ago API.php 2 days ago Endpoint.php 1 year ago Error.php 3 months ago ErrorHandler.php 1 year ago ErrorResponse.php 3 years ago Response.php 3 months ago SuccessResponse.php 3 years ago index.php 3 years ago
API.php
373 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\API\JSON;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Captcha\CaptchaConstants;
9 use MailPoet\Config\AccessControl;
10 use MailPoet\Exception;
11 use MailPoet\Logging\LoggerFactory;
12 use MailPoet\Settings\SettingsController;
13 use MailPoet\Tracy\ApiPanel\ApiPanel;
14 use MailPoet\Tracy\DIPanel\DIPanel;
15 use MailPoet\Util\Helpers;
16 use MailPoet\WP\Functions as WPFunctions;
17 use MailPoetVendor\Psr\Container\ContainerInterface;
18 use Throwable;
19 use Tracy\Debugger;
20 use Tracy\ILogger;
21
22 class API {
23 private $requestApiVersion;
24 private $requestEndpoint;
25 private $requestMethod;
26 private $requestToken;
27 private $requestType;
28 private $requestEndpointClass;
29 private $requestData = [];
30 private $endpointNamespaces = [];
31 private $availableApiVersions = [
32 'v1',
33 ];
34
35 /** @var ContainerInterface */
36 private $container;
37
38 /** @var AccessControl */
39 private $accessControl;
40
41 /** @var ErrorHandler */
42 private $errorHandler;
43
44 /** @var WPFunctions */
45 private $wp;
46
47 /** @var SettingsController */
48 private $settings;
49
50 /** @var LoggerFactory */
51 private $loggerFactory;
52
53 const CURRENT_VERSION = 'v1';
54
55 public function __construct(
56 ContainerInterface $container,
57 AccessControl $accessControl,
58 ErrorHandler $errorHandler,
59 SettingsController $settings,
60 LoggerFactory $loggerFactory,
61 WPFunctions $wp
62 ) {
63 $this->container = $container;
64 $this->accessControl = $accessControl;
65 $this->errorHandler = $errorHandler;
66 $this->settings = $settings;
67 $this->wp = $wp;
68 foreach ($this->availableApiVersions as $availableApiVersion) {
69 $this->addEndpointNamespace(
70 sprintf('%s\%s', __NAMESPACE__, $availableApiVersion),
71 $availableApiVersion
72 );
73 }
74 $this->loggerFactory = $loggerFactory;
75 }
76
77 public function init() {
78 // admin security token and API version
79 WPFunctions::get()->addAction(
80 'admin_head',
81 [$this, 'setTokenAndAPIVersion']
82 );
83
84 // ajax (logged in users)
85 WPFunctions::get()->addAction(
86 'wp_ajax_mailpoet',
87 [$this, 'setupAjax']
88 );
89
90 // ajax (logged out users)
91 WPFunctions::get()->addAction(
92 'wp_ajax_nopriv_mailpoet',
93 [$this, 'setupAjax']
94 );
95
96 // fresh-token endpoint for cached pages (e.g. edge-cached signup forms);
97 // intentionally separate from setupAjax() to break the chicken-and-egg of needing a token to fetch a token
98 WPFunctions::get()->addAction(
99 'wp_ajax_mailpoet_token',
100 [$this, 'getToken']
101 );
102 WPFunctions::get()->addAction(
103 'wp_ajax_nopriv_mailpoet_token',
104 [$this, 'getToken']
105 );
106
107 // nonce refreshing via heartbeats
108 WPFunctions::get()->addAction(
109 'wp_refresh_nonces',
110 [$this, 'addTokenToHeartbeatResponse']
111 );
112 }
113
114 public function setupAjax() {
115 $this->wp->doAction('mailpoet_api_setup', [$this]);
116
117 if (isset($_POST['api_version'])) {
118 $this->setRequestData($_POST, Endpoint::TYPE_POST);
119 } else {
120 $this->setRequestData($_GET, Endpoint::TYPE_GET);
121 }
122
123 $ignoreToken = (
124 $this->settings->get('captcha.type') != CaptchaConstants::TYPE_DISABLED &&
125 $this->requestEndpoint === 'subscribers' &&
126 $this->requestMethod === 'subscribe'
127 ) || (
128 $this->requestEndpoint === 'captcha'
129 );
130
131 if (!$ignoreToken && $this->wp->wpVerifyNonce($this->requestToken, 'mailpoet_token') === false) {
132 $errorMessage = __("Sorry, but we couldn't connect to the MailPoet server. Please refresh the web page and try again.", 'mailpoet');
133 $errorResponse = $this->createErrorResponse(Error::UNAUTHORIZED, $errorMessage, Response::STATUS_UNAUTHORIZED);
134 return $errorResponse->send();
135 }
136
137 $response = $this->processRoute();
138 $response->send();
139 }
140
141 public function setRequestData($data, $requestType) {
142 $this->requestApiVersion = (!empty($data['api_version']) && is_string($data['api_version'])) ? $data['api_version'] : false;
143
144 $this->requestEndpoint = (isset($data['endpoint']) && is_string($data['endpoint']))
145 ? Helpers::underscoreToCamelCase(trim($data['endpoint']))
146 : null;
147
148 // JS part of /wp-admin/customize.php does not like a 'method' field in a form widget
149 $methodParamName = isset($data['mailpoet_method']) ? 'mailpoet_method' : 'method';
150 $this->requestMethod = (isset($data[$methodParamName]) && is_string($data[$methodParamName]))
151 ? Helpers::underscoreToCamelCase(trim($data[$methodParamName]))
152 : null;
153 $this->requestType = $requestType;
154
155 $this->requestToken = (isset($data['token']) && is_string($data['token']))
156 ? trim($data['token'])
157 : null;
158
159 if (!$this->requestEndpoint || !$this->requestMethod || !$this->requestApiVersion) {
160 $errorMessage = __('Invalid API request.', 'mailpoet');
161 $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
162 return $errorResponse;
163 } else if (!empty($this->endpointNamespaces[$this->requestApiVersion])) {
164 foreach ($this->endpointNamespaces[$this->requestApiVersion] as $namespace) {
165 $endpointClass = sprintf(
166 '%s\%s',
167 $namespace,
168 ucfirst($this->requestEndpoint)
169 );
170 if ($this->container->has($endpointClass)) {
171 $this->requestEndpointClass = $endpointClass;
172 break;
173 }
174 }
175 $this->requestData = isset($data['data'])
176 ? WPFunctions::get()->stripslashesDeep($data['data'])
177 : [];
178
179 // remove reserved keywords from data
180 if (is_array($this->requestData) && !empty($this->requestData)) {
181 // filter out reserved keywords from data
182 $reservedKeywords = [
183 'token',
184 'endpoint',
185 'method',
186 'api_version',
187 'mailpoet_method', // alias of 'method'
188 'mailpoet_redirect',
189 ];
190 $this->requestData = array_diff_key(
191 $this->requestData,
192 array_flip($reservedKeywords)
193 );
194 }
195 }
196 }
197
198 public function processRoute() {
199 try {
200 if (
201 empty($this->requestEndpointClass) ||
202 !$this->container->has($this->requestEndpointClass)
203 ) {
204 throw new \Exception(__('Invalid API endpoint.', 'mailpoet'));
205 }
206
207 $endpoint = $this->container->get($this->requestEndpointClass);
208 if (!$endpoint instanceof Endpoint) {
209 throw new \Exception(__('Invalid API endpoint.', 'mailpoet'));
210 }
211 if (
212 !method_exists($endpoint, $this->requestMethod)
213 || !$this->isDispatchableEndpointMethod($endpoint, $this->requestMethod)
214 ) {
215 throw new \Exception(__('Invalid API endpoint method.', 'mailpoet'));
216 }
217
218 if (!$endpoint->isMethodAllowed($this->requestMethod, $this->requestType)) {
219 throw new \Exception(__('HTTP request method not allowed.', 'mailpoet'));
220 }
221
222 if (
223 class_exists(Debugger::class)
224 && class_exists(DIPanel::class)
225 && class_exists(ApiPanel::class)
226 ) {
227 ApiPanel::init($endpoint, $this->requestMethod, $this->requestData);
228 DIPanel::init();
229 }
230
231 // check the accessibility of the requested endpoint's action
232 // by default, an endpoint's action is considered "private"
233 if (!$this->validatePermissions($this->requestMethod, $endpoint->permissions)) {
234 $errorMessage = __('You do not have the required permissions.', 'mailpoet');
235 $errorResponse = $this->createErrorResponse(Error::FORBIDDEN, $errorMessage, Response::STATUS_FORBIDDEN);
236 return $errorResponse;
237 }
238 $response = $endpoint->{$this->requestMethod}($this->requestData);
239 if (!$response instanceof Response) {
240 throw new \Exception(__('Invalid API endpoint method.', 'mailpoet'));
241 }
242 return $response;
243 } catch (Exception $e) {
244 $this->logError($e);
245 return $this->errorHandler->convertToResponse($e);
246 } catch (Throwable $e) {
247 if (class_exists(Debugger::class) && Debugger::$logDirectory) {
248 Debugger::log($e, ILogger::EXCEPTION);
249 }
250 $this->logError($e);
251 $errorMessage = $e->getMessage();
252 $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
253 return $errorResponse;
254 }
255 }
256
257 /**
258 * The response-builder helpers on the Endpoint base class are framework plumbing,
259 * blocked by name so that a subclass override cannot re-expose them. Non-public
260 * methods are never dispatchable.
261 */
262 private function isDispatchableEndpointMethod(Endpoint $endpoint, string $requestMethod): bool {
263 // Magic methods (__construct, __call, __get, __invoke, ...) are never actions.
264 if (strpos($requestMethod, '__') === 0) {
265 return false;
266 }
267 if (method_exists(Endpoint::class, $requestMethod)) {
268 return false;
269 }
270 $method = new \ReflectionMethod($endpoint, $requestMethod);
271 // PHP resolves method names case-insensitively; require the exact declared casing
272 // so that every name-keyed lookup downstream matches the method that runs.
273 if ($method->getName() !== $requestMethod) {
274 return false;
275 }
276 return $method->isPublic();
277 }
278
279 public function validatePermissions($requestMethod, $permissions) {
280 // validate method permission if defined, otherwise validate global permission
281 return(!empty($permissions['methods'][$requestMethod])) ?
282 $this->accessControl->validatePermission($permissions['methods'][$requestMethod]) :
283 $this->accessControl->validatePermission($permissions['global']);
284 }
285
286 public function setTokenAndAPIVersion() {
287 echo sprintf(
288 '<script type="text/javascript">' .
289 'var mailpoet_token = "%s";' .
290 'var mailpoet_api_version = "%s";' .
291 '</script>',
292 esc_js($this->wp->wpCreateNonce('mailpoet_token')),
293 esc_js(self::CURRENT_VERSION)
294 );
295 }
296
297 public function getToken() {
298 // Bypass intermediate caches so the token is always fresh; the embedded form
299 // token in HTML can be served stale by edge caches and expire by submit time.
300 if (!headers_sent()) {
301 header('Cache-Control: no-store, max-age=0');
302 header('Content-Type: application/json; charset=UTF-8');
303 }
304
305 echo wp_json_encode([
306 'token' => $this->wp->wpCreateNonce('mailpoet_token'),
307 'api_version' => self::CURRENT_VERSION,
308 ]);
309 $this->wp->wpDie();
310 }
311
312 public function addTokenToHeartbeatResponse($response) {
313 $response['mailpoet_token'] = $this->wp->wpCreateNonce('mailpoet_token');
314 return $response;
315 }
316
317 public function addEndpointNamespace($namespace, $version) {
318 if (!empty($this->endpointNamespaces[$version][$namespace])) return;
319 $this->endpointNamespaces[$version][] = $namespace;
320 }
321
322 public function getEndpointNamespaces() {
323 return $this->endpointNamespaces;
324 }
325
326 public function getRequestedEndpointClass() {
327 return $this->requestEndpointClass;
328 }
329
330 public function getRequestedAPIVersion() {
331 return $this->requestApiVersion;
332 }
333
334 public function createErrorResponse($errorType, $errorMessage, $responseStatus) {
335 $errorMessages = [
336 $errorType => $errorMessage,
337 ];
338
339 if ($errorType === Error::BAD_REQUEST) {
340 $mpReinstallErrorMessage = __('The plugin has encountered an unexpected error. Please reload the page. If that does not help, [link]re-install the MailPoet Plugin.[/link]', 'mailpoet');
341 $mpReinstallErrorMessage = Helpers::replaceLinkTags(
342 $mpReinstallErrorMessage,
343 'https://kb.mailpoet.com/article/258-re-installing-updating-the-plugin-via-ftp',
344 ['target' => '_blank']
345 );
346 $errorMessages[Error::REINSTALL_PLUGIN] = $mpReinstallErrorMessage;
347 }
348
349 $errorResponse = new ErrorResponse(
350 $errorMessages,
351 [],
352 $responseStatus
353 );
354 return $errorResponse;
355 }
356
357 private function logError(Throwable $e): void {
358 // logging to the php log
359 if (function_exists('error_log')) {
360 // phpcs:disable QITStandard.PHP.DebugCode.DebugFunctionFound
361 error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
362 // phpcs:enable QITStandard.PHP.DebugCode.DebugFunctionFound
363 }
364 // logging to the MailPoet table
365 $this->loggerFactory->getLogger(LoggerFactory::TOPIC_API)->warning($e->getMessage(), [
366 'requestMethod' => $this->requestMethod,
367 'requestEndpoint' => $this->requestEndpoint,
368 'exceptionMessage' => $e->getMessage(),
369 'exceptionTrace' => $e->getTrace(),
370 ]);
371 }
372 }
373