<?php

use WPML\API\Sanitize;

/**
 * Class WPML_Language_Per_Domain_SSO
 */
class WPML_Language_Per_Domain_SSO {

	const SSO_NONCE = 'wpml_sso';

	const TRANSIENT_SSO_STARTED   = 'wpml_sso_started';
	const TRANSIENT_DOMAIN        = 'wpml_sso_domain_';
	const TRANSIENT_USER          = 'wpml_sso_user_';
	const TRANSIENT_SESSION_TOKEN = 'wpml_sso_session_';

	const IFRAME_USER_TOKEN_KEY            = 'wpml_sso_token';
	const IFRAME_USER_TOKEN_KEY_FOR_DOMAIN = 'wpml_sso_token_domain';
	const IFRAME_DOMAIN_HASH_KEY           = 'wpml_sso_iframe_hash';
	const IFRAME_USER_STATUS_KEY           = 'wpml_sso_user_status';

	const SSO_TIMEOUT = MINUTE_IN_SECONDS;

	/** @var SitePress $sitepress */
	private $sitepress;

	/** @var WPML_PHP_Functions $php_functions */
	private $php_functions;

	/** @var WPML_Cookie  */
	private $wpml_cookie;

	/** @var string */
	private $site_url;

	/** @var array */
	private $domains;

	/** @var int $current_user_id */
	private $current_user_id;

	public function __construct( SitePress $sitepress, WPML_PHP_Functions $php_functions, WPML_Cookie $wpml_cookie ) {
		$this->sitepress     = $sitepress;
		$this->php_functions = $php_functions;
		$this->wpml_cookie = $wpml_cookie;
		$this->site_url      = $this->sitepress->convert_url( get_home_url(), $this->sitepress->get_default_language() );
		$this->domains       = $this->get_domains();
	}

	public function init_hooks() {

		if ( $this->is_sso_started() ) {
			add_action( 'init', [ $this, 'init_action' ] );

			add_action( 'wp_footer', [ $this, 'add_iframes_to_footer' ] );
			add_action( 'admin_footer', [ $this, 'add_iframes_to_footer' ] );
			add_action( 'login_footer', [ $this, 'add_iframes_to_footer' ] );
		}

		add_action( 'wp_login', [ $this, 'wp_login_action' ], 10, 2 );
		add_filter( 'logout_redirect', [ $this, 'add_redirect_user_token' ], 10, 3 );
	}

	public function init_action() {
		$this->send_headers();
		$this->set_current_user_id();
		if ( $this->is_iframe_request() ) {
			$this->process_iframe_request();
		}
	}

	/**
	 * @param string  $user_login
	 * @param WP_User $user
	 */
	public function wp_login_action( $user_login, WP_User $user ) {
		$this->init_sso_transients( (int) $user->ID );
	}

	/**
	 * @param string           $redirect_to
	 * @param string           $requested_redirect_to
	 * @param WP_User|WP_Error $user
	 *
	 * @return string
	 */
	public function add_redirect_user_token( $redirect_to, $requested_redirect_to, $user ) {
		if ( ! is_wp_error( $user ) && ! $this->is_sso_started() ) {
			$this->init_sso_transients( (int) $user->ID );

			return add_query_arg( self::IFRAME_USER_TOKEN_KEY, $this->create_user_token( $user->ID ), $redirect_to );
		}

		return $redirect_to;
	}

	public function add_iframes_to_footer() {
		$is_user_logged_in = is_user_logged_in();

		if ( $is_user_logged_in && $this->is_sso_started() ) {
			$this->save_session_token( wp_get_session_token(), $this->current_user_id );
		}

		foreach ( $this->domains as $domain ) {
			if ( $domain !== $this->get_current_domain() && $this->is_sso_started_for_domain( $domain ) ) {

				$iframe_url = add_query_arg(
					[
						self::IFRAME_DOMAIN_HASH_KEY => $this->get_hash( $domain ),
						self::IFRAME_USER_STATUS_KEY => $is_user_logged_in ? 'wpml_user_signed_in' : 'wpml_user_signed_out',
						self::IFRAME_USER_TOKEN_KEY_FOR_DOMAIN => $this->create_user_token_for_domains( $this->current_user_id ),
					],
					trailingslashit( $domain )
				);
				?>
				<iframe class="wpml_iframe" style="display:none" src="<?php echo esc_url( $iframe_url ); ?>"></iframe>
				<?php
			}
		}
	}

	private function send_headers() {
		header( sprintf( 'Content-Security-Policy: frame-ancestors %s', implode( ' ', $this->domains ) ) );
	}

	/** @param int $user_id */
	private function set_current_user_id( $user_id = null ) {
		if ( $user_id ) {
			$this->current_user_id = $user_id;
		} else {
			$this->current_user_id = $this->get_user_id_from_token() ?: get_current_user_id();
		}
	}

	private function process_iframe_request() {
		if ( $this->validate_user_sign_request() ) {
			nocache_headers();
			wp_clear_auth_cookie();

			if ( $_GET[ self::IFRAME_USER_STATUS_KEY ] == 'wpml_user_signed_in' ) {
				$user = get_user_by( 'id', $this->current_user_id );

				if ( $user !== false ) {
					wp_set_current_user( $this->current_user_id );

					if ( is_ssl() ) {
						$this->set_auth_cookie(
							$this->current_user_id,
							$this->get_session_token( $this->current_user_id )
						);
					} else {
						wp_set_auth_cookie(
							$this->current_user_id,
							false,
							'',
							$this->get_session_token( $this->current_user_id )
						);
					}
					do_action( 'wp_login', $user->user_login, $user );
				}
			} else {
				$sessions = WP_Session_Tokens::get_instance( $this->current_user_id );
				$sessions->destroy_all();
			}
			$this->finish_sso_for_domain( $this->get_current_domain() );
		}

		$this->php_functions->exit_php();
	}

	/** @return bool */
	private function validate_user_sign_request() {
		return isset( $_GET[ self::IFRAME_USER_STATUS_KEY ] )
			   && $this->is_sso_started_for_domain( $this->get_current_domain() );
	}

	/** @return int */
	private function get_user_id_from_token() {
		$user_id = 0;

		if ( isset( $_GET[ self::IFRAME_USER_TOKEN_KEY ] ) ) {
			$transient_key = $this->create_transient_key(
				self::TRANSIENT_USER,
				null,
				Sanitize::stringProp( self::IFRAME_USER_TOKEN_KEY, $_GET )
			);
			$user_id       = (int) get_transient( $transient_key );
			delete_transient( $transient_key );
		} elseif ( isset( $_GET[ self::IFRAME_USER_TOKEN_KEY_FOR_DOMAIN ] ) ) {
			$transient_key = $this->create_transient_key(
				self::TRANSIENT_USER,
				$this->get_current_domain(),
				Sanitize::stringProp( self::IFRAME_USER_TOKEN_KEY_FOR_DOMAIN, $_GET )
			);
			$user_id       = (int) get_transient( $transient_key );
			delete_transient( $transient_key );
		}

		return $user_id;
	}

	/**
	 * @param int $user_id
	 */
	private function init_sso_transients( $user_id ) {
		set_transient( self::TRANSIENT_SSO_STARTED, true, self::SSO_TIMEOUT );

		foreach ( $this->domains as $domain ) {
			if ( $this->get_current_domain() !== $domain ) {
				set_transient(
					$this->create_transient_key( self::TRANSIENT_DOMAIN, $domain, $user_id ),
					$this->get_hash( $domain ),
					self::SSO_TIMEOUT
				);
			}
		}
	}

	/**
	 * @param string $domain
	 */
	private function finish_sso_for_domain( $domain ) {
		delete_transient(
			$this->create_transient_key(
				self::TRANSIENT_DOMAIN,
				$domain,
				$this->current_user_id
			)
		);
	}

	/**
	 * @param string $domain
	 *
	 * @return bool
	 */
	private function is_sso_started_for_domain( $domain ) {
		return (bool) get_transient(
			$this->create_transient_key(
				self::TRANSIENT_DOMAIN,
				$domain,
				$this->current_user_id
			)
		);
	}

	/**
	 * @return string
	 */
	private function get_current_domain() {
		$host = '';

		if ( array_key_exists( 'HTTP_HOST', $_SERVER ) ) {
			$host = (string) $_SERVER['HTTP_HOST'];
		}

		return $this->get_current_protocol() . $host;
	}

	/**
	 * @return string
	 */
	private function get_current_protocol() {
		return is_ssl() ? 'https://' : 'http://';
	}

	/**
	 * @return array
	 */
	private function get_domains() {
		$domains = $this->sitepress->get_setting( 'language_domains', array() );

		$active_codes = array_keys( $this->sitepress->get_active_languages() );
		$sso_domains  = array( $this->site_url );

		foreach ( $domains as $language_code => $domain ) {
			if ( in_array( $language_code, $active_codes ) ) {
				$sso_domains[] = $this->get_current_protocol() . $domain;
			}
		}

		return $sso_domains;
	}

	/**
	 * @return bool
	 */
	private function is_iframe_request() {
		return isset( $_GET[ self::IFRAME_DOMAIN_HASH_KEY ] )
			   && ! wpml_is_ajax()
			   && $this->is_sso_started_for_domain( $this->get_current_domain() )
			   && $this->get_hash( $this->get_current_domain() ) === $_GET[ self::IFRAME_DOMAIN_HASH_KEY ];
	}

	/**
	 * @return bool
	 */
	private function is_sso_started() {
		return (bool) get_transient( self::TRANSIENT_SSO_STARTED );
	}

	/**
	 * @param int $user_id
	 *
	 * @return string
	 */
	private function create_user_token( $user_id ) {
		$token = wp_create_nonce( self::SSO_NONCE );
		set_transient(
			$this->create_transient_key( self::TRANSIENT_USER, null, $token ),
			$user_id,
			self::SSO_TIMEOUT
		);

		return $token;
	}

	/**
	 * @param int $user_id
	 *
	 * @return bool|string
	 */
	private function create_user_token_for_domains( $user_id ) {
		$token = wp_create_nonce( self::SSO_NONCE );
		foreach ( $this->domains as $domain ) {
			if ( $this->get_current_domain() !== $domain ) {
				set_transient(
					$this->create_transient_key( self::TRANSIENT_USER, $domain, $token ),
					$user_id,
					self::SSO_TIMEOUT
				);
			}
		}

		return $token;
	}

	/**
	 * @param string $session_token
	 * @param int    $user_id
	 */
	private function save_session_token( $session_token, $user_id ) {
		set_transient(
			$this->create_transient_key( self::TRANSIENT_SESSION_TOKEN, null, $user_id ),
			$session_token,
			self::SSO_TIMEOUT
		);
	}

	/**
	 * @param int $user_id
	 *
	 * @return string
	 */
	private function get_session_token( $user_id ) {
		return (string) get_transient( $this->create_transient_key( self::TRANSIENT_SESSION_TOKEN, null, $user_id ) );
	}

	/**
	 * @param string      $prefix
	 * @param string|null $domain
	 * @param string|null $token
	 *
	 * @return string
	 */
	private function create_transient_key( $prefix, $domain = null, $token = null ) {
		return $prefix . ( $token !== null ? $token : '' ) . ( $domain ? '_' . $this->get_hash( $domain ) : '' );
	}

	/**
	 * @param string $value
	 *
	 * @return string
	 */
	private function get_hash( $value ) {
		return hash( 'sha256', self::SSO_NONCE . $value );
	}


	/**
	 * As the WP doesn't support "SameSite" parameter in cookies, we have to write our own
	 * function for saving authentication cookies to work with iframes.
	 *
	 * @param int $user_id
	 * @param string $token
	 */
	private function set_auth_cookie( $user_id, $token = '' ) {
		$expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user_id, false );
		$expire     = 0;
		$secure     = apply_filters( 'secure_auth_cookie', is_ssl(), $user_id );

		if ( $secure ) {
			$auth_cookie_name = SECURE_AUTH_COOKIE;
			$scheme           = 'secure_auth';
		} else {
			$auth_cookie_name = AUTH_COOKIE;
			$scheme           = 'auth';
		}

		if ( '' === $token ) {
			$manager = WP_Session_Tokens::get_instance( $user_id );
			$token   = $manager->create( $expiration );
		}

		$auth_cookie      = wp_generate_auth_cookie( $user_id, $expiration, $scheme, $token );
		$logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token );

		do_action( 'set_auth_cookie', $auth_cookie, $expire, $expiration, $user_id, $scheme, $token );
		do_action( 'set_logged_in_cookie', $logged_in_cookie, $expire, $expiration, $user_id, 'logged_in', $token );

		if ( ! apply_filters( 'send_auth_cookies', true ) ) {
			return;
		}

		$this->wpml_cookie->set_cookie( $auth_cookie_name, $auth_cookie, $expire, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN, true, 'None' );
		$this->wpml_cookie->set_cookie( $auth_cookie_name, $auth_cookie, $expire, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, true, 'None' );
		$this->wpml_cookie->set_cookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, COOKIEPATH, COOKIE_DOMAIN, true, 'None' );

		if ( COOKIEPATH != SITECOOKIEPATH ) {
			$this->wpml_cookie->set_cookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, true, 'None' );
		}
	}
}
