PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.0
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.0
2.0.4 2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / src / Database / Migrator.php

Migrator.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.0, at src/Database/Migrator.php

291 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Database installation, schema, and self-healing repair.
4 *
5 * @package EasyFonts
6 */
7
8 namespace EasyFonts\Database;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Creates, versions, verifies, and repairs the plugin's tables.
14 */
15 class Migrator {
16
17 const DB_VERSION = '2.0.0';
18
19 /**
20 * Install, upgrade, or repair tables. Safe to call on every load.
21 *
22 * Verifies the real schema (column presence), not just the version option,
23 * so a site left with an incompatible legacy schema heals automatically.
24 */
25 public function install(): void {
26 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
27
28 $tables_ok = $this->tables_ok();
29
30 // Up to date and intact → nothing to do.
31 if ( $tables_ok && get_option( 'easyfonts_db_version' ) === self::DB_VERSION ) {
32 return;
33 }
34
35 // An incompatible legacy schema is present → drop the empty legacy
36 // tables so we can create the correct ones. Scoped to legacy schema only.
37 if ( $this->has_legacy_schema() ) {
38 $this->drop_tables();
39 }
40
41 $this->create_tables();
42
43 if ( false === get_option( 'easyfonts_settings' ) ) {
44 add_option( 'easyfonts_settings', self::default_settings(), '', false );
45 }
46
47 // Only record the version once the schema is verified correct.
48 if ( $this->tables_ok() ) {
49 update_option( 'easyfonts_db_version', self::DB_VERSION, false );
50 }
51
52 if ( false === get_option( 'easyfonts_cache_buster' ) ) {
53 update_option( 'easyfonts_cache_buster', time(), false );
54 }
55 }
56
57 /**
58 * Force a clean rebuild (drop + recreate). Used by the repair action / CLI.
59 */
60 public function repair(): void {
61 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
62
63 $this->drop_tables();
64 $this->create_tables();
65
66 if ( $this->tables_ok() ) {
67 update_option( 'easyfonts_db_version', self::DB_VERSION, false );
68 }
69 }
70
71 /**
72 * Create (or, via dbDelta, reconcile) the three tables. Timestamps are set
73 * from PHP, so no DB-side DEFAULT CURRENT_TIMESTAMP is required (portable
74 * across MySQL/MariaDB versions and strict sql_modes).
75 */
76 private function create_tables(): void {
77 global $wpdb;
78
79 $charset = $wpdb->get_charset_collate();
80 $fonts = $wpdb->prefix . 'easyfonts_fonts';
81 $usage = $wpdb->prefix . 'easyfonts_usage';
82 $dec = $wpdb->prefix . 'easyfonts_decisions';
83
84 dbDelta(
85 "CREATE TABLE {$fonts} (
86 id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
87 family VARCHAR(190) NOT NULL,
88 weight VARCHAR(40) NOT NULL DEFAULT '400',
89 style VARCHAR(20) NOT NULL DEFAULT 'normal',
90 subset VARCHAR(60) NOT NULL DEFAULT 'latin',
91 variant_key VARCHAR(120) NOT NULL,
92 is_variable TINYINT(1) NOT NULL DEFAULT 0,
93 is_enabled TINYINT(1) NOT NULL DEFAULT 1,
94 is_preloaded TINYINT(1) NOT NULL DEFAULT 0,
95 load_user_set TINYINT(1) NOT NULL DEFAULT 0,
96 preload_user_set TINYINT(1) NOT NULL DEFAULT 0,
97 provider VARCHAR(40) NOT NULL DEFAULT 'google',
98 css_file VARCHAR(190) DEFAULT NULL,
99 font_file VARCHAR(190) DEFAULT NULL,
100 file_size BIGINT UNSIGNED NOT NULL DEFAULT 0,
101 metrics LONGTEXT DEFAULT NULL,
102 source_url TEXT,
103 created_at DATETIME DEFAULT NULL,
104 PRIMARY KEY (id),
105 UNIQUE KEY uq_variant (variant_key),
106 KEY idx_family (family)
107 ) {$charset};"
108 );
109
110 dbDelta(
111 "CREATE TABLE {$usage} (
112 id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
113 usage_key CHAR(40) NOT NULL,
114 page_url VARCHAR(500) NOT NULL,
115 page_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
116 family VARCHAR(190) NOT NULL,
117 weight VARCHAR(40) NOT NULL DEFAULT '400',
118 style VARCHAR(20) NOT NULL DEFAULT 'normal',
119 origin VARCHAR(30) NOT NULL DEFAULT 'buffer',
120 rendered TINYINT(1) NOT NULL DEFAULT 0,
121 above_fold TINYINT(1) NOT NULL DEFAULT 0,
122 hits BIGINT UNSIGNED NOT NULL DEFAULT 1,
123 last_seen DATETIME DEFAULT NULL,
124 PRIMARY KEY (id),
125 UNIQUE KEY uq_usage (usage_key),
126 KEY idx_family (family),
127 KEY idx_page (page_id)
128 ) {$charset};"
129 );
130
131 dbDelta(
132 "CREATE TABLE {$dec} (
133 id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
134 route_key CHAR(40) NOT NULL,
135 route VARCHAR(500) NOT NULL,
136 device VARCHAR(10) NOT NULL DEFAULT 'any',
137 preload LONGTEXT DEFAULT NULL,
138 unload LONGTEXT DEFAULT NULL,
139 updated_at DATETIME DEFAULT NULL,
140 PRIMARY KEY (id),
141 UNIQUE KEY uq_route (route_key)
142 ) {$charset};"
143 );
144 }
145
146 /**
147 * Drop all three tables (IF EXISTS).
148 */
149 private function drop_tables(): void {
150 global $wpdb;
151
152 foreach ( array( 'easyfonts_fonts', 'easyfonts_usage', 'easyfonts_decisions' ) as $name ) {
153 $table = $wpdb->prefix . $name;
154 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
155 $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
156 }
157 }
158
159 /**
160 * Are all three tables present with the current signature columns?
161 *
162 * @return bool
163 */
164 public function tables_ok(): bool {
165 return $this->column_exists( 'easyfonts_fonts', 'variant_key' )
166 && $this->column_exists( 'easyfonts_fonts', 'is_enabled' )
167 && $this->column_exists( 'easyfonts_fonts', 'load_user_set' )
168 && $this->column_exists( 'easyfonts_fonts', 'preload_user_set' )
169 && $this->column_exists( 'easyfonts_usage', 'usage_key' )
170 && $this->column_exists( 'easyfonts_decisions', 'route_key' );
171 }
172
173 /**
174 * Is a legacy fonts table present? Detected by an old column that
175 * the current schema never uses, alongside the absence of its signature column.
176 *
177 * @return bool
178 */
179 public function has_legacy_schema(): bool {
180 if ( ! $this->table_exists( 'easyfonts_fonts' ) ) {
181 return false;
182 }
183
184 $has_v3 = $this->column_exists( 'easyfonts_fonts', 'variant_key' );
185 $has_legacy = $this->column_exists( 'easyfonts_fonts', 'local_filename' )
186 || $this->column_exists( 'easyfonts_fonts', 'is_active' )
187 || $this->column_exists( 'easyfonts_fonts', 'downloaded_at' );
188
189 return $has_legacy && ! $has_v3;
190 }
191
192 /**
193 * Per-table status for diagnostics.
194 *
195 * @return array<string,array{exists:bool,schema_ok:bool,rows:int}>
196 */
197 public function status(): array {
198 global $wpdb;
199
200 $map = array(
201 'fonts' => array( 'easyfonts_fonts', 'variant_key' ),
202 'usage' => array( 'easyfonts_usage', 'usage_key' ),
203 'decisions' => array( 'easyfonts_decisions', 'route_key' ),
204 );
205
206 $out = array();
207
208 foreach ( $map as $label => [$name, $signature] ) {
209 $exists = $this->table_exists( $name );
210 $rows = 0;
211
212 if ( $exists ) {
213 $table = $wpdb->prefix . $name;
214 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
215 $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
216 }
217
218 $out[ $label ] = array(
219 'exists' => $exists,
220 'schema_ok' => $exists && $this->column_exists( $name, $signature ),
221 'rows' => $rows,
222 );
223 }
224
225 return $out;
226 }
227
228 /**
229 * Does a table exist?
230 *
231 * @param string $name Unprefixed table name.
232 * @return bool
233 */
234 private function table_exists( string $name ): bool {
235 global $wpdb;
236
237 $table = $wpdb->prefix . $name;
238
239 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
240 $found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
241
242 return $found === $table;
243 }
244
245 /**
246 * Does a column exist on a table?
247 *
248 * @param string $name Unprefixed table name.
249 * @param string $column Column name.
250 * @return bool
251 */
252 private function column_exists( string $name, string $column ): bool {
253 global $wpdb;
254
255 if ( ! $this->table_exists( $name ) ) {
256 return false;
257 }
258
259 $table = $wpdb->prefix . $name;
260
261 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
262 $found = $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM `{$table}` LIKE %s", $column ) );
263
264 return ! empty( $found );
265 }
266
267 /**
268 * Default settings. Detectors are managed by Auto-Config, not exposed as toggles.
269 *
270 * @return array<string,mixed>
271 */
272 public static function default_settings(): array {
273 return array(
274 'enabled' => 1,
275 'auto_config' => 1,
276 'font_display' => 'swap',
277 'strip_hints' => 1,
278 'smart_preload' => 1,
279 'metric_fallbacks' => 1,
280 'beacon' => 1,
281 'inline_css' => 0,
282 'async_blocker' => 0,
283 'subsets' => array( 'latin', 'latin-ext' ),
284 'detectors' => array(),
285 'excluded_urls' => array(),
286 'cdn_url' => '',
287 'remove_data_on_uninstall' => 0,
288 );
289 }
290 }
291