<?php

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

/**
 * Class DLM_Cookie_Manager
 *
 * @since 4.0
 */
class DLM_Cookie_Manager {

	/**
	 * Cookie key
	 *
	 * @var string
	 */
	public static $key = 'wp-dlm_cookie';

	/**
	 * Holds the class object.
	 *
	 * @var object
	 *
	 * @since 4.3.1
	 */
	public static $instance;

	/**
	 * Class constructor
	 *
	 * @since 5.0.0
	 */
	private function __construct() {
		// Delete expired cookies
		add_action( 'dlm_monthly_event', array( $this, 'delete_expired_cookies' ) );
	}

	/**
	 * Returns the singleton instance of the class
	 *
	 * @return object The DLM_Cookie_Manager object.
	 *
	 * @since 5.0.0
	 */
	public static function get_instance() {
		if ( ! isset( self::$instance ) && ! ( self::$instance instanceof DLM_Cookie_Manager ) ) {
			self::$instance = new DLM_Cookie_Manager();
		}

		return self::$instance;
	}


	/**
	 * Check if the cookie exists for this download & version. If it does exist the requester has requested the exact
	 * same download & version in the past minute.
	 *
	 * Deprecated since 5.0.0
	 *
	 * @param  DLM_Download  $download
	 *
	 * @return bool
	 */
	public static function exists( $download ) {
		$exists = false;

		// get JSON data
		$cdata = self::get_cookie_data();

		// check if no parse errors occurred
		if ( null != $cdata && is_array( $cdata ) && ! empty( $cdata ) ) {
			// check in cookie data for download AND version ID
			if ( $cdata['download'] == $download->get_id() && $cdata['version'] == $download->get_version()->get_version_number() ) {
				$exists = true;
			}
		}

		return $exists;
	}

	/**
	 * Get cookie data
	 *
	 * Deprecated since 5.0.0
	 *
	 * @return array|null
	 */
	public static function get_cookie_data() {
		$cdata = null;
		if ( ! empty( $_COOKIE[ self::$key ] ) ) {
			$cdata = json_decode( base64_decode( sanitize_text_field( wp_unslash( $_COOKIE[ self::$key ] ) ) ), true );
		}

		return $cdata;
	}

	/**
	 * Add new cookie
	 *
	 * @param  DLM_Download  $download     Download object
	 * @param  array         $cookie_data  Cookie data
	 *
	 * @since 5.0.0
	 */
	private function add_cookie( $download, $cookie_data ) {

		// If Download object or object id.
		if ( ! is_object( $download ) ) {
			// No object? let's get it!
			$download = download_monitor()->service( 'download_repository' )->retrieve_single( absint( $download ) );
		}

		$secure      = is_ssl();
		$cookie_data = wp_parse_args(
			$cookie_data,
			array(
				'expires'  => time() + 60,
				'secure'   => $secure,
				'httponly' => true,
				'meta'     => array(),
			)
		);

		/**
		 * Filter cookie data
		 * Old hook used to set cookie data for the wp_dlm_downloading cookie.
		 * Deprecated since 5.0.0
		 *
		 * @hook  wp_dlm_set_downloading_cookie
		 *
		 * @param  array  $cookie_data
		 *
		 * @since 4.0
		 *
		 */
		$cookie_data = apply_filters(
			'wp_dlm_set_downloading_cookie',
			$cookie_data
		);

		/**
		 * Filter cookie data
		 * New hook used to set cookie data for the general wp-dlm_cookie cookie.
		 * The cookie_data should contain the following:
		 * - expires: The expiration time of the cookie
		 * - secure: Whether the cookie should be secure or not
		 * - httponly: Whether the cookie should be httponly or not
		 * - meta: An array of meta data to be stored in the database in the cookie meta table. Each meta item should be
		 * an array and will be stored as a separate row in the database similar to post meta.
		 *
		 * @hook  _dlm_cookie_data
		 *
		 * @param  array  $cookie_data
		 *
		 * @since 5.0.0
		 *
		 */
		$cookie_data = apply_filters(
			'_dlm_cookie_data',
			$cookie_data
		);
		// This is used for the cookie expiration date difference for session cookies as we can't set the cookie to expire
		// at the end of the session in the DB.
		$cookie_expires = $cookie_data['expires'];

		// If set to 0, set cookie to expire in 1 day from the set date. This is used for session cookies.
		if ( 0 === $cookie_data['expires'] ) {
			$cookie_data['expires'] = time() + DAY_IN_SECONDS;
		}

		// Create cookie hash using download ID, site name, 'dlm' string and random number
		$hash = wp_hash( $download->get_id() . 'dlm' . get_bloginfo( 'name' ) . wp_rand() );

		// Insert cookie into database
		$this->insert_cookie( $hash, $download, $cookie_data );

		// Set the cookie
		setcookie(
			self::$key,
			$hash,
			$cookie_expires,
			apply_filters( 'dlm_cookie_path', COOKIEPATH ),
			COOKIE_DOMAIN,
			$cookie_data['secure'],
			$cookie_data['httponly']
		);

		return $hash;
	}

	/**
	 * Set cookie
	 *
	 * @param  DLM_Download  $download
	 */
	private function update_cookie( $hash, $download, $cookie_data = array() ) {
		$secure      = is_ssl();
		$cookie_data = wp_parse_args(
			$cookie_data,
			array(
				'expires'  => time() + 60,
				'secure'   => $secure,
				'httponly' => true,
				'meta'     => array(),
			)
		);

		/**
		 * Filter cookie data
		 * Old hook used to set cookie data for the wp_dlm_downloading cookie.
		 * Deprecated since 5.0.0
		 *
		 * @hook  wp_dlm_set_downloading_cookie
		 *
		 * @param  array  $cookie_data
		 *
		 * @since 4.0
		 *
		 */
		$cookie_data = apply_filters(
			'wp_dlm_set_downloading_cookie',
			$cookie_data
		);

		/**
		 * Filter cookie data
		 * New hook used to set cookie data for the general wp-dlm_cookie cookie.
		 * The cookie_data should contain the following:
		 * - expires: The expiration time of the cookie
		 * - secure: Whether the cookie should be secure or not
		 * - httponly: Whether the cookie should be httponly or not
		 * - meta: An array of meta data to be stored in the database in the cookie meta table. Each meta item should be
		 * an array and will be stored as a separate row in the database similar to post meta.
		 *
		 * @hook  _dlm_cookie_data
		 *
		 * @param  array  $cookie_data
		 *
		 * @since 5.0.0
		 *
		 */
		$cookie_data = apply_filters(
			'_dlm_cookie_data',
			$cookie_data
		);

		// Insert cookie into database
		$this->update_cookie_db( $hash, $download, $cookie_data );
		$cookie_id         = $this->get_cookie_id( $hash );
		$cookie_expiration = $this->get_cookie_expiration_date( $cookie_id );

		// Check if cookie expiration date is less than the new expiration date. If so, update the cookie expiration date
		if ( strtotime( $cookie_expiration ) < $cookie_data['expires'] ) {
			// Set the cookie only if the cookie expiration date is less than the new expiration date
			setcookie(
				self::$key,
				$hash,
				$cookie_data['expires'],
				apply_filters( 'dlm_cookie_path', COOKIEPATH ),
				COOKIE_DOMAIN,
				$cookie_data['secure'],
				$cookie_data['httponly']
			);
		}
	}

	/**
	 * Set cookie
	 *
	 * @param  int|DLM_Download  $download     Download object
	 * @param  array         $cookie_data  Optional cookie data
	 *
	 * @since 5.0.0
	 */
	public function set_cookie( $download, $cookie_data = array() ) {
		// Check if cookie hash is set
		$hash = $this->get_cookie_hash();
		// If hash is not set, create a new cookie, else update the cookie
		if ( ! $hash ) {
			$hash = $this->add_cookie( $download, $cookie_data );
		} else {
			$this->update_cookie( $hash, $download, $cookie_data );
		}

		return $hash;
	}

	/**
	 * Insert cookie into database
	 * The cookie_data should contain the following:
	 *  - expires: The expiration time of the cookie
	 *  - secure: Whether the cookie should be secure or not
	 *  - httponly: Whether the cookie should be httponly or not
	 *  - meta: An array of meta data to be stored in the database in the cookie meta table. Each meta item should be
	 *  an array and will be stored as a separate row in the database similar to post meta.
	 *
	 * @param $hash        string Cookie hash
	 * @param $download    DLM_Download Download object
	 * @param $cookie_data array Cookie data
	 *
	 * @return int Cookie ID
	 *
	 * @since 5.0.0
	 */
	private function insert_cookie( $hash, $download, $cookie_data ) {
		global $wpdb;

		$wpdb->insert(
			$wpdb->prefix . 'dlm_cookies',
			array(
				'hash'            => $hash,
				'creation_date'   => current_time( 'mysql' ),
				'expiration_date' => wp_date( 'Y-m-d H:i:s', $cookie_data['expires'] ),
			),
			array(
				'%s',
				'%s',
				'%s',
			)
		);

		$cookie_id = $wpdb->insert_id;
		// Check if cookie meta is set
		if ( ! empty( $cookie_data['meta'] ) ) {
			// Cycle through meta and insert into database
			foreach ( $cookie_data['meta'] as $meta ) {
				$meta_key   = key( $meta );
				$meta_value = $meta[ $meta_key ];
				$wpdb->insert(
					$wpdb->prefix . 'dlm_cookiemeta',
					array(
						'cookie_id' => $cookie_id,
						'meta_key'  => $meta_key,
						'meta_data' => $meta_value,
					),
					array(
						'%d',
						'%s',
						'%s',
					)
				);
			}
		}

		return $cookie_id;
	}

	/**
	 * Update cookie from database
	 * The cookie_data should contain the following:
	 *  - expires: The expiration time of the cookie
	 *  - secure: Whether the cookie should be secure or not
	 *  - httponly: Whether the cookie should be httponly or not
	 *  - meta: An array of meta data to be stored in the database in the cookie meta table. Each meta item should be
	 *  an array and will be stored as a separate row in the database similar to post meta.
	 *
	 * @param $hash        string Cookie hash
	 * @param $download    DLM_Download Download object
	 * @param $cookie_data array Cookie data
	 *
	 *
	 * @since 5.0.0
	 */
	private function update_cookie_db( $hash, $download, $cookie_data ) {
		global $wpdb;
		$cookie_id         = $this->get_cookie_id( $hash );
		$cookie_expiration = $this->get_cookie_expiration_date( $cookie_id );
		// Check if cookie expiration date is less than the new expiration date. If so, update the cookie expiration date
		if ( strtotime( $cookie_expiration ) < $cookie_data['expires'] ) {
			$wpdb->update(
				$wpdb->prefix . 'dlm_cookies',
				array(
					'expiration_date' => wp_date( 'Y-m-d H:i:s', $cookie_data['expires'] ),
				),
				array(
					'id' => $cookie_id,
				),
				array(
					'%s',
				),
				array(
					'%d',
				)
			);
		}
		// Check if cookie meta is set
		if ( ! empty( $cookie_data['meta'] ) ) {
			// Cycle through meta and insert into database
			foreach ( $cookie_data['meta'] as $meta ) {
				$meta_key   = key( $meta );
				$meta_value = $meta[ $meta_key ];
				$wpdb->insert(
					$wpdb->prefix . 'dlm_cookiemeta',
					array(
						'cookie_id' => $cookie_id,
						'meta_key'  => $meta_key,
						'meta_data' => $meta_value,
					),
					array(
						'%d',
						'%s',
						'%s',
					)
				);
			}
		}
	}

	/**
	 * Get cookie hash
	 *
	 * @return bool|string
	 *
	 * @since 5.0.0
	 */
	public function get_cookie_hash() {
		if ( ! empty( $_COOKIE[ self::$key ] ) ) {
			return sanitize_text_field( wp_unslash( $_COOKIE[ self::$key ] ) );
		}

		return false;
	}

	/**
	 * Get cookie ID from the database
	 *
	 * @param $hash string Cookie hash
	 *
	 * @return mixed
	 *
	 * @since 5.0.0
	 */
	public function get_cookie_id( $hash ) {
		global $wpdb;

		return $wpdb->get_var( $wpdb->prepare( "SELECT `id` FROM {$wpdb->prefix}dlm_cookies WHERE `hash` = %s", sanitize_text_field( $hash ) ) );
	}

	/**
	 * Get cookie creation date from the database
	 *
	 * @param $id int Cookie ID
	 *
	 * @return string|null
	 *
	 * @since 5.0.0
	 */
	public function get_cookie_set_date( $id ) {
		global $wpdb;

		return $wpdb->get_var( $wpdb->prepare( "SELECT `creation_date` FROM {$wpdb->prefix}dlm_cookies WHERE `id` = %s", absint( $id ) ) );
	}

	/**
	 * Get cookie expiration date from the database
	 *
	 * @param $id int Cookie ID
	 *
	 * @return mixed
	 *
	 * @since 5.0.0
	 */
	public function get_cookie_expiration_date( $id ) {
		global $wpdb;

		return $wpdb->get_var( $wpdb->prepare( "SELECT `expiration_date` FROM {$wpdb->prefix}dlm_cookies WHERE `id` = %s", absint( $id ) ) );
	}

	/**
	 * Get cookie meta data from the database
	 *
	 * @param $id          int Cookie ID
	 * @param $meta_key    string Meta key
	 *
	 * @return array|object|stdClass[]|null
	 *
	 * @since 5.0.0
	 */
	public function get_cookie_meta_data( $id, $meta_key ) {
		global $wpdb;

		return $wpdb->get_results( $wpdb->prepare( "SELECT `meta_data` FROM {$wpdb->prefix}dlm_cookiemeta WHERE `cookie_id` = %d AND `meta_key` = %s", absint( $id ), sanitize_text_field( $meta_key ) ) );
	}


	/**
	 * Get the cookie from the database
	 *
	 * @param $hash string Cookie hash
	 *
	 * @return array|object|stdClass|null
	 *
	 * @since 5.0.0
	 */
	private function get_cookie( $hash ) {
		global $wpdb;

		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}dlm_cookies WHERE `hash` = %s;", sanitize_text_field( $hash ) ) );
	}

	/**
	 * Check the cookie meta data
	 *
	 * @param $meta_key    string Meta key
	 * @param $value       string Meta value to check
	 * @param $check_type  string Check type. Can be 'per_download', 'grouped' or 'global'. Default is 'individual'
	 * @param $check_array array Array of meta keys to check. Default is empty array
	 *
	 * @return bool
	 *
	 * @since 5.0.0
	 */
	public function check_cookie_meta( $meta_key, $value, $check_type = 'per_download', $check_array = array() ) {
		// Get cookie hash
		$hash   = $this->get_cookie_hash();
		$return = false;
		// No hash found. Return false
		if ( ! $hash ) {
			return $return;
		}
		// Get cookie ID from database
		$cookie_id = $this->get_cookie_id( $hash );

		// No cookie ID found. Return false
		if ( ! $cookie_id ) {
			return $return;
		}
		// Check if cookie has expired
		if ( $this->check_expired_cookie( $cookie_id ) ) {
			return $return;
		}
		// Get cookie meta data from database
		$meta = $this->get_cookie_meta_data( $cookie_id, $meta_key );

		// Check if meta data is set
		if ( ! empty( $meta ) ) {
			// If check type is 'all' return true because meta data exists, so an unlock was previously made
			if ( 'global' === $check_type ) {
				$return = true;
			} else {
				// Cycle through meta data and check if value exists or if value is in group of values
				foreach ( $meta as $meta_item ) {
					switch ( $check_type ) {
						case 'per_download':
							// Check if value exists. Do not use strict comparison here as the meta data is stored as
							// a string in the database and the value might be an integer.
							if ( $meta_item->meta_data == $value ) {
								$return = true;
							}
							break;
						case 'grouped':
							if ( ! empty( $check_array ) ) {
								if ( in_array( $meta_item->meta_data, $check_array ) ) {
									$return = true;
								}
							}
							break;
						default:
							/**
							 * Filter cookie meta check. Allow other plugins to add their own check types based on the
							 * check type
							 *
							 * @hook  dlm_cookie_meta_check
							 *
							 * @param  bool    $return       Return value
							 * @param  object  $meta_item    Meta item object
							 * @param  string  $value        Value to check
							 * @param  array   $check_array  Array of values to check
							 *
							 * @since 4.9.5
							 *
							 */
							$return = apply_filters( 'dlm_cookie_meta_check_' . $check_type, $return, $meta_item, $value, $check_array );
							break;
					}
				}
			}
		}

		return $return;
	}

	/**
	 * Delete cookie from database by ID
	 *
	 * @param $cookie_id int Cookie hash
	 *
	 * @since 5.0.0
	 */
	public function delete_cookie( $cookie_id ) {
		global $wpdb;
		// Delete entry from dlm_cookies table
		$wpdb->delete( $wpdb->prefix . 'dlm_cookies', array( 'id' => absint( $cookie_id ) ) );
		// Delete entry from dlm_cookiemeta table
		$wpdb->delete( $wpdb->prefix . 'dlm_cookiemeta', array( 'cookie_id' => absint( $cookie_id ) ) );
	}

	/**
	 * Check expired cookie by ID
	 *
	 * @param $cookie_id int Cookie ID
	 *
	 * @return bool
	 *
	 * @since 5.0.0
	 */
	public function check_expired_cookie( $cookie_id ) {
		$expiration_date = $this->get_cookie_expiration_date( $cookie_id );
		// Check if cookie has expired
		if ( $expiration_date < current_time( 'mysql' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Delete expired cookies.
	 *
	 * @since 5.0.0
	 */
	public function delete_expired_cookies() {
		global $wpdb;

		// Select expired cookie IDs
		$expired_cookie_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}dlm_cookies WHERE expiration_date < %s",
				current_time( 'mysql' )
			)
		);

		if ( empty( $expired_cookie_ids ) ) {
			return;
		}

		$ids = implode( ',', array_map( 'absint', $expired_cookie_ids ) );

		// Delete all related metadata in one query
		$wpdb->query(
			"DELETE FROM {$wpdb->prefix}dlm_cookiemeta WHERE cookie_id IN ({$ids})"
		);

		// Delete all cookies in one query
		$wpdb->query(
			"DELETE FROM {$wpdb->prefix}dlm_cookies WHERE id IN ({$ids})"
		);
	}
}
