Slimstat Analytics - Version 4.6.8

Version Description

  • [Update] We should have known better: for every naysayer there is always a yaysayer that will ask for just the opposite feature, right? Fair enough: that's what the settings panels are for, after all. In order to make everybody happy, we introduced a new option (under Slimstat > Settings > Reports) that will allow you to decide what the default time span should be: current month or past 30 days, it's totally up to you.
  • [Update] We implemented Chris' idea to use a transient when recording the visit_id, to improve performance (thank you, Chris).
  • [Fix] If your website is using a caching plugin, the tracking code might still be appended to the page, even if the user turned off tracking. Slimstat now checks if Tracking is enabled in the settings, before processing any request it receives from the browser (thank you, Chris).
  • [Fix] A false positive alert of attempted XSS injection was being returned when the 'android-app' scheme was being used to access the website (thank you, Sasa).
Download this release

Release Info

Developer coolmann
Plugin Icon 128x128 Slimstat Analytics
Version 4.6.8
Comparing to
See all releases

Code changes from version 4.6.7 to 4.6.8

admin/config/index.php CHANGED
@@ -154,8 +154,8 @@ $settings = array(
154
  4 => array(
155
  'title' => __( 'Reports', 'wp-slimstat' ),
156
  'rows' => array(
157
- 'reports_basic_header' => array('description' => __('Formats and Conversions','wp-slimstat'), 'type' => 'section_header'),
158
- 'use_european_separators' => array('description' => __('Number Format','wp-slimstat'), 'type' => 'toggle', 'long_description' => __('Choose the number format you want to use for your reports.','wp-slimstat'), 'custom_label_on' => '1.234,5', 'custom_label_off' => '1,234.5'),
159
  'date_format' => array('description' => __('Date Format','wp-slimstat'), 'type' => 'text', 'long_description' => __("<a href='http://php.net/manual/en/function.date.php' target='_blank'>PHP Format</a> to use when displaying a pageview's date.", 'wp-slimstat')),
160
  'time_format' => array('description' => __('Time Format','wp-slimstat'), 'type' => 'text', 'long_description' => __("<a href='http://php.net/manual/en/function.date.php' target='_blank'>PHP Format</a> to use when displaying a pageview's time.", 'wp-slimstat')),
161
  'show_display_name' => array('description' => __('Use Display Name','wp-slimstat'), 'type' => 'toggle', 'long_description' => __('By default, users are listed by their usernames. Use this option to visualize their display names instead.','wp-slimstat')),
@@ -164,6 +164,7 @@ $settings = array(
164
 
165
  'reports_functionality_header' => array( 'description' => __( 'Functionality', 'wp-slimstat' ), 'type' => 'section_header' ),
166
  'async_load' => array( 'description' => __( 'Async Mode', 'wp-slimstat' ), 'type' => 'toggle', 'long_description' => __( 'Activate this feature if your reports take a while to load. It breaks down the load on your server into multiple requests, thus avoiding memory issues and performance problems.', 'wp-slimstat' ) ),
 
167
  'expand_details' => array('description' => __('Expand Details','wp-slimstat'), 'type' => 'toggle', 'long_description' => __("Expand each row's details by default, insted of on mousehover.",'wp-slimstat')),
168
  'rows_to_show' => array('description' => __('Rows to Display','wp-slimstat'), 'type' => 'integer', 'long_description' => __('Specify the number of items in each report.','wp-slimstat')),
169
  'limit_results' => array( 'description' => __( 'Max Results','wp-slimstat' ), 'type' => 'integer', 'long_description' => __( 'Decide how many records should be retrieved from the database in total. Depending on your server configuration, you may want to fine tune this value to avoid exceeding your PHP memory limit.', 'wp-slimstat' ) ),
154
  4 => array(
155
  'title' => __( 'Reports', 'wp-slimstat' ),
156
  'rows' => array(
157
+ 'reports_basic_header' => array( 'description' => __( 'Data Formats and Conversion', 'wp-slimstat' ), 'type' => 'section_header' ),
158
+ 'use_european_separators' => array( 'description' => __( 'Number Format', 'wp-slimstat' ), 'type' => 'toggle', 'long_description' => __( 'Choose the number format you want to use for your reports.','wp-slimstat' ), 'custom_label_on' => '1.234,5', 'custom_label_off' => '1,234.5' ),
159
  'date_format' => array('description' => __('Date Format','wp-slimstat'), 'type' => 'text', 'long_description' => __("<a href='http://php.net/manual/en/function.date.php' target='_blank'>PHP Format</a> to use when displaying a pageview's date.", 'wp-slimstat')),
160
  'time_format' => array('description' => __('Time Format','wp-slimstat'), 'type' => 'text', 'long_description' => __("<a href='http://php.net/manual/en/function.date.php' target='_blank'>PHP Format</a> to use when displaying a pageview's time.", 'wp-slimstat')),
161
  'show_display_name' => array('description' => __('Use Display Name','wp-slimstat'), 'type' => 'toggle', 'long_description' => __('By default, users are listed by their usernames. Use this option to visualize their display names instead.','wp-slimstat')),
164
 
165
  'reports_functionality_header' => array( 'description' => __( 'Functionality', 'wp-slimstat' ), 'type' => 'section_header' ),
166
  'async_load' => array( 'description' => __( 'Async Mode', 'wp-slimstat' ), 'type' => 'toggle', 'long_description' => __( 'Activate this feature if your reports take a while to load. It breaks down the load on your server into multiple requests, thus avoiding memory issues and performance problems.', 'wp-slimstat' ) ),
167
+ 'use_current_month_timespan' => array( 'description' => __( 'Default Time Span', 'wp-slimstat' ), 'type' => 'toggle', 'long_description' => __( 'Determine what is the default time period for calculating all the data in each report: current month or past 30 days. You can always use the time filter dropdown to customize this value even further.', 'wp-slimstat' ), 'custom_label_on' => 'Month', 'custom_label_off' => '30 Days' ),
168
  'expand_details' => array('description' => __('Expand Details','wp-slimstat'), 'type' => 'toggle', 'long_description' => __("Expand each row's details by default, insted of on mousehover.",'wp-slimstat')),
169
  'rows_to_show' => array('description' => __('Rows to Display','wp-slimstat'), 'type' => 'integer', 'long_description' => __('Specify the number of items in each report.','wp-slimstat')),
170
  'limit_results' => array( 'description' => __( 'Max Results','wp-slimstat' ), 'type' => 'integer', 'long_description' => __( 'Decide how many records should be retrieved from the database in total. Depending on your server configuration, you may want to fine tune this value to avoid exceeding your PHP memory limit.', 'wp-slimstat' ) ),
admin/view/index.php CHANGED
@@ -56,7 +56,7 @@
56
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals '.date_i18n('d', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&month equals '.date_i18n('m', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&year equals '.date_i18n('Y', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&interval equals 0') ?>"><?php _e('Yesterday','wp-slimstat') ?></a>
57
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals '.date_i18n('d').'&&&month equals '.date_i18n('m').'&&&year equals '.date_i18n('Y').'&&&interval equals 7&&&interval_direction equals 0') ?>"><?php _e('Last 7 Days','wp-slimstat') ?></a>
58
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url( 'hour equals 0&&&day equals ' . date_i18n( 'd' ) . '&&&month equals ' . date_i18n( 'm' ) . '&&&year equals ' . date_i18n( 'Y' ) . '&&&interval equals 60&&&interval_direction equals 0' ) ?>"><?php _e( 'Last 60 Days', 'wp-slimstat' ) ?></a>
59
- <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url( 'hour equals 0&&&day equals 0&&&month equals ' . date_i18n( 'm' ) . '&&&year equals ' . date_i18n( 'Y' ) . '&&&interval equals 0&&&interval_direction equals 0' ) ?>"><?php _e( 'This month so far', 'wp-slimstat' ) ?></a>
60
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals 0&&&month equals 0&&&year equals '.date_i18n('Y').'&&&interval equals 0') ?>"><?php _e('This Year So Far','wp-slimstat') ?></a>
61
  <strong><?php _e('Date Range','wp-slimstat') ?></strong>
62
 
56
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals '.date_i18n('d', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&month equals '.date_i18n('m', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&year equals '.date_i18n('Y', mktime(0, 0, 0, date_i18n('m'), date_i18n('d')-1, date_i18n('Y'))).'&&&interval equals 0') ?>"><?php _e('Yesterday','wp-slimstat') ?></a>
57
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals '.date_i18n('d').'&&&month equals '.date_i18n('m').'&&&year equals '.date_i18n('Y').'&&&interval equals 7&&&interval_direction equals 0') ?>"><?php _e('Last 7 Days','wp-slimstat') ?></a>
58
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url( 'hour equals 0&&&day equals ' . date_i18n( 'd' ) . '&&&month equals ' . date_i18n( 'm' ) . '&&&year equals ' . date_i18n( 'Y' ) . '&&&interval equals 60&&&interval_direction equals 0' ) ?>"><?php _e( 'Last 60 Days', 'wp-slimstat' ) ?></a>
59
+ <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals '.date_i18n('d').'&&&month equals '.date_i18n('m').'&&&year equals '.date_i18n('Y').'&&&interval equals 90&&&interval_direction equals minus') ?>"><?php _e('Last 90 Days','wp-slimstat') ?></a>
60
  <a class="slimstat-filter-link slimstat-date-choice noslimstat" href="<?php echo wp_slimstat_reports::fs_url('hour equals 0&&&day equals 0&&&month equals 0&&&year equals '.date_i18n('Y').'&&&interval equals 0') ?>"><?php _e('This Year So Far','wp-slimstat') ?></a>
61
  <strong><?php _e('Date Range','wp-slimstat') ?></strong>
62
 
admin/view/wp-slimstat-db.php CHANGED
@@ -541,7 +541,7 @@ class wp_slimstat_db {
541
  ) - 1;
542
  $filters_normalized[ 'utime' ][ 'type' ] = 'm';
543
  }
544
- else {
545
  $filters_normalized[ 'utime' ][ 'end' ] = mktime(
546
  date_i18n( 'H' ),
547
  date_i18n( 'i' ),
@@ -556,6 +556,23 @@ class wp_slimstat_db {
556
  $filters_normalized[ 'date' ][ 'interval' ] = isset( $filters_normalized[ 'date' ][ 'interval' ] ) ? $filters_normalized[ 'date' ][ 'interval' ] : 30;
557
  $filters_normalized[ 'date' ][ 'interval_direction' ] = 0;
558
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  }
560
  else { // An interval was specified
561
  $filters_normalized[ 'utime' ][ 'type' ] = 'interval';
541
  ) - 1;
542
  $filters_normalized[ 'utime' ][ 'type' ] = 'm';
543
  }
544
+ else if ( !empty( wp_slimstat::$settings[ 'use_current_month_timespan' ] ) && wp_slimstat::$settings[ 'use_current_month_timespan' ] == 'no' ) {
545
  $filters_normalized[ 'utime' ][ 'end' ] = mktime(
546
  date_i18n( 'H' ),
547
  date_i18n( 'i' ),
556
  $filters_normalized[ 'date' ][ 'interval' ] = isset( $filters_normalized[ 'date' ][ 'interval' ] ) ? $filters_normalized[ 'date' ][ 'interval' ] : 30;
557
  $filters_normalized[ 'date' ][ 'interval_direction' ] = 0;
558
  }
559
+ else {
560
+ $filters_normalized[ 'utime' ][ 'start' ] = mktime(
561
+ 0,
562
+ 0,
563
+ 0,
564
+ !empty( $filters_normalized[ 'date' ][ 'month' ] )?$filters_normalized[ 'date' ][ 'month' ]:date_i18n( 'n' ),
565
+ 1,
566
+ !empty( $filters_normalized[ 'date' ][ 'year' ] )?$filters_normalized[ 'date' ][ 'year' ]:date_i18n( 'Y' )
567
+ );
568
+
569
+ $filters_normalized[ 'utime' ][ 'end' ] = strtotime(
570
+ ( !empty( $filters_normalized[ 'date' ][ 'year' ] )?$filters_normalized[ 'date' ][ 'year' ]:date_i18n( 'Y' ) ).'-'.
571
+ ( !empty( $filters_normalized[ 'date' ][ 'month' ] )?$filters_normalized[ 'date' ][ 'month' ]:date_i18n( 'n' ) ).
572
+ '-01 00:00 +1 month UTC'
573
+ )-1;
574
+ $filters_normalized[ 'utime' ][ 'type' ] = 'm';
575
+ }
576
  }
577
  else { // An interval was specified
578
  $filters_normalized[ 'utime' ][ 'type' ] = 'interval';
admin/wp-slimstat-admin.php CHANGED
@@ -11,9 +11,9 @@ class wp_slimstat_admin {
11
  * Init -- Sets things up.
12
  */
13
  public static function init() {
14
- self::$admin_notice = "Every first of the month we get at least one or two support requests from alarmed users who think their data has disappeared. Some of them end up leaving <a href='https://wordpress.org/support/topic/limited-service/' target='_blank'>negative reviews</a> because of this, which really sends the wrong message about our plugin. The fundamental misunderstanding has been related to the fact that Slimstat uses the <strong>current month</strong> as the default time range for calculating its reports and metrics. So every first of the month all the reports can look <strong>empty</strong> because there is not much data to be crunched, since the month has just started. In order to address this problem, we've changed the default time range from the current month to the last 20 days. Hopefully this will avoid further negative reviews and take care of those recurring misunderstandings. Please feel free to contact our support team if you have any questions.";
15
 
16
- // "This has been an incredible year so far for our team. To celebrate all the great achievements, we've decided to launch our Summer Madness Discounts season. From June to September we are offering a crazy FIFTY PERCENT discount on all our premium add-ons. Including the bundles! Extend Slimstat with features like Excel exports, email reports, heatmaps and more, for just a fraction of the price you would normally pay. Not only: if you purchased a premium add-on in April or May, you are eligible for getting back FIFTY PERCENT of what you paid in discounts on future purchases. <strong>ON TOP</strong> of the Summer Madness Discount. Stack both to get basically free add-ons for your website! Please <a href='http://support.wp-slimstat.com' target='_blank'>contact us</a> to get your personal RollBack coupon today or for any questions you might have regarding this promo!"
17
 
18
  self::$admin_notice .= '<br/><br/><a id="slimstat-hide-admin-notice" href="#" class="button-secondary">Got it, thanks</a>';
19
 
11
  * Init -- Sets things up.
12
  */
13
  public static function init() {
14
+ self::$admin_notice = "This has been an incredible year so far for our team. To celebrate all the great achievements, we've decided to launch our Summer Madness Discounts season. From June to September we are <strong>discounting by 50%</strong> all our <a href='http://www.wp-slimstat.com/addons/' target='_blank'>premium add-ons</a>. Including the bundles! No special codes are required: all discounts are automatically applied during checkout. Extend Slimstat with features like Excel exports, email reports, heatmaps and more, for just a fraction of the price you would normally pay. Not only: if you purchased a premium add-on in April or May, you are eligible for getting back FIFTY PERCENT of what you paid in discounts on future purchases. <strong>ON TOP</strong> of the Summer Madness Discount. Stack both to get basically free add-ons for your website! Please <a href='http://support.wp-slimstat.com' target='_blank'>contact us</a> to get your personal RollBack coupon today or for any questions you might have regarding this promo!";
15
 
16
+ // "As those who have been using Slimstat for a while know, we never stop doing our good share of research and development to imrpove this plugin. One request that has been sitting on our wishlist for a while is to make our geolocation functionality more accurate, and track not just a user's Country of origin, but possibly his State (where applicable) and city. In order to geolocate visitors, our code has been leveraging a third-party data file provided by <a href='https://www.maxmind.com/en/home' target='_blank'>MaxMind.com</a>. A while ago, they launched a new data format, which improves performance and offers a way to determine the city of origin. However, the new library required a higher version of PHP, and up until now we had preferred allowing more people to use our plugin, over the chance of offering this feature. Now, we found a way to get the best of both worlds: by customizing their PHP library, we were able to make it work with PHP 5.3! Which means that soon Slimstat will be able to tell you your visitors' city of origin right out of the box. Please contact us if you would like to test this feature in advance."
17
 
18
  self::$admin_notice .= '<br/><br/><a id="slimstat-hide-admin-notice" href="#" class="button-secondary">Got it, thanks</a>';
19
 
maxmind.php ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class maxmind_geolite2 {
4
+ public static $upload_dir = '';
5
+ public static $maxmind_path = '';
6
+
7
+ public function get_geolocation_info( $_ip = '' ) {
8
+
9
+ self::$upload_dir = wp_upload_dir();
10
+ self::$upload_dir = self::$upload_dir[ 'basedir' ];
11
+
12
+ if ( is_multisite() && ! ( is_main_network() && is_main_site() && defined( 'MULTISITE' ) ) ) {
13
+ self::$upload_dir = str_replace( '/sites/' . get_current_blog_id(), '', self::$upload_dir );
14
+ }
15
+
16
+ self::$upload_dir .= '/wp-slimstat';
17
+ self::$upload_dir = apply_filters( 'slimstat_maxmind_path', self::$upload_dir );
18
+
19
+ self::$maxmind_path = self::$upload_dir . '/GeoLite2-City.mmdb';
20
+
21
+ $reader = new Reader( self::$maxmind_path );
22
+ $record = $reader->get( $_ip );
23
+
24
+ if ( !empty( $record[ 'city' ][ 'names' ][ 'en' ] ) ) {
25
+ // Work in progress
26
+ }
27
+ }
28
+
29
+ public function download_maxmind_db() {
30
+ // Work in progress
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Instances of this class provide a reader for the MaxMind DB format. IP
36
+ * addresses can be looked up using the <code>get</code> method.
37
+ */
38
+ class Reader
39
+ {
40
+ private static $DATA_SECTION_SEPARATOR_SIZE = 16;
41
+ private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
42
+ private static $METADATA_START_MARKER_LENGTH = 14;
43
+ private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB
44
+
45
+ private $decoder;
46
+ private $fileHandle;
47
+ private $fileSize;
48
+ private $ipV4Start;
49
+ private $metadata;
50
+
51
+ /**
52
+ * Constructs a Reader for the MaxMind DB format. The file passed to it must
53
+ * be a valid MaxMind DB file such as a GeoIp2 database file.
54
+ *
55
+ * @param string $database
56
+ * the MaxMind DB file to use.
57
+ * @throws \InvalidArgumentException for invalid database path or unknown arguments
58
+ * @throws \MaxMind\Db\Reader\InvalidDatabaseException
59
+ * if the database is invalid or there is an error reading
60
+ * from it.
61
+ */
62
+ public function __construct($database)
63
+ {
64
+ if (func_num_args() != 1) {
65
+ throw new \InvalidArgumentException(
66
+ 'The constructor takes exactly one argument.'
67
+ );
68
+ }
69
+
70
+ if (!is_readable($database)) {
71
+ throw new \InvalidArgumentException(
72
+ "The file \"$database\" does not exist or is not readable."
73
+ );
74
+ }
75
+ $this->fileHandle = @fopen($database, 'rb');
76
+ if ($this->fileHandle === false) {
77
+ throw new \InvalidArgumentException(
78
+ "Error opening \"$database\"."
79
+ );
80
+ }
81
+ $this->fileSize = @filesize($database);
82
+ if ($this->fileSize === false) {
83
+ throw new \UnexpectedValueException(
84
+ "Error determining the size of \"$database\"."
85
+ );
86
+ }
87
+
88
+ $start = $this->findMetadataStart($database);
89
+ $metadataDecoder = new Decoder($this->fileHandle, $start);
90
+ list($metadataArray) = $metadataDecoder->decode($start);
91
+ $this->metadata = new Metadata($metadataArray);
92
+ $this->decoder = new Decoder(
93
+ $this->fileHandle,
94
+ $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Looks up the <code>address</code> in the MaxMind DB.
100
+ *
101
+ * @param string $ipAddress
102
+ * the IP address to look up.
103
+ * @return array the record for the IP address.
104
+ * @throws \BadMethodCallException if this method is called on a closed database.
105
+ * @throws \InvalidArgumentException if something other than a single IP address is passed to the method.
106
+ * @throws InvalidDatabaseException
107
+ * if the database is invalid or there is an error reading
108
+ * from it.
109
+ */
110
+ public function get($ipAddress)
111
+ {
112
+ if (func_num_args() != 1) {
113
+ throw new \InvalidArgumentException(
114
+ 'Method takes exactly one argument.'
115
+ );
116
+ }
117
+
118
+ if (!is_resource($this->fileHandle)) {
119
+ throw new \BadMethodCallException(
120
+ 'Attempt to read from a closed MaxMind DB.'
121
+ );
122
+ }
123
+
124
+ if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
125
+ throw new \InvalidArgumentException(
126
+ "The value \"$ipAddress\" is not a valid IP address."
127
+ );
128
+ }
129
+
130
+ if ($this->metadata->ipVersion == 4 && strrpos($ipAddress, ':')) {
131
+ throw new \InvalidArgumentException(
132
+ "Error looking up $ipAddress. You attempted to look up an"
133
+ . " IPv6 address in an IPv4-only database."
134
+ );
135
+ }
136
+ $pointer = $this->findAddressInTree($ipAddress);
137
+ if ($pointer == 0) {
138
+ return null;
139
+ }
140
+ return $this->resolveDataPointer($pointer);
141
+ }
142
+
143
+ private function findAddressInTree($ipAddress)
144
+ {
145
+ // XXX - could simplify. Done as a byte array to ease porting
146
+ $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
147
+
148
+ $bitCount = count($rawAddress) * 8;
149
+
150
+ // The first node of the tree is always node 0, at the beginning of the
151
+ // value
152
+ $node = $this->startNode($bitCount);
153
+
154
+ for ($i = 0; $i < $bitCount; $i++) {
155
+ if ($node >= $this->metadata->nodeCount) {
156
+ break;
157
+ }
158
+ $tempBit = 0xFF & $rawAddress[$i >> 3];
159
+ $bit = 1 & ($tempBit >> 7 - ($i % 8));
160
+
161
+ $node = $this->readNode($node, $bit);
162
+ }
163
+ if ($node == $this->metadata->nodeCount) {
164
+ // Record is empty
165
+ return 0;
166
+ } elseif ($node > $this->metadata->nodeCount) {
167
+ // Record is a data pointer
168
+ return $node;
169
+ }
170
+ throw new InvalidDatabaseException("Something bad happened");
171
+ }
172
+
173
+
174
+ private function startNode($length)
175
+ {
176
+ // Check if we are looking up an IPv4 address in an IPv6 tree. If this
177
+ // is the case, we can skip over the first 96 nodes.
178
+ if ($this->metadata->ipVersion == 6 && $length == 32) {
179
+ return $this->ipV4StartNode();
180
+ }
181
+ // The first node of the tree is always node 0, at the beginning of the
182
+ // value
183
+ return 0;
184
+ }
185
+
186
+ private function ipV4StartNode()
187
+ {
188
+ // This is a defensive check. There is no reason to call this when you
189
+ // have an IPv4 tree.
190
+ if ($this->metadata->ipVersion == 4) {
191
+ return 0;
192
+ }
193
+
194
+ if ($this->ipV4Start != 0) {
195
+ return $this->ipV4Start;
196
+ }
197
+ $node = 0;
198
+
199
+ for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
200
+ $node = $this->readNode($node, 0);
201
+ }
202
+ $this->ipV4Start = $node;
203
+ return $node;
204
+ }
205
+
206
+ private function readNode($nodeNumber, $index)
207
+ {
208
+ $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
209
+
210
+ // XXX - probably could condense this.
211
+ switch ($this->metadata->recordSize) {
212
+ case 24:
213
+ $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
214
+ list(, $node) = unpack('N', "\x00" . $bytes);
215
+ return $node;
216
+ case 28:
217
+ $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
218
+ list(, $middle) = unpack('C', $middleByte);
219
+ if ($index == 0) {
220
+ $middle = (0xF0 & $middle) >> 4;
221
+ } else {
222
+ $middle = 0x0F & $middle;
223
+ }
224
+ $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
225
+ list(, $node) = unpack('N', chr($middle) . $bytes);
226
+ return $node;
227
+ case 32:
228
+ $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
229
+ list(, $node) = unpack('N', $bytes);
230
+ return $node;
231
+ default:
232
+ throw new InvalidDatabaseException(
233
+ 'Unknown record size: '
234
+ . $this->metadata->recordSize
235
+ );
236
+ }
237
+ }
238
+
239
+ private function resolveDataPointer($pointer)
240
+ {
241
+ $resolved = $pointer - $this->metadata->nodeCount
242
+ + $this->metadata->searchTreeSize;
243
+ if ($resolved > $this->fileSize) {
244
+ throw new InvalidDatabaseException(
245
+ "The MaxMind DB file's search tree is corrupt"
246
+ );
247
+ }
248
+
249
+ list($data) = $this->decoder->decode($resolved);
250
+ return $data;
251
+ }
252
+
253
+ /*
254
+ * This is an extremely naive but reasonably readable implementation. There
255
+ * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
256
+ * an issue, but I suspect it won't be.
257
+ */
258
+ private function findMetadataStart($filename)
259
+ {
260
+ $handle = $this->fileHandle;
261
+ $fstat = fstat($handle);
262
+ $fileSize = $fstat['size'];
263
+ $marker = self::$METADATA_START_MARKER;
264
+ $markerLength = self::$METADATA_START_MARKER_LENGTH;
265
+ $metadataMaxLengthExcludingMarker
266
+ = min(self::$METADATA_MAX_SIZE, $fileSize) - $markerLength;
267
+
268
+ for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; $i++) {
269
+ for ($j = 0; $j < $markerLength; $j++) {
270
+ fseek($handle, $fileSize - $i - $j - 1);
271
+ $matchBit = fgetc($handle);
272
+ if ($matchBit != $marker[$markerLength - $j - 1]) {
273
+ continue 2;
274
+ }
275
+ }
276
+ return $fileSize - $i;
277
+ }
278
+ throw new InvalidDatabaseException(
279
+ "Error opening database file ($filename). " .
280
+ 'Is this a valid MaxMind DB file?'
281
+ );
282
+ }
283
+
284
+ /**
285
+ * @throws \InvalidArgumentException if arguments are passed to the method.
286
+ * @throws \BadMethodCallException if the database has been closed.
287
+ * @return Metadata object for the database.
288
+ */
289
+ public function metadata()
290
+ {
291
+ if (func_num_args()) {
292
+ throw new \InvalidArgumentException(
293
+ 'Method takes no arguments.'
294
+ );
295
+ }
296
+
297
+ // Not technically required, but this makes it consistent with
298
+ // C extension and it allows us to change our implementation later.
299
+ if (!is_resource($this->fileHandle)) {
300
+ throw new \BadMethodCallException(
301
+ 'Attempt to read from a closed MaxMind DB.'
302
+ );
303
+ }
304
+
305
+ return $this->metadata;
306
+ }
307
+
308
+ /**
309
+ * Closes the MaxMind DB and returns resources to the system.
310
+ *
311
+ * @throws \Exception
312
+ * if an I/O error occurs.
313
+ */
314
+ public function close()
315
+ {
316
+ if (!is_resource($this->fileHandle)) {
317
+ throw new \BadMethodCallException(
318
+ 'Attempt to close a closed MaxMind DB.'
319
+ );
320
+ }
321
+ fclose($this->fileHandle);
322
+ }
323
+ }
324
+
325
+ class Decoder
326
+ {
327
+
328
+ private $fileStream;
329
+ private $pointerBase;
330
+ // This is only used for unit testing
331
+ private $pointerTestHack;
332
+ private $switchByteOrder;
333
+
334
+ private $types = array(
335
+ 0 => 'extended',
336
+ 1 => 'pointer',
337
+ 2 => 'utf8_string',
338
+ 3 => 'double',
339
+ 4 => 'bytes',
340
+ 5 => 'uint16',
341
+ 6 => 'uint32',
342
+ 7 => 'map',
343
+ 8 => 'int32',
344
+ 9 => 'uint64',
345
+ 10 => 'uint128',
346
+ 11 => 'array',
347
+ 12 => 'container',
348
+ 13 => 'end_marker',
349
+ 14 => 'boolean',
350
+ 15 => 'float',
351
+ );
352
+
353
+ public function __construct(
354
+ $fileStream,
355
+ $pointerBase = 0,
356
+ $pointerTestHack = false
357
+ ) {
358
+ $this->fileStream = $fileStream;
359
+ $this->pointerBase = $pointerBase;
360
+ $this->pointerTestHack = $pointerTestHack;
361
+
362
+ $this->switchByteOrder = $this->isPlatformLittleEndian();
363
+ }
364
+
365
+
366
+ public function decode($offset)
367
+ {
368
+ list(, $ctrlByte) = unpack(
369
+ 'C',
370
+ Util::read($this->fileStream, $offset, 1)
371
+ );
372
+ $offset++;
373
+
374
+ $type = $this->types[$ctrlByte >> 5];
375
+
376
+ // Pointers are a special case, we don't read the next $size bytes, we
377
+ // use the size to determine the length of the pointer and then follow
378
+ // it.
379
+ if ($type == 'pointer') {
380
+ list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
381
+
382
+ // for unit testing
383
+ if ($this->pointerTestHack) {
384
+ return array($pointer);
385
+ }
386
+
387
+ list($result) = $this->decode($pointer);
388
+
389
+ return array($result, $offset);
390
+ }
391
+
392
+ if ($type == 'extended') {
393
+ list(, $nextByte) = unpack(
394
+ 'C',
395
+ Util::read($this->fileStream, $offset, 1)
396
+ );
397
+
398
+ $typeNum = $nextByte + 7;
399
+
400
+ if ($typeNum < 8) {
401
+ throw new InvalidDatabaseException(
402
+ "Something went horribly wrong in the decoder. An extended type "
403
+ . "resolved to a type number < 8 ("
404
+ . $this->types[$typeNum]
405
+ . ")"
406
+ );
407
+ }
408
+
409
+ $type = $this->types[$typeNum];
410
+ $offset++;
411
+ }
412
+
413
+ list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
414
+
415
+ return $this->decodeByType($type, $offset, $size);
416
+ }
417
+
418
+ private function decodeByType($type, $offset, $size)
419
+ {
420
+ switch ($type) {
421
+ case 'map':
422
+ return $this->decodeMap($size, $offset);
423
+ case 'array':
424
+ return $this->decodeArray($size, $offset);
425
+ case 'boolean':
426
+ return array($this->decodeBoolean($size), $offset);
427
+ }
428
+
429
+ $newOffset = $offset + $size;
430
+ $bytes = Util::read($this->fileStream, $offset, $size);
431
+ switch ($type) {
432
+ case 'utf8_string':
433
+ return array($this->decodeString($bytes), $newOffset);
434
+ case 'double':
435
+ $this->verifySize(8, $size);
436
+ return array($this->decodeDouble($bytes), $newOffset);
437
+ case 'float':
438
+ $this->verifySize(4, $size);
439
+ return array($this->decodeFloat($bytes), $newOffset);
440
+ case 'bytes':
441
+ return array($bytes, $newOffset);
442
+ case 'uint16':
443
+ case 'uint32':
444
+ return array($this->decodeUint($bytes), $newOffset);
445
+ case 'int32':
446
+ return array($this->decodeInt32($bytes), $newOffset);
447
+ case 'uint64':
448
+ case 'uint128':
449
+ return array($this->decodeBigUint($bytes, $size), $newOffset);
450
+ default:
451
+ throw new InvalidDatabaseException(
452
+ "Unknown or unexpected type: " . $type
453
+ );
454
+ }
455
+ }
456
+
457
+ private function verifySize($expected, $actual)
458
+ {
459
+ if ($expected != $actual) {
460
+ throw new InvalidDatabaseException(
461
+ "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
462
+ );
463
+ }
464
+ }
465
+
466
+ private function decodeArray($size, $offset)
467
+ {
468
+ $array = array();
469
+
470
+ for ($i = 0; $i < $size; $i++) {
471
+ list($value, $offset) = $this->decode($offset);
472
+ array_push($array, $value);
473
+ }
474
+
475
+ return array($array, $offset);
476
+ }
477
+
478
+ private function decodeBoolean($size)
479
+ {
480
+ return $size == 0 ? false : true;
481
+ }
482
+
483
+ private function decodeDouble($bits)
484
+ {
485
+ // XXX - Assumes IEEE 754 double on platform
486
+ list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
487
+ return $double;
488
+ }
489
+
490
+ private function decodeFloat($bits)
491
+ {
492
+ // XXX - Assumes IEEE 754 floats on platform
493
+ list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
494
+ return $float;
495
+ }
496
+
497
+ private function decodeInt32($bytes)
498
+ {
499
+ $bytes = $this->zeroPadLeft($bytes, 4);
500
+ list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
501
+ return $int;
502
+ }
503
+
504
+ private function decodeMap($size, $offset)
505
+ {
506
+
507
+ $map = array();
508
+
509
+ for ($i = 0; $i < $size; $i++) {
510
+ list($key, $offset) = $this->decode($offset);
511
+ list($value, $offset) = $this->decode($offset);
512
+ $map[$key] = $value;
513
+ }
514
+
515
+ return array($map, $offset);
516
+ }
517
+
518
+ private $pointerValueOffset = array(
519
+ 1 => 0,
520
+ 2 => 2048,
521
+ 3 => 526336,
522
+ 4 => 0,
523
+ );
524
+
525
+ private function decodePointer($ctrlByte, $offset)
526
+ {
527
+ $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
528
+
529
+ $buffer = Util::read($this->fileStream, $offset, $pointerSize);
530
+ $offset = $offset + $pointerSize;
531
+
532
+ $packed = $pointerSize == 4
533
+ ? $buffer
534
+ : (pack('C', $ctrlByte & 0x7)) . $buffer;
535
+
536
+ $unpacked = $this->decodeUint($packed);
537
+ $pointer = $unpacked + $this->pointerBase
538
+ + $this->pointerValueOffset[$pointerSize];
539
+
540
+ return array($pointer, $offset);
541
+ }
542
+
543
+ private function decodeUint($bytes)
544
+ {
545
+ list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4));
546
+ return $int;
547
+ }
548
+
549
+ private function decodeBigUint($bytes, $byteLength)
550
+ {
551
+ $maxUintBytes = log(PHP_INT_MAX, 2) / 8;
552
+
553
+ if ($byteLength == 0) {
554
+ return 0;
555
+ }
556
+
557
+ $numberOfLongs = ceil($byteLength / 4);
558
+ $paddedLength = $numberOfLongs * 4;
559
+ $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength);
560
+ $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes));
561
+
562
+ $integer = 0;
563
+
564
+ // 2^32
565
+ $twoTo32 = '4294967296';
566
+
567
+ foreach ($unpacked as $part) {
568
+ // We only use gmp or bcmath if the final value is too big
569
+ if ($byteLength <= $maxUintBytes) {
570
+ $integer = ($integer << 32) + $part;
571
+ } elseif (extension_loaded('gmp')) {
572
+ $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part));
573
+ } elseif (extension_loaded('bcmath')) {
574
+ $integer = bcadd(bcmul($integer, $twoTo32), $part);
575
+ } else {
576
+ throw new \RuntimeException(
577
+ 'The gmp or bcmath extension must be installed to read this database.'
578
+ );
579
+ }
580
+ }
581
+ return $integer;
582
+ }
583
+
584
+ private function decodeString($bytes)
585
+ {
586
+ // XXX - NOOP. As far as I know, the end user has to explicitly set the
587
+ // encoding in PHP. Strings are just bytes.
588
+ return $bytes;
589
+ }
590
+
591
+ private function sizeFromCtrlByte($ctrlByte, $offset)
592
+ {
593
+ $size = $ctrlByte & 0x1f;
594
+ $bytesToRead = $size < 29 ? 0 : $size - 28;
595
+ $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
596
+ $decoded = $this->decodeUint($bytes);
597
+
598
+ if ($size == 29) {
599
+ $size = 29 + $decoded;
600
+ } elseif ($size == 30) {
601
+ $size = 285 + $decoded;
602
+ } elseif ($size > 30) {
603
+ $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
604
+ + 65821;
605
+ }
606
+
607
+ return array($size, $offset + $bytesToRead);
608
+ }
609
+
610
+ private function zeroPadLeft($content, $desiredLength)
611
+ {
612
+ return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
613
+ }
614
+
615
+ private function maybeSwitchByteOrder($bytes)
616
+ {
617
+ return $this->switchByteOrder ? strrev($bytes) : $bytes;
618
+ }
619
+
620
+ private function isPlatformLittleEndian()
621
+ {
622
+ $testint = 0x00FF;
623
+ $packed = pack('S', $testint);
624
+ return $testint === current(unpack('v', $packed));
625
+ }
626
+ }
627
+
628
+ /**
629
+ * This class should be thrown when unexpected data is found in the database.
630
+ */
631
+ class InvalidDatabaseException extends \Exception
632
+ {
633
+ }
634
+
635
+ /**
636
+ * This class provides the metadata for the MaxMind DB file.
637
+ *
638
+ * @property integer nodeCount This is an unsigned 32-bit integer indicating
639
+ * the number of nodes in the search tree.
640
+ *
641
+ * @property integer recordSize This is an unsigned 16-bit integer. It
642
+ * indicates the number of bits in a record in the search tree. Note that each
643
+ * node consists of two records.
644
+ *
645
+ * @property integer ipVersion This is an unsigned 16-bit integer which is
646
+ * always 4 or 6. It indicates whether the database contains IPv4 or IPv6
647
+ * address data.
648
+ *
649
+ * @property string databaseType This is a string that indicates the structure
650
+ * of each data record associated with an IP address. The actual definition of
651
+ * these structures is left up to the database creator.
652
+ *
653
+ * @property array languages An array of strings, each of which is a language
654
+ * code. A given record may contain data items that have been localized to
655
+ * some or all of these languages. This may be undefined.
656
+ *
657
+ * @property integer binaryFormatMajorVersion This is an unsigned 16-bit
658
+ * integer indicating the major version number for the database's binary
659
+ * format.
660
+ *
661
+ * @property integer binaryFormatMinorVersion This is an unsigned 16-bit
662
+ * integer indicating the minor version number for the database's binary format.
663
+ *
664
+ * @property integer buildEpoch This is an unsigned 64-bit integer that
665
+ * contains the database build timestamp as a Unix epoch value.
666
+ *
667
+ * @property array description This key will always point to a map
668
+ * (associative array). The keys of that map will be language codes, and the
669
+ * values will be a description in that language as a UTF-8 string. May be
670
+ * undefined for some databases.
671
+ */
672
+ class Metadata
673
+ {
674
+ private $binaryFormatMajorVersion;
675
+ private $binaryFormatMinorVersion;
676
+ private $buildEpoch;
677
+ private $databaseType;
678
+ private $description;
679
+ private $ipVersion;
680
+ private $languages;
681
+ private $nodeByteSize;
682
+ private $nodeCount;
683
+ private $recordSize;
684
+ private $searchTreeSize;
685
+
686
+ public function __construct($metadata)
687
+ {
688
+ $this->binaryFormatMajorVersion =
689
+ $metadata['binary_format_major_version'];
690
+ $this->binaryFormatMinorVersion =
691
+ $metadata['binary_format_minor_version'];
692
+ $this->buildEpoch = $metadata['build_epoch'];
693
+ $this->databaseType = $metadata['database_type'];
694
+ $this->languages = $metadata['languages'];
695
+ $this->description = $metadata['description'];
696
+ $this->ipVersion = $metadata['ip_version'];
697
+ $this->nodeCount = $metadata['node_count'];
698
+ $this->recordSize = $metadata['record_size'];
699
+ $this->nodeByteSize = $this->recordSize / 4;
700
+ $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
701
+ }
702
+
703
+ public function __get($var)
704
+ {
705
+ return $this->$var;
706
+ }
707
+ }
708
+
709
+ class Util
710
+ {
711
+ public static function read($stream, $offset, $numberOfBytes)
712
+ {
713
+ if ($numberOfBytes == 0) {
714
+ return '';
715
+ }
716
+ if (fseek($stream, $offset) == 0) {
717
+ $value = fread($stream, $numberOfBytes);
718
+
719
+ // We check that the number of bytes read is equal to the number
720
+ // asked for. We use ftell as getting the length of $value is
721
+ // much slower.
722
+ if (ftell($stream) - $offset === $numberOfBytes) {
723
+ return $value;
724
+ }
725
+ }
726
+ throw new InvalidDatabaseException(
727
+ "The MaxMind DB file contains bad data"
728
+ );
729
+ }
730
+ }
readme.txt CHANGED
@@ -5,7 +5,7 @@ Tags: analytics, statistics, counter, tracking, reports, wassup, geolocation, on
5
  Text Domain: wp-slimstat
6
  Requires at least: 3.8
7
  Tested up to: 4.8
8
- Stable tag: 4.6.7
9
 
10
  == Description ==
11
  The leading web analytics plugin for WordPress. Track returning customers and registered users, monitor Javascript events, detect intrusions, analyze email campaigns. Thousands of WordPress sites are already using it.
@@ -71,6 +71,12 @@ Our knowledge base is available on our [support center](http://docs.wp-slimstat.
71
  5. **Responsive layout** - Keep an eye on your reports on the go
72
 
73
  == Changelog ==
 
 
 
 
 
 
74
  = 4.6.7 =
75
  * [Update] Same-domain referrers are now tracked by default, when Slimstat is activated for the first time. Pre-existing installations are not affected. This change will allow us to introduce new features as suggested [by one of our users](https://wordpress.org/support/topic/can-we-see-top-visitor-path/). Stay tuned.
76
  * [Update] Every first of the month we get at least one or two support requests from alarmed users who think their data has disappeared. Some of them end up leaving [negative reviews](https://wordpress.org/support/topic/limited-service/) because of this. The fundamental misunderstanding was related to the fact that Slimstat uses the CURRENT MONTH time span as the default range for calculating its reports and metrics. So every first of the month all the reports can look "empty" because there is not much data to be crunched. In order to address this problem, we've changed the default time range from the current month to the last 20 days. Hopefully this will avoid further negative reviews in the future!
5
  Text Domain: wp-slimstat
6
  Requires at least: 3.8
7
  Tested up to: 4.8
8
+ Stable tag: 4.6.8
9
 
10
  == Description ==
11
  The leading web analytics plugin for WordPress. Track returning customers and registered users, monitor Javascript events, detect intrusions, analyze email campaigns. Thousands of WordPress sites are already using it.
71
  5. **Responsive layout** - Keep an eye on your reports on the go
72
 
73
  == Changelog ==
74
+ = 4.6.8 =
75
+ * [Update] We should have known better: for every naysayer there is always a yaysayer that will [ask for just the opposite feature](https://wordpress.org/support/topic/recent-month-change/), right? Fair enough: that's what the settings panels are for, after all. In order to make everybody happy, we introduced a new option (under Slimstat > Settings > Reports) that will allow you to decide what the default time span should be: current month or past 30 days, it's totally up to you.
76
+ * [Update] We implemented Chris' idea to use a transient when recording the visit_id, to improve performance (thank you, [Chris](https://wordpress.org/support/topic/recommend-switching-slimstat_visit_id-to-use-a-transient/)).
77
+ * [Fix] If your website is using a caching plugin, the tracking code might still be appended to the page, even if the user turned off tracking. Slimstat now checks if Tracking is enabled in the settings, before processing any request it receives from the browser (thank you, Chris).
78
+ * [Fix] A false positive alert of attempted XSS injection was being returned when the 'android-app' scheme was being used to access the website (thank you, [Sasa](https://wordpress.org/support/topic/false-positive-tracker-error-attempted-xss-injection/)).
79
+
80
  = 4.6.7 =
81
  * [Update] Same-domain referrers are now tracked by default, when Slimstat is activated for the first time. Pre-existing installations are not affected. This change will allow us to introduce new features as suggested [by one of our users](https://wordpress.org/support/topic/can-we-see-top-visitor-path/). Stay tuned.
82
  * [Update] Every first of the month we get at least one or two support requests from alarmed users who think their data has disappeared. Some of them end up leaving [negative reviews](https://wordpress.org/support/topic/limited-service/) because of this. The fundamental misunderstanding was related to the fact that Slimstat uses the CURRENT MONTH time span as the default range for calculating its reports and metrics. So every first of the month all the reports can look "empty" because there is not much data to be crunched. In order to address this problem, we've changed the default time range from the current month to the last 20 days. Hopefully this will avoid further negative reviews in the future!
wp-slimstat.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: Slimstat Analytics
4
  Plugin URI: http://wordpress.org/plugins/wp-slimstat/
5
  Description: The leading web analytics plugin for WordPress
6
- Version: 4.6.7
7
  Author: Jason Crouse
8
  Author URI: http://www.wp-slimstat.com/
9
  Text Domain: wp-slimstat
@@ -15,7 +15,7 @@ if ( !empty( wp_slimstat::$settings ) ) {
15
  }
16
 
17
  class wp_slimstat {
18
- public static $version = '4.6.7';
19
  public static $settings = array();
20
  public static $options = array(); // To be removed, here just for backward compatibility
21
 
@@ -133,6 +133,14 @@ class wp_slimstat {
133
  * Ajax Tracking
134
  */
135
  public static function slimtrack_ajax() {
 
 
 
 
 
 
 
 
136
  // This function also initializes self::$data_js and removes the checksum from self::$data_js['id']
137
  self::_check_data_integrity( self::$raw_post_array );
138
 
@@ -244,6 +252,13 @@ class wp_slimstat {
244
  * Core tracking functionality
245
  */
246
  public static function slimtrack( $_argument = '' ) {
 
 
 
 
 
 
 
247
  self::toggle_date_i18n_filters( false );
248
  self::$stat[ 'dt' ] = date_i18n( 'U' );
249
  self::$stat[ 'notes' ] = array();
@@ -286,7 +301,7 @@ class wp_slimstat {
286
  unset( self::$stat[ 'referer' ] );
287
  }
288
 
289
- if ( !empty( $referer[ 'scheme' ] ) && !in_array( strtolower( $referer[ 'scheme' ] ), array( 'http', 'https' ) ) ) {
290
  self::_set_error_array( sprintf( __( 'Attempted XSS Injection: %s (IP: %s)', 'wp-slimstat' ), self::$stat[ 'referer' ], self::$stat[ 'ip' ] ), false, 203 );
291
  self::$stat[ 'notes' ][] = sprintf( __( 'Attempted XSS Injection: %s', 'wp-slimstat' ), self::$stat[ 'referer' ] );
292
  unset( self::$stat[ 'referer' ] );
@@ -1007,14 +1022,16 @@ class wp_slimstat {
1007
 
1008
  // User doesn't have an active session
1009
  if ( $is_new_session && ( $_force_assign || self::$settings[ 'javascript_mode' ] == 'yes' ) ) {
1010
- if ( empty( self::$settings[ 'session_duration' ] ) ) self::$settings[ 'session_duration' ] = 1800;
 
 
1011
 
1012
- self::$stat[ 'visit_id' ] = get_option( 'slimstat_visit_id', -1 );
1013
- if ( self::$stat[ 'visit_id' ] == -1 ) {
1014
  self::$stat[ 'visit_id' ] = intval( self::$wpdb->get_var( "SELECT MAX( visit_id ) FROM {$GLOBALS[ 'wpdb' ]->prefix}slim_stats" ) );
1015
  }
1016
  self::$stat[ 'visit_id' ]++;
1017
- update_option( 'slimstat_visit_id', self::$stat[ 'visit_id' ] );
1018
 
1019
  $is_set_cookie = apply_filters( 'slimstat_set_visit_cookie', true );
1020
  if ( $is_set_cookie ) {
@@ -1473,6 +1490,7 @@ class wp_slimstat {
1473
  'convert_resource_urls_to_titles' => 'yes',
1474
  'convert_ip_addresses' => 'no',
1475
  'async_load' => 'no',
 
1476
  'expand_details' => 'no',
1477
  'rows_to_show' => '20',
1478
  'limit_results' => '1000',
3
  Plugin Name: Slimstat Analytics
4
  Plugin URI: http://wordpress.org/plugins/wp-slimstat/
5
  Description: The leading web analytics plugin for WordPress
6
+ Version: 4.6.8
7
  Author: Jason Crouse
8
  Author URI: http://www.wp-slimstat.com/
9
  Text Domain: wp-slimstat
15
  }
16
 
17
  class wp_slimstat {
18
+ public static $version = '4.6.8';
19
  public static $settings = array();
20
  public static $options = array(); // To be removed, here just for backward compatibility
21
 
133
  * Ajax Tracking
134
  */
135
  public static function slimtrack_ajax() {
136
+ // If the website is using a caching plugin, the tracking code might still be there, even if the user turned off tracking
137
+ if ( self::$settings[ 'is_tracking' ] != 'yes' ) {
138
+ self::$stat[ 'id' ] = -204;
139
+ self::_set_error_array( __( 'Tracker is turned off, but client-side tracking code is still running.', 'wp-slimstat' ), true );
140
+ self::slimstat_save_options();
141
+ exit( self::_get_id_with_checksum( self::$stat[ 'id' ] ) );
142
+ }
143
+
144
  // This function also initializes self::$data_js and removes the checksum from self::$data_js['id']
145
  self::_check_data_integrity( self::$raw_post_array );
146
 
252
  * Core tracking functionality
253
  */
254
  public static function slimtrack( $_argument = '' ) {
255
+ // If the website is using a caching plugin, the tracking code might still be there, even if the user turned off tracking
256
+ if ( self::$settings['is_tracking'] != 'yes' ) {
257
+ self::$stat[ 'id' ] = -204;
258
+ self::_set_error_array( __( 'Tracker is turned off, but client-side tracking code is still running.', 'wp-slimstat' ), true );
259
+ return $_argument;
260
+ }
261
+
262
  self::toggle_date_i18n_filters( false );
263
  self::$stat[ 'dt' ] = date_i18n( 'U' );
264
  self::$stat[ 'notes' ] = array();
301
  unset( self::$stat[ 'referer' ] );
302
  }
303
 
304
+ if ( !empty( $referer[ 'scheme' ] ) && !in_array( strtolower( $referer[ 'scheme' ] ), array( 'http', 'https', 'android-app' ) ) ) {
305
  self::_set_error_array( sprintf( __( 'Attempted XSS Injection: %s (IP: %s)', 'wp-slimstat' ), self::$stat[ 'referer' ], self::$stat[ 'ip' ] ), false, 203 );
306
  self::$stat[ 'notes' ][] = sprintf( __( 'Attempted XSS Injection: %s', 'wp-slimstat' ), self::$stat[ 'referer' ] );
307
  unset( self::$stat[ 'referer' ] );
1022
 
1023
  // User doesn't have an active session
1024
  if ( $is_new_session && ( $_force_assign || self::$settings[ 'javascript_mode' ] == 'yes' ) ) {
1025
+ if ( empty( self::$settings[ 'session_duration' ] ) ) {
1026
+ self::$settings[ 'session_duration' ] = 1800;
1027
+ }
1028
 
1029
+ self::$stat[ 'visit_id' ] = get_transient( 'slimstat_visit_id' );
1030
+ if ( self::$stat[ 'visit_id' ] === false ) {
1031
  self::$stat[ 'visit_id' ] = intval( self::$wpdb->get_var( "SELECT MAX( visit_id ) FROM {$GLOBALS[ 'wpdb' ]->prefix}slim_stats" ) );
1032
  }
1033
  self::$stat[ 'visit_id' ]++;
1034
+ set_transient( 'slimstat_visit_id', self::$stat[ 'visit_id' ] );
1035
 
1036
  $is_set_cookie = apply_filters( 'slimstat_set_visit_cookie', true );
1037
  if ( $is_set_cookie ) {
1490
  'convert_resource_urls_to_titles' => 'yes',
1491
  'convert_ip_addresses' => 'no',
1492
  'async_load' => 'no',
1493
+ 'use_current_month_timespan' => 'no',
1494
  'expand_details' => 'no',
1495
  'rows_to_show' => '20',
1496
  'limit_results' => '1000',