Skip to content

Commit f9e4bb8

Browse files
feature: add importer for slimstat analytics
1 parent 4a3f8a6 commit f9e4bb8

6 files changed

Lines changed: 483 additions & 1 deletion

File tree

src/Admin/Actions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use KokoAnalytics\Import\Independent_Analytics_Importer;
1616
use KokoAnalytics\Import\Jetpack_Importer;
1717
use KokoAnalytics\Import\Plausible_Importer;
18+
use KokoAnalytics\Import\SlimStat_Importer;
1819
use KokoAnalytics\Import\Statify_Importer;
1920
use KokoAnalytics\Import\WP_Statistics_Importer;
2021
use KokoAnalytics\Post_Stats_Migrator;
@@ -57,6 +58,8 @@ public function run()
5758
'start_jetpack_import' => lazy(Jetpack_Importer::class, 'start_import'),
5859
'jetpack_import_chunk' => lazy(Jetpack_Importer::class, 'import_chunk'),
5960
'start_plausible_import' => lazy(Plausible_Importer::class, 'start_import'),
61+
'start_slimstat_import' => lazy(SlimStat_Importer::class, 'start_import'),
62+
'slimstat_import_chunk' => lazy(SlimStat_Importer::class, 'import_chunk'),
6063
'start_statify_import' => lazy(Statify_Importer::class, 'start_import'),
6164
'statify_import_chunk' => lazy(Statify_Importer::class, 'import_chunk'),
6265
'start_wp_statistics_import' => lazy(WP_Statistics_Importer::class, 'start_import'),

src/Admin/Pages.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function show_settings_page(): void
6464
'performance' => __('Performance', 'koko-analytics'),
6565
'help' => __('Help', 'koko-analytics'),
6666
]);
67-
$extra_tabs = ['burst_importer', 'independent_analytics_importer', 'jetpack_importer', 'plausible_importer', 'statify_importer', 'wp_statistics_importer'];
67+
$extra_tabs = ['burst_importer', 'independent_analytics_importer', 'jetpack_importer', 'plausible_importer', 'slimstat_importer', 'statify_importer', 'wp_statistics_importer'];
6868
$active_tab = wp_unslash($_GET['tab'] ?? '');
6969
// ensure tab is one of the above
7070
if (! array_key_exists($active_tab, $tabs) && ! in_array($active_tab, $extra_tabs, true)) {

src/Import/SlimStat_Importer.php

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
<?php
2+
3+
/**
4+
* @package koko-analytics
5+
* @license GPL-3.0+
6+
* @author Danny van Kooten
7+
*/
8+
9+
namespace KokoAnalytics\Import;
10+
11+
use DateTimeImmutable;
12+
use DateTimeZone;
13+
use Exception;
14+
use KokoAnalytics\Normalizers\Path;
15+
use KokoAnalytics\Normalizers\Referrer;
16+
17+
class SlimStat_Importer extends Importer
18+
{
19+
private const CHUNK_SIZE = 30;
20+
21+
protected function get_admin_url(): string
22+
{
23+
return admin_url('options-general.php?page=koko-analytics-settings&tab=slimstat_importer');
24+
}
25+
26+
/**
27+
* @return array{start: string, end: string}|null
28+
*/
29+
public function get_available_date_range(): ?array
30+
{
31+
/** @var \wpdb $wpdb */
32+
global $wpdb;
33+
34+
$tables = $this->get_source_tables();
35+
if (count($tables) === 0) {
36+
return null;
37+
}
38+
39+
$source = $this->build_source_query($tables);
40+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
41+
$range = $wpdb->get_row("SELECT MIN(dt) AS start, MAX(dt) AS end FROM ({$source}) slimstat_stats");
42+
if (!$range || !$range->start || !$range->end) {
43+
return null;
44+
}
45+
46+
return [
47+
'start' => gmdate('Y-m-d', (int) $range->start),
48+
'end' => gmdate('Y-m-d', (int) $range->end),
49+
];
50+
}
51+
52+
public function start_import(): void
53+
{
54+
if (!current_user_can('manage_koko_analytics') || !check_admin_referer('koko_analytics_start_slimstat_import')) {
55+
return;
56+
}
57+
58+
if (count($this->get_source_tables()) === 0) {
59+
$this->redirect_with_error($this->get_admin_url(), __('Could not find the SlimStat Analytics database table.', 'koko-analytics'));
60+
exit;
61+
}
62+
63+
$date_start = trim(wp_unslash($_POST['date-start'] ?? ''));
64+
$date_end = trim(wp_unslash($_POST['date-end'] ?? ''));
65+
if ($date_start === '' || $date_end === '') {
66+
$this->redirect_with_error($this->get_admin_url(), __('A required field was missing', 'koko-analytics'));
67+
exit;
68+
}
69+
70+
try {
71+
$date_start = new DateTimeImmutable($date_start, wp_timezone());
72+
$date_end = new DateTimeImmutable($date_end, wp_timezone());
73+
if ($date_end < $date_start) {
74+
throw new Exception('End date must be after start date');
75+
}
76+
} catch (Exception $e) {
77+
$this->redirect_with_error($this->get_admin_url(), __('Invalid date fields', 'koko-analytics'));
78+
exit;
79+
}
80+
81+
$this->redirect($this->get_admin_url(), [
82+
'koko_analytics_action' => 'slimstat_import_chunk',
83+
'date-start' => $date_start->format('Y-m-d'),
84+
'date-end' => $date_end->format('Y-m-d'),
85+
'_wpnonce' => wp_create_nonce('koko_analytics_slimstat_import_chunk'),
86+
]);
87+
}
88+
89+
public function import_chunk(): void
90+
{
91+
if (!current_user_can('manage_koko_analytics')) {
92+
return;
93+
}
94+
95+
check_admin_referer('koko_analytics_slimstat_import_chunk');
96+
97+
try {
98+
$date_start_value = trim(wp_unslash($_GET['date-start'] ?? ''));
99+
$date_end_value = trim(wp_unslash($_GET['date-end'] ?? ''));
100+
if ($date_start_value === '' || $date_end_value === '') {
101+
throw new Exception('Missing date fields');
102+
}
103+
104+
$date_start = new DateTimeImmutable($date_start_value, wp_timezone());
105+
$date_end = new DateTimeImmutable($date_end_value, wp_timezone());
106+
if ($date_end < $date_start) {
107+
throw new Exception('End date must be after start date');
108+
}
109+
110+
$chunk_end = $date_start->modify('+' . (self::CHUNK_SIZE - 1) . ' days');
111+
if ($chunk_end > $date_end) {
112+
$chunk_end = $date_end;
113+
}
114+
115+
$this->perform_chunk_import($date_start, $chunk_end);
116+
} catch (Exception $e) {
117+
$this->redirect_with_error($this->get_admin_url(), $e->getMessage());
118+
exit;
119+
}
120+
121+
$next_date_start = $chunk_end->modify('+1 day');
122+
if ($next_date_start > $date_end) {
123+
$this->redirect($this->get_admin_url(), ['success' => 1]);
124+
exit;
125+
}
126+
127+
$url = add_query_arg([
128+
'koko_analytics_action' => 'slimstat_import_chunk',
129+
'date-start' => $next_date_start->format('Y-m-d'),
130+
'date-end' => $date_end->format('Y-m-d'),
131+
'_wpnonce' => wp_create_nonce('koko_analytics_slimstat_import_chunk'),
132+
]);
133+
134+
$days_left = $next_date_start->diff($date_end)->days + 1;
135+
$chunks_left = (int) ceil($days_left / self::CHUNK_SIZE);
136+
?>
137+
<style>
138+
body {
139+
background: #f0f0f1;
140+
color: #3c434a;
141+
font-family: sans-serif;
142+
font-size: 16px;
143+
line-height: 1.5;
144+
padding: 32px;
145+
}
146+
</style>
147+
<meta http-equiv="refresh" content="1; url=<?php echo esc_attr($url); ?>">
148+
<h1><?php esc_html_e('Liberating your data... Please wait.', 'koko-analytics'); ?></h1>
149+
<p>
150+
<?php
151+
echo wp_kses(sprintf(
152+
/* translators: 1: import start date, 2: import end date. */
153+
__('Imported stats between %1$s and %2$s.', 'koko-analytics'),
154+
'<strong>' . esc_html($date_start->format('Y-m-d')) . '</strong>',
155+
'<strong>' . esc_html($chunk_end->format('Y-m-d')) . '</strong>'
156+
), ['strong' => []]);
157+
?>
158+
</p>
159+
<p><?php esc_html_e('Please do not close this browser tab while the importer is running.', 'koko-analytics'); ?></p>
160+
<?php /* translators: %s: estimated number of seconds remaining. */ ?>
161+
<p><?php printf(esc_html__('Estimated time left: %s seconds.', 'koko-analytics'), esc_html((string) round($chunks_left * 1.5))); ?></p>
162+
<?php
163+
exit;
164+
}
165+
166+
public function perform_chunk_import(DateTimeImmutable $date_start, DateTimeImmutable $date_end): void
167+
{
168+
@set_time_limit(90);
169+
170+
/** @var \wpdb $wpdb */
171+
global $wpdb;
172+
173+
$tables = $this->get_source_tables();
174+
if (count($tables) === 0) {
175+
throw new Exception(esc_html__('Could not find the SlimStat Analytics database table.', 'koko-analytics'));
176+
}
177+
178+
$source = $this->build_source_query($tables);
179+
$site_stats = [];
180+
$utc = new DateTimeZone('UTC');
181+
$date = new DateTimeImmutable($date_start->format('Y-m-d'), $utc);
182+
$last_date = new DateTimeImmutable($date_end->format('Y-m-d'), $utc);
183+
184+
while ($date <= $last_date) {
185+
$next_date = $date->modify('+1 day');
186+
$range = [$date->getTimestamp(), $next_date->getTimestamp()];
187+
$date_key = $date->format('Y-m-d');
188+
189+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
190+
$site = $wpdb->get_row($wpdb->prepare("SELECT COUNT(*) AS pageviews, COUNT(DISTINCT NULLIF(ip, '')) AS visitors FROM ({$source}) slimstat_stats WHERE dt >= %d AND dt < %d AND visit_id > 0 AND COALESCE(browser_type, 0) <> 1", $range));
191+
$this->throw_if_database_error();
192+
193+
if ($site && (int) $site->pageviews > 0) {
194+
$site_stats[] = [$date_key, (int) $site->visitors, (int) $site->pageviews];
195+
}
196+
197+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
198+
$pages = $wpdb->get_results($wpdb->prepare("SELECT resource, MAX(CASE WHEN content_type IN ('post', 'page') OR LEFT(content_type, 4) = 'cpt:' THEN content_id ELSE 0 END) AS post_id, COUNT(*) AS pageviews, COUNT(DISTINCT NULLIF(ip, '')) AS visitors FROM ({$source}) slimstat_stats WHERE dt >= %d AND dt < %d AND visit_id > 0 AND COALESCE(browser_type, 0) <> 1 AND resource IS NOT NULL AND resource != '' GROUP BY resource", $range));
199+
$this->throw_if_database_error();
200+
201+
$page_stats = [];
202+
foreach ($pages as $page) {
203+
$page_stats[] = [$date_key, Path::normalize($page->resource), (int) $page->post_id, (int) $page->visitors, (int) $page->pageviews];
204+
}
205+
$this->bulk_insert_page_stats($page_stats);
206+
207+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
208+
$referrers = $wpdb->get_results($wpdb->prepare("SELECT referer, COUNT(*) AS pageviews, COUNT(DISTINCT NULLIF(ip, '')) AS visitors FROM ({$source}) slimstat_stats WHERE dt >= %d AND dt < %d AND visit_id > 0 AND COALESCE(browser_type, 0) <> 1 AND referer IS NOT NULL AND referer != '' GROUP BY referer", $range));
209+
$this->throw_if_database_error();
210+
211+
$referrer_stats = [];
212+
foreach ($referrers as $referrer) {
213+
$value = Referrer::normalize($referrer->referer);
214+
if ($value !== '') {
215+
$referrer_stats[] = [$date_key, $value, (int) $referrer->visitors, (int) $referrer->pageviews];
216+
}
217+
}
218+
$this->bulk_insert_referrer_stats($referrer_stats);
219+
220+
$date = $next_date;
221+
}
222+
223+
$this->bulk_insert_site_stats($site_stats);
224+
}
225+
226+
/**
227+
* @return array<string>
228+
*/
229+
private function get_source_tables(): array
230+
{
231+
/** @var \wpdb $wpdb */
232+
global $wpdb;
233+
234+
$tables = [];
235+
foreach (['slim_stats', 'slim_stats_archive'] as $suffix) {
236+
$table = $wpdb->prefix . $suffix;
237+
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) === $table) {
238+
$tables[] = $table;
239+
}
240+
}
241+
242+
return $tables;
243+
}
244+
245+
/**
246+
* @param array<string> $tables
247+
*/
248+
private function build_source_query(array $tables): string
249+
{
250+
return implode(' UNION ALL ', array_map(function (string $table): string {
251+
return "SELECT dt, ip, referer, resource, visit_id, browser_type, content_type, content_id FROM {$table}";
252+
}, $tables));
253+
}
254+
255+
private function throw_if_database_error(): void
256+
{
257+
/** @var \wpdb $wpdb */
258+
global $wpdb;
259+
260+
if ($wpdb->last_error !== '') {
261+
throw new Exception(esc_html__("A database error occurred: ", 'koko-analytics') . esc_html(" {$wpdb->last_error}"));
262+
}
263+
}
264+
}

src/Resources/views/settings/data.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=independent_analytics_importer')); ?>"><?php esc_html_e('Import from Independent Analytics', 'koko-analytics'); ?></a></li>
9494
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=jetpack_importer')); ?>"><?php esc_html_e('Import from Jetpack Stats', 'koko-analytics'); ?></a></li>
9595
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=plausible_importer')); ?>"><?php esc_html_e('Import from Plausible', 'koko-analytics'); ?></a></li>
96+
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=slimstat_importer')); ?>"><?php esc_html_e('Import from SlimStat Analytics', 'koko-analytics'); ?></a></li>
9697
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=statify_importer')); ?>"><?php esc_html_e('Import from Statify', 'koko-analytics'); ?></a></li>
9798
<li><a href="<?= esc_attr(admin_url('options-general.php?page=koko-analytics-settings&tab=wp_statistics_importer')); ?>"><?php esc_html_e('Import from WP Statistics', 'koko-analytics'); ?></a></li>
9899
</ul>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
defined('ABSPATH') || exit;
4+
5+
$date_range = (new \KokoAnalytics\Import\SlimStat_Importer())->get_available_date_range();
6+
7+
if (isset($_GET['success']) && $_GET['success'] == 1) {
8+
?>
9+
<div class="ka-alert ka-alert-success ka-alert-dismissible" role="alert">
10+
<?php esc_html_e('Big success! Your stats are now imported into Koko Analytics.', 'koko-analytics'); ?>
11+
<button type="button" class="btn-close" aria-label="<?php esc_attr_e('Close', 'koko-analytics'); ?>" onclick="this.parentElement.remove()"></button>
12+
</div>
13+
<?php } ?>
14+
15+
<h1 class="mt-0"><?php esc_html_e('Import from SlimStat Analytics', 'koko-analytics'); ?></h1>
16+
<p><?php esc_html_e('Import your historical site, page and referrer statistics from SlimStat Analytics into Koko Analytics.', 'koko-analytics'); ?></p>
17+
18+
<?php if ($date_range === null) { ?>
19+
<div class="ka-alert ka-alert-warning" role="alert">
20+
<?php esc_html_e('No SlimStat Analytics data was found.', 'koko-analytics'); ?>
21+
</div>
22+
<?php } else { ?>
23+
<form method="post" onsubmit="return confirm('<?php esc_attr_e('Are you sure you want to import statistics between', 'koko-analytics'); ?> ' + this['date-start'].value + '<?php esc_attr_e(' and ', 'koko-analytics'); ?>' + this['date-end'].value + '<?php esc_attr_e('? This will add to any existing data in your Koko Analytics database tables.', 'koko-analytics'); ?>');" action="">
24+
<input type="hidden" name="koko_analytics_action" value="start_slimstat_import">
25+
<?php wp_nonce_field('koko_analytics_start_slimstat_import'); ?>
26+
<?php wp_referer_field(); ?>
27+
28+
<table class="form-table">
29+
<tr>
30+
<th><label for="date-start"><?php esc_html_e('Start date', 'koko-analytics'); ?></label></th>
31+
<td>
32+
<input id="date-start" name="date-start" type="date" value="<?php echo esc_attr($date_range['start']); ?>" required>
33+
<p class="description"><?php esc_html_e('The earliest date for which to import data.', 'koko-analytics'); ?></p>
34+
</td>
35+
</tr>
36+
<tr>
37+
<th><label for="date-end"><?php esc_html_e('End date', 'koko-analytics'); ?></label></th>
38+
<td>
39+
<input id="date-end" name="date-end" type="date" value="<?php echo esc_attr($date_range['end']); ?>" required>
40+
<p class="description"><?php esc_html_e('The last date for which to import data.', 'koko-analytics'); ?></p>
41+
</td>
42+
</tr>
43+
</table>
44+
45+
<p style="color: indianred;">
46+
<strong><?php esc_html_e('Warning: ', 'koko-analytics'); ?></strong>
47+
<?php esc_html_e('Importing data for a given date range will add to any existing data. The import process can not be reverted unless you reinstate a back-up of your database in its current state.', 'koko-analytics'); ?>
48+
</p>
49+
50+
<p>
51+
<button type="submit" class="btn btn-primary"><?php esc_html_e('Import analytics data', 'koko-analytics'); ?></button>
52+
</p>
53+
</form>
54+
<?php } ?>

0 commit comments

Comments
 (0)