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