PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.31.0
MailPoet – Newsletters, Email Marketing, and Automation v5.31.0
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 1 month ago v1 1 month ago API.php 2 months ago Endpoint.php 1 year ago Error.php 2 months ago ErrorHandler.php 1 year ago ErrorResponse.php 3 years ago Response.php 2 months ago SuccessResponse.php 3 years ago index.php 3 years ago
API.php
345 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 (!method_exists($endpoint, $this->requestMethod)) {
212 throw new \Exception(__('Invalid API endpoint method.', 'mailpoet'));
213 }
214
215 if (!$endpoint->isMethodAllowed($this->requestMethod, $this->requestType)) {
216 throw new \Exception(__('HTTP request method not allowed.', 'mailpoet'));
217 }
218
219 if (
220 class_exists(Debugger::class)
221 && class_exists(DIPanel::class)
222 && class_exists(ApiPanel::class)
223 ) {
224 ApiPanel::init($endpoint, $this->requestMethod, $this->requestData);
225 DIPanel::init();
226 }
227
228 // check the accessibility of the requested endpoint's action
229 // by default, an endpoint's action is considered "private"
230 if (!$this->validatePermissions($this->requestMethod, $endpoint->permissions)) {
231 $errorMessage = __('You do not have the required permissions.', 'mailpoet');
232 $errorResponse = $this->createErrorResponse(Error::FORBIDDEN, $errorMessage, Response::STATUS_FORBIDDEN);
233 return $errorResponse;
234 }
235 $response = $endpoint->{$this->requestMethod}($this->requestData);
236 return $response;
237 } catch (Exception $e) {
238 $this->logError($e);
239 return $this->errorHandler->convertToResponse($e);
240 } catch (Throwable $e) {
241 if (class_exists(Debugger::class) && Debugger::$logDirectory) {
242 Debugger::log($e, ILogger::EXCEPTION);
243 }
244 $this->logError($e);
245 $errorMessage = $e->getMessage();
246 $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
247 return $errorResponse;
248 }
249 }
250
251 public function validatePermissions($requestMethod, $permissions) {
252 // validate method permission if defined, otherwise validate global permission
253 return(!empty($permissions['methods'][$requestMethod])) ?
254 $this->accessControl->validatePermission($permissions['methods'][$requestMethod]) :
255 $this->accessControl->validatePermission($permissions['global']);
256 }
257
258 public function setTokenAndAPIVersion() {
259 echo sprintf(
260 '<script type="text/javascript">' .
261 'var mailpoet_token = "%s";' .
262 'var mailpoet_api_version = "%s";' .
263 '</script>',
264 esc_js($this->wp->wpCreateNonce('mailpoet_token')),
265 esc_js(self::CURRENT_VERSION)
266 );
267 }
268
269 public function getToken() {
270 // Bypass intermediate caches so the token is always fresh; the embedded form
271 // token in HTML can be served stale by edge caches and expire by submit time.
272 if (!headers_sent()) {
273 header('Cache-Control: no-store, max-age=0');
274 header('Content-Type: application/json; charset=UTF-8');
275 }
276
277 echo wp_json_encode([
278 'token' => $this->wp->wpCreateNonce('mailpoet_token'),
279 'api_version' => self::CURRENT_VERSION,
280 ]);
281 $this->wp->wpDie();
282 }
283
284 public function addTokenToHeartbeatResponse($response) {
285 $response['mailpoet_token'] = $this->wp->wpCreateNonce('mailpoet_token');
286 return $response;
287 }
288
289 public function addEndpointNamespace($namespace, $version) {
290 if (!empty($this->endpointNamespaces[$version][$namespace])) return;
291 $this->endpointNamespaces[$version][] = $namespace;
292 }
293
294 public function getEndpointNamespaces() {
295 return $this->endpointNamespaces;
296 }
297
298 public function getRequestedEndpointClass() {
299 return $this->requestEndpointClass;
300 }
301
302 public function getRequestedAPIVersion() {
303 return $this->requestApiVersion;
304 }
305
306 public function createErrorResponse($errorType, $errorMessage, $responseStatus) {
307 $errorMessages = [
308 $errorType => $errorMessage,
309 ];
310
311 if ($errorType === Error::BAD_REQUEST) {
312 $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');
313 $mpReinstallErrorMessage = Helpers::replaceLinkTags(
314 $mpReinstallErrorMessage,
315 'https://kb.mailpoet.com/article/258-re-installing-updating-the-plugin-via-ftp',
316 ['target' => '_blank']
317 );
318 $errorMessages[Error::REINSTALL_PLUGIN] = $mpReinstallErrorMessage;
319 }
320
321 $errorResponse = new ErrorResponse(
322 $errorMessages,
323 [],
324 $responseStatus
325 );
326 return $errorResponse;
327 }
328
329 private function logError(Throwable $e): void {
330 // logging to the php log
331 if (function_exists('error_log')) {
332 // phpcs:disable QITStandard.PHP.DebugCode.DebugFunctionFound
333 error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions
334 // phpcs:enable QITStandard.PHP.DebugCode.DebugFunctionFound
335 }
336 // logging to the MailPoet table
337 $this->loggerFactory->getLogger(LoggerFactory::TOPIC_API)->warning($e->getMessage(), [
338 'requestMethod' => $this->requestMethod,
339 'requestEndpoint' => $this->requestEndpoint,
340 'exceptionMessage' => $e->getMessage(),
341 'exceptionTrace' => $e->getTrace(),
342 ]);
343 }
344 }
345