PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Core/Migration.php +75 -18 4.4.04.9.2 View file →
@@ -1,7 +1,11 @@
1 1 <?php
2 +// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- one-shot data/schema migrations; caching unwanted.
3 +namespace WPDeveloper\BetterDocs\Core;
2 4
3 -namespace WPDeveloper\BetterDocs\Core;
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit;
7 +}
4 8
5 9 use WP_Query;
6 10 use WPDeveloper\BetterDocs\Utils\Base;
7 11 use WPDeveloper\BetterDocs\Utils\Database;
@@ -32,8 +36,9 @@
32 36 }
33 37
34 38 $this->search_migration();
35 39 $this->fix_search_table_collation();
40 + $this->backfill_search_keyword_hash();
36 41
37 42 /**
38 43 * Settings Migration
39 44 */
@@ -50,9 +55,9 @@
50 55 $args = [
51 56 'post_type' => 'docs',
52 57 'post_status' => 'publish',
53 58 'posts_per_page' => -1,
54 - 'suppress_filters' => true,
59 + 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- migration must run on raw posts without language filters.
55 60 's' => $key
56 61 ];
57 62
58 63 $loop = new WP_Query( $args );
@@ -78,12 +83,13 @@
78 83 if ( $keyword == null ) {
79 84 $insert = $wpdb->query(
80 85 $wpdb->prepare(
81 86 "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
82 - ( keyword )
83 - VALUES ( %s )",
87 + ( keyword, keyword_hash )
88 + VALUES ( %s, %s )",
84 89 [
85 - $key
90 + $key,
91 + md5( $key )
86 92 ]
87 93 )
88 94 );
89 95
@@ -96,9 +102,9 @@
96 102 [
97 103 $wpdb->insert_id,
98 104 $count,
99 105 $not_found_count,
100 - date( 'Y-m-d' )
106 + gmdate( 'Y-m-d' )
101 107 ]
102 108 )
103 109 );
104 110 }
@@ -125,29 +131,80 @@
125 131 if ( $this->database->get( 'betterdocs_search_collation_fixed', false ) ) {
126 132 return;
127 133 }
128 134
129 - // Get the WordPress default charset and collation
130 - $charset = $wpdb->charset ? $wpdb->charset : 'utf8mb4';
131 - $collate = $wpdb->collate ? $wpdb->collate : 'utf8mb4_unicode_520_ci';
135 + // Get the WordPress default charset and collation; restrict to a safe identifier
136 + // alphabet because they're interpolated into ALTER TABLE statements below.
137 + $charset = preg_replace( '/[^A-Za-z0-9_]/', '', $wpdb->charset ? $wpdb->charset : 'utf8mb4' );
138 + $collate = preg_replace( '/[^A-Za-z0-9_]/', '', $wpdb->collate ? $wpdb->collate : 'utf8mb4_unicode_520_ci' );
132 139
133 140 // Fix search_keyword table
134 141 $search_keyword_table = $wpdb->prefix . 'betterdocs_search_keyword';
135 - if ( $wpdb->get_var( "SHOW TABLES LIKE '$search_keyword_table'" ) == $search_keyword_table ) {
136 - // Convert table charset and collation
137 - $wpdb->query( "ALTER TABLE {$search_keyword_table} CONVERT TO CHARACTER SET {$charset} COLLATE {$collate}" );
138 -
139 - // Explicitly set keyword column collation
140 - $wpdb->query( "ALTER TABLE {$search_keyword_table} MODIFY keyword TEXT CHARACTER SET {$charset} COLLATE {$collate} NOT NULL" );
142 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- one-shot collation migration; identifiers come from $wpdb only.
143 + if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $search_keyword_table ) ) === $search_keyword_table ) {
144 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter
145 + $wpdb->query( "ALTER TABLE `{$search_keyword_table}` CONVERT TO CHARACTER SET {$charset} COLLATE {$collate}" );
146 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter
147 + $wpdb->query( "ALTER TABLE `{$search_keyword_table}` MODIFY keyword TEXT CHARACTER SET {$charset} COLLATE {$collate} NOT NULL" );
141 148 }
142 149
143 150 // Fix search_log table
144 151 $search_log_table = $wpdb->prefix . 'betterdocs_search_log';
145 - if ( $wpdb->get_var( "SHOW TABLES LIKE '$search_log_table'" ) == $search_log_table ) {
146 - // Convert table charset and collation
147 - $wpdb->query( "ALTER TABLE {$search_log_table} CONVERT TO CHARACTER SET {$charset} COLLATE {$collate}" );
152 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
153 + if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $search_log_table ) ) === $search_log_table ) {
154 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter
155 + $wpdb->query( "ALTER TABLE `{$search_log_table}` CONVERT TO CHARACTER SET {$charset} COLLATE {$collate}" );
148 156 }
149 157
150 158 // Mark migration as complete
151 159 $this->database->save( 'betterdocs_search_collation_fixed', '1.0' );
160 + }
161 +
162 + /**
163 + * Populate keyword_hash for rows that predate the column.
164 + *
165 + * The column is added by dbDelta with an empty default; until it holds the
166 + * hash, insert_search_keyword() cannot find those rows by index and would
167 + * insert duplicates. Runs in bounded batches so a large keyword table does
168 + * not stall the request that triggers the upgrade — anything left over is
169 + * picked up on the next admin load.
170 + *
171 + * @since 1.0.3
172 + * @return void
173 + */
174 + public function backfill_search_keyword_hash() {
175 + global $wpdb;
176 +
177 + if ( $this->database->get( 'betterdocs_search_keyword_hash_filled', false ) ) {
178 + return;
179 + }
180 +
181 + $table = $wpdb->prefix . 'betterdocs_search_keyword';
182 +
183 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- one-shot schema backfill; identifier comes from $wpdb.
184 + if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
185 + return;
186 + }
187 +
188 + // Bail if the column never made it (dbDelta failed) — retry next load.
189 + if ( ! $wpdb->get_results( "SHOW COLUMNS FROM `{$table}` LIKE 'keyword_hash'" ) ) {
190 + return;
191 + }
192 +
193 + $batches = 0;
194 + do {
195 + $updated = $wpdb->query(
196 + "UPDATE `{$table}` SET keyword_hash = MD5( keyword ) WHERE keyword_hash = '' LIMIT 2000"
197 + );
198 + $batches++;
199 + } while ( $updated > 0 && $batches < 25 );
200 +
201 + // Only finish once nothing is left, so a table larger than 50k keywords
202 + // resumes instead of being marked done half-filled.
203 + $remaining = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}` WHERE keyword_hash = ''" );
204 + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
205 +
206 + if ( 0 === $remaining ) {
207 + $this->database->save( 'betterdocs_search_keyword_hash_filled', '1.0.3' );
208 + }
152 209 }
153 210 }