PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.0.7
Post Views Counter v1.0.7
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 11 years ago counter.php 11 years ago cron.php 11 years ago frontend.php 11 years ago functions.php 11 years ago query.php 11 years ago settings.php 11 years ago update.php 11 years ago widgets.php 11 years ago
counter.php
501 lines
1 <?php
2 if(!defined('ABSPATH')) exit;
3
4 new Post_Views_Counter_Counter();
5
6 class Post_Views_Counter_Counter {
7
8 const GROUP = 'pvc';
9 const NAME_ALLKEYS = 'cached_key_names';
10 const CACHE_KEY_SEPARATOR = '.';
11
12 private $cookie = array(
13 'exists' => false,
14 'visited_posts' => array(),
15 'expiration' => 0
16 );
17
18 public function __construct() {
19 // set instance
20 Post_Views_Counter()->add_instance('counter', $this);
21
22 // actions
23 add_action('plugins_loaded', array(&$this, 'check_cookie'), 1);
24 add_action('wp', array(&$this, 'check_post'));
25 add_action('deleted_post', array(&$this, 'delete_post_views'));
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 * Remove post views from database when post is deleted
32 */
33 public function delete_post_views($post_id) {
34 global $wpdb;
35
36 $wpdb->delete($wpdb->prefix.'post_views', array('id' => $post_id), array('%d'));
37 }
38
39 /**
40 * Check whether user has excluded roles
41 */
42 public function is_user_roles_excluded($option) {
43 $user = wp_get_current_user();
44
45 if (empty($user))
46 return false;
47
48 $roles = (array)$user->roles;
49
50 if (!empty($roles)) {
51 foreach ($roles as $role) {
52 if (in_array($role, $option, true))
53 return true;
54 }
55 }
56
57 return false;
58 }
59
60 /**
61 * Get timestamp convertion
62 */
63 public function get_timestamp($type, $number, $timestamp = true) {
64 $converter = array(
65 'minutes' => 60,
66 'hours' => 3600,
67 'days' => 86400,
68 'weeks' => 604800,
69 'months' => 2592000,
70 'years' => 946080000
71 );
72
73 return ($timestamp ? current_time('timestamp', true) : 0) + $number * $converter[$type];
74 }
75
76 /**
77 * Check whether to count visit via AJAX request
78 */
79 public function check_post_ajax() {
80 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 && Post_Views_Counter()->get_attribute('options', 'general', 'counter_mode') === 'js') {
81 // get countable post types
82 $post_types = Post_Views_Counter()->get_attribute('options', 'general', 'post_types_count');
83
84 // get post type
85 $post_type = get_post_type($post_id);
86
87 // whether to count this post type or not
88 if (empty($post_types) || empty($post_type) || $post_type !== $_POST['post_type'] || !in_array($post_type, $post_types, true))
89 exit;
90
91 // get excluded ips
92 $excluded_ips = Post_Views_Counter()->get_attribute('options', 'general', 'exclude_ips');
93
94 // whether to count this ip or not
95 if (!empty($excluded_ips) && filter_var(preg_replace('/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP) && in_array($_SERVER['REMOTE_ADDR'], $excluded_ips, true))
96 exit;
97
98 // get groups to check them faster
99 $groups = Post_Views_Counter()->get_attribute('options', 'general', 'exclude', 'groups');
100
101 // whether to count this user
102 if (is_user_logged_in()) {
103 // exclude logged in users?
104 if (in_array('users', $groups, true))
105 exit;
106 // exclude specific roles?
107 elseif (in_array('roles', $groups, true) && $this->is_user_roles_excluded(Post_Views_Counter()->get_attribute('options', 'general', 'exclude', 'roles')))
108 exit;
109 }
110 // exclude guests?
111 elseif (in_array('guests', $groups, true))
112 exit;
113
114 // whether to count robots
115 if ($this->is_robot())
116 exit;
117
118 // cookie already existed?
119 if ($this->cookie['exists']) {
120 // post already viewed but not expired?
121 if(in_array($post_id, array_keys($this->cookie['visited_posts']), true) && current_time('timestamp', true) < $this->cookie['visited_posts'][$post_id]) {
122 // updates cookie but do not count visit
123 $this->save_cookie($post_id, $this->cookie, false);
124
125 exit;
126 }
127 else
128 // updates cookie
129 $this->save_cookie($post_id, $this->cookie);
130 } else {
131 // set new cookie
132 $this->save_cookie($post_id);
133 }
134
135 // count visit
136 $this->count_visit($post_id);
137 }
138
139 exit;
140 }
141
142 /**
143 * Check whether to count visit
144 */
145 public function check_post() {
146 // do not count admin entries
147 if (is_admin())
148 return;
149
150 // do we use PHP as counter?
151 if (Post_Views_Counter()->get_attribute('options', 'general', 'counter_mode') === 'php') {
152 $post_types = Post_Views_Counter()->get_attribute('options', 'general', 'post_types_count');
153
154 // whether to count this post type
155 if (empty($post_types) || !is_singular($post_types))
156 return;
157
158 $ips = Post_Views_Counter()->get_attribute('options', 'general', 'exclude_ips');
159
160 // whether to count this ip
161 if (!empty($ips) && filter_var(preg_replace('/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP) && in_array($_SERVER['REMOTE_ADDR'], $ips, true))
162 return;
163
164 // get groups to check them faster
165 $groups = Post_Views_Counter()->get_attribute('options', 'general', 'exclude', 'groups');
166
167 // whether to count this user
168 if (is_user_logged_in()) {
169 // exclude logged in users?
170 if (in_array('users', $groups, true))
171 return;
172 // exclude specific roles?
173 elseif (in_array('roles', $groups, true) && $this->is_user_roles_excluded(Post_Views_Counter()->get_attribute('options', 'general', 'exclude', 'roles')))
174 return;
175 }
176 // exclude guests?
177 elseif (in_array('guests', $groups, true))
178 return;
179
180 // whether to count robots
181 if ($this->is_robot())
182 return;
183
184 // get post id
185 $id = get_the_ID();
186
187 // cookie already existed?
188 if ($this->cookie['exists']) {
189 // post already viewed but not expired?
190 if (in_array($id, array_keys($this->cookie['visited_posts']), true) && current_time('timestamp', true) < $this->cookie['visited_posts'][$id]) {
191 // update cookie but do not count visit
192 $this->save_cookie($id, $this->cookie, false);
193
194 return;
195 }
196 else
197 // update cookie
198 $this->save_cookie($id, $this->cookie);
199 }
200 else
201 // set new cookie
202 $this->save_cookie($id);
203
204 // count visit
205 $this->count_visit($id);
206 }
207 }
208
209 /**
210 * Initialize cookie session
211 */
212 public function check_cookie() {
213 // do not run in admin except for ajax requests
214 if (is_admin() && !(defined('DOING_AJAX') && DOING_AJAX))
215 return;
216
217 // is cookie set?
218 if (isset($_COOKIE['pvc_visits']) && !empty($_COOKIE['pvc_visits'])) {
219 $visited_posts = $expirations = array();
220
221 foreach ($_COOKIE['pvc_visits'] as $content) {
222 // is cookie valid?
223 if (preg_match('/^(([0-9]+b[0-9]+a?)+)$/', $content) === 1) {
224 // get single id with expiration
225 $expiration_ids = explode('a', $content);
226
227 // check every expiration => id pair
228 foreach ($expiration_ids as $pair) {
229 $pair = explode('b', $pair);
230 $expirations[] = (int)$pair[0];
231 $visited_posts[(int)$pair[1]] = (int)$pair[0];
232 }
233 }
234 }
235
236 $this->cookie = array(
237 'exists' => true,
238 'visited_posts' => $visited_posts,
239 'expiration' => max($expirations)
240 );
241 }
242 }
243
244 /**
245 * Save cookie function
246 */
247 private function save_cookie($id, $cookie = array(), $expired = true) {
248 $expiration = $this->get_timestamp(Post_Views_Counter()->get_attribute('options', 'general', 'time_between_counts', 'type'), Post_Views_Counter()->get_attribute('options', 'general', 'time_between_counts', 'number'));
249
250 // is this a new cookie?
251 if (empty($cookie)) {
252 // set cookie
253 setcookie('pvc_visits[0]', $expiration.'b'.$id, $expiration, COOKIEPATH, COOKIE_DOMAIN, (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? true : false), true);
254 } else {
255 if ($expired) {
256 // add new id or chang expiration date if id already exists
257 $cookie['visited_posts'][$id] = $expiration;
258 }
259
260 // create copy for better foreach performance
261 $visited_posts_expirations = $cookie['visited_posts'];
262
263 // get current gmt time
264 $time = current_time('timestamp', true);
265
266 // check whether viewed id has expired - no need to keep it in cookie (less size)
267 foreach ($visited_posts_expirations as $post_id => $post_expiration) {
268 if ($time > $post_expiration)
269 unset($cookie['visited_posts'][$post_id]);
270 }
271
272 // set new last expiration date if needed
273 $cookie['expiration'] = max($cookie['visited_posts']);
274
275 $cookies = $imploded = array();
276
277 // create pairs
278 foreach ($cookie['visited_posts'] as $id => $exp) {
279 $imploded[] = $exp.'b'.$id;
280 }
281
282 // split cookie into chunks (4000 bytes to make sure it is safe for every browser)
283 $chunks = str_split(implode('a', $imploded), 4000);
284
285 // more then one chunk?
286 if (count($chunks) > 1) {
287 $last_id = '';
288
289 foreach ($chunks as $chunk_id => $chunk) {
290 // new chunk
291 $chunk_c = $last_id.$chunk;
292
293 // is it full-length chunk?
294 if (strlen($chunk) === 4000) {
295 // get last part
296 $last_part = strrchr($chunk_c, 'a');
297
298 // get last id
299 $last_id = substr($last_part, 1);
300
301 // add new full-lenght chunk
302 $cookies[$chunk_id] = substr($chunk_c, 0, strlen($chunk_c) - strlen($last_part));
303 } else {
304 // add last chunk
305 $cookies[$chunk_id] = $chunk_c;
306 }
307 }
308
309 } else {
310 // only one chunk
311 $cookies[] = $chunks[0];
312 }
313
314 foreach ($cookies as $key => $value) {
315 // set cookie
316 setcookie('pvc_visits['.$key.']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? true : false), true);
317 }
318 }
319 }
320
321
322 public function using_object_cache($using = null) {
323 $using = wp_using_ext_object_cache( $using );
324
325 if ( $using ) {
326 // Check if explicitly disabled by flush_interval setting/option <= 0
327 $flush_interval_number = Post_Views_Counter()->get_attribute('options', 'general', 'flush_interval', 'number');
328 $using = ( $flush_interval_number <= 0 ) ? false : true;
329 }
330
331 return $using;
332 }
333
334
335 /**
336 *
337 */
338 private function count_visit($id) {
339 global $wpdb;
340
341 $cache_key_names = array();
342 $using_object_cache = $this->using_object_cache();
343
344 // gets day, week, month and year
345 $date = explode('-', date('W-d-m-Y', current_time('timestamp')));
346
347 foreach (array(
348 0 => $date[3].$date[2].$date[1], // day like 20140324
349 1 => $date[3].$date[0], // week like 201439
350 2 => $date[3].$date[2], // month like 201405
351 3 => $date[3], // year like 2014
352 4 => 'total' // total views
353 ) as $type => $period)
354 {
355 if ( $using_object_cache ) {
356 $cache_key = $id . self::CACHE_KEY_SEPARATOR . $type . self::CACHE_KEY_SEPARATOR . $period;
357 wp_cache_add( $cache_key, 0, self::GROUP );
358 wp_cache_incr( $cache_key, 1, self::GROUP );
359 $cache_key_names[] = $cache_key;
360 } else {
361 // Hit the db directly
362 // TODO: investigate queueing these queries on the 'shutdown' hook instead instead of running them instantly?
363 $this->db_insert($id, $type, $period, 1);
364 }
365 }
366
367 // Update the list of cache keys to be flushed
368 if ( $using_object_cache && ! empty( $cache_key_names ) ) {
369 $this->update_cached_keys_list_if_needed( $cache_key_names );
370 }
371
372 do_action('pvc_after_count_visit', $id);
373
374 return true;
375 }
376
377
378 /**
379 * Updates the single cache key which holds a list of all the cache keys
380 * that need to be flushed to the db.
381 *
382 * The value of that special cache key is a giant string containing key names separated with the `|` character.
383 * Each such key name then consists of 3 elements: $id, $type, $period (separated by a `.` character).
384 * Examples:
385 * 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
386 * A single key is `62053.0.20150327` and that key's data is: $id = 62053, $type = 0, $period = 20150327
387 *
388 * This data format proved more efficient (avoids the (un)serialization overhead completely + duplicates filtering is a string search now)
389 */
390 private function update_cached_keys_list_if_needed($key_names = array()) {
391 $existing_list = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
392 if ( ! $existing_list ) {
393 $existing_list = '';
394 }
395
396 $list_modified = false;
397
398 // Modifying the list contents if/when needed
399 if ( empty( $existing_list ) ) {
400 // The simpler case of an empty initial list where we just
401 // transform the specified key names into a string
402 $existing_list = implode( '|', $key_names );
403 $list_modified = true;
404 } else {
405 // Searching each specified key name and appending it if it's not found
406 foreach ( $key_names as $key_name ) {
407 if ( false === strpos( $existing_list, $key_name ) ) {
408 $existing_list .= '|' . $key_name;
409 $list_modified = true;
410 }
411 }
412 }
413
414 // save modified list back in cache
415 if ( $list_modified ) {
416 wp_cache_set( self::NAME_ALLKEYS, $existing_list, self::GROUP );
417 }
418 }
419
420
421 /**
422 * Flushes views data stored in the persistent object cache into
423 * our custom table and clears the object cache keys when done
424 */
425 public function flush_cache_to_db() {
426 global $wpdb;
427
428 $key_names = wp_cache_get( self::NAME_ALLKEYS, self::GROUP );
429
430 if ( ! $key_names ) {
431 $key_names = array();
432 } else {
433 // create an array out of a string that's stored in the cache
434 $key_names = explode( '|', $key_names );
435 }
436
437 foreach ( $key_names as $key_name ) {
438 // Get values stored within the key name itself
439 list( $id, $type, $period ) = explode( self::CACHE_KEY_SEPARATOR, $key_name );
440 // Get the cached count value
441 $count = wp_cache_get( $key_name, self::GROUP );
442
443 // Store cached value in the db
444 $this->db_insert($id, $type, $period, $count);
445
446 // Clear the cache key we just flushed
447 wp_cache_delete( $key_name, self::GROUP );
448 }
449
450 // Delete the key holding the list itself after we've successfully flushed it
451 if ( ! empty( $key_names ) ) {
452 wp_cache_delete( self::NAME_ALLKEYS, self::GROUP );
453 }
454
455 return true;
456 }
457
458
459 private function db_insert($id, $type, $period, $count = 1) {
460 global $wpdb;
461
462 $count = (int) $count;
463
464 if ( ! $count ) {
465 $count = 1;
466 }
467
468 return $wpdb->query(
469 $wpdb->prepare("
470 INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count)
471 VALUES (%d, %d, %s, %d)
472 ON DUPLICATE KEY UPDATE count = count + %d",
473 $id,
474 $type,
475 $period,
476 $count,
477 $count
478 )
479 );
480 }
481
482 /**
483 * Checks whether visitor is a bot
484 */
485 private function is_robot() {
486 if (!isset($_SERVER['HTTP_USER_AGENT']) || (isset($_SERVER['HTTP_USER_AGENT']) && trim($_SERVER['HTTP_USER_AGENT']) === ''))
487 return false;
488
489 $robots = array(
490 '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'
491 );
492
493 foreach ($robots as $robot) {
494 if (stripos($_SERVER['HTTP_USER_AGENT'], $robot) !== false)
495 return true;
496 }
497
498 return false;
499 }
500 }
501 ?>