PluginProbe
MailPoet – Newsletters, Email Marketing, and Automation / 3.89.4
MailPoet – Newsletters, Email Marketing, and Automation v3.89.4
5.38.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 All 542 releases
mailpoet / lib / API / JSON / API.php

API.php in MailPoet – Newsletters, Email Marketing, and Automation 3.89.4, at lib/API/JSON/API.php

277 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MailPoet\API\JSON;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Config\AccessControl;
9 use MailPoet\Exception;
10 use MailPoet\Settings\SettingsController;
11 use MailPoet\Subscription\Captcha;
12 use MailPoet\Tracy\ApiPanel\ApiPanel;
13 use MailPoet\Tracy\DIPanel\DIPanel;
14 use MailPoet\Util\Helpers;
15 use MailPoet\WP\Functions as WPFunctions;
16 use MailPoetVendor\Psr\Container\ContainerInterface;
17 use Throwable;
18 use Tracy\Debugger;
19 use Tracy\ILogger;
20
21 class API {
22 private $requestApiVersion;
23 private $requestEndpoint;
24 private $requestMethod;
25 private $requestToken;
26 private $requestType;
27 private $requestEndpointClass;
28 private $requestData = [];
29 private $endpointNamespaces = [];
30 private $availableApiVersions = [
31 'v1',
32 ];
33 /** @var ContainerInterface */
34 private $container;
35
36 /** @var AccessControl */
37 private $accessControl;
38
39 /** @var ErrorHandler */
40 private $errorHandler;
41
42 /** @var WPFunctions */
43 private $wp;
44
45 /** @var SettingsController */
46 private $settings;
47
48 const CURRENT_VERSION = 'v1';
49
50 public function __construct(
51 ContainerInterface $container,
52 AccessControl $accessControl,
53 ErrorHandler $errorHandler,
54 SettingsController $settings,
55 WPFunctions $wp
56 ) {
57 $this->container = $container;
58 $this->accessControl = $accessControl;
59 $this->errorHandler = $errorHandler;
60 $this->settings = $settings;
61 $this->wp = $wp;
62 foreach ($this->availableApiVersions as $availableApiVersion) {
63 $this->addEndpointNamespace(
64 sprintf('%s\%s', __NAMESPACE__, $availableApiVersion),
65 $availableApiVersion
66 );
67 }
68 }
69
70 public function init() {
71 // admin security token and API version
72 WPFunctions::get()->addAction(
73 'admin_head',
74 [$this, 'setTokenAndAPIVersion']
75 );
76
77 // ajax (logged in users)
78 WPFunctions::get()->addAction(
79 'wp_ajax_mailpoet',
80 [$this, 'setupAjax']
81 );
82
83 // ajax (logged out users)
84 WPFunctions::get()->addAction(
85 'wp_ajax_nopriv_mailpoet',
86 [$this, 'setupAjax']
87 );
88
89 // nonce refreshing via heartbeats
90 WPFunctions::get()->addAction(
91 'wp_refresh_nonces',
92 [$this, 'addTokenToHeartbeatResponse']
93 );
94 }
95
96 public function setupAjax() {
97 $this->wp->doAction('mailpoet_api_setup', [$this]);
98
99 if (isset($_POST['api_version'])) {
100 $this->setRequestData($_POST, Endpoint::TYPE_POST);
101 } else {
102 $this->setRequestData($_GET, Endpoint::TYPE_GET);
103 }
104
105 $ignoreToken = (
106 $this->settings->get('captcha.type') != Captcha::TYPE_DISABLED &&
107 $this->requestEndpoint === 'subscribers' &&
108 $this->requestMethod === 'subscribe'
109 );
110
111 if (!$ignoreToken && $this->wp->wpVerifyNonce($this->requestToken, 'mailpoet_token') === false) {
112 $errorMessage = WPFunctions::get()->__("Sorry, but we couldn't connect to the MailPoet server. Please refresh the web page and try again.", 'mailpoet');
113 $errorResponse = $this->createErrorResponse(Error::UNAUTHORIZED, $errorMessage, Response::STATUS_UNAUTHORIZED);
114 return $errorResponse->send();
115 }
116
117 $response = $this->processRoute();
118 $response->send();
119 }
120
121 public function setRequestData($data, $requestType) {
122 $this->requestApiVersion = !empty($data['api_version']) ? $data['api_version'] : false;
123
124 $this->requestEndpoint = isset($data['endpoint'])
125 ? Helpers::underscoreToCamelCase(trim($data['endpoint']))
126 : null;
127
128 // JS part of /wp-admin/customize.php does not like a 'method' field in a form widget
129 $methodParamName = isset($data['mailpoet_method']) ? 'mailpoet_method' : 'method';
130 $this->requestMethod = isset($data[$methodParamName])
131 ? Helpers::underscoreToCamelCase(trim($data[$methodParamName]))
132 : null;
133 $this->requestType = $requestType;
134
135 $this->requestToken = isset($data['token'])
136 ? trim($data['token'])
137 : null;
138
139 if (!$this->requestEndpoint || !$this->requestMethod || !$this->requestApiVersion) {
140 $errorMessage = WPFunctions::get()->__('Invalid API request.', 'mailpoet');
141 $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
142 return $errorResponse;
143 } else if (!empty($this->endpointNamespaces[$this->requestApiVersion])) {
144 foreach ($this->endpointNamespaces[$this->requestApiVersion] as $namespace) {
145 $endpointClass = sprintf(
146 '%s\%s',
147 $namespace,
148 ucfirst($this->requestEndpoint)
149 );
150 if ($this->container->has($endpointClass)) {
151 $this->requestEndpointClass = $endpointClass;
152 break;
153 }
154 }
155 $this->requestData = isset($data['data'])
156 ? WPFunctions::get()->stripslashesDeep($data['data'])
157 : [];
158
159 // remove reserved keywords from data
160 if (is_array($this->requestData) && !empty($this->requestData)) {
161 // filter out reserved keywords from data
162 $reservedKeywords = [
163 'token',
164 'endpoint',
165 'method',
166 'api_version',
167 'mailpoet_method', // alias of 'method'
168 'mailpoet_redirect',
169 ];
170 $this->requestData = array_diff_key(
171 $this->requestData,
172 array_flip($reservedKeywords)
173 );
174 }
175 }
176 }
177
178 public function processRoute() {
179 try {
180 if (
181 empty($this->requestEndpointClass) ||
182 !$this->container->has($this->requestEndpointClass)
183 ) {
184 throw new \Exception(__('Invalid API endpoint.', 'mailpoet'));
185 }
186
187 $endpoint = $this->container->get($this->requestEndpointClass);
188 if (!method_exists($endpoint, $this->requestMethod)) {
189 throw new \Exception(__('Invalid API endpoint method.', 'mailpoet'));
190 }
191
192 if (!$endpoint->isMethodAllowed($this->requestMethod, $this->requestType)) {
193 throw new \Exception(__('HTTP request method not allowed.', 'mailpoet'));
194 }
195
196 if (
197 class_exists(Debugger::class)
198 && class_exists(DIPanel::class)
199 && class_exists(ApiPanel::class)
200 ) {
201 ApiPanel::init($endpoint, $this->requestMethod, $this->requestData);
202 DIPanel::init();
203 }
204
205 // check the accessibility of the requested endpoint's action
206 // by default, an endpoint's action is considered "private"
207 if (!$this->validatePermissions($this->requestMethod, $endpoint->permissions)) {
208 $errorMessage = WPFunctions::get()->__('You do not have the required permissions.', 'mailpoet');
209 $errorResponse = $this->createErrorResponse(Error::FORBIDDEN, $errorMessage, Response::STATUS_FORBIDDEN);
210 return $errorResponse;
211 }
212 $response = $endpoint->{$this->requestMethod}($this->requestData);
213 return $response;
214 } catch (Exception $e) {
215 return $this->errorHandler->convertToResponse($e);
216 } catch (Throwable $e) {
217 if (class_exists(Debugger::class) && Debugger::$logDirectory) {
218 Debugger::log($e, ILogger::EXCEPTION);
219 }
220 $errorMessage = $e->getMessage();
221 $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST);
222 return $errorResponse;
223 }
224 }
225
226 public function validatePermissions($requestMethod, $permissions) {
227 // validate method permission if defined, otherwise validate global permission
228 return(!empty($permissions['methods'][$requestMethod])) ?
229 $this->accessControl->validatePermission($permissions['methods'][$requestMethod]) :
230 $this->accessControl->validatePermission($permissions['global']);
231 }
232
233 public function setTokenAndAPIVersion() {
234 echo sprintf(
235 '<script type="text/javascript">' .
236 'var mailpoet_token = "%s";' .
237 'var mailpoet_api_version = "%s";' .
238 '</script>',
239 esc_js($this->wp->wpCreateNonce('mailpoet_token')),
240 esc_js(self::CURRENT_VERSION)
241 );
242 }
243
244 public function addTokenToHeartbeatResponse($response) {
245 $response['mailpoet_token'] = $this->wp->wpCreateNonce('mailpoet_token');
246 return $response;
247 }
248
249 public function addEndpointNamespace($namespace, $version) {
250 if (!empty($this->endpointNamespaces[$version][$namespace])) return;
251 $this->endpointNamespaces[$version][] = $namespace;
252 }
253
254 public function getEndpointNamespaces() {
255 return $this->endpointNamespaces;
256 }
257
258 public function getRequestedEndpointClass() {
259 return $this->requestEndpointClass;
260 }
261
262 public function getRequestedAPIVersion() {
263 return $this->requestApiVersion;
264 }
265
266 public function createErrorResponse($errorType, $errorMessage, $responseStatus) {
267 $errorResponse = new ErrorResponse(
268 [
269 $errorType => $errorMessage,
270 ],
271 [],
272 $responseStatus
273 );
274 return $errorResponse;
275 }
276 }
277