<?php
/**
 * The Jetpack Connection Rest Authentication file.
 *
 * @package automattic/jetpack-connection
 */

namespace Automattic\Jetpack\Connection;

use WP_Error;

/**
 * The Jetpack Connection Rest Authentication class.
 */
class Rest_Authentication {

	/**
	 * The rest authentication status.
	 *
	 * @since 1.17.0
	 * @var boolean
	 */
	private $rest_authentication_status = null;

	/**
	 * The rest authentication type.
	 * Can be either 'user' or 'blog' depending on whether the request
	 * is signed with a user or a blog token.
	 *
	 * @since 1.29.0
	 * @var string
	 */
	private $rest_authentication_type = null;

	/**
	 * The Manager object.
	 *
	 * @since 1.17.0
	 * @var Object
	 */
	private $connection_manager = null;

	/**
	 * Holds the singleton instance of this class
	 *
	 * @since 1.17.0
	 * @var Object
	 */
	private static $instance = false;

	/**
	 * Flag used to avoid determine_current_user filter to enter an infinite loop
	 *
	 * @since 1.26.0
	 * @var boolean
	 */
	private $doing_determine_current_user_filter = false;

	/**
	 * The constructor.
	 */
	private function __construct() {
		$this->connection_manager = new Manager();
	}

	/**
	 * Controls the single instance of this class.
	 *
	 * @static
	 */
	public static function init() {
		if ( ! self::$instance ) {
			self::$instance = new self();

			add_filter( 'determine_current_user', array( self::$instance, 'wp_rest_authenticate' ) );
			add_filter( 'rest_authentication_errors', array( self::$instance, 'wp_rest_authentication_errors' ) );
		}

		return self::$instance;
	}

	/**
	 * Authenticates requests from Jetpack server to WP REST API endpoints.
	 * Uses the existing XMLRPC request signing implementation.
	 *
	 * @param int|bool $user User ID if one has been determined, false otherwise.
	 *
	 * @return int|null The user id or null if the request was authenticated via blog token, or not authenticated at all.
	 */
	public function wp_rest_authenticate( $user ) {
		if ( $this->doing_determine_current_user_filter ) {
			return $user;
		}

		$this->doing_determine_current_user_filter = true;

		try {
			if ( ! empty( $user ) ) {
				// Another authentication method is in effect.
				return $user;
			}

			add_filter(
				'jetpack_constant_default_value',
				__NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
				10,
				2
			);

			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( ! isset( $_GET['_for'] ) || 'jetpack' !== $_GET['_for'] ) {
				// Nothing to do for this authentication method.
				return null;
			}

			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( ! isset( $_GET['token'] ) && ! isset( $_GET['signature'] ) ) {
				// Nothing to do for this authentication method.
				return null;
			}

			if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
				$this->rest_authentication_status = new WP_Error(
					'rest_invalid_request',
					__( 'The request method is missing.', 'jetpack-connection' ),
					array( 'status' => 400 )
				);
				return null;
			}

			// Only support specific request parameters that have been tested and
			// are known to work with signature verification.  A different method
			// can be passed to the WP REST API via the '?_method=' parameter if
			// needed.
			if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
				$this->rest_authentication_status = new WP_Error(
					'rest_invalid_request',
					__( 'This request method is not supported.', 'jetpack-connection' ),
					array( 'status' => 400 )
				);
				return null;
			}
			if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
				$this->rest_authentication_status = new WP_Error(
					'rest_invalid_request',
					__( 'This request method does not support body parameters.', 'jetpack-connection' ),
					array( 'status' => 400 )
				);
				return null;
			}

			$verified = $this->connection_manager->verify_xml_rpc_signature();

			if (
				$verified &&
				isset( $verified['type'] ) &&
				'blog' === $verified['type']
			) {
				// Site-level authentication successful.
				$this->rest_authentication_status = true;
				$this->rest_authentication_type   = 'blog';
				return null;
			}

			if (
				$verified &&
				isset( $verified['type'] ) &&
				'user' === $verified['type'] &&
				! empty( $verified['user_id'] )
			) {
				// User-level authentication successful.
				$this->rest_authentication_status = true;
				$this->rest_authentication_type   = 'user';
				return $verified['user_id'];
			}

			// Something else went wrong.  Probably a signature error.
			$this->rest_authentication_status = new WP_Error(
				'rest_invalid_signature',
				__( 'The request is not signed correctly.', 'jetpack-connection' ),
				array( 'status' => 400 )
			);
			return null;
		} finally {
			$this->doing_determine_current_user_filter = false;
		}
	}

	/**
	 * Report authentication status to the WP REST API.
	 *
	 * @param  WP_Error|mixed $value Error from another authentication handler, null if we should handle it, or another value if not.
	 * @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication}
	 */
	public function wp_rest_authentication_errors( $value ) {
		if ( null !== $value ) {
			return $value;
		}
		return $this->rest_authentication_status;
	}

	/**
	 * Resets the saved authentication state in between testing requests.
	 */
	public function reset_saved_auth_state() {
		$this->rest_authentication_status = null;
		$this->connection_manager->reset_saved_auth_state();
	}

	/**
	 * Whether the request was signed with a blog token.
	 *
	 * @since 1.29.0
	 *
	 * @return bool True if the request was signed with a valid blog token, false otherwise.
	 */
	public static function is_signed_with_blog_token() {
		$instance = self::init();

		return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type;
	}
}
