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