<?php
/**
 * Postomatic Plugin - This file handles automatic
 * class loading for the Postomatic plugin.
 *
 *  PHP Version 5.6
 *
 * @package  Postomatic
 * @category Plugin
 * @license  GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link     https://buildomatics.com/postomatics
 * @since    1.0.0
 */

namespace Postomatic\Api;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}


/**
 * Handles REST API endpoint for code validation and API key management.
 *
 * This class registers and manages a REST API endpoint for validating codes
 * and managing API keys. It processes incoming POST requests, validates the
 * provided codes against stored values, and handles the generation and
 * storage of new secure keys.
 *
 * @category API
 * @license  GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link     https://buildomatics.com/postomatics
 * @since    1.0.0
 */
class Code_Endpoint {

	/**
	 * Initialize the REST API endpoint for code validation and secure key
	 * management.
	 */
	public function __construct() {
		add_action(
			'rest_api_init',
			array( $this, 'register_endpoint' )
		);
	}

	/**
	 * Register the REST route for code validation.
	 *
	 * @return void
	 */
	public function register_endpoint() {
		register_rest_route(
			\Postomatic\Constants::API_WP_ENDPOINT_BASE,
			\Postomatic\Constants::API_WP_ENDPOINT_CODE,
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle_code' ),
				'permission_callback' => '__return_true',
			// No special permissions required.
			)
		);
	}

	/**
	 * Handle the code validation request.
	 *
	 * @param \WP_REST_Request $request The incoming request object.
	 *
	 * @return \WP_REST_Response Response object with validation results.
	 */
	public function handle_code( \WP_REST_Request $request ) {
		// Get parameters from the request.
		$api_key         = $request->get_param( 'api_key' );
		$code            = $request->get_param( 'code' );
		$header_wp_key   = $request->get_header( 'Authorization-Code' );

		$unauthorized = new \WP_REST_Response(
			array(
				'success' => false,
				'message' => 'Invalid activation code',
			),
			401
		);

		/*
		 * This is the ownership proof for an activation, and it is the only thing
		 * standing between an anonymous caller and a valid credential for this
		 * site's account.
		 *
		 * The service stages a re-activation and only promotes it once this
		 * handshake succeeds. That is a real proof of ownership *only* if this
		 * endpoint refuses handshakes for codes the plugin did not generate: an
		 * attacker can trigger a staged re-activation against a victim's site, and
		 * if the plugin accepts any /cr call it hands the attacker a working
		 * credential. The service cannot detect that on its own.
		 *
		 * So the decision is made on `code`, matched against the code this site
		 * staged just before calling /active.
		 */
		if ( ! is_string( $code ) || '' === trim( $code ) ) {
			return $unauthorized;
		}

		$pending = \Postomatic\Utils::get_pending_activation();

		/*
		 * No staged activation means either nothing was requested from this site,
		 * the code was already consumed, or the 30 minute window has passed.
		 * get_pending_activation() enforces the expiry and the single-use
		 * behaviour comes from clearing it below.
		 */
		if ( null === $pending ) {
			return $unauthorized;
		}

		// hash_equals() rather than ===, so a wrong code cannot be recovered byte
		// by byte from response timing.
		if ( ! hash_equals( $pending['code'], trim( $code ) ) ) {
			return $unauthorized;
		}

		/*
		 * The Authorization-Code header is corroboration, not the decision. On a
		 * staged re-activation the service sends the secret associated with the
		 * activation attempt, but if the plugin sent none it falls back to a
		 * previously stored value that may be stale or empty. Rejecting on a
		 * header mismatch would therefore break legitimate re-activations, so the
		 * mismatch is only recorded.
		 */
		if ( ! is_string( $header_wp_key )
			|| '' === $header_wp_key
			|| ! hash_equals( (string) $pending['wp_api_secret'], $header_wp_key )
		) {
			self::log( 'Authorization-Code header did not match the staged WP API secret; proceeding on the activation code.' );
		}

		if ( ! is_string( $api_key ) || '' === trim( $api_key ) ) {
			return new \WP_REST_Response(
				array(
					'success' => false,
					'message' => 'Missing api_key',
				),
				400
			);
		}

		/*
		 * Promote the staged credentials and store the new service key. This also
		 * clears the staging record, which is what makes the activation code
		 * single use: a replayed /cr call finds nothing staged and is rejected.
		 */
		\Postomatic\Utils::promote_pending_activation( $pending, sanitize_text_field( $api_key ) );

		return rest_ensure_response(
			array(
				'success'    => true,
				'wp_api_key' => \Postomatic\Utils::get_wp_api_secure_key(),
			)
		);
	}

	/**
	 * Record a handshake anomaly without exposing it to the caller.
	 *
	 * @param  string $message Message to log.
	 * @return void
	 */
	private static function log( $message ) {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- diagnostics for a security-relevant handshake, gated behind WP_DEBUG_LOG.
			error_log( '[Postomatic] ' . $message );
		}
	}

	/**
	 * Initialize the endpoint.
	 *
	 * @return void
	 */
	public static function init() {
		new self();
	}
}
