PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.5.4
Post Views Counter v1.5.4
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
1418 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 // increment amount
1021 $increment_amount = (int) apply_filters( 'pvc_views_increment_amount', 1, $id, 'post' );
1022
1023 if ( $increment_amount < 1 )
1024 $increment_amount = 1;
1025
1026 // get day, week, month and year
1027 $date = explode( '-', date( 'W-d-m-Y-o', current_time( 'timestamp', true ) ) );
1028
1029 foreach ( [
1030 0 => $date[3] . $date[2] . $date[1], // day like 20140324
1031 1 => $date[4] . $date[0], // week like 201439
1032 2 => $date[3] . $date[2], // month like 201405
1033 3 => $date[3], // year like 2014
1034 4 => 'total' // total views
1035 ] as $type => $period ) {
1036 //TODO investigate queueing these queries on the 'shutdown' hook instead of running them instantly?
1037 // hit the database directly
1038 $this->db_insert( $id, $type, $period, $increment_amount );
1039 }
1040
1041 do_action( 'pvc_after_count_visit', $id, 'post' );
1042
1043 return $id;
1044 }
1045
1046 /**
1047 * Remove post views from database when post is deleted.
1048 *
1049 * @global object $wpdb
1050 *
1051 * @param int $post_id
1052 * @return void
1053 */
1054 public function delete_post_views( $post_id ) {
1055 global $wpdb;
1056
1057 $data = [
1058 'where' => [ 'id' => $post_id ],
1059 'format' => [ '%d' ]
1060 ];
1061
1062 $data = apply_filters( 'pvc_delete_post_views_where_clause', $data, $post_id );
1063
1064 $wpdb->delete( $wpdb->prefix . 'post_views', $data['where'], $data['format'] );
1065 }
1066
1067 /**
1068 * Get timestamp convertion.
1069 *
1070 * @param string $type
1071 * @param int $number
1072 * @param bool $timestamp
1073 * @return int
1074 */
1075 public function get_timestamp( $type, $number, $timestamp = true ) {
1076 $converter = [
1077 'minutes' => MINUTE_IN_SECONDS,
1078 'hours' => HOUR_IN_SECONDS,
1079 'days' => DAY_IN_SECONDS,
1080 'weeks' => WEEK_IN_SECONDS,
1081 'months' => MONTH_IN_SECONDS,
1082 'years' => YEAR_IN_SECONDS
1083 ];
1084
1085 return (int) ( ( $timestamp ? current_time( 'timestamp', true ) : 0 ) + $number * $converter[$type] );
1086 }
1087
1088 /**
1089 * Check if object cache is in use.
1090 *
1091 * @param bool $only_interval
1092 * @return bool
1093 */
1094 public function using_object_cache( $only_interval = false ) {
1095 $using = wp_using_ext_object_cache();
1096
1097 // is object cache active?
1098 if ( $using ) {
1099 // get main instance
1100 $pvc = Post_Views_Counter();
1101
1102 // check object cache
1103 if ( ! $only_interval && ! $pvc->options['general']['object_cache'] )
1104 $using = false;
1105
1106 // check interval
1107 if ( $pvc->options['general']['flush_interval']['number'] <= 0 )
1108 $using = false;
1109 }
1110
1111 return $using;
1112 }
1113
1114 /**
1115 * Flush views data stored in the persistent object cache into
1116 * our custom table and clear the object cache keys when done.
1117 *
1118 * @return bool
1119 */
1120 public function flush_cache_to_db() {
1121 // get keys
1122 $key_names = wp_cache_get( 'cached_key_names', 'pvc' );
1123
1124 if ( ! $key_names )
1125 $key_names = [];
1126 else {
1127 // create an array out of a string that's stored in the cache
1128 $key_names = explode( '|', $key_names );
1129 }
1130
1131 // any data?
1132 if ( ! empty( $key_names ) ) {
1133 foreach ( $key_names as $key_name ) {
1134 // get values stored within the key name itself
1135 list( $id, $type, $period ) = explode( '.', $key_name );
1136
1137 // get the cached count value
1138 $count = wp_cache_get( $key_name, 'pvc' );
1139
1140 // store cached value in the database
1141 $this->db_prepare_insert( $id, $type, $period, $count );
1142
1143 // clear the cache key we just flushed
1144 wp_cache_delete( $key_name, 'pvc' );
1145 }
1146
1147 // flush values to database
1148 $this->db_commit_insert();
1149
1150 // delete the key holding the list
1151 wp_cache_delete( 'cached_key_names', 'pvc' );
1152 }
1153
1154 // remove last flush
1155 wp_cache_delete( 'last-flush', 'pvc' );
1156
1157 return true;
1158 }
1159
1160 /**
1161 * Insert or update views count.
1162 *
1163 * @global object $wpdb
1164 *
1165 * @param int $id
1166 * @param int $type
1167 * @param string $period
1168 * @param int $count
1169 * @return int|bool
1170 */
1171 private function db_insert( $id, $type, $period, $count ) {
1172 // check whether skip single query
1173 $skip_single_query = (bool) apply_filters( 'pvc_skip_single_query', false, $id, $type, $period, $count, 'post' );
1174
1175 // skip query?
1176 if ( $skip_single_query )
1177 return false;
1178
1179 global $wpdb;
1180
1181 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 ) );
1182 }
1183
1184 /**
1185 * Prepare bulk insert or update views count.
1186 *
1187 * @param int $id
1188 * @param int $type
1189 * @param string $period
1190 * @param int $count
1191 * @return void
1192 */
1193 private function db_prepare_insert( $id, $type, $period, $count = 1 ) {
1194 // cast count
1195 $count = (int) $count;
1196
1197 if ( ! $count )
1198 $count = 1;
1199
1200 // any queries?
1201 if ( ! empty( $this->db_insert_values ) )
1202 $this->db_insert_values .= ', ';
1203
1204 // append insert queries
1205 $this->db_insert_values .= sprintf( '(%d, %d, "%s", %d)', $id, $type, $period, $count );
1206
1207 if ( strlen( $this->db_insert_values ) > 25000 )
1208 $this->db_commit_insert();
1209 }
1210
1211 /**
1212 * Insert accumulated values to database.
1213 *
1214 * @global object $wpdb
1215 *
1216 * @return int|bool
1217 */
1218 private function db_commit_insert() {
1219 if ( empty( $this->db_insert_values ) )
1220 return false;
1221
1222 global $wpdb;
1223
1224 $result = $wpdb->query(
1225 "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
1226 VALUES " . $this->db_insert_values . "
1227 ON DUPLICATE KEY UPDATE count = count + VALUES(count)"
1228 );
1229
1230 $this->db_insert_values = '';
1231
1232 return $result;
1233 }
1234
1235 /**
1236 * Check whether user has excluded roles.
1237 *
1238 * @param int $user_id
1239 * @param string $option
1240 * @return bool
1241 */
1242 public function is_user_role_excluded( $user_id, $option = [] ) {
1243 // get user by ID
1244 $user = get_user_by( 'id', $user_id );
1245
1246 // no user?
1247 if ( empty( $user ) )
1248 return false;
1249
1250 // get user roles
1251 $roles = (array) $user->roles;
1252
1253 // any roles?
1254 if ( ! empty( $roles ) ) {
1255 foreach ( $roles as $role ) {
1256 if ( in_array( $role, $option, true ) )
1257 return true;
1258 }
1259 }
1260
1261 return false;
1262 }
1263
1264 /**
1265 * Check if IPv4 is in range.
1266 *
1267 * @param string $ip IP address
1268 * @param string $range IP range
1269 * @return bool
1270 */
1271 public function ipv4_in_range( $ip, $range ) {
1272 $start = str_replace( '*', '0', $range );
1273 $end = str_replace( '*', '255', $range );
1274 $ip = (float) sprintf( "%u", ip2long( $ip ) );
1275
1276 return ( $ip >= (float) sprintf( "%u", ip2long( $start ) ) && $ip <= (float) sprintf( "%u", ip2long( $end ) ) );
1277 }
1278
1279 /**
1280 * Get user real IP address.
1281 *
1282 * @return string
1283 */
1284 public function get_user_ip() {
1285 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
1286
1287 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 ) {
1288 if ( array_key_exists( $key, $_SERVER ) === true ) {
1289 foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
1290 // trim for safety measures
1291 $ip = trim( $ip );
1292
1293 // attempt to validate IP
1294 if ( $this->validate_user_ip( $ip ) )
1295 continue;
1296 }
1297 }
1298 }
1299
1300 return (string) $ip;
1301 }
1302
1303 /**
1304 * Ensure an IP address is both a valid IP and does not fall within a private network range.
1305 *
1306 * @param $ip string IP address
1307 * @return bool
1308 */
1309 public function validate_user_ip( $ip ) {
1310 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false )
1311 return false;
1312
1313 return true;
1314 }
1315
1316 /**
1317 * Register REST API endpoints.
1318 *
1319 * @return void
1320 */
1321 public function rest_api_init() {
1322 // view post route
1323 register_rest_route(
1324 'post-views-counter',
1325 '/view-post/(?P<id>\d+)|/view-post/',
1326 [
1327 'methods' => [ 'POST' ],
1328 'callback' => [ $this, 'check_post_rest_api' ],
1329 'permission_callback' => [ $this, 'view_post_permissions_check' ],
1330 'args' => apply_filters( 'pvc_rest_api_view_post_args', [
1331 'id' => [
1332 'default' => 0,
1333 'sanitize_callback' => 'absint'
1334 ],
1335 'storage_type' => [
1336 'default' => 'cookies'
1337 ],
1338 'storage_data' => [
1339 'default' => ''
1340 ]
1341 ] )
1342 ]
1343 );
1344
1345 // get views route
1346 register_rest_route(
1347 'post-views-counter',
1348 '/get-post-views/(?P<id>(\d+,?)+)',
1349 [
1350 'methods' => [ 'GET', 'POST' ],
1351 'callback' => [ $this, 'get_post_views_rest_api' ],
1352 'permission_callback' => [ $this, 'get_post_views_permissions_check' ],
1353 'args' => apply_filters( 'pvc_rest_api_get_post_views_args', [
1354 'id' => [
1355 'default' => 0,
1356 'sanitize_callback' => [ $this, 'validate_rest_api_data' ]
1357 ]
1358 ] )
1359 ]
1360 );
1361 }
1362
1363 /**
1364 * Get post views via REST API request.
1365 *
1366 * @param object $request
1367 * @return int
1368 */
1369 public function get_post_views_rest_api( $request ) {
1370 return pvc_get_post_views( $request->get_param( 'id' ) );
1371 }
1372
1373 /**
1374 * Check if a given request has access to get views.
1375 *
1376 * @param object $request
1377 * @return bool
1378 */
1379 public function get_post_views_permissions_check( $request ) {
1380 return (bool) apply_filters( 'pvc_rest_api_get_post_views_check', true, $request );
1381 }
1382
1383 /**
1384 * Check if a given request has access to view post.
1385 *
1386 * @param object $request
1387 * @return bool
1388 */
1389 public function view_post_permissions_check( $request ) {
1390 return (bool) apply_filters( 'pvc_rest_api_view_post_check', true, $request );
1391 }
1392
1393 /**
1394 * Validate REST API incoming data.
1395 *
1396 * @param int|array $data
1397 * @return int|array
1398 */
1399 public function validate_rest_api_data( $data ) {
1400 // POST array?
1401 if ( is_array( $data ) )
1402 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1403 // multiple comma-separated values?
1404 elseif ( strpos( $data, ',' ) !== false ) {
1405 $data = explode( ',', $data );
1406
1407 if ( is_array( $data ) && ! empty( $data ) )
1408 $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC );
1409 else
1410 $data = [];
1411 // single value?
1412 } else
1413 $data = absint( $data );
1414
1415 return $data;
1416 }
1417 }
1418