PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.3.13
Post Views Counter v1.3.13
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / includes / class-counter.php
post-views-counter / includes Last commit date
class-admin.php 3 years ago class-columns.php 3 years ago class-counter.php 3 years ago class-crawler-detect.php 3 years ago class-cron.php 3 years ago class-dashboard.php 3 years ago class-frontend.php 3 years ago class-functions.php 3 years ago class-query.php 3 years ago class-settings-api.php 3 years ago class-settings.php 3 years ago class-update.php 3 years ago class-widgets.php 3 years ago functions.php 3 years ago
class-counter.php
1121 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Counter class.
8 *
9 * @class Post_Views_Counter_Counter
10 */
11 class Post_Views_Counter_Counter {
12
13 const GROUP = 'pvc';
14 const NAME_ALLKEYS = 'cached_key_names';
15 const CACHE_KEY_SEPARATOR = '.';
16 const MAX_INSERT_STRING_LENGTH = 25000;
17
18 private $queue = [];
19 private $queue_mode = false;
20 private $db_insert_values = '';
21 private $cookie = [
22 'exists' => false,
23 'visited_posts' => [],
24 'expiration' => 0
25 ];
26
27 /**
28 * Class constructor.
29 *
30 * @return void
31 */
32 public function __construct() {
33 // actions
34 add_action( 'plugins_loaded', [ $this, 'check_cookie' ], 1 );
35 add_action( 'init', [ $this, 'init_counter' ] );
36 add_action( 'deleted_post', [ $this, 'delete_post_views' ] );
37 }
38
39 /**
40 * Add Post ID to queue.
41 *
42 * @param int $post_id
43 * @return void
44 */
45 public function add_to_queue( $post_id ) {
46 $this->queue[] = (int) $post_id;
47 }
48
49 /**
50 * Run manual pvc_view_post queue.
51 *
52 * @return void
53 */
54 public function queue_count() {
55 // check conditions
56 if ( ! isset( $_POST['action'], $_POST['ids'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-view-posts' ) || $_POST['ids'] === '' || ! is_string( $_POST['ids'] ) )
57 exit;
58
59 // get post ids
60 $ids = explode( ',', $_POST['ids'] );
61
62 $counted = [];
63
64 if ( ! empty( $ids ) ) {
65 $ids = array_filter( array_map( 'intval', $ids ) );
66
67 if ( ! empty( $ids ) ) {
68 // turn on queue mode
69 $this->queue_mode = true;
70
71 foreach ( $ids as $id ) {
72 $counted[$id] = ! ( $this->check_post( $id ) === null );
73 }
74
75 // turn off queue mode
76 $this->queue_mode = false;
77 }
78 }
79
80 echo wp_json_encode(
81 [
82 'post_ids' => $ids,
83 'counted' => $counted
84 ]
85 );
86
87 exit;
88 }
89
90 /**
91 * Print JavaScript with queue in the footer.
92 *
93 * @return void
94 */
95 public function print_queue_count() {
96 // any ids to "view"?
97 if ( ! empty( $this->queue ) ) {
98 echo "
99 <script>
100 ( function( window, document, undefined ) {
101 document.addEventListener( 'DOMContentLoaded', function() {
102 let pvcLoadManualCounter = function( url, counter ) {
103 let pvcScriptTag = document.createElement( 'script' );
104
105 // append script
106 document.body.appendChild( pvcScriptTag );
107
108 // set attributes
109 pvcScriptTag.onload = counter;
110 pvcScriptTag.onreadystatechange = counter;
111 pvcScriptTag.src = url;
112 };
113
114 let pvcExecuteManualCounter = function() {
115 let pvcManualCounterArgs = {
116 url: '" . esc_url( admin_url( 'admin-ajax.php' ) ) . "',
117 nonce: '" . wp_create_nonce( 'pvc-view-posts' ) . "',
118 ids: '" . implode( ',', $this->queue ) . "'
119 };
120
121 // main javascript file was loaded?
122 if ( typeof PostViewsCounter !== 'undefined' && PostViewsCounter.promise !== null ) {
123 PostViewsCounter.promise.then( function() {
124 PostViewsCounterManual.init( pvcManualCounterArgs );
125 } );
126 // PostViewsCounter is undefined or promise is null
127 } else {
128 PostViewsCounterManual.init( pvcManualCounterArgs );
129 }
130 }
131
132 pvcLoadManualCounter( '" . POST_VIEWS_COUNTER_URL . "/js/counter" . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . ".js', pvcExecuteManualCounter );
133 }, false );
134 } )( window, document );
135 </script>";
136 }
137 }
138
139 /**
140 * Initialize counter.
141 *
142 * @return void
143 */
144 public function init_counter() {
145 // admin?
146 if ( is_admin() && ! wp_doing_ajax() )
147 return;
148
149 // get main instance
150 $pvc = Post_Views_Counter();
151
152 add_action( 'wp_ajax_pvc-view-posts', [ $this, 'queue_count' ] );
153 add_action( 'wp_ajax_nopriv_pvc-view-posts', [ $this, 'queue_count' ] );
154 add_action( 'wp_print_footer_scripts', [ $this, 'print_queue_count' ], 11 );
155
156 // php counter
157 if ( $pvc->options['general']['counter_mode'] === 'php' )
158 add_action( 'wp', [ $this, 'check_post_php' ] );
159 // javascript (ajax) counter
160 elseif ( $pvc->options['general']['counter_mode'] === 'js' ) {
161 add_action( 'wp_ajax_pvc-check-post', [ $this, 'check_post_js' ] );
162 add_action( 'wp_ajax_nopriv_pvc-check-post', [ $this, 'check_post_js' ] );
163 // rest api counter
164 } elseif ( $pvc->options['general']['counter_mode'] === 'rest_api' )
165 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
166 }
167
168 /**
169 * Check whether to count visit.
170 *
171 * @param int $id
172 * @return void|int
173 */
174 public function check_post( $id = 0 ) {
175 // force check cookie in SHORTINIT mode
176 if ( defined( 'SHORTINIT' ) && SHORTINIT )
177 $this->check_cookie();
178
179 // get post id
180 $id = (int) ( empty( $id ) ? get_the_ID() : $id );
181
182 // empty id?
183 if ( empty( $id ) )
184 return;
185
186 // get user id, from current user or static var in rest api request
187 $user_id = get_current_user_id();
188
189 // get user IP address
190 $user_ip = $this->get_user_ip();
191
192 do_action( 'pvc_before_check_visit', $id, $user_id, $user_ip );
193
194 // get main instance
195 $pvc = Post_Views_Counter();
196
197 // get ips
198 $ips = $pvc->options['general']['exclude_ips'];
199
200 // whether to count this ip
201 if ( ! empty( $ips ) && filter_var( preg_replace( '/[^0-9a-fA-F:., ]/', '', $user_ip ), FILTER_VALIDATE_IP ) ) {
202 // check ips
203 foreach ( $ips as $ip ) {
204 if ( strpos( $ip, '*' ) !== false ) {
205 if ( $this->ipv4_in_range( $user_ip, $ip ) )
206 return;
207 } else {
208 if ( $user_ip === $ip )
209 return;
210 }
211 }
212 }
213
214 // strict counts?
215 if ( $pvc->options['general']['strict_counts'] ) {
216 // get IP cached visits
217 $ip_cache = get_transient( 'post_views_counter_ip_cache' );
218
219 if ( ! $ip_cache )
220 $ip_cache = [];
221
222 // get user IP address
223 $user_ip = $this->encrypt_ip( $user_ip );
224
225 // visit exists in transient?
226 if ( isset( $ip_cache[$id][$user_ip] ) ) {
227 // get current time
228 $current_time = current_time( 'timestamp', true );
229
230 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 ) )
231 return;
232 }
233 }
234
235 // get groups to check them faster
236 $groups = $pvc->options['general']['exclude']['groups'];
237
238 // whether to count this user
239 if ( ! empty( $user_id ) ) {
240 // exclude logged in users?
241 if ( in_array( 'users', $groups, true ) )
242 return;
243 // exclude specific roles?
244 elseif ( in_array( 'roles', $groups, true ) && $this->is_user_role_excluded( $user_id, $pvc->options['general']['exclude']['roles'] ) )
245 return;
246 // exclude guests?
247 } elseif ( in_array( 'guests', $groups, true ) )
248 return;
249
250 // whether to count robots
251 if ( in_array( 'robots', $groups, true ) && $pvc->crawler->is_crawler() )
252 return;
253
254 // cookie already existed?
255 if ( $this->cookie['exists'] ) {
256 // get current time if needed
257 if ( ! isset( $current_time ) )
258 $current_time = current_time( 'timestamp', true );
259
260 // post already viewed but not expired?
261 if ( in_array( $id, array_keys( $this->cookie['visited_posts'] ), true ) && $current_time < $this->cookie['visited_posts'][$id] ) {
262 // update cookie but do not count visit
263 $this->save_cookie( $id, $this->cookie, false );
264
265 return;
266 // update cookie
267 } else
268 $this->save_cookie( $id, $this->cookie );
269 } else {
270 // set new cookie
271 $this->save_cookie( $id );
272 }
273
274 $count_visit = (bool) apply_filters( 'pvc_count_visit', true, $id );
275
276 // count visit
277 if ( $count_visit ) {
278 // strict counts?
279 if ( $pvc->options['general']['strict_counts'] )
280 $this->save_ip( $id );
281
282 return $this->count_visit( $id );
283 }
284 }
285
286 /**
287 * Check whether to count visit via PHP request.
288 *
289 * @return void
290 */
291 public function check_post_php() {
292 // do not count admin entries
293 if ( is_admin() && ! wp_doing_ajax() )
294 return;
295
296 // get main instance
297 $pvc = Post_Views_Counter();
298
299 // do we use php as counter?
300 if ( $pvc->options['general']['counter_mode'] !== 'php' )
301 return;
302
303 // get countable post types
304 $post_types = $pvc->options['general']['post_types_count'];
305
306 // whether to count this post type
307 if ( empty( $post_types ) || ! is_singular( $post_types ) )
308 return;
309
310 $this->check_post( get_the_ID() );
311 }
312
313 /**
314 * Check whether to count visit via JavaScript (AJAX) request.
315 *
316 * @return void
317 */
318 public function check_post_js() {
319 // check conditions
320 if ( ! isset( $_POST['action'], $_POST['id'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-check-post' ) )
321 exit;
322
323 // get post id
324 $post_id = (int) $_POST['id'];
325
326 if ( $post_id <= 0 )
327 exit;
328
329 // get main instance
330 $pvc = Post_Views_Counter();
331
332 // do we use javascript as counter?
333 if ( $pvc->options['general']['counter_mode'] !== 'js' )
334 exit;
335
336 // get countable post types
337 $post_types = $pvc->options['general']['post_types_count'];
338
339 // check if post exists
340 $post = get_post( $post_id );
341
342 // whether to count this post type or not
343 if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) )
344 exit;
345
346 echo wp_json_encode(
347 [
348 'post_id' => $post_id,
349 'counted' => ! ( $this->check_post( $post_id ) === null )
350 ]
351 );
352
353 exit;
354 }
355
356 /**
357 * Check whether to count visit via REST API request.
358 *
359 * @param object $request
360 * @return int|WP_Error
361 */
362 public function check_post_rest_api( $request ) {
363 // get main instance
364 $pvc = Post_Views_Counter();
365
366 // get post id (already sanitized)
367 $post_id = $request->get_param( 'id' );
368
369 // do we use REST API as counter?
370 if ( $pvc->options['general']['counter_mode'] !== 'rest_api' )
371 return new WP_Error( 'pvc_rest_api_disabled', __( 'REST API method is disabled.', 'post-views-counter' ), [ 'status' => 404 ] );
372
373 // @todo: get current user id in direct api endpoint calls
374 // check if post exists
375 $post = get_post( $post_id );
376
377 if ( ! $post )
378 return new WP_Error( 'pvc_post_invalid_id', __( 'Invalid post ID.', 'post-views-counter' ), [ 'status' => 404 ] );
379
380 // get countable post types
381 $post_types = $pvc->options['general']['post_types_count'];
382
383 // whether to count this post type
384 if ( empty( $post_types ) || ! in_array( $post->post_type, $post_types, true ) )
385 return new WP_Error( 'pvc_post_type_excluded', __( 'Post type excluded.', 'post-views-counter' ), [ 'status' => 404 ] );
386
387 return [
388 'post_id' => $post_id,
389 'counted' => ! ( $this->check_post( $post_id ) === null )
390 ];
391 }
392
393 /**
394 * Initialize cookie session.
395 *
396 * @param array $cookie Use this data instead of real $_COOKIE
397 * @return void
398 */
399 public function check_cookie( $cookie = [] ) {
400 // do not run in admin except for ajax requests
401 if ( is_admin() && ! wp_doing_ajax() )
402 return;
403
404 if ( empty( $cookie ) || ! is_array( $cookie ) ) {
405 // assign cookie name
406 $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );
407
408 // is cookie set?
409 if ( isset( $_COOKIE[$cookie_name] ) && ! empty( $_COOKIE[$cookie_name] ) )
410 $cookie = $_COOKIE[$cookie_name];
411 }
412
413 // cookie data?
414 if ( $cookie && is_array( $cookie ) ) {
415 $visited_posts = $expirations = [];
416
417 foreach ( $cookie as $content ) {
418 // is cookie valid?
419 if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $content ) === 1 ) {
420 // get single id with expiration
421 $expiration_ids = explode( 'a', $content );
422
423 // check every expiration => id pair
424 foreach ( $expiration_ids as $pair ) {
425 $pair = explode( 'b', $pair );
426 $expirations[] = (int) $pair[0];
427 $visited_posts[(int) $pair[1]] = (int) $pair[0];
428 }
429 }
430 }
431
432 // update cookie
433 $this->cookie = [
434 'exists' => true,
435 'visited_posts' => $visited_posts,
436 'expiration' => empty( $expirations ) ? 0 : max( $expirations )
437 ];
438 }
439 }
440
441 /**
442 * Save cookie function.
443 *
444 * @param int $id
445 * @param array $cookie
446 * @param bool $expired
447 * @return void
448 */
449 private function save_cookie( $id, $cookie = [], $expired = true ) {
450 // early return?
451 if ( apply_filters( 'pvc_maybe_set_cookie', true, $id, 'post', $cookie, $expired ) !== true )
452 return;
453
454 // get main instance
455 $pvc = Post_Views_Counter();
456
457 // get expiration
458 $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] );
459
460 // assign cookie name
461 $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );
462
463 // check whether php version is at least 7.3
464 $php_at_least_73 = version_compare( phpversion(), '7.3', '>=' );
465
466 // is this a new cookie?
467 if ( empty( $cookie ) ) {
468 if ( $php_at_least_73 ) {
469 // set cookie
470 setcookie(
471 $cookie_name . '[0]',
472 $expiration . 'b' . $id,
473 [
474 'expires' => $expiration,
475 'path' => COOKIEPATH,
476 'domain' => COOKIE_DOMAIN,
477 'secure' => is_ssl(),
478 'httponly' => true,
479 'samesite' => 'LAX'
480 ]
481 );
482 } else {
483 // set cookie
484 setcookie( $cookie_name . '[0]', $expiration . 'b' . $id, $expiration, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
485 }
486
487 if ( $this->queue_mode )
488 $this->check_cookie( [ 0 => $expiration . 'b' . $id ] );
489 } else {
490 if ( $expired ) {
491 // add new id or change expiration date if id already exists
492 $cookie['visited_posts'][$id] = $expiration;
493 }
494
495 // create copy for better foreach performance
496 $visited_posts_expirations = $cookie['visited_posts'];
497
498 // get current gmt time
499 $time = current_time( 'timestamp', true );
500
501 // check whether viewed id has expired - no need to keep it in cookie (less size)
502 foreach ( $visited_posts_expirations as $post_id => $post_expiration ) {
503 if ( $time > $post_expiration )
504 unset( $cookie['visited_posts'][$post_id] );
505 }
506
507 // set new last expiration date if needed
508 $cookie['expiration'] = empty( $cookie['visited_posts'] ) ? 0 : max( $cookie['visited_posts'] );
509
510 $cookies = $imploded = [];
511
512 // create pairs
513 foreach ( $cookie['visited_posts'] as $id => $exp ) {
514 $imploded[] = $exp . 'b' . $id;
515 }
516
517 // split cookie into chunks (3980 bytes to make sure it is safe for every browser)
518 $chunks = str_split( implode( 'a', $imploded ), 3980 );
519
520 // more then one chunk?
521 if ( count( $chunks ) > 1 ) {
522 $last_id = '';
523
524 foreach ( $chunks as $chunk_id => $chunk ) {
525 // new chunk
526 $chunk_c = $last_id . $chunk;
527
528 // is it full-length chunk?
529 if ( strlen( $chunk ) === 3980 ) {
530 // get last part
531 $last_part = strrchr( $chunk_c, 'a' );
532
533 // get last id
534 $last_id = substr( $last_part, 1 );
535
536 // add new full-lenght chunk
537 $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) );
538 } else {
539 // add last chunk
540 $cookies[$chunk_id] = $chunk_c;
541 }
542 }
543 } else {
544 // only one chunk
545 $cookies[] = $chunks[0];
546 }
547
548 foreach ( $cookies as $key => $value ) {
549 if ( $php_at_least_73 ) {
550 // set cookie
551 setcookie(
552 $cookie_name . '[' . $key . ']',
553 $value,
554 [
555 'expires' => $cookie['expiration'],
556 'path' => COOKIEPATH,
557 'domain' => COOKIE_DOMAIN,
558 'secure' => is_ssl(),
559 'httponly' => true,
560 'samesite' => 'LAX'
561 ]
562 );
563 } else {
564 // set cookie
565 setcookie( $cookie_name . '[' . $key . ']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
566 }
567 }
568
569 if ( $this->queue_mode )
570 $this->check_cookie( $cookies );
571 }
572 }
573
574 /**
575 * Save user IP address.
576 *
577 * @param int $id
578 * @return void
579 */
580 private function save_ip( $id ) {
581 // early return?
582 if ( apply_filters( 'pvc_maybe_set_ip', true, $id, 'post' ) !== true )
583 return;
584
585 // get ip cached visits
586 $ip_cache = get_transient( 'post_views_counter_ip_cache' );
587
588 if ( ! $ip_cache )
589 $ip_cache = [];
590
591 // get user ip address
592 $user_ip = $this->encrypt_ip( $this->get_user_ip() );
593
594 // get current time
595 $current_time = current_time( 'timestamp', true );
596
597 // visit exists in transient?
598 if ( isset( $ip_cache[$id][$user_ip] ) ) {
599 // get main instance
600 $pvc = Post_Views_Counter();
601
602 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 ) )
603 $ip_cache[$id][$user_ip] = $current_time;
604 else
605 return;
606 } else
607 $ip_cache[$id][$user_ip] = $current_time;
608
609 // keep it light, only 10 records per post and maximum 100 post records (max. 1000 ip entries)
610 // also, the data gets deleted after a week if there's no activity during this time
611 if ( count( $ip_cache[$id] ) > 10 )
612 $ip_cache[$id] = array_slice( $ip_cache[$id], -10, 10, true );
613
614 if ( count( $ip_cache ) > 100 )
615 $ip_cache = array_slice( $ip_cache, -100, 100, true );
616
617 set_transient( 'post_views_counter_ip_cache', $ip_cache, WEEK_IN_SECONDS );
618 }
619
620 /**
621 * Count visit.
622 *
623 * @param int $id
624 * @return int
625 */
626 private function count_visit( $id ) {
627 $cache_key_names = [];
628 $using_object_cache = $this->using_object_cache();
629 $increment_amount = (int) apply_filters( 'pvc_views_increment_amount', 1, $id );
630
631 // get day, week, month and year
632 $date = explode( '-', date( 'W-d-m-Y-o', current_time( 'timestamp', true ) ) );
633
634 foreach ( [
635 0 => $date[3] . $date[2] . $date[1], // day like 20140324
636 1 => $date[4] . $date[0], // week like 201439
637 2 => $date[3] . $date[2], // month like 201405
638 3 => $date[3], // year like 2014
639 4 => 'total' // total views
640 ] as $type => $period ) {
641 if ( $using_object_cache ) {
642 $cache_key = $id . self::CACHE_KEY_SEPARATOR . $type . self::CACHE_KEY_SEPARATOR . $period;
643 wp_cache_add( $cache_key, 0, self::GROUP );
644 wp_cache_incr( $cache_key, $increment_amount, self::GROUP );
645 $cache_key_names[] = $cache_key;
646 } else {
647 // hit the database directly
648 // @TODO: investigate queueing these queries on the 'shutdown' hook instead of running them instantly?
649 $this->db_insert( $id, $type, $period, $increment_amount );
650 }
651 }
652
653 // update the list of cache keys to be flushed
654 if ( $using_object_cache && ! empty( $cache_key_names ) )
655 $this->update_cached_keys_list_if_needed( $cache_key_names );
656
657 do_action( 'pvc_after_count_visit', $id );
658
659 return $id;
660 }
661
662 /**
663 * Remove post views from database when post is deleted.
664 *
665 * @global object $wpdb
666 *
667 * @param int $post_id
668 * @return void
669 */
670 public function delete_post_views( $post_id ) {
671 global $wpdb;
672
673 $wpdb->delete( $wpdb->prefix . 'post_views', [ 'id' => $post_id ], [ '%d' ] );
674 }
675
676 /**
677 * Get timestamp convertion.
678 *
679 * @param string $type
680 * @param int $number
681 * @param bool $timestamp
682 * @return int
683 */
684 public function get_timestamp( $type, $number, $timestamp = true ) {
685 $converter = [
686 'minutes' => MINUTE_IN_SECONDS,
687 'hours' => HOUR_IN_SECONDS,
688 'days' => DAY_IN_SECONDS,
689 'weeks' => WEEK_IN_SECONDS,
690 'months' => MONTH_IN_SECONDS,
691 'years' => YEAR_IN_SECONDS
692 ];
693
694 return (int) ( ( $timestamp ? current_time( 'timestamp', true ) : 0 ) + $number * $converter[$type] );
695 }
696
697 /**
698 * Check if object cache is in use.
699 *
700 * @param bool $using
701 * @return bool|null
702 */
703 public function using_object_cache( $using = null ) {
704 $using = wp_using_ext_object_cache( $using );
705
706 if ( $using ) {
707 // check if explicitly disabled by flush_interval setting/option <= 0
708 $flush_interval_number = Post_Views_Counter()->options['general']['flush_interval']['number'];
709 $using = ( $flush_interval_number <= 0 ) ? false : true;
710 }
711
712 return $using;
713 }
714
715 /**
716 * Update the single cache key which holds a list of all the cache keys
717 * that need to be flushed to the database.
718 *
719 * The value of that special cache key is a giant string containing key names separated with the `|` character.
720 * Each such key name then consists of 3 elements: $id, $type, $period (separated by a `.` character).
721 * Examples:
722 * 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
723 * A single key is `62053.0.20150327` and that key's data is: $id = 62053, $type = 0, $period = 20150327
724 *
725 * This data format proved more efficient (avoids the (un)serialization overhead completely + duplicates filtering is a string search now)
726 *
727 * @param array $key_names
728 * @return void
729 */
730 private function update_cached_keys_list_if_needed( $key_names = [] ) {
731 $existing_list = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
732
733 if ( ! $existing_list )
734 $existing_list = '';
735
736 $list_modified = false;
737
738 // modify the list contents if/when needed
739 if ( empty( $existing_list ) ) {
740 // the simpler case of an empty initial list where we just
741 // transform the specified key names into a string
742 $existing_list = implode( '|', $key_names );
743 $list_modified = true;
744 } else {
745 // search each specified key name and append it if it's not found
746 foreach ( $key_names as $key_name ) {
747 if ( false === strpos( $existing_list, $key_name ) ) {
748 $existing_list .= '|' . $key_name;
749 $list_modified = true;
750 }
751 }
752 }
753
754 // save modified list back in cache
755 if ( $list_modified )
756 wp_cache_set( self::NAME_ALLKEYS, $existing_list, self::GROUP );
757 }
758
759 /**
760 * Flush views data stored in the persistent object cache into
761 * our custom table and clear the object cache keys when done.
762 *
763 * @return bool
764 */
765 public function flush_cache_to_db() {
766 $key_names = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
767
768 if ( ! $key_names )
769 $key_names = [];
770 else {
771 // create an array out of a string that's stored in the cache
772 $key_names = explode( '|', $key_names );
773 }
774
775 foreach ( $key_names as $key_name ) {
776 // get values stored within the key name itself
777 list( $id, $type, $period ) = explode( self::CACHE_KEY_SEPARATOR, $key_name );
778
779 // get the cached count value
780 $count = wp_cache_get( $key_name, self::GROUP );
781
782 // store cached value in the db
783 $this->db_prepare_insert( $id, $type, $period, $count );
784
785 // clear the cache key we just flushed
786 wp_cache_delete( $key_name, self::GROUP );
787 }
788
789 // actually flush values to db (if any left)
790 $this->db_commit_insert();
791
792 // remember last flush to db time
793 wp_cache_set( 'last-flush', time(), self::GROUP );
794
795 // delete the key holding the list itself after we've successfully flushed it
796 if ( ! empty( $key_names ) )
797 wp_cache_delete( self::NAME_ALLKEYS, self::GROUP );
798
799 return true;
800 }
801
802 /**
803 * Insert or update views count.
804 *
805 * @global object $wpdb
806 *
807 * @param int $id
808 * @param string $type
809 * @param string $period
810 * @param int $count
811 * @return int|bool
812 */
813 private function db_insert( $id, $type, $period, $count = 1 ) {
814 global $wpdb;
815
816 $count = (int) $count;
817
818 if ( ! $count )
819 $count = 1;
820
821 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 ) );
822 }
823
824 /**
825 * Prepare bulk insert or update views count.
826 *
827 * @param int $id
828 * @param string $type
829 * @param string $period
830 * @param int $count
831 * @return void
832 */
833 private function db_prepare_insert( $id, $type, $period, $count = 1 ) {
834 // cast count
835 $count = (int) $count;
836
837 if ( ! $count )
838 $count = 1;
839
840 // any queries?
841 if ( ! empty( $this->db_insert_values ) )
842 $this->db_insert_values .= ', ';
843
844 // append insert queries
845 $this->db_insert_values .= sprintf( '(%d, %d, "%s", %d)', $id, $type, $period, $count );
846
847 if ( strlen( $this->db_insert_values ) > self::MAX_INSERT_STRING_LENGTH )
848 $this->db_commit_insert();
849 }
850
851 /**
852 * Insert accumulated values to database.
853 *
854 * @global object $wpdb
855 *
856 * @return int|bool
857 */
858 private function db_commit_insert() {
859 if ( empty( $this->db_insert_values ) )
860 return false;
861
862 global $wpdb;
863
864 $result = $wpdb->query(
865 "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
866 VALUES " . $this->db_insert_values . "
867 ON DUPLICATE KEY UPDATE count = count + VALUES(count)"
868 );
869
870 $this->db_insert_values = '';
871
872 return $result;
873 }
874
875 /**
876 * Check whether user has excluded roles.
877 *
878 * @param int $user_id
879 * @param string $option
880 * @return bool
881 */
882 public function is_user_role_excluded( $user_id, $option = [] ) {
883 // get user by ID
884 $user = get_user_by( 'id', $user_id );
885
886 // no user?
887 if ( empty( $user ) )
888 return false;
889
890 // get user roles
891 $roles = (array) $user->roles;
892
893 // any roles?
894 if ( ! empty( $roles ) ) {
895 foreach ( $roles as $role ) {
896 if ( in_array( $role, $option, true ) )
897 return true;
898 }
899 }
900
901 return false;
902 }
903
904 /**
905 * Check if IPv4 is in range.
906 *
907 * @param string $ip IP address
908 * @param string $range IP range
909 * @return bool
910 */
911 public function ipv4_in_range( $ip, $range ) {
912 $start = str_replace( '*', '0', $range );
913 $end = str_replace( '*', '255', $range );
914 $ip = (float) sprintf( "%u", ip2long( $ip ) );
915
916 return ( $ip >= (float) sprintf( "%u", ip2long( $start ) ) && $ip <= (float) sprintf( "%u", ip2long( $end ) ) );
917 }
918
919 /**
920 * Get user real IP address.
921 *
922 * @return string
923 */
924 public function get_user_ip() {
925 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
926
927 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 ) {
928 if ( array_key_exists( $key, $_SERVER ) === true ) {
929 foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
930 // trim for safety measures
931 $ip = trim( $ip );
932
933 // attempt to validate IP
934 if ( $this->validate_user_ip( $ip ) )
935 continue;
936 }
937 }
938 }
939
940 return (string) $ip;
941 }
942
943 /**
944 * Ensure an IP address is both a valid IP and does not fall within a private network range.
945 *
946 * @param $ip string IP address
947 * @return bool
948 */
949 public function validate_user_ip( $ip ) {
950 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false )
951 return false;
952
953 return true;
954 }
955
956 /**
957 * Encrypt user IP.
958 *
959 * @param string $ip
960 * @return string
961 */
962 public function encrypt_ip( $ip ) {
963 $auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : false;
964 $auth_iv = defined( 'NONCE_KEY' ) ? NONCE_KEY : false;
965 $cipher = 'AES-256-CBC';
966 $php_71x = version_compare( phpversion(), '7.1.0', '>=' ) && version_compare( phpversion(), '7.2.0', '<' );
967
968 // openssl encryption
969 if ( $auth_key && $auth_iv && function_exists( 'openssl_encrypt' ) && in_array( $cipher, array_map( 'strtoupper', openssl_get_cipher_methods() ) ) )
970 $encrypted_ip = base64_encode( openssl_encrypt( $ip, $cipher, $auth_key, 0, mb_strimwidth( $auth_iv, 0, openssl_cipher_iv_length( $cipher ), '', 'UTF-8' ) ) );
971 // mcrypt encryption
972 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' ) ) {
973 // get max key size of the mcrypt mode
974 $max_key_size = mcrypt_get_key_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
975 $max_iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
976
977 $encrypt_key = mb_strimwidth( $auth_key, 0, $max_key_size );
978 $encrypt_iv = mb_strimwidth( $auth_iv, 0, $max_iv_size );
979
980 $encrypted_ip = base64_encode( mcrypt_encrypt( MCRYPT_BLOWFISH, $encrypt_key, $ip, MCRYPT_MODE_CBC, $encrypt_iv ) );
981 // simple encryption
982 } elseif ( function_exists( 'gzdeflate' ) )
983 $encrypted_ip = base64_encode( convert_uuencode( gzdeflate( $ip ) ) );
984 // no encryption
985 else
986 $encrypted_ip = base64_encode( convert_uuencode( $ip ) );
987
988 return $encrypted_ip;
989 }
990
991 /**
992 * Decrypt user IP.
993 *
994 * @param string $encrypted_ip
995 * @return string
996 */
997 public function decrypt_ip( $encrypted_ip ) {
998 $auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : false;
999 $auth_iv = defined( 'NONCE_KEY' ) ? NONCE_KEY : false;
1000 $cipher = 'AES-256-CBC';
1001 $php_71x = version_compare( phpversion(), '7.1.0', '>=' ) && version_compare( phpversion(), '7.2.0', '<' );
1002
1003 // openssl decryption
1004 if ( $auth_key && $auth_iv && function_exists( 'openssl_encrypt' ) && in_array( $cipher, array_map( 'strtoupper', openssl_get_cipher_methods() ) ) )
1005 $ip = openssl_decrypt( base64_decode( $encrypted_ip ), $cipher, $auth_key, 0, mb_strimwidth( $auth_iv, 0, openssl_cipher_iv_length( $cipher ), '', 'UTF-8' ) );
1006 // mcrypt decryption
1007 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' ) ) {
1008 // get max key size of the mcrypt mode
1009 $max_key_size = mcrypt_get_key_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
1010 $max_iv_size = mcrypt_get_iv_size( MCRYPT_BLOWFISH, MCRYPT_MODE_CBC );
1011
1012 $encrypt_key = mb_strimwidth( $auth_key, 0, $max_key_size );
1013 $encrypt_iv = mb_strimwidth( $auth_iv, 0, $max_iv_size );
1014
1015 $ip = rtrim( mcrypt_decrypt( MCRYPT_BLOWFISH, $encrypt_key, base64_decode( $encrypted_ip ), MCRYPT_MODE_CBC, $encrypt_iv ), "\0" );
1016 // simple decryption
1017 } elseif ( function_exists( 'gzinflate' ) )
1018 $ip = gzinflate( convert_uudecode( base64_decode( $encrypted_ip ) ) );
1019 // no decryption
1020 else
1021 $ip = convert_uudecode( base64_decode( $encrypted_ip ) );
1022
1023 return $ip;
1024 }
1025
1026 /**
1027 * Register REST API endpoints.
1028 *
1029 * @return void
1030 */
1031 public function rest_api_init() {
1032 // view post route
1033 register_rest_route(
1034 'post-views-counter',
1035 '/view-post/(?P<id>\d+)|/view-post/',
1036 [
1037 'methods' => [ 'GET', 'POST' ],
1038 'callback' => [ $this, 'check_post_rest_api' ],
1039 'permission_callback' => [ $this, 'post_view_permissions_check' ],
1040 'args' => [
1041 'id' => [
1042 'default' => 0,
1043 'sanitize_callback' => 'absint'
1044 ]
1045 ]
1046 ]
1047 );
1048
1049 // get views route
1050 register_rest_route(
1051 'post-views-counter',
1052 '/get-post-views/(?P<id>(\d+,?)+)',
1053 [
1054 'methods' => [ 'GET', 'POST' ],
1055 'callback' => [ $this, 'get_post_views_rest_api' ],
1056 'permission_callback' => [ $this, 'get_post_views_permissions_check' ],
1057 'args' => [
1058 'id' => [
1059 'default' => 0,
1060 'sanitize_callback' => [ $this, 'validate_rest_api_data' ]
1061 ]
1062 ]
1063 ]
1064 );
1065 }
1066
1067 /**
1068 * Get post views via REST API request.
1069 *
1070 * @param object $request
1071 * @return int
1072 */
1073 public function get_post_views_rest_api( $request ) {
1074 return pvc_get_post_views( $request->get_param( 'id' ) );
1075 }
1076
1077 /**
1078 * Check if a given request has access to view post.
1079 *
1080 * @param object $request
1081 * @return bool
1082 */
1083 public function post_view_permissions_check( $request ) {
1084 return (bool) apply_filters( 'pvc_rest_api_post_views_check', true, $request );
1085 }
1086
1087 /**
1088 * Check if a given request has access to get views.
1089 *
1090 * @param object $request
1091 * @return bool
1092 */
1093 public function get_post_views_permissions_check( $request ) {
1094 return (bool) apply_filters( 'pvc_rest_api_get_post_views_check', true, $request );
1095 }
1096
1097 /**
1098 * Validate REST API incoming data.
1099 *
1100 * @param int|array $data
1101 * @return int|array
1102 */
1103 public function validate_rest_api_data( $data ) {
1104 // POST array?
1105 if ( is_array( $data ) )
1106 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1107 // multiple comma-separated values?
1108 elseif ( strpos( $data, ',' ) !== false ) {
1109 $data = explode( ',', $data );
1110
1111 if ( is_array( $data ) && ! empty( $data ) )
1112 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1113 else
1114 $data = [];
1115 // single value?
1116 } else
1117 $data = absint( $data );
1118
1119 return $data;
1120 }
1121 }