説明なし

API.php 8.7KB

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