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