$payload */ public static function enforcePayload(string $contractId, array $payload): void { if (!self::requireValidPayload($contractId, $payload)) { throw new ABJ_404_Solution_AjaxContractViolationException(self::MESSAGE); } } public static function requireValidCurrentRequest(string $contractId): bool { $result = self::validate($contractId, self::currentRequestPayload()); if ($result['valid']) { return true; } if (self::enforcementPolicy()->shouldProceedDespiteViolations($contractId, $result['violations'])) { return true; } self::sendValidationError($contractId, $result['violations']); return false; } /** * @param array $payload */ public static function requireValidPayload(string $contractId, array $payload): bool { $result = self::validate($contractId, $payload); if ($result['valid']) { return true; } self::sendValidationError($contractId, $result['violations']); return false; } /** * Live-request variant of requireValidPayload() for handlers that validate a * decoded packed payload rather than the superglobals directly (the * settings-save handler decodes $_POST['encodedData'] first). Applies the * same production tolerance as requireValidCurrentRequest() so a missing or * corrupt schema degrades gracefully in production instead of bricking the * feature; off production it still fails fast. * * @param array $payload */ public static function requireValidLivePayload(string $contractId, array $payload): bool { $result = self::validate($contractId, $payload); if ($result['valid']) { return true; } if (self::enforcementPolicy()->shouldProceedDespiteViolations($contractId, $result['violations'])) { return true; } self::sendValidationError($contractId, $result['violations']); return false; } /** * @param array $payload * @return array{valid: bool, violations: array} */ public static function validate(string $contractId, array $payload): array { $schema = self::schemaRepository()->loadSchema($contractId); if (!is_array($schema)) { return self::invalid(array( ABJ_404_Solution_AjaxRequestContractEnforcementPolicy::SCHEMA_NOT_FOUND_PREFIX . $contractId, )); } return self::schemaValidator()->validate($contractId, $payload, $schema); } /** * Merge request data the way admin-ajax handlers use it. POST wins over * GET for duplicate keys, matching AjaxSecurityGate's nonce lookup order. * * @return array */ public static function currentRequestPayload(): array { $payload = array(); foreach (array($_GET, $_REQUEST, $_POST) as $source) { foreach ($source as $key => $value) { $payload[(string)$key] = $value; } } unset($payload['action']); return $payload; } private static function schemaRepository(): ABJ_404_Solution_AjaxRequestContractSchemaRepository { if (self::$schemaRepository === null) { self::$schemaRepository = new ABJ_404_Solution_AjaxRequestContractSchemaRepository(); } return self::$schemaRepository; } private static function schemaValidator(): ABJ_404_Solution_AjaxRequestContractSchemaValidator { if (self::$schemaValidator === null) { self::$schemaValidator = new ABJ_404_Solution_AjaxRequestContractSchemaValidator(); } return self::$schemaValidator; } private static function enforcementPolicy(): ABJ_404_Solution_AjaxRequestContractEnforcementPolicy { if (self::$enforcementPolicy === null) { self::$enforcementPolicy = new ABJ_404_Solution_AjaxRequestContractEnforcementPolicy(); } return self::$enforcementPolicy; } /** * @param array $violations */ private static function sendValidationError(string $contractId, array $violations): void { $message = function_exists('__') ? __('Invalid AJAX request.', '404-solution') : self::MESSAGE; wp_send_json_error(array( 'message' => $message, 'contract' => $contractId, 'violations' => array_values($violations), ), 400); } /** * @param array $violations * @return array{valid: bool, violations: array} */ private static function invalid(array $violations): array { return array('valid' => false, 'violations' => array_values($violations)); } }