PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.5.5
Post Views Counter v1.5.5
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 1 year ago class-columns.php 1 year ago class-counter.php 1 year ago class-crawler-detect.php 1 year ago class-cron.php 1 year ago class-dashboard.php 1 year ago class-frontend.php 1 year ago class-functions.php 1 year ago class-query.php 1 year ago class-settings-api.php 1 year ago class-settings.php 1 year ago class-update.php 1 year ago class-widgets.php 1 year ago functions.php 1 year ago
class-counter.php
1424 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 private $storage = [];
14 private $storage_type = 'cookies';
15 /* COUNT_POST_AS_AUTHOR_VIEW | removed property
16 private $storage_modified = false;
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 * Get storage data.
41 *
42 * @return array
43 */
44 public function get_storage() {
45 return $this->storage;
46 }
47
48 /**
49 * Set storage data. Used only for additional authors counting.
50 *
51 * @return bool
52 */
53 /* COUNT_POST_AS_AUTHOR_VIEW | removed function
54 public function set_storage( $data, $class ) {
55 if ( ! is_a( $class, 'Post_Views_Counter_Pro_Counter' ) )
56 return false;
57
58 if ( ! $class->is_main_storage_allowed() )
59 return false;
60
61 // is it active content type?
62 if ( ! $class->is_content_type_active( 'user', 'posts' ) )
63 return false;
64
65 if ( $this->storage_type === 'cookies' )
66 $this->storage = $data;
67 else
68 $this->storage['user'] = $data;
69
70 $this->storage_modified = true;
71
72 return true;
73 }
74 */
75
76 /**
77 * Get storage type.
78 *
79 * @return array
80 */
81 public function get_storage_type() {
82 return $this->storage_type;
83 }
84
85 /**
86 * Set storage type. Used only for fast ajax requests.
87 *
88 * @param string $storage_type
89 * @return array
90 */
91 public function set_storage_type( $storage_type, $class ) {
92 // allow only from pro counter class
93 if ( ! is_a( $class, 'Post_Views_Counter_Pro_Counter' ) )
94 return false;
95
96 // allow only fast ajax requests
97 if ( ! ( defined( 'SHORTINIT' ) && SHORTINIT ) )
98 return false;
99
100 // check post data
101 if ( ! isset( $_POST['action'], $_POST['content'], $_POST['type'], $_POST['subtype'], $_POST['storage_type'], $_POST['storage_data'], $_POST['pvcp_nonce'] ) )
102 return false;
103
104 // verify nonce
105 if ( ! wp_verify_nonce( $_POST['pvcp_nonce'], 'pvcp-check-post' ) )
106 return false;
107
108 // allow only valid storage type
109 if ( in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) ) {
110 $this->storage_type = $storage_type;
111
112 return true;
113 }
114
115 return false;
116 }
117
118 /**
119 * Add Post ID to queue.
120 *
121 * @param int $post_id
122 * @return void
123 */
124 public function add_to_queue( $post_id ) {
125 $this->queue[] = (int) $post_id;
126 }
127
128 /**
129 * Run manual pvc_view_post queue.
130 *
131 * @return void
132 */
133 public function queue_count() {
134 // check conditions
135 if ( ! isset( $_POST['action'], $_POST['ids'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-view-posts' ) || $_POST['ids'] === '' || ! is_string( $_POST['ids'] ) )
136 exit;
137
138 // get post ids
139 $ids = explode( ',', $_POST['ids'] );
140
141 $counted = [];
142
143 if ( ! empty( $ids ) ) {
144 $ids = array_filter( array_map( 'intval', $ids ) );
145
146 if ( ! empty( $ids ) ) {
147 // turn on queue mode
148 $this->queue_mode = true;
149
150 foreach ( $ids as $id ) {
151 $counted[$id] = ! ( $this->check_post( $id ) === null );
152 }
153
154 // turn off queue mode
155 $this->queue_mode = false;
156 }
157 }
158
159 echo wp_json_encode(
160 [
161 'post_ids' => $ids,
162 'counted' => $counted
163 ]
164 );
165
166 exit;
167 }
168
169 /**
170 * Print JavaScript with queue in the footer.
171 *
172 * @return void
173 */
174 public function print_queue_count() {
175 // any ids to "view"?
176 if ( ! empty( $this->queue ) ) {
177 echo "
178 <script>
179 ( function( window, document, undefined ) {
180 document.addEventListener( 'DOMContentLoaded', function() {
181 let pvcLoadManualCounter = function( url, counter ) {
182 let pvcScriptTag = document.createElement( 'script' );
183
184 // append script
185 document.body.appendChild( pvcScriptTag );
186
187 // set attributes
188 pvcScriptTag.onload = counter;
189 pvcScriptTag.onreadystatechange = counter;
190 pvcScriptTag.src = url;
191 };
192
193 let pvcExecuteManualCounter = function() {
194 let pvcManualCounterArgs = {
195 url: '" . esc_url( admin_url( 'admin-ajax.php' ) ) . "',
196 nonce: '" . wp_create_nonce( 'pvc-view-posts' ) . "',
197 ids: '" . implode( ',', $this->queue ) . "'
198 };
199
200 // main javascript file was loaded?
201 if ( typeof PostViewsCounter !== 'undefined' && PostViewsCounter.promise !== null ) {
202 PostViewsCounter.promise.then( function() {
203 PostViewsCounterManual.init( pvcManualCounterArgs );
204 } );
205 // PostViewsCounter is undefined or promise is null
206 } else {
207 PostViewsCounterManual.init( pvcManualCounterArgs );
208 }
209 }
210
211 pvcLoadManualCounter( '" . POST_VIEWS_COUNTER_URL . "/js/counter" . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . ".js', pvcExecuteManualCounter );
212 }, false );
213 } )( window, document );
214 </script>";
215 }
216 }
217
218 /**
219 * Initialize counter.
220 *
221 * @return void
222 */
223 public function init_counter() {
224 // admin?
225 if ( is_admin() && ! wp_doing_ajax() )
226 return;
227
228 // get main instance
229 $pvc = Post_Views_Counter();
230
231 // actions
232 add_action( 'wp_ajax_pvc-view-posts', [ $this, 'queue_count' ] );
233 add_action( 'wp_ajax_nopriv_pvc-view-posts', [ $this, 'queue_count' ] );
234 add_action( 'wp_print_footer_scripts', [ $this, 'print_queue_count' ], 11 );
235
236 // php counter
237 if ( $pvc->options['general']['counter_mode'] === 'php' )
238 add_action( 'wp', [ $this, 'check_post_php' ] );
239 // javascript (ajax) counter
240 elseif ( $pvc->options['general']['counter_mode'] === 'js' ) {
241 add_action( 'wp_ajax_pvc-check-post', [ $this, 'check_post_js' ] );
242 add_action( 'wp_ajax_nopriv_pvc-check-post', [ $this, 'check_post_js' ] );
243 }
244
245 // rest api
246 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
247 }
248
249 /**
250 * Check whether to count visit.
251 *
252 * @param int $post_id
253 * @param array $content_data
254 * @return void|int
255 */
256 public function check_post( $post_id = 0, $content_data = [] ) {
257 // force check cookie in short init mode
258 if ( defined( 'SHORTINIT' ) && SHORTINIT )
259 $this->check_cookie();
260
261 // get post id
262 $post_id = (int) ( empty( $post_id ) ? get_the_ID() : $post_id );
263
264 // empty id?
265 if ( empty( $post_id ) )
266 return;
267
268 // get main instance
269 $pvc = Post_Views_Counter();
270
271 // get user id, from current user or static var in rest api request
272 $user_id = get_current_user_id();
273
274 // get user ip address
275 $user_ip = $this->get_user_ip();
276
277 // before visit action
278 do_action( 'pvc_before_check_visit', $post_id, $user_id, $user_ip, 'post', $content_data );
279
280 // check all conditions to count visit
281 add_filter( 'pvc_count_conditions_met', [ $this, 'check_conditions' ], 10, 6 );
282
283 // check conditions - excluded ips, excluded groups
284 $conditions_met = apply_filters( 'pvc_count_conditions_met', true, $post_id, $user_id, $user_ip, 'post', $content_data );
285
286 // conditions failed?
287 if ( ! $conditions_met )
288 return;
289
290 // do not count visit by default
291 $count_visit = false;
292
293 // cookieless data storage?
294 if ( $pvc->options['general']['data_storage'] === 'cookieless' && $this->storage_type === 'cookieless' ) {
295 $count_visit = $this->save_data_storage( $post_id, 'post', $content_data );
296 } elseif ( $pvc->options['general']['data_storage'] === 'cookies' && $this->storage_type === 'cookies' ) {
297 // php counter mode?
298 if ( $pvc->options['general']['counter_mode'] === 'php' ) {
299 if ( $this->cookie['exists'] ) {
300 // update cookie
301 $count_visit = $this->save_cookie( $post_id, $this->cookie );
302 } else {
303 // set new cookie
304 $count_visit = $this->save_cookie( $post_id );
305 }
306 } else
307 $count_visit = $this->save_cookie_storage( $post_id, $content_data );
308 }
309
310 // filter visit counting
311 $count_visit = (bool) apply_filters( 'pvc_count_visit', $count_visit, $post_id, $user_id, $user_ip, 'post', $content_data );
312
313 // count visit
314 if ( $count_visit ) {
315 // before count visit action
316 do_action( 'pvc_before_count_visit', $post_id, $user_id, $user_ip, 'post', $content_data );
317
318 return $this->count_visit( $post_id );
319 }
320 }
321
322 /**
323 * Check whether counting conditions are met.
324 *
325 * @param bool $allow_counting
326 * @param int $post_id
327 * @param int $user_id
328 * @param string $user_ip
329 * @param string $content_type
330 * @param array $content_data
331 * @return bool
332 */
333 public function check_conditions( $allow_counting, $post_id, $user_id, $user_ip, $content_type, $content_data ) {
334 // already failed?
335 if ( ! $allow_counting )
336 return false;
337
338 // get main instance
339 $pvc = Post_Views_Counter();
340
341 // get ips
342 $ips = $pvc->options['general']['exclude_ips'];
343
344 // whether to count this ip
345 if ( ! empty( $ips ) && filter_var( preg_replace( '/[^0-9a-fA-F:., ]/', '', $user_ip ), FILTER_VALIDATE_IP ) ) {
346 // check ips
347 foreach ( $ips as $ip ) {
348 if ( strpos( $ip, '*' ) !== false ) {
349 if ( $this->ipv4_in_range( $user_ip, $ip ) )
350 return false;
351 } else {
352 if ( $user_ip === $ip )
353 return false;
354 }
355 }
356 }
357
358 // get groups to check them faster
359 $groups = $pvc->options['general']['exclude']['groups'];
360
361 // whether to count this user
362 if ( ! empty( $user_id ) ) {
363 // exclude logged in users?
364 if ( in_array( 'users', $groups, true ) )
365 return false;
366 // exclude specific roles?
367 elseif ( in_array( 'roles', $groups, true ) && $this->is_user_role_excluded( $user_id, $pvc->options['general']['exclude']['roles'] ) )
368 return false;
369 // exclude guests?
370 } elseif ( in_array( 'guests', $groups, true ) )
371 return false;
372
373 // whether to count robots
374 if ( in_array( 'robots', $groups, true ) && $pvc->crawler->is_crawler() )
375 return false;
376
377 return $allow_counting;
378 }
379
380 /**
381 * Check whether real home page is displayed.
382 *
383 * @param object $object
384 * @return bool
385 */
386 public function is_homepage( $object ) {
387 $is_homepage = false;
388
389 // get show on front option
390 $show_on_front = get_option( 'show_on_front' );
391
392 if ( $show_on_front === 'posts' )
393 $is_homepage = is_home() && is_front_page();
394 else {
395 // home page
396 $homepage = (int) get_option( 'page_on_front' );
397
398 // posts page
399 $postspage = (int) get_option( 'page_for_posts' );
400
401 // both pages are set
402 if ( $homepage && $postspage )
403 $is_homepage = is_front_page();
404 // only home page is set
405 elseif ( $homepage && ! $postspage )
406 $is_homepage = is_front_page();
407 // only posts page is set
408 elseif( ! $homepage && $postspage )
409 $is_homepage = is_home() && ( empty( $object ) || get_queried_object_id() === 0 );
410 }
411
412 return $is_homepage;
413 }
414
415 /**
416 * Check whether posts page (archive) is displayed.
417 *
418 * @param object $object
419 * @return bool
420 */
421 public function is_posts_page( $object ) {
422 // get show on front option
423 $show_on_front = get_option( 'show_on_front' );
424
425 // get page for posts option
426 $page_for_posts = (int) get_option( 'page_for_posts' );
427
428 // check page
429 $result = ( $show_on_front === 'page' && ! empty( $object ) && is_home() && is_a( $object, 'WP_Post' ) && (int) $object->ID === $page_for_posts );
430
431 return apply_filters( 'pvc_is_posts_page', $result, $object );
432 }
433
434 /**
435 * Check whether to count visit via PHP request.
436 *
437 * @return void
438 */
439 public function check_post_php() {
440 // do not count admin entries
441 if ( is_admin() && ! wp_doing_ajax() )
442 return;
443
444 // skip special requests
445 if ( is_preview() || is_feed() || is_trackback() || ( function_exists( 'is_favicon' ) && is_favicon() ) || is_customize_preview() )
446 return;
447
448 // get main instance
449 $pvc = Post_Views_Counter();
450
451 // do we use php as counter?
452 if ( $pvc->options['general']['counter_mode'] !== 'php' )
453 return;
454
455 // get countable post types
456 $post_types = $pvc->options['general']['post_types_count'];
457
458 // whether to count this post type
459 if ( empty( $post_types ) || ! is_singular( $post_types ) )
460 return;
461
462 // get current post id
463 $post_id = (int) get_the_ID();
464
465 // allow to run check post?
466 if ( ! (bool) apply_filters( 'pvc_run_check_post', true, $post_id ) )
467 return;
468
469 $this->check_post( $post_id );
470 }
471
472 /**
473 * Check whether to count visit via JavaScript (AJAX) request.
474 *
475 * @return void
476 */
477 public function check_post_js() {
478 // check conditions
479 if ( ! isset( $_POST['action'], $_POST['id'], $_POST['storage_type'], $_POST['storage_data'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-check-post' ) )
480 exit;
481
482 // get post id
483 $post_id = (int) $_POST['id'];
484
485 if ( $post_id <= 0 )
486 exit;
487
488 // get main instance
489 $pvc = Post_Views_Counter();
490
491 // do we use javascript as counter?
492 if ( $pvc->options['general']['counter_mode'] !== 'js' )
493 exit;
494
495 // get countable post types
496 $post_types = $pvc->options['general']['post_types_count'];
497
498 // check if post exists
499 $post = get_post( $post_id );
500
501 // whether to count this post type or not
502 if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) )
503 exit;
504
505 // get storage type
506 $storage_type = sanitize_key( $_POST['storage_type'] );
507
508 // invalid storage type?
509 if ( ! in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) )
510 exit;
511
512 // set storage type
513 $this->storage_type = $storage_type;
514
515 // cookieless data storage?
516 if ( $storage_type === 'cookieless' && $pvc->options['general']['data_storage'] === 'cookieless' ) {
517 // sanitize storage data
518 $storage_data = $this->sanitize_storage_data( $_POST['storage_data'] );
519 // cookies?
520 } elseif ( $storage_type === 'cookies' && $pvc->options['general']['data_storage'] === 'cookies' ) {
521 // sanitize cookies data
522 $storage_data = $this->sanitize_cookies_data( $_POST['storage_data'] );
523 } else
524 $storage_data = [];
525
526 echo wp_json_encode(
527 [
528 'post_id' => $post_id,
529 'counted' => ! ( $this->check_post( $post_id, $storage_data ) === null ),
530 'storage' => $this->storage,
531 'type' => 'post'
532 ]
533 );
534
535 exit;
536 }
537
538 /**
539 * Check whether to count visit via REST API request.
540 *
541 * @param object $request
542 * @return int|WP_Error
543 */
544 public function check_post_rest_api( $request ) {
545 // get main instance
546 $pvc = Post_Views_Counter();
547
548 // get post id (already sanitized)
549 $post_id = $request->get_param( 'id' );
550
551 // do we use REST API as counter?
552 if ( $pvc->options['general']['counter_mode'] !== 'rest_api' )
553 return new WP_Error( 'pvc_rest_api_disabled', __( 'REST API method is disabled.', 'post-views-counter' ), [ 'status' => 404 ] );
554
555 //TODO get current user id in direct api endpoint calls
556 // check if post exists
557 $post = get_post( $post_id );
558
559 if ( ! $post )
560 return new WP_Error( 'pvc_post_invalid_id', __( 'Invalid post ID.', 'post-views-counter' ), [ 'status' => 404 ] );
561
562 // get countable post types
563 $post_types = $pvc->options['general']['post_types_count'];
564
565 // whether to count this post type
566 if ( empty( $post_types ) || ! in_array( $post->post_type, $post_types, true ) )
567 return new WP_Error( 'pvc_post_type_excluded', __( 'Post type excluded.', 'post-views-counter' ), [ 'status' => 404 ] );
568
569 // get storage type
570 $storage_type = sanitize_key( $request->get_param( 'storage_type' ) );
571
572 // invalid storage type?
573 if ( ! in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) )
574 return new WP_Error( 'pvc_invalid_storage_type', __( 'Invalid storage type.', 'post-views-counter' ), [ 'status' => 404 ] );
575
576 // set storage type
577 $this->storage_type = $storage_type;
578
579 // cookieless data storage?
580 if ( $storage_type === 'cookieless' && $pvc->options['general']['data_storage'] === 'cookieless' ) {
581 // sanitize storage data
582 $storage_data = $this->sanitize_storage_data( $request->get_param( 'storage_data' ) );
583 // cookies?
584 } elseif ( $storage_type === 'cookies' && $pvc->options['general']['data_storage'] === 'cookies' ) {
585 // sanitize cookies data
586 $storage_data = $this->sanitize_cookies_data( $request->get_param( 'storage_data' ) );
587 } else
588 $storage_data = [];
589
590 return [
591 'post_id' => $post_id,
592 'counted' => ! ( $this->check_post( $post_id, $storage_data ) === null ),
593 'storage' => $this->storage,
594 'type' => 'post'
595 ];
596 }
597
598 /**
599 * Initialize cookie session.
600 *
601 * @param array $cookie Use this data instead of real $_COOKIE
602 * @return void
603 */
604 public function check_cookie( $cookie = [] ) {
605 // do not run in admin except for ajax requests
606 if ( is_admin() && ! wp_doing_ajax() )
607 return;
608
609 if ( empty( $cookie ) || ! is_array( $cookie ) ) {
610 // assign cookie name
611 $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );
612
613 // is cookie set?
614 if ( isset( $_COOKIE[$cookie_name] ) && ! empty( $_COOKIE[$cookie_name] ) )
615 $cookie = $_COOKIE[$cookie_name];
616 }
617
618 // cookie data?
619 if ( $cookie && is_array( $cookie ) ) {
620 $visited_posts = $expirations = [];
621
622 foreach ( $cookie as $content ) {
623 // is cookie valid?
624 if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $content ) === 1 ) {
625 // get single id with expiration
626 $expiration_ids = explode( 'a', $content );
627
628 // check every expiration => id pair
629 foreach ( $expiration_ids as $pair ) {
630 $pair = explode( 'b', $pair );
631 $expirations[] = (int) $pair[0];
632 $visited_posts[(int) $pair[1]] = (int) $pair[0];
633 }
634 }
635 }
636
637 // update cookie
638 $this->cookie = [
639 'exists' => true,
640 'visited_posts' => $visited_posts,
641 'expiration' => empty( $expirations ) ? 0 : max( $expirations )
642 ];
643 }
644 }
645
646 /**
647 * Sanitize storage data.
648 *
649 * @param string $storage_data
650 * @return array
651 */
652 public function sanitize_storage_data( $storage_data ) {
653 try {
654 // strip slashes
655 $storage_data = stripslashes( $storage_data );
656
657 // decode storage data
658 $storage_data = json_decode( $storage_data, true, 2 );
659 } finally {
660 // valid data?
661 if ( json_last_error() === JSON_ERROR_NONE && is_array( $storage_data ) && ! empty( $storage_data ) ) {
662 $content_data = [];
663
664 foreach ( $storage_data as $content_id => $content_expiration ) {
665 $content_data[(int) $content_id] = (int) $content_expiration;
666 }
667
668 return array_unique( $content_data, SORT_NUMERIC );
669 } else
670 return [];
671 }
672 }
673
674 /**
675 * Sanitize cookies.
676 *
677 * @param string $storage_data
678 * @return array
679 */
680 public function sanitize_cookies_data( $storage_data ) {
681 $content_data = $expirations = [];
682
683 // is cookie valid?
684 if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $storage_data ) === 1 ) {
685 // get single id with expiration
686 $expiration_ids = explode( 'a', $storage_data );
687
688 // check every expiration => id pair
689 foreach ( $expiration_ids as $pair ) {
690 $pair = explode( 'b', $pair );
691 $expirations[] = (int) $pair[0];
692 $content_data[(int) $pair[1]] = (int) $pair[0];
693 }
694 }
695
696 return [
697 'visited' => array_unique( $content_data, SORT_NUMERIC ),
698 'expiration' => empty( $expirations ) ? 0 : max( $expirations )
699 ];
700 }
701
702 /**
703 * Save data storage.
704 *
705 * @param int $content
706 * @param string $content_type
707 * @param array $content_data
708 * @return bool
709 */
710 private function save_data_storage( $content, $content_type, $content_data ) {
711 // get base instance
712 $pvc = Post_Views_Counter();
713
714 // set default flag
715 $count_visit = true;
716
717 // get expiration
718 $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] );
719
720 // is this a new cookie?
721 if ( empty( $content_data ) ) {
722 $storage = [
723 $content => $expiration
724 ];
725 } else {
726 // get current gmt time
727 $current_time = current_time( 'timestamp', true );
728
729 // post already viewed but not expired?
730 if ( in_array( $content, array_keys( $content_data ), true ) && $current_time < $content_data[$content] )
731 $count_visit = false;
732
733 // create copy for better foreach performance
734 $content_data_tmp = $content_data;
735
736 // check whether viewed id has expired - no need to keep it anymore
737 foreach ( $content_data_tmp as $content_id => $content_expiration ) {
738 if ( $current_time > $content_expiration )
739 unset( $content_data[$content_id] );
740 }
741
742 // add new id or change expiration date if id already exists
743 if ( $count_visit )
744 $content_data[$content] = $expiration;
745
746 $storage = $content_data;
747 }
748
749 $this->storage[$content_type] = $storage;
750
751 return $count_visit;
752 }
753
754 /**
755 * Save cookie storage.
756 *
757 * @param int $content
758 * @param array $content_data
759 * @return bool
760 */
761 private function save_cookie_storage( $content, $content_data ) {
762 // early return?
763 //TODO check this filter in js
764 // if ( apply_filters( 'pvc_maybe_set_cookie', true, $content, $content_type, $content_data ) !== true )
765 // return;
766
767 // get base instance
768 $pvc = Post_Views_Counter();
769
770 // set default flag
771 $count_visit = true;
772
773 // get expiration
774 $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] );
775
776 // assign cookie name
777 $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );
778
779 $cookies_data = [
780 'name' => [],
781 'value' => [],
782 'expiry' => []
783 ];
784
785 // is this a new cookie?
786 if ( empty( $content_data['visited'] ) ) {
787 $cookies_data['name'][] = $cookie_name . '[0]';
788 $cookies_data['value'][] = $expiration . 'b' . $content;
789 $cookies_data['expiry'][] = $expiration;
790 } else {
791 // get current gmt time
792 $current_time = current_time( 'timestamp', true );
793
794 if ( in_array( $content, array_keys( $content_data['visited'] ), true ) && $current_time < $content_data['visited'][$content] ) {
795 $count_visit = false;
796 } else {
797 // add new id or change expiration date if id already exists
798 $content_data['visited'][$content] = $expiration;
799 }
800
801 // create copy for better foreach performance
802 $visited_expirations = $content_data['visited'];
803
804 // check whether viewed id has expired - no need to keep it in cookie (less size)
805 foreach ( $visited_expirations as $content_id => $content_expiration ) {
806 if ( $current_time > $content_expiration )
807 unset( $content_data['visited'][$content_id] );
808 }
809
810 // set new last expiration date if needed
811 $content_data['expiration'] = empty( $content_data['visited'] ) ? 0 : max( $content_data['visited'] );
812
813 $cookies = $imploded = [];
814
815 // create pairs
816 foreach ( $content_data['visited'] as $id => $exp ) {
817 $imploded[] = $exp . 'b' . $id;
818 }
819
820 // split cookie into chunks (3980 bytes to make sure it is safe for every browser)
821 $chunks = str_split( implode( 'a', $imploded ), 3980 );
822
823 // more then one chunk?
824 if ( count( $chunks ) > 1 ) {
825 $last_id = '';
826
827 foreach ( $chunks as $chunk_id => $chunk ) {
828 // new chunk
829 $chunk_c = $last_id . $chunk;
830
831 // is it full-length chunk?
832 if ( strlen( $chunk ) === 3980 ) {
833 // get last part
834 $last_part = strrchr( $chunk_c, 'a' );
835
836 // get last id
837 $last_id = substr( $last_part, 1 );
838
839 // add new full-lenght chunk
840 $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) );
841 } else {
842 // add last chunk
843 $cookies[$chunk_id] = $chunk_c;
844 }
845 }
846 } else {
847 // only one chunk
848 $cookies[] = $chunks[0];
849 }
850
851 foreach ( $cookies as $key => $value ) {
852 $cookies_data['name'][] = $cookie_name . '[' . $key . ']';
853 $cookies_data['value'][] = $value;
854 $cookies_data['expiry'][] = $content_data['expiration'];
855 }
856 }
857
858 /* COUNT_POST_AS_AUTHOR_VIEW | removed additional data
859 if ( $this->storage_modified && ! empty( $this->storage ) ) {
860 foreach ( $this->storage as $key => $value ) {
861 foreach ( $value as $subkey => $subvalue ) {
862 $cookies_data[$key][] = $subvalue;
863 }
864 }
865 }
866 */
867
868 $this->storage = $cookies_data;
869
870 return $count_visit;
871 }
872
873 /**
874 * Save cookie function.
875 *
876 * @param int $id
877 * @param array $cookie
878 * @return bool
879 */
880 private function save_cookie( $id, $cookie = [] ) {
881 // early return?
882 if ( apply_filters( 'pvc_maybe_set_cookie', true, $id, 'post', $cookie ) !== true )
883 return;
884
885 // get main instance
886 $pvc = Post_Views_Counter();
887
888 // set default flag
889 $count_visit = true;
890
891 // get expiration
892 $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] );
893
894 // assign cookie name
895 $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' );
896
897 // check whether php version is at least 7.3
898 $php_at_least_73 = version_compare( phpversion(), '7.3', '>=' );
899
900 // is this a new cookie?
901 if ( empty( $cookie ) ) {
902 if ( $php_at_least_73 ) {
903 // set cookie
904 setcookie(
905 $cookie_name . '[0]',
906 $expiration . 'b' . $id,
907 [
908 'expires' => $expiration,
909 'path' => COOKIEPATH,
910 'domain' => COOKIE_DOMAIN,
911 'secure' => is_ssl(),
912 'httponly' => false,
913 'samesite' => 'LAX'
914 ]
915 );
916 } else {
917 // set cookie
918 setcookie( $cookie_name . '[0]', $expiration . 'b' . $id, $expiration, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false );
919 }
920
921 if ( $this->queue_mode )
922 $this->check_cookie( [ 0 => $expiration . 'b' . $id ] );
923 } else {
924 // get current gmt time
925 $current_time = current_time( 'timestamp', true );
926
927 // post already viewed but not expired?
928 if ( in_array( $id, array_keys( $cookie['visited_posts'] ), true ) && $current_time < $cookie['visited_posts'][$id] )
929 $count_visit = false;
930 else {
931 // add new id or change expiration date if id already exists
932 $cookie['visited_posts'][$id] = $expiration;
933 }
934
935 // create copy for better foreach performance
936 $visited_posts_expirations = $cookie['visited_posts'];
937
938 // check whether viewed id has expired - no need to keep it in cookie (less size)
939 foreach ( $visited_posts_expirations as $post_id => $post_expiration ) {
940 if ( $current_time > $post_expiration )
941 unset( $cookie['visited_posts'][$post_id] );
942 }
943
944 // set new last expiration date if needed
945 $cookie['expiration'] = empty( $cookie['visited_posts'] ) ? 0 : max( $cookie['visited_posts'] );
946
947 $cookies = $imploded = [];
948
949 // create pairs
950 foreach ( $cookie['visited_posts'] as $id => $exp ) {
951 $imploded[] = $exp . 'b' . $id;
952 }
953
954 // split cookie into chunks (3980 bytes to make sure it is safe for every browser)
955 $chunks = str_split( implode( 'a', $imploded ), 3980 );
956
957 // more then one chunk?
958 if ( count( $chunks ) > 1 ) {
959 $last_id = '';
960
961 foreach ( $chunks as $chunk_id => $chunk ) {
962 // new chunk
963 $chunk_c = $last_id . $chunk;
964
965 // is it full-length chunk?
966 if ( strlen( $chunk ) === 3980 ) {
967 // get last part
968 $last_part = strrchr( $chunk_c, 'a' );
969
970 // get last id
971 $last_id = substr( $last_part, 1 );
972
973 // add new full-lenght chunk
974 $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) );
975 } else {
976 // add last chunk
977 $cookies[$chunk_id] = $chunk_c;
978 }
979 }
980 } else {
981 // only one chunk
982 $cookies[] = $chunks[0];
983 }
984
985 foreach ( $cookies as $key => $value ) {
986 if ( $php_at_least_73 ) {
987 // set cookie
988 setcookie(
989 $cookie_name . '[' . $key . ']',
990 $value,
991 [
992 'expires' => $cookie['expiration'],
993 'path' => COOKIEPATH,
994 'domain' => COOKIE_DOMAIN,
995 'secure' => is_ssl(),
996 'httponly' => false,
997 'samesite' => 'LAX'
998 ]
999 );
1000 } else {
1001 // set cookie
1002 setcookie( $cookie_name . '[' . $key . ']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false );
1003 }
1004 }
1005
1006 if ( $this->queue_mode )
1007 $this->check_cookie( $cookies );
1008 }
1009
1010 return $count_visit;
1011 }
1012
1013 /**
1014 * Count visit.
1015 *
1016 * @param int $id
1017 * @return int
1018 */
1019 private function count_visit( $id ) {
1020 // get main instance
1021 $pvc = Post_Views_Counter();
1022
1023 // increment amount
1024 $increment_amount = (int) apply_filters( 'pvc_views_increment_amount', 1, $id, 'post' );
1025
1026 if ( $increment_amount < 1 )
1027 $increment_amount = 1;
1028
1029 $use_gmt = $pvc->options['general']['count_time'] === 'local' ? false : true;
1030 $count_time = current_time( 'timestamp', $use_gmt );
1031
1032 // get day, week, month and year
1033 $date = explode( '-', date( 'W-d-m-Y-o', $count_time ) );
1034
1035 foreach ( [
1036 0 => $date[3] . $date[2] . $date[1], // day like 20140324
1037 1 => $date[4] . $date[0], // week like 201439
1038 2 => $date[3] . $date[2], // month like 201405
1039 3 => $date[3], // year like 2014
1040 4 => 'total' // total views
1041 ] as $type => $period ) {
1042 //TODO investigate queueing these queries on the 'shutdown' hook instead of running them instantly?
1043 // hit the database directly
1044 $this->db_insert( $id, $type, $period, $increment_amount );
1045 }
1046
1047 do_action( 'pvc_after_count_visit', $id, 'post' );
1048
1049 return $id;
1050 }
1051
1052 /**
1053 * Remove post views from database when post is deleted.
1054 *
1055 * @global object $wpdb
1056 *
1057 * @param int $post_id
1058 * @return void
1059 */
1060 public function delete_post_views( $post_id ) {
1061 global $wpdb;
1062
1063 $data = [
1064 'where' => [ 'id' => $post_id ],
1065 'format' => [ '%d' ]
1066 ];
1067
1068 $data = apply_filters( 'pvc_delete_post_views_where_clause', $data, $post_id );
1069
1070 $wpdb->delete( $wpdb->prefix . 'post_views', $data['where'], $data['format'] );
1071 }
1072
1073 /**
1074 * Get timestamp convertion.
1075 *
1076 * @param string $type
1077 * @param int $number
1078 * @param bool $timestamp
1079 * @return int
1080 */
1081 public function get_timestamp( $type, $number, $timestamp = true ) {
1082 $converter = [
1083 'minutes' => MINUTE_IN_SECONDS,
1084 'hours' => HOUR_IN_SECONDS,
1085 'days' => DAY_IN_SECONDS,
1086 'weeks' => WEEK_IN_SECONDS,
1087 'months' => MONTH_IN_SECONDS,
1088 'years' => YEAR_IN_SECONDS
1089 ];
1090
1091 return (int) ( ( $timestamp ? current_time( 'timestamp', true ) : 0 ) + $number * $converter[$type] );
1092 }
1093
1094 /**
1095 * Check if object cache is in use.
1096 *
1097 * @param bool $only_interval
1098 * @return bool
1099 */
1100 public function using_object_cache( $only_interval = false ) {
1101 $using = wp_using_ext_object_cache();
1102
1103 // is object cache active?
1104 if ( $using ) {
1105 // get main instance
1106 $pvc = Post_Views_Counter();
1107
1108 // check object cache
1109 if ( ! $only_interval && ! $pvc->options['general']['object_cache'] )
1110 $using = false;
1111
1112 // check interval
1113 if ( $pvc->options['general']['flush_interval']['number'] <= 0 )
1114 $using = false;
1115 }
1116
1117 return $using;
1118 }
1119
1120 /**
1121 * Flush views data stored in the persistent object cache into
1122 * our custom table and clear the object cache keys when done.
1123 *
1124 * @return bool
1125 */
1126 public function flush_cache_to_db() {
1127 // get keys
1128 $key_names = wp_cache_get( 'cached_key_names', 'pvc' );
1129
1130 if ( ! $key_names )
1131 $key_names = [];
1132 else {
1133 // create an array out of a string that's stored in the cache
1134 $key_names = explode( '|', $key_names );
1135 }
1136
1137 // any data?
1138 if ( ! empty( $key_names ) ) {
1139 foreach ( $key_names as $key_name ) {
1140 // get values stored within the key name itself
1141 list( $id, $type, $period ) = explode( '.', $key_name );
1142
1143 // get the cached count value
1144 $count = wp_cache_get( $key_name, 'pvc' );
1145
1146 // store cached value in the database
1147 $this->db_prepare_insert( $id, $type, $period, $count );
1148
1149 // clear the cache key we just flushed
1150 wp_cache_delete( $key_name, 'pvc' );
1151 }
1152
1153 // flush values to database
1154 $this->db_commit_insert();
1155
1156 // delete the key holding the list
1157 wp_cache_delete( 'cached_key_names', 'pvc' );
1158 }
1159
1160 // remove last flush
1161 wp_cache_delete( 'last-flush', 'pvc' );
1162
1163 return true;
1164 }
1165
1166 /**
1167 * Insert or update views count.
1168 *
1169 * @global object $wpdb
1170 *
1171 * @param int $id
1172 * @param int $type
1173 * @param string $period
1174 * @param int $count
1175 * @return int|bool
1176 */
1177 private function db_insert( $id, $type, $period, $count ) {
1178 // check whether skip single query
1179 $skip_single_query = (bool) apply_filters( 'pvc_skip_single_query', false, $id, $type, $period, $count, 'post' );
1180
1181 // skip query?
1182 if ( $skip_single_query )
1183 return false;
1184
1185 global $wpdb;
1186
1187 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 ) );
1188 }
1189
1190 /**
1191 * Prepare bulk insert or update views count.
1192 *
1193 * @param int $id
1194 * @param int $type
1195 * @param string $period
1196 * @param int $count
1197 * @return void
1198 */
1199 private function db_prepare_insert( $id, $type, $period, $count = 1 ) {
1200 // cast count
1201 $count = (int) $count;
1202
1203 if ( ! $count )
1204 $count = 1;
1205
1206 // any queries?
1207 if ( ! empty( $this->db_insert_values ) )
1208 $this->db_insert_values .= ', ';
1209
1210 // append insert queries
1211 $this->db_insert_values .= sprintf( '(%d, %d, "%s", %d)', $id, $type, $period, $count );
1212
1213 if ( strlen( $this->db_insert_values ) > 25000 )
1214 $this->db_commit_insert();
1215 }
1216
1217 /**
1218 * Insert accumulated values to database.
1219 *
1220 * @global object $wpdb
1221 *
1222 * @return int|bool
1223 */
1224 private function db_commit_insert() {
1225 if ( empty( $this->db_insert_values ) )
1226 return false;
1227
1228 global $wpdb;
1229
1230 $result = $wpdb->query(
1231 "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
1232 VALUES " . $this->db_insert_values . "
1233 ON DUPLICATE KEY UPDATE count = count + VALUES(count)"
1234 );
1235
1236 $this->db_insert_values = '';
1237
1238 return $result;
1239 }
1240
1241 /**
1242 * Check whether user has excluded roles.
1243 *
1244 * @param int $user_id
1245 * @param string $option
1246 * @return bool
1247 */
1248 public function is_user_role_excluded( $user_id, $option = [] ) {
1249 // get user by ID
1250 $user = get_user_by( 'id', $user_id );
1251
1252 // no user?
1253 if ( empty( $user ) )
1254 return false;
1255
1256 // get user roles
1257 $roles = (array) $user->roles;
1258
1259 // any roles?
1260 if ( ! empty( $roles ) ) {
1261 foreach ( $roles as $role ) {
1262 if ( in_array( $role, $option, true ) )
1263 return true;
1264 }
1265 }
1266
1267 return false;
1268 }
1269
1270 /**
1271 * Check if IPv4 is in range.
1272 *
1273 * @param string $ip IP address
1274 * @param string $range IP range
1275 * @return bool
1276 */
1277 public function ipv4_in_range( $ip, $range ) {
1278 $start = str_replace( '*', '0', $range );
1279 $end = str_replace( '*', '255', $range );
1280 $ip = (float) sprintf( "%u", ip2long( $ip ) );
1281
1282 return ( $ip >= (float) sprintf( "%u", ip2long( $start ) ) && $ip <= (float) sprintf( "%u", ip2long( $end ) ) );
1283 }
1284
1285 /**
1286 * Get user real IP address.
1287 *
1288 * @return string
1289 */
1290 public function get_user_ip() {
1291 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
1292
1293 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 ) {
1294 if ( array_key_exists( $key, $_SERVER ) === true ) {
1295 foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
1296 // trim for safety measures
1297 $ip = trim( $ip );
1298
1299 // attempt to validate IP
1300 if ( $this->validate_user_ip( $ip ) )
1301 continue;
1302 }
1303 }
1304 }
1305
1306 return (string) $ip;
1307 }
1308
1309 /**
1310 * Ensure an IP address is both a valid IP and does not fall within a private network range.
1311 *
1312 * @param $ip string IP address
1313 * @return bool
1314 */
1315 public function validate_user_ip( $ip ) {
1316 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false )
1317 return false;
1318
1319 return true;
1320 }
1321
1322 /**
1323 * Register REST API endpoints.
1324 *
1325 * @return void
1326 */
1327 public function rest_api_init() {
1328 // view post route
1329 register_rest_route(
1330 'post-views-counter',
1331 '/view-post/(?P<id>\d+)|/view-post/',
1332 [
1333 'methods' => [ 'POST' ],
1334 'callback' => [ $this, 'check_post_rest_api' ],
1335 'permission_callback' => [ $this, 'view_post_permissions_check' ],
1336 'args' => apply_filters( 'pvc_rest_api_view_post_args', [
1337 'id' => [
1338 'default' => 0,
1339 'sanitize_callback' => 'absint'
1340 ],
1341 'storage_type' => [
1342 'default' => 'cookies'
1343 ],
1344 'storage_data' => [
1345 'default' => ''
1346 ]
1347 ] )
1348 ]
1349 );
1350
1351 // get views route
1352 register_rest_route(
1353 'post-views-counter',
1354 '/get-post-views/(?P<id>(\d+,?)+)',
1355 [
1356 'methods' => [ 'GET', 'POST' ],
1357 'callback' => [ $this, 'get_post_views_rest_api' ],
1358 'permission_callback' => [ $this, 'get_post_views_permissions_check' ],
1359 'args' => apply_filters( 'pvc_rest_api_get_post_views_args', [
1360 'id' => [
1361 'default' => 0,
1362 'sanitize_callback' => [ $this, 'validate_rest_api_data' ]
1363 ]
1364 ] )
1365 ]
1366 );
1367 }
1368
1369 /**
1370 * Get post views via REST API request.
1371 *
1372 * @param object $request
1373 * @return int
1374 */
1375 public function get_post_views_rest_api( $request ) {
1376 return pvc_get_post_views( $request->get_param( 'id' ) );
1377 }
1378
1379 /**
1380 * Check if a given request has access to get views.
1381 *
1382 * @param object $request
1383 * @return bool
1384 */
1385 public function get_post_views_permissions_check( $request ) {
1386 return (bool) apply_filters( 'pvc_rest_api_get_post_views_check', true, $request );
1387 }
1388
1389 /**
1390 * Check if a given request has access to view post.
1391 *
1392 * @param object $request
1393 * @return bool
1394 */
1395 public function view_post_permissions_check( $request ) {
1396 return (bool) apply_filters( 'pvc_rest_api_view_post_check', true, $request );
1397 }
1398
1399 /**
1400 * Validate REST API incoming data.
1401 *
1402 * @param int|array $data
1403 * @return int|array
1404 */
1405 public function validate_rest_api_data( $data ) {
1406 // POST array?
1407 if ( is_array( $data ) )
1408 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1409 // multiple comma-separated values?
1410 elseif ( strpos( $data, ',' ) !== false ) {
1411 $data = explode( ',', $data );
1412
1413 if ( is_array( $data ) && ! empty( $data ) )
1414 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1415 else
1416 $data = [];
1417 // single value?
1418 } else
1419 $data = absint( $data );
1420
1421 return $data;
1422 }
1423 }
1424