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