HEX
Server: Apache
System: Linux digivps 5.15.0-163-generic #173-Ubuntu SMP Tue Oct 14 17:51:00 UTC 2025 x86_64
User: root (0)
PHP: 8.3.15
Disabled: passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
Upload Files
File: /www/wwwroot/biographybirthday.com/wp-content/plugins/post-views-counter/includes/class-counter.php
<?php
// exit if accessed directly
if ( ! defined( 'ABSPATH' ) )
	exit;

/**
 * Post_Views_Counter_Counter class.
 *
 * @class Post_Views_Counter_Counter
 */
class Post_Views_Counter_Counter {

	const GROUP = 'pvc';
	const NAME_ALLKEYS = 'cached_key_names';
	const CACHE_KEY_SEPARATOR = '.';
	const MAX_INSERT_STRING_LENGTH = 25000;

	private $queue = [];
	private $queue_mode = false;
	private $db_insert_values = '';
	private $cookie = [
		'exists'		 => false,
		'visited_posts'	 => [],
		'expiration'	 => 0
	];

	/**
	 * Class constructor.
	 *
	 * @return void
	 */
	public function __construct() {
		// actions
		add_action( 'plugins_loaded', [ $this, 'check_cookie' ], 1 );
		add_action( 'init', [ $this, 'init_counter' ] );
		add_action( 'deleted_post', [ $this, 'delete_post_views' ] );
	}

	/**
	 * Add Post ID to queue.
	 *
	 * @param int $post_id
	 * @return void
	 */
	public function add_to_queue( $post_id ) {
		$this->queue[] = (int) $post_id;
	}

	/**
	 * Run manual pvc_view_post queue.
	 *
	 * @return void
	 */
	public function queue_count() {
		if ( isset( $_POST['action'], $_POST['ids'], $_POST['pvc_nonce'] ) && $_POST['action'] === 'pvc-view-posts' && wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-view-posts' ) !== false && $_POST['ids'] !== '' && is_string( $_POST['ids'] ) ) {
			// get post ids
			$ids = explode( ',', $_POST['ids'] );

			$counted = [];

			if ( ! empty( $ids ) ) {
				$ids = array_filter( array_map( 'intval', $ids ) );

				if ( ! empty( $ids ) ) {
					// turn on queue mode
					$this->queue_mode = true;

					foreach ( $ids as $id ) {
						$counted[$id] = ! ( $this->check_post( $id ) === null );
					}

					// turn off queue mode
					$this->queue_mode = false;
				}
			}

			echo json_encode(
				[
					'post_ids'	=> $ids,
					'counted'	=> $counted
				]
			);
		}

		exit;
	}

	/**
	 * Print JavaScript with queue in the footer.
	 *
	 * @return void
	 */
	public function print_queue_count() {
		// any ids to "view"?
		if ( ! empty( $this->queue ) ) {
			echo "
			<script>
				( function( window, document, undefined ) {
					document.addEventListener( 'DOMContentLoaded', function() {
						let pvcLoadManualCounter = function( url, counter ) {
							let pvcScriptTag = document.createElement( 'script' );

							// append script
							document.body.appendChild( pvcScriptTag );

							// set attributes
							pvcScriptTag.onload = counter;
							pvcScriptTag.onreadystatechange = counter;
							pvcScriptTag.src = url;
						};

						let pvcExecuteManualCounter = function() {
							let pvcManualCounterArgs = {
								url: '" . esc_url( admin_url( 'admin-ajax.php' ) ) . "',
								nonce: '" . wp_create_nonce( 'pvc-view-posts' ) . "',
								ids: '" . implode( ',', $this->queue ) . "'
							};

							// main javascript file was loaded?
							if ( typeof PostViewsCounter !== 'undefined' && PostViewsCounter.promise !== null ) {
								PostViewsCounter.promise.then( function() {
									PostViewsCounterManual.init( pvcManualCounterArgs );
								} );
							// PostViewsCounter is undefined or promise is null
							} else {
								PostViewsCounterManual.init( pvcManualCounterArgs );
							}
						}

						pvcLoadManualCounter( '" . POST_VIEWS_COUNTER_URL . "/js/counter" . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . ".js', pvcExecuteManualCounter );
					}, false );
				} )( window, document );
			</script>";
		}
	}

	/**
	 * Initialize counter.
	 *
	 * @return void
	 */
	public function init_counter() {
		// admin?
		if ( is_admin() && ! wp_doing_ajax() )
			return;

		// get main instance
		$pvc = Post_Views_Counter();

		add_action( 'wp_ajax_pvc-view-posts', [ $this, 'queue_count' ] );
		add_action( 'wp_ajax_nopriv_pvc-view-posts', [ $this, 'queue_count' ] );
		add_action( 'wp_print_footer_scripts', [ $this, 'print_queue_count' ], 11 );

		// php counter
		if ( $pvc->options['general']['counter_mode'] === 'php' )
			add_action( 'wp', [ $this, 'check_post_php' ] );
		// javascript (ajax) counter
		elseif ( $pvc->options['general']['counter_mode'] === 'js' ) {
			add_action( 'wp_ajax_pvc-check-post', [ $this, 'check_post_js' ] );
			add_action( 'wp_ajax_nopriv_pvc-check-post', [ $this, 'check_post_js' ] );
		// rest api counter
		} elseif ( $pvc->options['general']['counter_mode'] === 'rest_api' )
			add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
	}

	/**
	 * Check whether to count visit.
	 *
	 * @param int $id
	 * @return void|int
	 */
	public function check_post( $id = 0 ) {
		// short init?
		if ( defined( 'SHORTINIT' ) && SHORTINIT )
			$this->check_cookie();

		// get post id
		$id = (int) ( empty( $id ) ? get_the_ID() : $id );

		// empty id?
		if ( empty( $id ) )
			return;

		// get user id, from current user or static var in rest api request
		$user_id = get_current_user_id();

		// get user IP address
		$user_ip = $this->get_user_ip();

		do_action( 'pvc_before_check_visit', $id, $user_id, $user_ip );

		// get main instance
		$pvc = Post_Views_Counter();

		// get ips
		$ips = $pvc->options['general']['exclude_ips'];

		// whether to count this ip
		if ( ! empty( $ips ) && filter_var( preg_replace( '/[^0-9a-fA-F:., ]/', '', $user_ip ), FILTER_VALIDATE_IP ) ) {
			// check ips
			foreach ( $ips as $ip ) {
				if ( strpos( $ip, '*' ) !== false ) {
					if ( $this->ipv4_in_range( $user_ip, $ip ) )
						return;
				} else {
					if ( $user_ip === $ip )
						return;
				}
			}
		}

		// strict counts?
		if ( $pvc->options['general']['strict_counts'] ) {
			// get IP cached visits
			$ip_cache = get_transient( 'post_views_counter_ip_cache' );

			if ( ! $ip_cache )
				$ip_cache = [];

			// get user IP address
			$user_ip = $this->encrypt_ip( $user_ip );

			// visit exists in transient?
			if ( isset( $ip_cache[$id][$user_ip] ) ) {
				// get current time
				$current_time = current_time( 'timestamp', true );

				if ( $current_time < $ip_cache[$id][$user_ip] + $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'], false ) )
					return;
			}
		}

		// get groups to check them faster
		$groups = $pvc->options['general']['exclude']['groups'];

		// whether to count this user
		if ( ! empty( $user_id ) ) {
			// exclude logged in users?
			if ( in_array( 'users', $groups, true ) )
				return;
			// exclude specific roles?
			elseif ( in_array( 'roles', $groups, true ) && $this->is_user_role_excluded( $user_id, $pvc->options['general']['exclude']['roles'] ) )
				return;
		// exclude guests?
		} elseif ( in_array( 'guests', $groups, true ) )
			return;

		// whether to count robots
		if ( in_array( 'robots', $groups, true ) && $pvc->crawler_detect->is_crawler() )
			return;

		// cookie already existed?
		if ( $this->cookie['exists'] ) {
			// get current time if needed
			if ( ! isset( $current_time ) )
				$current_time = current_time( 'timestamp', true );

			// post already viewed but not expired?
			if ( in_array( $id, array_keys( $this->cookie['visited_posts'] ), true ) && $current_time < $this->cookie['visited_posts'][$id] ) {
				// update cookie but do not count visit
				$this->save_cookie( $id, $this->cookie, false );

				return;
			// update cookie
			} else
				$this->save_cookie( $id, $this->cookie );
		} else {
			// set new cookie
			$this->save_cookie( $id );
		}

		$count_visit = (bool) apply_filters( 'pvc_count_visit', true, $id );

		// count visit
		if ( $count_visit ) {
			// strict counts?
			if ( $pvc->options['general']['strict_counts'] )
				$this->save_ip( $id );

			return $this->count_visit( $id );
		}
	}

	/**
	 * Check whether to count visit via PHP request.
	 *
	 * @return void
	 */
	public function check_post_php() {
		// do not count admin entries
		if ( is_admin() && ! wp_doing_ajax() )
			return;

		// get main instance
		$pvc = Post_Views_Counter();

		// do we use php as counter?
		if ( $pvc->options['general']['counter_mode'] !== 'php' )
			return;

		// get countable post types
		$post_types = $pvc->options['general']['post_types_count'];

		// whether to count this post type
		if ( empty( $post_types ) || ! is_singular( $post_types ) )
			return;

		$this->check_post( get_the_ID() );
	}

	/**
	 * Check whether to count visit via JavaScript (AJAX) request.
	 *
	 * @return void
	 */
	public function check_post_js() {
		if ( isset( $_POST['action'], $_POST['id'], $_POST['pvc_nonce'] ) && $_POST['action'] === 'pvc-check-post' && ( $post_id = (int) $_POST['id'] ) > 0 && wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-check-post' ) !== false ) {
			// get main instance
			$pvc = Post_Views_Counter();

			// do we use javascript as counter?
			if ( $pvc->options['general']['counter_mode'] !== 'js' )
				exit;

			// get countable post types
			$post_types = $pvc->options['general']['post_types_count'];

			// check if post exists
			$post = get_post( $post_id );

			// whether to count this post type or not
			if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) )
				exit;

			echo json_encode(
				[
					'post_id'	=> $post_id,
					'counted'	=> ! ( $this->check_post( $post_id ) === null )
				]
			);
		}

		exit;
	}

	/**
	 * Check whether to count visit via REST API request.
	 *
	 * @param object $request
	 * @return int|WP_Error
	 */
	public function check_post_rest_api( $request ) {
		// get main instance
		$pvc = Post_Views_Counter();

		// get post id (already sanitized)
		$post_id = $request->get_param( 'id' );

		// do we use REST API as counter?
		if ( $pvc->options['general']['counter_mode'] !== 'rest_api' )
			return new WP_Error( 'pvc_rest_api_disabled', __( 'REST API method is disabled.', 'post-views-counter' ), [ 'status' => 404 ] );

		// @todo: get current user id in direct api endpoint calls
		// check if post exists
		$post = get_post( $post_id );

		if ( ! $post )
			return new WP_Error( 'pvc_post_invalid_id', __( 'Invalid post ID.', 'post-views-counter' ), [ 'status' => 404 ] );

		// get countable post types
		$post_types = $pvc->options['general']['post_types_count'];

		// whether to count this post type
		if ( empty( $post_types ) || ! in_array( $post->post_type, $post_types, true ) )
			return new WP_Error( 'pvc_post_type_excluded', __( 'Post type excluded.', 'post-views-counter' ), [ 'status' => 404 ] );

		return [
			'post_id'	=> $post_id,
			'counted'	=> ! ( $this->check_post( $post_id ) === null )
		];
	}

	/**
	 * Initialize cookie session.
	 *
	 * @param array $cookie Use this cookie instead of $_COOKIE
	 * @return void
	 */
	public function check_cookie( $cookie = [] ) {
		// do not run in admin except for ajax requests
		if ( is_admin() && ! wp_doing_ajax() )
			return;

		if ( empty( $cookie ) ) {
			// assign cookie name
			$cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );

			// is cookie set?
			if ( isset( $_COOKIE[$cookie_name] ) && ! empty( $_COOKIE[$cookie_name] ) )
				$cookie = $_COOKIE[$cookie_name];
		}

		// cookie data?
		if ( $cookie ) {
			$visited_posts = $expirations = [];

			foreach ( $cookie as $content ) {
				// is cookie valid?
				if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $content ) === 1 ) {
					// get single id with expiration
					$expiration_ids = explode( 'a', $content );

					// check every expiration => id pair
					foreach ( $expiration_ids as $pair ) {
						$pair = explode( 'b', $pair );
						$expirations[] = (int) $pair[0];
						$visited_posts[(int) $pair[1]] = (int) $pair[0];
					}
				}
			}

			// update cookie
			$this->cookie = [
				'exists'		 => true,
				'visited_posts'	 => $visited_posts,
				'expiration'	 => empty( $expirations ) ? 0 : max( $expirations )
			];
		}
	}

	/**
	 * Save cookie function.
	 *
	 * @param int $id
	 * @param array $cookie
	 * @param bool $expired
	 * @return void
	 */
	private function save_cookie( $id, $cookie = [], $expired = true ) {
		$set_cookie = apply_filters( 'pvc_maybe_set_cookie', true );

		if ( $set_cookie !== true )
			return;

		// get main instance
		$pvc = Post_Views_Counter();

		// get expiration
		$expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] );

		// assign cookie name
		$cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );

		// check whether php version is at least 7.3
		$php_at_least_73 = version_compare( phpversion(), '7.3', '>=' );

		// is this a new cookie?
		if ( empty( $cookie ) ) {
			if ( $php_at_least_73 ) {
				// set cookie
				setcookie(
					$cookie_name . '[0]',
					$expiration . 'b' . $id,
					[
						'expires'	=> $expiration,
						'path'		=> COOKIEPATH,
						'domain'	=> COOKIE_DOMAIN,
						'secure'	=> isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off',
						'httponly'	=> true,
						'samesite'	=> 'LAX'
					]
				);
			} else {
				// set cookie
				setcookie( $cookie_name . '[0]', $expiration . 'b' . $id, $expiration, COOKIEPATH, COOKIE_DOMAIN, ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ), true );
			}

			if ( $this->queue_mode )
				$this->check_cookie( [ 0 => $expiration . 'b' . $id ] );
		} else {
			if ( $expired ) {
				// add new id or change expiration date if id already exists
				$cookie['visited_posts'][$id] = $expiration;
			}

			// create copy for better foreach performance
			$visited_posts_expirations = $cookie['visited_posts'];

			// get current gmt time
			$time = current_time( 'timestamp', true );

			// check whether viewed id has expired - no need to keep it in cookie (less size)
			foreach ( $visited_posts_expirations as $post_id => $post_expiration ) {
				if ( $time > $post_expiration )
					unset( $cookie['visited_posts'][$post_id] );
			}

			// set new last expiration date if needed
			$cookie['expiration'] = empty( $cookie['visited_posts'] ) ? 0 : max( $cookie['visited_posts'] );

			$cookies = $imploded = [];

			// create pairs
			foreach ( $cookie['visited_posts'] as $id => $exp ) {
				$imploded[] = $exp . 'b' . $id;
			}

			// split cookie into chunks (3980 bytes to make sure it is safe for every browser)
			$chunks = str_split( implode( 'a', $imploded ), 3980 );

			// more then one chunk?
			if ( count( $chunks ) > 1 ) {
				$last_id = '';

				foreach ( $chunks as $chunk_id => $chunk ) {
					// new chunk
					$chunk_c = $last_id . $chunk;

					// is it full-length chunk?
					if ( strlen( $chunk ) === 3980 ) {
						// get last part
						$last_part = strrchr( $chunk_c, 'a' );

						// get last id
						$last_id = substr( $last_part, 1 );

						// add new full-lenght chunk
						$cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) );
					} else {
						// add last chunk
						$cookies[$chunk_id] = $chunk_c;
					}
				}
			} else {
				// only one chunk
				$cookies[] = $chunks[0];
			}

			foreach ( $cookies as $key => $value ) {
				if ( $php_at_least_73 ) {
					// set cookie
					setcookie(
						$cookie_name . '[' . $key . ']',
						$value,
						[
							'expires'	=> $cookie['expiration'],
							'path'		=> COOKIEPATH,
							'domain'	=> COOKIE_DOMAIN,
							'secure'	=> isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off',
							'httponly'	=> true,
							'samesite'	=> 'LAX'
						]
					);
				} else {
					// set cookie
					setcookie( $cookie_name . '[' . $key . ']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ), true );
				}
			}

			if ( $this->queue_mode )
				$this->check_cookie( $cookies );
		}
	}

	/**
	 * Save user IP address.
	 *
	 * @param int $id
	 * @return int|void
	 */
	private function save_ip( $id ) {
		$set_cookie = apply_filters( 'pvc_maybe_set_cookie', true );

		if ( $set_cookie !== true )
			return $id;

		// get IP cached visits
		$ip_cache = get_transient( 'post_views_counter_ip_cache' );

		if ( ! $ip_cache )
			$ip_cache = [];

		// get user IP address
		$user_ip = $this->encrypt_ip( $this->get_user_ip() );

		// get current time
		$current_time = current_time( 'timestamp', true );

		// visit exists in transient?
		if ( isset( $ip_cache[$id][$user_ip] ) ) {
			// get main instance
			$pvc = Post_Views_Counter();

			if ( $current_time > $ip_cache[$id][$user_ip] + $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'], false ) )
				$ip_cache[$id][$user_ip] = $current_time;
			else
				return;
		} else
			$ip_cache[$id][$user_ip] = $current_time;

		// keep it light, only 10 records per post and maximum 100 post records (max. 1000 ip entries)
		// also, the data gets deleted after a week if there's no activity during this time
		if ( count( $ip_cache[$id] ) > 10 )
			$ip_cache[$id] = array_slice( $ip_cache[$id], -10, 10, true );

		if ( count( $ip_cache ) > 100 )
			$ip_cache = array_slice( $ip_cache, -100, 100, true );

		set_transient( 'post_views_counter_ip_cache', $ip_cache, WEEK_IN_SECONDS );
	}

	/**
	 * Count visit.
	 *
	 * @param int $id
	 * @return int
	 */
	private function count_visit( $id ) {
		$cache_key_names = [];
		$using_object_cache = $this->using_object_cache();
		$increment_amount = (int) apply_filters( 'pvc_views_increment_amount', 1, $id );

		// get day, week, month and year
		$date = explode( '-', date( 'W-d-m-Y-o', current_time( 'timestamp', true ) ) );

		foreach ( [
			0	 => $date[3] . $date[2] . $date[1], // day like 20140324
			1	 => $date[4] . $date[0], // week like 201439
			2	 => $date[3] . $date[2], // month like 201405
			3	 => $date[3], // year like 2014
			4	 => 'total'	   // total views
		] as $type => $period ) {
			if ( $using_object_cache ) {
				$cache_key = $id . self::CACHE_KEY_SEPARATOR . $type . self::CACHE_KEY_SEPARATOR . $period;
				wp_cache_add( $cache_key, 0, self::GROUP );
				wp_cache_incr( $cache_key, $increment_amount, self::GROUP );
				$cache_key_names[] = $cache_key;
			} else {
				// hit the database directly
				// @TODO: investigate queueing these queries on the 'shutdown' hook instead of running them instantly?
				$this->db_insert( $id, $type, $period, $increment_amount );
			}
		}

		// update the list of cache keys to be flushed
		if ( $using_object_cache && ! empty( $cache_key_names ) )
			$this->update_cached_keys_list_if_needed( $cache_key_names );

		do_action( 'pvc_after_count_visit', $id );

		return $id;
	}

	/**
	 * Remove post views from database when post is deleted.
	 *
	 * @global object $wpdb
	 *
	 * @param int $post_id
	 * @return void
	 */
	public function delete_post_views( $post_id ) {
		global $wpdb;

		$wpdb->delete( $wpdb->prefix . 'post_views', [ 'id' => $post_id ], [ '%d' ] );
	}

	/**
	 * Get timestamp convertion.
	 *
	 * @param string $type
	 * @param int $number
	 * @param bool $timestamp
	 * @return int
	 */
	public function get_timestamp( $type, $number, $timestamp = true ) {
		$converter = [
			'minutes'	=> 60,
			'hours'		=> 3600,
			'days'		=> 86400,
			'weeks'		=> 604800,
			'months'	=> 2592000,
			'years'		=> 946080000
		];

		return (int) ( ( $timestamp ? current_time( 'timestamp', true ) : 0 ) + $number * $converter[$type] );
	}

	/**
	 * Check if object cache is in use.
	 *
	 * @param bool $using
	 * @return bool|null
	 */
	public function using_object_cache( $using = null ) {
		$using = wp_using_ext_object_cache( $using );

		if ( $using ) {
			// check if explicitly disabled by flush_interval setting/option <= 0
			$flush_interval_number = Post_Views_Counter()->options['general']['flush_interval']['number'];
			$using = ( $flush_interval_number <= 0 ) ? false : true;
		}

		return $using;
	}

	/**
	 * Update the single cache key which holds a list of all the cache keys
	 * that need to be flushed to the database.
	 *
	 * The value of that special cache key is a giant string containing key names separated with the `|` character.
	 * Each such key name then consists of 3 elements: $id, $type, $period (separated by a `.` character).
	 * Examples:
	 * 62053.0.20150327|62053.1.201513|62053.2.201503|62053.3.2015|62053.4.total|62180.0.20150327|62180.1.201513|62180.2.201503|62180.3.2015|62180.4.total
	 * A single key is `62053.0.20150327` and that key's data is: $id = 62053, $type = 0, $period = 20150327
	 *
	 * This data format proved more efficient (avoids the (un)serialization overhead completely + duplicates filtering is a string search now)
	 *
	 * @param array $key_names
	 * @return void
	 */
	private function update_cached_keys_list_if_needed( $key_names = [] ) {
		$existing_list = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );

		if ( ! $existing_list )
			$existing_list = '';

		$list_modified = false;

		// modify the list contents if/when needed
		if ( empty( $existing_list ) ) {
			// the simpler case of an empty initial list where we just
			// transform the specified key names into a string
			$existing_list = implode( '|', $key_names );
			$list_modified = true;
		} else {
			// search each specified key name and append it if it's not found
			foreach ( $key_names as $key_name ) {
				if ( false === strpos( $existing_list, $key_name ) ) {
					$existing_list .= '|' . $key_name;
					$list_modified = true;
				}
			}
		}

		// save modified list back in cache
		if ( $list_modified )
			wp_cache_set( self::NAME_ALLKEYS, $existing_list, self::GROUP );
	}

	/**
	 * Flush views data stored in the persistent object cache into
	 * our custom table and clear the object cache keys when done.
	 *
	 * @return bool
	 */
	public function flush_cache_to_db() {
		$key_names = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );

		if ( ! $key_names )
			$key_names = [];
		else {
			// create an array out of a string that's stored in the cache
			$key_names = explode( '|', $key_names );
		}

		foreach ( $key_names as $key_name ) {
			// get values stored within the key name itself
			list( $id, $type, $period ) = explode( self::CACHE_KEY_SEPARATOR, $key_name );

			// get the cached count value
			$count = wp_cache_get( $key_name, self::GROUP );

			// store cached value in the db
			$this->db_prepare_insert( $id, $type, $period, $count );

			// clear the cache key we just flushed
			wp_cache_delete( $key_name, self::GROUP );
		}

		// actually flush values to db (if any left)
		$this->db_commit_insert();

		// remember last flush to db time
		wp_cache_set( 'last-flush', time(), self::GROUP );

		// delete the key holding the list itself after we've successfully flushed it
		if ( ! empty( $key_names ) )
			wp_cache_delete( self::NAME_ALLKEYS, self::GROUP );

		return true;
	}

	/**
	 * Insert or update views count.
	 *
	 * @global object $wpdb
	 *
	 * @param int $id
	 * @param string $type
	 * @param string $period
	 * @param int $count
	 * @return int|bool
	 */
	private function db_insert( $id, $type, $period, $count = 1 ) {
		global $wpdb;

		$count = (int) $count;

		if ( ! $count )
			$count = 1;

		return $wpdb->query( $wpdb->prepare( "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count) VALUES (%d, %d, %s, %d) ON DUPLICATE KEY UPDATE count = count + %d", $id, $type, $period, $count, $count ) );
	}

	/**
	 * Prepare bulk insert or update views count.
	 *
	 * @param int $id
	 * @param string $type
	 * @param string $period
	 * @param int $count
	 * @return void
	 */
	private function db_prepare_insert( $id, $type, $period, $count = 1 ) {
		// cast count
		$count = (int) $count;

		if ( ! $count )
			$count = 1;

		// any queries?
		if ( ! empty( $this->db_insert_values ) )
			$this->db_insert_values .= ', ';

		// append insert queries
		$this->db_insert_values .= sprintf( '(%d, %d, "%s", %d)', $id, $type, $period, $count );

		if ( strlen( $this->db_insert_values ) > self::MAX_INSERT_STRING_LENGTH )
			$this->db_commit_insert();
	}

	/**
	 * Insert accumulated values to database.
	 *
	 * @global object $wpdb
	 *
	 * @return int|bool
	 */
	private function db_commit_insert() {
		if ( empty( $this->db_insert_values ) )
			return false;

		global $wpdb;

		$result = $wpdb->query(
			"INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
			VALUES " . $this->db_insert_values . "
			ON DUPLICATE KEY UPDATE count = count + VALUES(count)"
		);

		$this->db_insert_values = '';

		return $result;
	}

	/**
	 * Check whether user has excluded roles.
	 *
	 * @param int $user_id
	 * @param string $option
	 * @return bool
	 */
	public function is_user_role_excluded( $user_id, $option = [] ) {
		// get user by ID
		$user = get_user_by( 'id', $user_id );

		// no user?
		if ( empty( $user ) )
			return false;

		// get user roles
		$roles = (array) $user->roles;

		// any roles?
		if ( ! empty( $roles ) ) {
			foreach ( $roles as $role ) {
				if ( in_array( $role, $option, true ) )
					return true;
			}
		}

		return false;
	}

	/**
	 * Check if IPv4 is in range.
	 *
	 * @param string $ip IP address
	 * @param string $range IP range
	 * @return bool
	 */
	public function ipv4_in_range( $ip, $range ) {
		$start = str_replace( '*', '0', $range );
		$end = str_replace( '*', '255', $range );
		$ip = (float) sprintf( "%u", ip2long( $ip ) );

		return ( $ip >= (float) sprintf( "%u", ip2long( $start ) ) && $ip <= (float) sprintf( "%u", ip2long( $end ) ) );
	}

	/**
	 * Get user real IP address.
	 *
	 * @return string
	 */
	public function get_user_ip() {
		$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';

		foreach ( [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ] as $key ) {
			if ( array_key_exists( $key, $_SERVER ) === true ) {
				foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
					// trim for safety measures
					$ip = trim( $ip );

					// attempt to validate IP
					if ( $this->validate_user_ip( $ip ) )
						continue;
				}
			}
		}

		return (string) $ip;
	}

	/**
	 * Ensure an IP address is both a valid IP and does not fall within a private network range.
	 *
	 * @param $ip string IP address
	 * @return bool
	 */
	public function validate_user_ip( $ip ) {
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false )
			return false;

		return true;
	}

	/**
	 * Encrypt user IP.
	 *
	 * @param string $ip
	 * @return string
	 */
	public function encrypt_ip( $ip ) {
		$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : false;
		$auth_iv = defined( 'NONCE_KEY' ) ? NONCE_KEY : false;
		$cipher = 'AES-256-CBC';
		$php_71x = version_compare( phpversion(), '7.1.0', '>=' ) && version_compare( phpversion(), '7.2.0', '<' );

		// openssl encryption
		if ( $auth_key && $auth_iv && function_exists( 'openssl_encrypt' ) && in_array( $cipher, array_map( 'strtoupper', openssl_get_cipher_methods() ) ) )
			$encrypted_ip = base64_encode( openssl_encrypt( $ip, $cipher, $auth_key, 0, mb_strimwidth( $auth_iv, 0, openssl_cipher_iv_length( $cipher ), '', 'UTF-8' ) ) );
		// mcrypt encryption
		elseif ( $auth_key && $auth_iv && ! $php_71x && function_exists( 'mcrypt_encrypt' ) && function_exists( 'mcrypt_get_key_size' ) && function_exists( 'mcrypt_get_iv_size' ) && defined( 'MCRYPT_BLOWFISH' ) ) {
			// get max key size of the mcrypt mode
			$max_key_size = mcrypt_get_key_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
			$max_iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );

			$encrypt_key = mb_strimwidth( $auth_key, 0, $max_key_size );
			$encrypt_iv = mb_strimwidth( $auth_iv, 0, $max_iv_size );

			$encrypted_ip = base64_encode( mcrypt_encrypt( MCRYPT_BLOWFISH, $encrypt_key, $ip, MCRYPT_MODE_CBC, $encrypt_iv ) );
		// simple encryption
		} elseif ( function_exists( 'gzdeflate' ) )
			$encrypted_ip = base64_encode( convert_uuencode( gzdeflate( $ip ) ) );
		// no encryption
		else
			$encrypted_ip = base64_encode( convert_uuencode( $ip ) );

		return $encrypted_ip;
	}

	/**
	 * Decrypt user IP.
	 *
	 * @param string $encrypted_ip
	 * @return string
	 */
	public function decrypt_ip( $encrypted_ip ) {
		$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : false;
		$auth_iv = defined( 'NONCE_KEY' ) ? NONCE_KEY : false;
		$cipher = 'AES-256-CBC';
		$php_71x = version_compare( phpversion(), '7.1.0', '>=' ) && version_compare( phpversion(), '7.2.0', '<' );

		// openssl decryption
		if ( $auth_key && $auth_iv && function_exists( 'openssl_encrypt' ) && in_array( $cipher, array_map( 'strtoupper', openssl_get_cipher_methods() ) ) )
			$ip = openssl_decrypt( base64_decode( $encrypted_ip ), $cipher, $auth_key, 0, mb_strimwidth( $auth_iv, 0, openssl_cipher_iv_length( $cipher ), '', 'UTF-8' ) );
		// mcrypt decryption
		elseif ( $auth_key && $auth_iv && ! $php_71x && function_exists( 'mcrypt_decrypt' ) && function_exists( 'mcrypt_get_key_size' ) && function_exists( 'mcrypt_get_iv_size' ) && defined( 'MCRYPT_BLOWFISH' ) ) {
			// get max key size of the mcrypt mode
			$max_key_size = mcrypt_get_key_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
			$max_iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );

			$encrypt_key = mb_strimwidth( $auth_key, 0, $max_key_size );
			$encrypt_iv = mb_strimwidth( $auth_iv, 0, $max_iv_size );

			$ip = rtrim( mcrypt_decrypt( MCRYPT_BLOWFISH, $encrypt_key, base64_decode( $encrypted_ip ), MCRYPT_MODE_CBC, $encrypt_iv ), "\0" );
		// simple decryption
		} elseif ( function_exists( 'gzinflate' ) )
			$ip = gzinflate( convert_uudecode( base64_decode( $encrypted_ip ) ) );
		// no decryption
		else
			$ip = convert_uudecode( base64_decode( $encrypted_ip ) );

		return $ip;
	}

	/**
	 * Register REST API endpoints.
	 *
	 * @return void
	 */
	public function rest_api_init() {
		// view post route
		register_rest_route(
			'post-views-counter',
			'/view-post/(?P<id>\d+)|/view-post/',
			[
				'methods'				 => [ 'GET', 'POST' ],
				'callback'				 => [ $this, 'check_post_rest_api' ],
				'permission_callback'	 => [ $this, 'post_view_permissions_check' ],
				'args'					 => [
					'id' => [
						'default'			 => 0,
						'sanitize_callback'	 => 'absint'
					]
				]
			]
		);

		// get views route
		register_rest_route(
			'post-views-counter',
			'/get-post-views/(?P<id>(\d+,?)+)',
			[
				'methods'				 => [ 'GET', 'POST' ],
				'callback'				 => [ $this, 'get_post_views_rest_api' ],
				'permission_callback'	 => [ $this, 'get_post_views_permissions_check' ],
				'args'					 => [
					'id' => [
						'default'			=> 0,
						'sanitize_callback'	=> [ $this, 'validate_rest_api_data' ]
					]
				]
			]
		);
	}

	/**
	 * Get post views via REST API request.
	 *
	 * @param object $request
	 * @return int
	 */
	public function get_post_views_rest_api( $request ) {
		return pvc_get_post_views( $request->get_param( 'id' ) );
	}

	/**
	 * Check if a given request has access to view post.
	 *
	 * @param object $request
	 * @return bool
	 */
	public function post_view_permissions_check( $request ) {
		return (bool) apply_filters( 'pvc_rest_api_post_views_check', true, $request );
	}

	/**
	 * Check if a given request has access to get views.
	 *
	 * @param object $request
	 * @return bool
	 */
	public function get_post_views_permissions_check( $request ) {
		return (bool) apply_filters( 'pvc_rest_api_get_post_views_check', true, $request );
	}

	/**
	 * Validate REST API incoming data.
	 *
	 * @param int|array $data
	 * @return int|array
	 */
	public function validate_rest_api_data( $data ) {
		// POST array?
		if ( is_array( $data ) )
			$data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
		// multiple comma-separated values?
		elseif ( strpos( $data, ',' ) !== false ) {
			$data = explode( ',', $data );

			if ( is_array( $data ) && ! empty( $data ) )
				$data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
			else
				$data = [];
		// single value?
		} else
			$data = absint( $data );

		return $data;
	}
}