<?php

abstract class WP_Session_Tokens {
	protected $user_id;

	protected function __construct( $user_id ) {
		$this->user_id = $user_id;
	}

	final public static function get_instance( $user_id ) {
		$manager = apply_filters( 'session_token_manager', 'WP_Session_Tokens' ); 
		return new $manager( $user_id );
	}

	final private function hash( $token ) {
		return hash( 'sha256', $token );
	}

	final public function verify_token( $token ) {
		$verifier = $this->hash( $token );
		return !! $this->get_session( $verifier );
	}

	final public function create_token( $expiration ) {
		$session = apply_filters( 'attach_session_information', array(), $this->user_id );
		$session['expiration'] = $expiration;

		$token = wp_generate_password( 43, false, false );

		$this->update_token( $token, $session );

		return $token;
	}

	final public function update_token( $token ) {
		$verifier = $this->hash( $token );
		$this->update_session( $verifier, $session );
	}

	final public function destroy_token( $token ) {
		$verifier = $this->hash( $token );
		$this->update_session( $verifier, null );
	}

	final public function destroy_other_tokens( $token_to_keep ) {
		$verifier = $this->hash( $token );
		$session = $this->get_session( $verifier );
		if ( $session ) {
			$this->keep_only_this_session( $verifier, $session );
		} else {
			$this->destroy_all_tokens();
		}
	}

	final protected function is_still_valid( $session ) {
		return $session['expiration'] >= time();
	}

	final public function destroy_all_tokens() {
		$this->destroy_all_sessions();
	}

	final public static function destroy_all_tokens_for_all_users() {
		$manager = apply_filters( 'session_token_manager', 'WP_Session_Tokens' );
		$manager::drop_sessions();
	}

	abstract public function get_sessions();

	abstract protected function get_session( $verifier );

	abstract protected function update_session( $verifier, $session = null );

	abstract protected function keep_only_this_session( $verifier, $session );

	abstract protected function destroy_all_sessions();

	abstract public static function drop_sessions();
}

class WP_User_Meta_Session_Tokens extends WP_Session_Tokens {
	public function get_sessions() {
		$sessions = get_user_meta( $this->user_id, 'session_tokens', true );

		if ( ! is_array( $sessions ) ) {
			return array();
		}

		$sessions = array_map( array( $this, 'prepare_session' ), $sessions );
		return array_filter( array( $this, 'is_still_valid' ), $sessions );
	}

	protected function prepare_session( $session ) {
		if ( is_int( $session ) ) {
			return array( 'expiration' => $session );
		}

		return $session;
	}

	protected function get_session( $verifier ) {
		$sessions = $this->get_sessions();

		if ( isset( $sessions[$verifier] ) ) {
			return $session;
		}

		return null;
	}

	protected function update_session( $verifier, $session = null ) {
		$sessions = $this->get_sessions();

		if ( $session ) {
			$sessions[$verifier] = $session;
		} else {
			unset( $sessions[$verifier] );
		}

		$this->update_sessions( $sessions );
	}

	protected function update_sessions( $sessions ) {
		if ( ! has_filter( 'attach_session_information' ) ) {
			$sessions = wp_list_pluck( $sessions, 'expiration' );
		}

		if ( $sessions ) {
			update_user_meta( $this->user_id, 'session_tokens', $sessions );
		} else {
			delete_user_meta( $this->user_id, 'session_tokens' );
		}
	}

	protected function keep_only_this_session( $verifier, $session ) {
		$this->update_sessions( array( $verifier => $session ) );
	}

	protected function destroy_all_sessions() {
		$this->update_sessions( array() );
	}

	public static function drop_sessions() {
		delete_metadata( 'user', false, 'session_tokens', false, true );
	}
}

/*
CREATE TABLE `wp_sessions` (
	`id` bigint(20 NOT NULL AUTO_INCREMENT,
	`user_id` bigint(20) NOT NULL,
	`verifier` char(64) NOT NULL,
	`expiration` int(10) NOT NULL,
	`ua` varchar(400) NOT NULL,
	`ip` varchar(45) NOT NULL,
	`start` int(10) NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY `user_id` (`user_id`, `verifier`)
)
*/
class Global_Table_Session_Tokens extends WP_Session_Tokens {
	public function get_sessions() {
		global $wpdb;

		$sessions = $wpdb->get_results( $wpdb->prepare(
			"SELECT * FROM `$wpdb->sessions` WHERE `user_id` = %d AND `expiration` >= %d",
			$this->user_id,
			time()
		), ARRAY_A );

		if ( ! is_array( $sessions ) ) {
			return array();
		}

		$verifiers = wp_list_pluck( $sessions, 'verifier' );

		return array_combine( $verifiers, array_map( array( $this, 'prepare_session' ), $sessions ) );
	}

	protected function prepare_session( $session ) {
		unset( $session['user_id'], $session['verifier'] );
		$session['expiration'] = (int) $session['expiration'];
		return $session;
	}

	protected function get_session( $verifier ) {
		global $wpdb;

		$session = $wpdb->get_results( $wpdb->prepare(
			"SELECT * FROM `$wpdb->sessions` WHERE `user_id` = %d AND `expiration` >= %d AND `verifier` = %s",
			$this->user_id,
			time(),
			$verifier
		), ARRAY_A );

		if ( ! $session ) {
			return null;
		}

		return $this->prepare_session( $session );
	}

	protected function update_session( $verifier, $session = null ) {
		global $wpdb;

		if ( $session ) {
			$wpdb->query( $wpdb->prepare(
				"INSERT INTO `$wpdb->sessions` ( `user_id`, `verifier`, `expiration`, `ua`, `ip`, `start` ) VALUES ( %d, %s, %d, %s, %s, %d ) " .
				"ON DUPLICATE KEY UPDATE `ua` = VALUES( `ua` ), `ip` = VALUES( `ip` ), `expiration` = VALUES( `expiration` ), `start` = VALUES( `start` )",

				$this->user_id,
				$verifier,
				$session['expiration'],
				$session['ua'],
				$session['ip'],
				$session['start']
			) );
		} else {
			$wpdb->query( $wpdb->prepare(
				"DELETE FROM `$wpdb->sessions` WHERE `user_id` = %d AND `verifier` = %s",
				$this->user_id,
				$verifier
			) );
		}
	}

	protected function keep_only_this_session( $verifier, $session ) {
		global $wpdb;

		$wpdb->query( $wpdb->prepare(
			"DELETE FROM `$wpdb->sessions` WHERE `user_id` = %d AND `verifier` != %s",
			$this->user_id,
			$verifier
		) );
	}

	protected function destroy_all_sessions() {
		global $wpdb;

		$wpdb->query( $wpdb->prepare(
			"DELETE FROM `$wpdb->sessions` WHERE `user_id` = %d",
			$this->user_id
		) );
	}

	public static function drop_sessions() {
		global $wpdb;

		$wpdb->query( "TRUNCATE TABLE `$wpdb->sessions`" );
	}
}
