Skip to content

Commit 2fd416f

Browse files
add nonce verification to missing actions; process pruning in chunks
1 parent d6dcc20 commit 2fd416f

7 files changed

Lines changed: 148 additions & 111 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
use KokoAnalytics\Admin\Actions;
3+
use KokoAnalytics\Post_Stats_Migrator;
44

55
defined('ABSPATH') or exit;
66

@@ -9,5 +9,5 @@
99

1010
$count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}koko_analytics_post_stats");
1111
if ($count && $count < 25000) {
12-
(new Actions())->migrate_post_stats_to_v2();
12+
(new Post_Stats_Migrator())->migrate_to_v2();
1313
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
use KokoAnalytics\Admin\Actions;
3+
use KokoAnalytics\Post_Stats_Migrator;
44

55
defined('ABSPATH') or exit;
66

@@ -9,5 +9,5 @@
99

1010
$count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}koko_analytics_post_stats");
1111
if ($count && $count < 25000) {
12-
(new Actions())->fix_post_paths_after_v2();
12+
(new Post_Stats_Migrator())->fix_paths();
1313
}

src/Admin/Actions.php

Lines changed: 7 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
use KokoAnalytics\Fingerprinter;
1414
use KokoAnalytics\Import\Jetpack_Importer;
1515
use KokoAnalytics\Import\Plausible_Importer;
16-
use KokoAnalytics\Normalizers\Path;
17-
use KokoAnalytics\Upserter;
16+
use KokoAnalytics\Post_Stats_Migrator;
1817

1918
use function KokoAnalytics\get_settings;
2019
use function KokoAnalytics\lazy;
2120

2221
class Actions
2322
{
24-
// TODO: Add nonce verification to all mapped actions so that we can remove this from callbacks
2523
public function run()
2624
{
2725
if (isset($_GET['koko_analytics_action'])) {
@@ -37,8 +35,6 @@ public function run()
3735
}
3836

3937
// TODO: Allow plugins to hook into this to register their own actions
40-
// TODO: Use light callback methods in this class with the main lifting done in instance methods on decoupled classes
41-
// TODO: Do all HTTP manipulation in this class so we can re-use the other classes in WP_CLI etc.
4238
$map = [
4339
'install_optimized_endpoint' => [$this, 'install_optimized_endpoint'],
4440
'save_settings' => [$this, 'save_settings'],
@@ -68,6 +64,8 @@ public function run()
6864

6965
public function install_optimized_endpoint()
7066
{
67+
check_admin_referer('koko_analytics_install_optimized_endpoint');
68+
7169
$result = (new Endpoint_Installer())->install();
7270
if ($result !== true) {
7371
wp_safe_redirect(add_query_arg(['error' => urlencode($result)], wp_get_referer()));
@@ -131,105 +129,13 @@ public function save_component_order()
131129

132130
public function migrate_post_stats_to_v2()
133131
{
134-
@set_time_limit(0);
135-
136-
/** @var \wpdb $wpdb */
137-
global $wpdb;
138-
139-
do {
140-
// Select all rows with a post ID but no path ID
141-
// Note: there is no need for an OFFSET here because we are updating rows as we go
142-
$results = $wpdb->get_results("SELECT DISTINCT(post_id) FROM {$wpdb->prefix}koko_analytics_post_stats WHERE post_id IS NOT NULL AND path_id IS NULL LIMIT 1000");
143-
144-
// Stop once there are no more rows in result set
145-
if (!$results) {
146-
break;
147-
}
148-
149-
// create a mapping of post_id => path
150-
$post_id_to_path_map = [];
151-
foreach ($results as $r) {
152-
$post_id_to_path_map["{$r->post_id}"] = $this->get_path_by_post_id($r->post_id);
153-
}
154-
155-
// bulk insert all paths
156-
$upserter = new Upserter('paths', 'path');
157-
$path_to_path_id_map = $upserter->upsert(array_values($post_id_to_path_map));
158-
159-
// update post_stats table to point to paths we just inserted
160-
foreach ($post_id_to_path_map as $post_id => $path) {
161-
$path_id = $path_to_path_id_map[$path];
162-
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}koko_analytics_post_stats SET path_id = %d WHERE post_id = %d", [$path_id, $post_id]));
163-
}
164-
} while (true);
165-
166-
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}koko_analytics_post_stats_old");
167-
$wpdb->query("RENAME TABLE {$wpdb->prefix}koko_analytics_post_stats TO {$wpdb->prefix}koko_analytics_post_stats_old");
168-
169-
$wpdb->query("CREATE TABLE {$wpdb->prefix}koko_analytics_post_stats (
170-
date DATE NOT NULL,
171-
path_id INT UNSIGNED NOT NULL,
172-
post_id INT UNSIGNED NOT NULL DEFAULT 0,
173-
visitors INT UNSIGNED NOT NULL DEFAULT 0,
174-
pageviews INT UNSIGNED NOT NULL DEFAULT 0,
175-
PRIMARY KEY (date, path_id)
176-
) ENGINE=INNODB CHARACTER SET=ascii");
177-
178-
$wpdb->query("INSERT INTO {$wpdb->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) SELECT date, path_id, post_id, SUM(visitors), SUM(pageviews) FROM {$wpdb->prefix}koko_analytics_post_stats_old GROUP BY date, path_id");
179-
180-
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}koko_analytics_post_stats_old");
132+
check_admin_referer('koko_analytics_migrate_post_stats_to_v2');
133+
(new Post_Stats_Migrator())->migrate_to_v2();
181134
}
182135

183-
/**
184-
* Between version 2.0 and 2.0.10, there was an issue with the migration script above which would result in incorrect path ID's being returned when bulk inserting new paths.
185-
* This fixes every entry in the post_stats table by checking each path whether it is correct
186-
*/
187136
public function fix_post_paths_after_v2()
188137
{
189-
@set_time_limit(0);
190-
191-
/** @var \wpdb $wpdb */
192-
global $wpdb;
193-
194-
$offset = 0;
195-
$limit = 1000;
196-
$upserter = new Upserter('paths', 'path');
197-
198-
do {
199-
$results = $wpdb->get_results($wpdb->prepare("SELECT post_id, path_id, p.path FROM {$wpdb->prefix}koko_analytics_post_stats s LEFT JOIN {$wpdb->prefix}koko_analytics_paths p ON p.id = s.path_id WHERE post_id IS NOT NULL AND post_id != 0 GROUP BY post_id LIMIT %d OFFSET %d", [$limit, $offset]));
200-
$offset += $limit;
201-
if (!$results) {
202-
break;
203-
}
204-
205-
foreach ($results as $r) {
206-
$correct_path = $this->get_path_by_post_id($r->post_id);
207-
if ($r->path != $correct_path) {
208-
// get correct path id
209-
$path_to_id_map = $upserter->upsert([$correct_path]);
210-
$correct_path_id = $path_to_id_map[$correct_path];
211-
212-
// update all post_stats to point to correct path_id
213-
$wpdb->query($wpdb->prepare("UPDATE IGNORE {$wpdb->prefix}koko_analytics_post_stats SET path_id = %d WHERE post_id = %d", [$correct_path_id, $r->post_id]));
214-
}
215-
}
216-
} while (true);
217-
}
218-
219-
private function get_path_by_post_id($post_id)
220-
{
221-
$home_url = home_url('/');
222-
$post_permalink = $post_id ? get_permalink($post_id) : $home_url;
223-
if (!$post_permalink) {
224-
$post_permalink = "$home_url?p={$post_id}";
225-
}
226-
227-
$url_parts = parse_url($post_permalink);
228-
$path = $url_parts['path'] ?? '/';
229-
if (!empty($url_parts['query'])) {
230-
$path .= '?' . $url_parts['query'];
231-
}
232-
233-
return Path::normalize($path);
138+
check_admin_referer('koko_analytics_fix_post_paths_after_v2');
139+
(new Post_Stats_Migrator())->fix_paths();
234140
}
235141
}

src/Admin/Controller.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public function action_admin_notices(): void
130130
<?php esc_html_e('Click the button below to proceed with the database migration, this can take some time if you have a large site.', 'koko-analytics'); ?>
131131
</p>
132132
<form action="" method="post">
133+
<?php wp_nonce_field('koko_analytics_migrate_post_stats_to_v2'); ?>
133134
<input type="hidden" name="koko_analytics_action" value="migrate_post_stats_to_v2">
134135
<p><button type="submit" class="button button-primary"><?php esc_html_e('Migrate', 'koko-analytics'); ?></button></p>
135136
</form>

src/Command.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
namespace KokoAnalytics;
1010

11-
use KokoAnalytics\Admin\Actions;
1211
use WP_CLI;
1312

1413
class Command
@@ -47,7 +46,7 @@ public function prune($args, $assoc_args)
4746
public function migrate_post_stats_to_v2($args, $assoc_args)
4847
{
4948
WP_CLI::line('Migrating post stats...');
50-
(new Actions())->migrate_post_stats_to_v2();
49+
(new Post_Stats_Migrator())->migrate_to_v2();
5150
WP_CLI::success('Post stats migrated');
5251
}
5352

src/Post_Stats_Migrator.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
/**
4+
* @package koko-analytics
5+
* @license GPL-3.0+
6+
* @author Danny van Kooten
7+
*
8+
* Handles migrating post_stats data from the old (post_id based) format
9+
* to the new (path_id based) format.
10+
*
11+
* This class contains no HTTP concerns (nonces, redirects, etc.)
12+
* and can be safely called from WP-CLI, cron, migration scripts, or admin actions.
13+
*/
14+
15+
namespace KokoAnalytics;
16+
17+
use KokoAnalytics\Normalizers\Path;
18+
19+
class Post_Stats_Migrator
20+
{
21+
public function migrate_to_v2(): void
22+
{
23+
@set_time_limit(0);
24+
25+
/** @var \wpdb $wpdb */
26+
global $wpdb;
27+
28+
do {
29+
// Select all rows with a post ID but no path ID
30+
// Note: there is no need for an OFFSET here because we are updating rows as we go
31+
$results = $wpdb->get_results("SELECT DISTINCT(post_id) FROM {$wpdb->prefix}koko_analytics_post_stats WHERE post_id IS NOT NULL AND path_id IS NULL LIMIT 1000");
32+
33+
// Stop once there are no more rows in result set
34+
if (!$results) {
35+
break;
36+
}
37+
38+
// create a mapping of post_id => path
39+
$post_id_to_path_map = [];
40+
foreach ($results as $r) {
41+
$post_id_to_path_map["{$r->post_id}"] = $this->get_path_by_post_id($r->post_id);
42+
}
43+
44+
// bulk insert all paths
45+
$upserter = new Upserter('paths', 'path');
46+
$path_to_path_id_map = $upserter->upsert(array_values($post_id_to_path_map));
47+
48+
// update post_stats table to point to paths we just inserted
49+
foreach ($post_id_to_path_map as $post_id => $path) {
50+
$path_id = $path_to_path_id_map[$path];
51+
$wpdb->query($wpdb->prepare("UPDATE {$wpdb->prefix}koko_analytics_post_stats SET path_id = %d WHERE post_id = %d", [$path_id, $post_id]));
52+
}
53+
} while (true);
54+
55+
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}koko_analytics_post_stats_old");
56+
$wpdb->query("RENAME TABLE {$wpdb->prefix}koko_analytics_post_stats TO {$wpdb->prefix}koko_analytics_post_stats_old");
57+
58+
$wpdb->query("CREATE TABLE {$wpdb->prefix}koko_analytics_post_stats (
59+
date DATE NOT NULL,
60+
path_id INT UNSIGNED NOT NULL,
61+
post_id INT UNSIGNED NOT NULL DEFAULT 0,
62+
visitors INT UNSIGNED NOT NULL DEFAULT 0,
63+
pageviews INT UNSIGNED NOT NULL DEFAULT 0,
64+
PRIMARY KEY (date, path_id)
65+
) ENGINE=INNODB CHARACTER SET=ascii");
66+
67+
$wpdb->query("INSERT INTO {$wpdb->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) SELECT date, path_id, post_id, SUM(visitors), SUM(pageviews) FROM {$wpdb->prefix}koko_analytics_post_stats_old GROUP BY date, path_id");
68+
69+
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}koko_analytics_post_stats_old");
70+
}
71+
72+
/**
73+
* Between version 2.0 and 2.0.10, there was an issue with the migration script above which would result in incorrect path ID's being returned when bulk inserting new paths.
74+
* This fixes every entry in the post_stats table by checking each path whether it is correct
75+
*/
76+
public function fix_paths(): void
77+
{
78+
@set_time_limit(0);
79+
80+
/** @var \wpdb $wpdb */
81+
global $wpdb;
82+
83+
$offset = 0;
84+
$limit = 1000;
85+
$upserter = new Upserter('paths', 'path');
86+
87+
do {
88+
$results = $wpdb->get_results($wpdb->prepare("SELECT post_id, path_id, p.path FROM {$wpdb->prefix}koko_analytics_post_stats s LEFT JOIN {$wpdb->prefix}koko_analytics_paths p ON p.id = s.path_id WHERE post_id IS NOT NULL AND post_id != 0 GROUP BY post_id LIMIT %d OFFSET %d", [$limit, $offset]));
89+
$offset += $limit;
90+
if (!$results) {
91+
break;
92+
}
93+
94+
foreach ($results as $r) {
95+
$correct_path = $this->get_path_by_post_id($r->post_id);
96+
if ($r->path != $correct_path) {
97+
// get correct path id
98+
$path_to_id_map = $upserter->upsert([$correct_path]);
99+
$correct_path_id = $path_to_id_map[$correct_path];
100+
101+
// update all post_stats to point to correct path_id
102+
$wpdb->query($wpdb->prepare("UPDATE IGNORE {$wpdb->prefix}koko_analytics_post_stats SET path_id = %d WHERE post_id = %d", [$correct_path_id, $r->post_id]));
103+
}
104+
}
105+
} while (true);
106+
}
107+
108+
private function get_path_by_post_id(int $post_id): string
109+
{
110+
$home_url = home_url('/');
111+
$post_permalink = $post_id ? get_permalink($post_id) : $home_url;
112+
if (!$post_permalink) {
113+
$post_permalink = "$home_url?p={$post_id}";
114+
}
115+
116+
$url_parts = parse_url($post_permalink);
117+
$path = $url_parts['path'] ?? '/';
118+
if (!empty($url_parts['query'])) {
119+
$path .= '?' . $url_parts['query'];
120+
}
121+
122+
return Path::normalize($path);
123+
}
124+
}

src/Pruner.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,17 @@ public function run()
2828

2929
$date = (new \DateTime("-{$settings['prune_data_after_months']} months", wp_timezone()))->format('Y-m-d');
3030

31-
// delete stats older than date above
32-
$this->db->query($this->db->prepare("DELETE FROM {$this->db->prefix}koko_analytics_site_stats WHERE date < %s", $date));
33-
$this->db->query($this->db->prepare("DELETE FROM {$this->db->prefix}koko_analytics_post_stats WHERE date < %s", $date));
34-
$this->db->query($this->db->prepare("DELETE FROM {$this->db->prefix}koko_analytics_referrer_stats WHERE date < %s", $date));
31+
// delete stats older than date above, in chunks to avoid long-running transactions
32+
$tables = [
33+
'koko_analytics_site_stats',
34+
'koko_analytics_post_stats',
35+
'koko_analytics_referrer_stats',
36+
];
37+
foreach ($tables as $table) {
38+
do {
39+
$affected = $this->db->query($this->db->prepare("DELETE FROM {$this->db->prefix}{$table} WHERE date < %s LIMIT 10000", $date));
40+
} while ($affected > 0);
41+
}
3542

3643
$this->delete_orphaned_referrer_urls();
3744
$this->delete_orphaned_paths();

0 commit comments

Comments
 (0)