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