| @@ -207,9 +207,12 @@ | ||
| 207 | 207 | $endpoint = $this->container->get($this->requestEndpointClass); |
| 208 | 208 | if (!$endpoint instanceof Endpoint) { |
| 209 | 209 | throw new \Exception(__('Invalid API endpoint.', 'mailpoet')); |
| 210 | 210 | } |
| 211 | - if (!method_exists($endpoint, $this->requestMethod)) { | |
| 211 | + if ( | |
| 212 | + !method_exists($endpoint, $this->requestMethod) | |
| 213 | + || !$this->isDispatchableEndpointMethod($endpoint, $this->requestMethod) | |
| 214 | + ) { | |
| 212 | 215 | throw new \Exception(__('Invalid API endpoint method.', 'mailpoet')); |
| 213 | 216 | } |
| 214 | 217 | |
| 215 | 218 | if (!$endpoint->isMethodAllowed($this->requestMethod, $this->requestType)) { |
| @@ -232,8 +235,11 @@ | ||
| 232 | 235 | $errorResponse = $this->createErrorResponse(Error::FORBIDDEN, $errorMessage, Response::STATUS_FORBIDDEN); |
| 233 | 236 | return $errorResponse; |
| 234 | 237 | } |
| 235 | 238 | $response = $endpoint->{$this->requestMethod}($this->requestData); |
| 239 | + if (!$response instanceof Response) { | |
| 240 | + throw new \Exception(__('Invalid API endpoint method.', 'mailpoet')); | |
| 241 | + } | |
| 236 | 242 | return $response; |
| 237 | 243 | } catch (Exception $e) { |
| 238 | 244 | $this->logError($e); |
| 239 | 245 | return $this->errorHandler->convertToResponse($e); |
| @@ -245,8 +251,30 @@ | ||
| 245 | 251 | $errorMessage = $e->getMessage(); |
| 246 | 252 | $errorResponse = $this->createErrorResponse(Error::BAD_REQUEST, $errorMessage, Response::STATUS_BAD_REQUEST); |
| 247 | 253 | return $errorResponse; |
| 248 | 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(); | |
| 249 | 277 | } |
| 250 | 278 | |
| 251 | 279 | public function validatePermissions($requestMethod, $permissions) { |
| 252 | 280 | // validate method permission if defined, otherwise validate global permission |