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