PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.2.2
Post Views Counter v1.2.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 / counter.php
post-views-counter / includes Last commit date
columns.php 10 years ago counter.php 10 years ago cron.php 10 years ago dashboard.php 10 years ago frontend.php 10 years ago functions.php 10 years ago query.php 10 years ago settings.php 10 years ago update.php 10 years ago widgets.php 10 years ago
counter.php
504 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 const GROUP = 'pvc';
12 const NAME_ALLKEYS = 'cached_key_names';
13 const CACHE_KEY_SEPARATOR = '.';
14
15 private $cookie = array(
16 'exists' => false,
17 'visited_posts' => array(),
18 'expiration' => 0
19 );
20
21 public function __construct() {
22 // actions
23 add_action( 'plugins_loaded', array( $this, 'check_cookie' ), 1 );
24 add_action( 'deleted_post', array( $this, 'delete_post_views' ) );
25 add_action( 'wp', array( $this, 'check_post_php' ) );
26 add_action( 'wp_ajax_pvc-check-post', array( $this, 'check_post_ajax' ) );
27 add_action( 'wp_ajax_nopriv_pvc-check-post', array( $this, 'check_post_ajax' ) );
28 }
29
30 /**
31 * Check whether to count visit.
32 *
33 * @param int $id
34 */
35 public function check_post( $id = 0 ) {
36 // get post id
37 $id = (int) (empty( $id ) ? get_the_ID() : $id);
38
39 if ( empty( $id ) )
40 return;
41
42 $ips = Post_Views_Counter()->options['general']['exclude_ips'];
43
44 // whether to count this ip
45 if ( ! empty( $ips ) && filter_var( preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) && in_array( $_SERVER['REMOTE_ADDR'], $ips, true ) )
46 return;
47
48 // get groups to check them faster
49 $groups = Post_Views_Counter()->options['general']['exclude']['groups'];
50
51 // whether to count this user
52 if ( is_user_logged_in() ) {
53 // exclude logged in users?
54 if ( in_array( 'users', $groups, true ) )
55 return;
56 // exclude specific roles?
57 elseif ( in_array( 'roles', $groups, true ) && $this->is_user_role_excluded( Post_Views_Counter()->options['general']['exclude']['roles'] ) )
58 return;
59 }
60 // exclude guests?
61 elseif ( in_array( 'guests', $groups, true ) )
62 return;
63
64 // whether to count robots
65 if ( $this->is_robot() )
66 return;
67
68 // cookie already existed?
69 if ( $this->cookie['exists'] ) {
70 // post already viewed but not expired?
71 if ( in_array( $id, array_keys( $this->cookie['visited_posts'] ), true ) && current_time( 'timestamp', true ) < $this->cookie['visited_posts'][$id] ) {
72 // update cookie but do not count visit
73 $this->save_cookie( $id, $this->cookie, false );
74
75 return;
76 } else
77 // update cookie
78 $this->save_cookie( $id, $this->cookie );
79 } else
80 // set new cookie
81 $this->save_cookie( $id );
82
83 // count visit
84 $this->count_visit( $id );
85 }
86
87 /**
88 * Check whether to count visit via PHP request.
89 *
90 * @param int $id
91 */
92 public function check_post_php() {
93 // do not count admin entries
94 if ( is_admin() && ! (defined( 'DOING_AJAX' ) && DOING_AJAX) )
95 return;
96
97 // do we use PHP as counter?
98 if ( Post_Views_Counter()->options['general']['counter_mode'] != 'php' )
99 return;
100
101 $post_types = Post_Views_Counter()->options['general']['post_types_count'];
102
103 // whether to count this post type
104 if ( empty( $post_types ) || ! is_singular( $post_types ) )
105 return;
106
107 $this->check_post( get_the_ID() );
108 }
109
110 /**
111 * Check whether to count visit via AJAX request.
112 */
113 public function check_post_ajax() {
114 if ( isset( $_POST['action'], $_POST['post_id'], $_POST['pvc_nonce'], $_POST['post_type'] ) && $_POST['action'] === 'pvc-check-post' && ($post_id = (int) $_POST['post_id']) > 0 && wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-check-post' ) !== false ) {
115
116 // do we use Ajax as counter?
117 if ( Post_Views_Counter()->options['general']['counter_mode'] != 'js' )
118 exit;
119
120 // get countable post types
121 $post_types = Post_Views_Counter()->options['general']['post_types_count'];
122
123 // get post type
124 $post_type = get_post_type( $post_id );
125
126 // whether to count this post type or not
127 if ( empty( $post_types ) || empty( $post_type ) || $post_type !== $_POST['post_type'] || ! in_array( $post_type, $post_types, true ) )
128 exit;
129
130 $this->check_post( $post_id );
131
132 }
133
134 exit;
135 }
136
137 /**
138 * Initialize cookie session.
139 */
140 public function check_cookie() {
141 // do not run in admin except for ajax requests
142 if ( is_admin() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) )
143 return;
144
145 // is cookie set?
146 if ( isset( $_COOKIE['pvc_visits'] ) && ! empty( $_COOKIE['pvc_visits'] ) ) {
147 $visited_posts = $expirations = array();
148
149 foreach ( $_COOKIE['pvc_visits'] as $content ) {
150 // is cookie valid?
151 if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $content ) === 1 ) {
152 // get single id with expiration
153 $expiration_ids = explode( 'a', $content );
154
155 // check every expiration => id pair
156 foreach ( $expiration_ids as $pair ) {
157 $pair = explode( 'b', $pair );
158 $expirations[] = (int) $pair[0];
159 $visited_posts[(int) $pair[1]] = (int) $pair[0];
160 }
161 }
162 }
163
164 $this->cookie = array(
165 'exists' => true,
166 'visited_posts' => $visited_posts,
167 'expiration' => max( $expirations )
168 );
169 }
170 }
171
172 /**
173 * Save cookie function.
174 *
175 * @param int $id
176 * @param array $cookie
177 * @param bool $expired
178 */
179 private function save_cookie( $id, $cookie = array(), $expired = true ) {
180 $expiration = $this->get_timestamp( Post_Views_Counter()->options['general']['time_between_counts']['type'], Post_Views_Counter()->options['general']['time_between_counts']['number'] );
181
182 // is this a new cookie?
183 if ( empty( $cookie ) ) {
184 // set cookie
185 setcookie( 'pvc_visits[0]', $expiration . 'b' . $id, $expiration, COOKIEPATH, COOKIE_DOMAIN, (isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ? true : false ), true );
186 } else {
187 if ( $expired ) {
188 // add new id or chang expiration date if id already exists
189 $cookie['visited_posts'][$id] = $expiration;
190 }
191
192 // create copy for better foreach performance
193 $visited_posts_expirations = $cookie['visited_posts'];
194
195 // get current gmt time
196 $time = current_time( 'timestamp', true );
197
198 // check whether viewed id has expired - no need to keep it in cookie (less size)
199 foreach ( $visited_posts_expirations as $post_id => $post_expiration ) {
200 if ( $time > $post_expiration )
201 unset( $cookie['visited_posts'][$post_id] );
202 }
203
204 // set new last expiration date if needed
205 $cookie['expiration'] = max( $cookie['visited_posts'] );
206
207 $cookies = $imploded = array();
208
209 // create pairs
210 foreach ( $cookie['visited_posts'] as $id => $exp ) {
211 $imploded[] = $exp . 'b' . $id;
212 }
213
214 // split cookie into chunks (4000 bytes to make sure it is safe for every browser)
215 $chunks = str_split( implode( 'a', $imploded ), 4000 );
216
217 // more then one chunk?
218 if ( count( $chunks ) > 1 ) {
219 $last_id = '';
220
221 foreach ( $chunks as $chunk_id => $chunk ) {
222 // new chunk
223 $chunk_c = $last_id . $chunk;
224
225 // is it full-length chunk?
226 if ( strlen( $chunk ) === 4000 ) {
227 // get last part
228 $last_part = strrchr( $chunk_c, 'a' );
229
230 // get last id
231 $last_id = substr( $last_part, 1 );
232
233 // add new full-lenght chunk
234 $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) );
235 } else {
236 // add last chunk
237 $cookies[$chunk_id] = $chunk_c;
238 }
239 }
240 } else {
241 // only one chunk
242 $cookies[] = $chunks[0];
243 }
244
245 foreach ( $cookies as $key => $value ) {
246 // set cookie
247 setcookie( 'pvc_visits[' . $key . ']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, (isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ? true : false ), true );
248 }
249 }
250 }
251
252 /**
253 * Count visit function.
254 *
255 * @global object $wpdb
256 * @param int $id
257 * @return bool
258 */
259 private function count_visit( $id ) {
260 global $wpdb;
261
262 $cache_key_names = array();
263 $using_object_cache = $this->using_object_cache();
264
265 // get day, week, month and year
266 $date = explode( '-', date( 'W-d-m-Y', current_time( 'timestamp' ) ) );
267
268 foreach( array(
269 0 => $date[3] . $date[2] . $date[1], // day like 20140324
270 1 => $date[3] . $date[0], // week like 201439
271 2 => $date[3] . $date[2], // month like 201405
272 3 => $date[3], // year like 2014
273 4 => 'total' // total views
274 ) as $type => $period ) {
275 if ( $using_object_cache ) {
276 $cache_key = $id . self::CACHE_KEY_SEPARATOR . $type . self::CACHE_KEY_SEPARATOR . $period;
277 wp_cache_add( $cache_key, 0, self::GROUP );
278 wp_cache_incr( $cache_key, 1, self::GROUP );
279 $cache_key_names[] = $cache_key;
280 } else {
281 // hit the db directly
282 // @TODO: investigate queueing these queries on the 'shutdown' hook instead instead of running them instantly?
283 $this->db_insert( $id, $type, $period, 1 );
284 }
285 }
286
287 // update the list of cache keys to be flushed
288 if ( $using_object_cache && ! empty( $cache_key_names ) ) {
289 $this->update_cached_keys_list_if_needed( $cache_key_names );
290 }
291
292 do_action( 'pvc_after_count_visit', $id );
293
294 return true;
295 }
296
297 /**
298 * Remove post views from database when post is deleted.
299 *
300 * @param int $post_id
301 */
302 public function delete_post_views( $post_id ) {
303 global $wpdb;
304
305 $wpdb->delete( $wpdb->prefix . 'post_views', array( 'id' => $post_id ), array( '%d' ) );
306 }
307
308 /**
309 *
310 * Get timestamp convertion.
311 *
312 * @param string $type
313 * @param int $number
314 * @param int $timestamp
315 * @return string
316 */
317 public function get_timestamp( $type, $number, $timestamp = true ) {
318 $converter = array(
319 'minutes' => 60,
320 'hours' => 3600,
321 'days' => 86400,
322 'weeks' => 604800,
323 'months' => 2592000,
324 'years' => 946080000
325 );
326
327 return ($timestamp ? current_time( 'timestamp', true ) : 0) + $number * $converter[$type];
328 }
329
330 /**
331 * Check if object cache is in use.
332 *
333 * @param bool $using
334 * @return bool
335 */
336 public function using_object_cache( $using = null ) {
337 $using = wp_using_ext_object_cache( $using );
338
339 if ( $using ) {
340 // check if explicitly disabled by flush_interval setting/option <= 0
341 $flush_interval_number = Post_Views_Counter()->options['general']['flush_interval']['number'];
342 $using = ( $flush_interval_number <= 0 ) ? false : true;
343 }
344
345 return $using;
346 }
347
348 /**
349 * Update the single cache key which holds a list of all the cache keys
350 * that need to be flushed to the db.
351 *
352 * The value of that special cache key is a giant string containing key names separated with the `|` character.
353 * Each such key name then consists of 3 elements: $id, $type, $period (separated by a `.` character).
354 * Examples:
355 * 62053.0.20150327|62053.1.201513|62053.2.201503|62053.3.2015|62053.4.total|62180.0.20150327|62180.1.201513|62180.2.201503|62180.3.2015|62180.4.total
356 * A single key is `62053.0.20150327` and that key's data is: $id = 62053, $type = 0, $period = 20150327
357 *
358 * This data format proved more efficient (avoids the (un)serialization overhead completely + duplicates filtering is a string search now)
359 *
360 * @param array $key_names
361 */
362 private function update_cached_keys_list_if_needed( $key_names = array() ) {
363 $existing_list = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
364 if ( ! $existing_list ) {
365 $existing_list = '';
366 }
367
368 $list_modified = false;
369
370 // modify the list contents if/when needed
371 if ( empty( $existing_list ) ) {
372 // the simpler case of an empty initial list where we just
373 // transform the specified key names into a string
374 $existing_list = implode( '|', $key_names );
375 $list_modified = true;
376 } else {
377 // search each specified key name and append it if it's not found
378 foreach ( $key_names as $key_name ) {
379 if ( false === strpos( $existing_list, $key_name ) ) {
380 $existing_list .= '|' . $key_name;
381 $list_modified = true;
382 }
383 }
384 }
385
386 // save modified list back in cache
387 if ( $list_modified ) {
388 wp_cache_set( self::NAME_ALLKEYS, $existing_list, self::GROUP );
389 }
390 }
391
392 /**
393 * Flush views data stored in the persistent object cache into
394 * our custom table and clear the object cache keys when done.
395 *
396 * @global object $wpdb
397 * @return bool
398 */
399 public function flush_cache_to_db() {
400 global $wpdb;
401
402 $key_names = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
403
404 if ( ! $key_names ) {
405 $key_names = array();
406 } else {
407 // create an array out of a string that's stored in the cache
408 $key_names = explode( '|', $key_names );
409 }
410
411 foreach( $key_names as $key_name ) {
412 // get values stored within the key name itself
413 list( $id, $type, $period ) = explode( self::CACHE_KEY_SEPARATOR, $key_name );
414 // get the cached count value
415 $count = wp_cache_get( $key_name, self::GROUP );
416
417 // store cached value in the db
418 $this->db_insert( $id, $type, $period, $count );
419
420 // clear the cache key we just flushed
421 wp_cache_delete( $key_name, self::GROUP );
422 }
423
424 // delete the key holding the list itself after we've successfully flushed it
425 if ( ! empty( $key_names ) ) {
426 wp_cache_delete( self::NAME_ALLKEYS, self::GROUP );
427 }
428
429 return true;
430 }
431
432 /**
433 * Insert or update views count.
434 *
435 * @global object $wpdb
436 * @param int $id
437 * @param string $type
438 * @param string $period
439 * @param int $count
440 * @return bool
441 */
442 private function db_insert( $id, $type, $period, $count = 1 ) {
443 global $wpdb;
444
445 $count = (int) $count;
446
447 if ( ! $count ) {
448 $count = 1;
449 }
450
451 return $wpdb->query(
452 $wpdb->prepare( "
453 INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
454 VALUES (%d, %d, %s, %d)
455 ON DUPLICATE KEY UPDATE count = count + %d", $id, $type, $period, $count, $count
456 )
457 );
458 }
459
460 /**
461 * Check whether user has excluded roles.
462 *
463 * @param string $option
464 * @return bool
465 */
466 public function is_user_role_excluded( $option ) {
467 $user = wp_get_current_user();
468
469 if ( empty( $user ) )
470 return false;
471
472 $roles = (array) $user->roles;
473
474 if ( ! empty( $roles ) ) {
475 foreach ( $roles as $role ) {
476 if ( in_array( $role, $option, true ) )
477 return true;
478 }
479 }
480
481 return false;
482 }
483
484 /**
485 * Check whether visitor is a bot.
486 */
487 private function is_robot() {
488 if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) || (isset( $_SERVER['HTTP_USER_AGENT'] ) && trim( $_SERVER['HTTP_USER_AGENT'] ) === '') )
489 return false;
490
491 $robots = array(
492 'bot', 'b0t', 'Acme.Spider', 'Ahoy! The Homepage Finder', 'Alkaline', 'Anthill', 'Walhello appie', 'Arachnophilia', 'Arale', 'Araneo', 'ArchitextSpider', 'Aretha', 'ARIADNE', 'arks', 'AskJeeves', 'ASpider (Associative Spider)', 'ATN Worldwide', 'AURESYS', 'BackRub', 'Bay Spider', 'Big Brother', 'Bjaaland', 'BlackWidow', 'Die Blinde Kuh', 'Bloodhound', 'BSpider', 'CACTVS Chemistry Spider', 'Calif', 'Cassandra', 'Digimarc Marcspider/CGI', 'ChristCrawler.com', 'churl', 'cIeNcIaFiCcIoN.nEt', 'CMC/0.01', 'Collective', 'Combine System', 'Web Core / Roots', 'Cusco', 'CyberSpyder Link Test', 'CydralSpider', 'Desert Realm Spider', 'DeWeb(c) Katalog/Index', 'DienstSpider', 'Digger', 'Direct Hit Grabber', 'DownLoad Express', 'DWCP (Dridus\' Web Cataloging Project)', 'e-collector', 'EbiNess', 'Emacs-w3 Search Engine', 'ananzi', 'esculapio', 'Esther', 'Evliya Celebi', 'FastCrawler', 'Felix IDE', 'Wild Ferret Web Hopper #1, #2, #3', 'FetchRover', 'fido', 'KIT-Fireball', 'Fish search', 'Fouineur', 'Freecrawl', 'FunnelWeb', 'gammaSpider, FocusedCrawler', 'gazz', 'GCreep', 'GetURL', 'Golem', 'Grapnel/0.01 Experiment', 'Griffon', 'Gromit', 'Northern Light Gulliver', 'Harvest', 'havIndex', 'HI (HTML Index) Search', 'Hometown Spider Pro', 'ht://Dig', 'HTMLgobble', 'Hyper-Decontextualizer', 'IBM_Planetwide', 'Popular Iconoclast', 'Ingrid', 'Imagelock', 'IncyWincy', 'Informant', 'Infoseek Sidewinder', 'InfoSpiders', 'Inspector Web', 'IntelliAgent', 'Iron33', 'Israeli-search', 'JavaBee', 'JCrawler', 'Jeeves', 'JumpStation', 'image.kapsi.net', 'Katipo', 'KDD-Explorer', 'Kilroy', 'LabelGrabber', 'larbin', 'legs', 'Link Validator', 'LinkScan', 'LinkWalker', 'Lockon', 'logo.gif Crawler', 'Lycos', 'Mac WWWWorm', 'Magpie', 'marvin/infoseek', 'Mattie', 'MediaFox', 'MerzScope', 'NEC-MeshExplorer', 'MindCrawler', 'mnoGoSearch search engine software', 'moget', 'MOMspider', 'Monster', 'Motor', 'Muncher', 'Muninn', 'Muscat Ferret', 'Mwd.Search', 'Internet Shinchakubin', 'NDSpider', 'Nederland.zoek', 'NetCarta WebMap Engine', 'NetMechanic', 'NetScoop', 'newscan-online', 'NHSE Web Forager', 'Nomad', 'nzexplorer', 'ObjectsSearch', 'Occam', 'HKU WWW Octopus', 'OntoSpider', 'Openfind data gatherer', 'Orb Search', 'Pack Rat', 'PageBoy', 'ParaSite', 'Patric', 'pegasus', 'The Peregrinator', 'PerlCrawler 1.0', 'Phantom', 'PhpDig', 'PiltdownMan', 'Pioneer', 'html_analyzer', 'Portal Juice Spider', 'PGP Key Agent', 'PlumtreeWebAccessor', 'Poppi', 'PortalB Spider', 'GetterroboPlus Puu', 'Raven Search', 'RBSE Spider', 'RoadHouse Crawling System', 'ComputingSite Robi/1.0', 'RoboCrawl Spider', 'RoboFox', 'Robozilla', 'RuLeS', 'Scooter', 'Sleek', 'Search.Aus-AU.COM', 'SearchProcess', 'Senrigan', 'SG-Scout', 'ShagSeeker', 'Shai\'Hulud', 'Sift', 'Site Valet', 'SiteTech-Rover', 'Skymob.com', 'SLCrawler', 'Inktomi Slurp', 'Smart Spider', 'Snooper', 'Spanner', 'Speedy Spider', 'spider_monkey', 'Spiderline Crawler', 'SpiderMan', 'SpiderView(tm)', 'Site Searcher', 'Suke', 'suntek search engine', 'Sven', 'Sygol', 'TACH Black Widow', 'Tarantula', 'tarspider', 'Templeton', 'TeomaTechnologies', 'TITAN', 'TitIn', 'TLSpider', 'UCSD Crawl', 'UdmSearch', 'URL Check', 'URL Spider Pro', 'Valkyrie', 'Verticrawl', 'Victoria', 'vision-search', 'Voyager', 'W3M2', 'WallPaper (alias crawlpaper)', 'the World Wide Web Wanderer', 'w@pSpider by wap4.com', 'WebBandit Web Spider', 'WebCatcher', 'WebCopy', 'webfetcher', 'Webinator', 'weblayers', 'WebLinker', 'WebMirror', 'The Web Moose', 'WebQuest', 'Digimarc MarcSpider', 'WebReaper', 'webs', 'Websnarf', 'WebSpider', 'WebVac', 'webwalk', 'WebWalker', 'WebWatch', 'Wget', 'whatUseek Winona', 'Wired Digital', 'Weblog Monitor', 'w3mir', 'WebStolperer', 'The Web Wombat', 'The World Wide Web Worm', 'WWWC Ver 0.2.5', 'WebZinger', 'XGET'
493 );
494
495 foreach( $robots as $robot ) {
496 if ( stripos( $_SERVER['HTTP_USER_AGENT'], $robot ) !== false )
497 return true;
498 }
499
500 return false;
501 }
502
503 }
504