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