PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.4
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.4
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
← All changes | src/Fonts/Registry.php +117 -69 2.0.0 → 2.0.4 View file →
@@ -115,8 +115,27 @@
115 115 $row['created_at'] = current_time( 'mysql' );
116 116 $formats[] = '%s';
117 117 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
118 118 $wpdb->insert( $table, $row, $formats );
119 +
120 + // Duplicate key: a concurrent request inserted the same variant
121 + // between our SELECT and INSERT. Fall back to an update so the
122 + // write still lands instead of surfacing a spurious error.
123 + if ( ! empty( $wpdb->last_error ) && false !== stripos( $wpdb->last_error, 'duplicate' ) ) {
124 + unset( $row['created_at'] );
125 + array_pop( $formats );
126 +
127 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery
128 + $winner = $wpdb->get_var(
129 + $wpdb->prepare( "SELECT id FROM {$table} WHERE variant_key = %s", $key )
130 + );
131 +
132 + if ( $winner ) {
133 + $wpdb->last_error = '';
134 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery
135 + $wpdb->update( $table, $row, array( 'id' => (int) $winner ), $formats, array( '%d' ) );
136 + }
137 + }
119 138 }
120 139
121 140 if ( ! empty( $wpdb->last_error ) ) {
122 141 self::$last_error = $wpdb->last_error;
@@ -142,12 +161,58 @@
142 161 return $keys ? array_fill_keys( $keys, true ) : array();
143 162 }
144 163
145 164 /**
146 - * Distinct family/weight/style targets the user marked for preloading
147 - * (and which are still enabled), each resolved to a font file. Deduped:
148 - * one entry per family+weight+style, preferring the latin subset file.
165 + * Batch-fetch the currently-stored font file for a set of variant keys, so
166 + * the consolidator can register a page's variants with a single SELECT
167 + * instead of one per variant. Returns a map of variant_key => font_file
168 + * (only for keys that exist).
149 169 *
170 + * @param string[] $keys Variant keys.
171 + * @return array<string,string>
172 + */
173 + public function existing_map( array $keys ): array {
174 + global $wpdb;
175 +
176 + $keys = array_values( array_unique( array_filter( $keys, 'strlen' ) ) );
177 +
178 + if ( empty( $keys ) ) {
179 + return array();
180 + }
181 +
182 + $placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) );
183 +
184 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
185 + $rows = $wpdb->get_results(
186 + $wpdb->prepare(
187 + "SELECT variant_key, font_file FROM {$this->table()} WHERE variant_key IN ( {$placeholders} )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
188 + ...$keys
189 + ),
190 + ARRAY_A
191 + );
192 +
193 + $out = array();
194 +
195 + if ( $rows ) {
196 + foreach ( $rows as $r ) {
197 + $out[ $r['variant_key'] ] = (string) $r['font_file'];
198 + }
199 + }
200 +
201 + return $out;
202 + }
203 +
204 + /**
205 + * User-FORCED preload targets: variants the user explicitly toggled to
206 + * "always preload" (is_preloaded = 1 AND preload_user_set = 1) and that are
207 + * still enabled, each resolved to a font file. These are intentional global
208 + * overrides — they apply on every page that actually uses the family (the
209 + * caller intersects with the fonts present on the current page). Automatic,
210 + * above-the-fold preloads are NOT sourced here; they come from the per-route
211 + * beacon decision (see UsageTracker::get_decision / SmartPreload).
212 + *
213 + * Deduped: one entry per family+weight+style, preferring the latin subset.
214 + *
150 215 * @return array<int,array{family:string,weight:string,style:string,file:string}>
151 216 */
152 217 public function preload_targets(): array {
153 218 global $wpdb;
@@ -155,9 +220,9 @@
155 220 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
156 221 $rows = $wpdb->get_results(
157 222 "SELECT family, weight, style, font_file, subset
158 223 FROM {$this->table()}
159 - WHERE is_preloaded = 1 AND is_enabled = 1 AND font_file <> ''
224 + WHERE is_preloaded = 1 AND preload_user_set = 1 AND is_enabled = 1 AND font_file <> ''
160 225 ORDER BY ( subset = 'latin' ) DESC",
161 226 ARRAY_A
162 227 );
163 228
@@ -193,73 +258,8 @@
193 258 * @param string $field 'is_enabled' or 'is_preloaded'.
194 259 * @param bool $value New value.
195 260 * @return void
196 261 */
197 - /**
198 - * Auto-enable preload for a variant, but only if the user hasn't set their
199 - * own preload choice for it (preload_user_set = 0). Does NOT mark user_set,
200 - * so a later user toggle still wins. Returns true if a row was actually
201 - * flipped on (so the caller can count it against the per-page cap).
202 - *
203 - * @param string $family Family.
204 - * @param string $weight Weight.
205 - * @param string $style Style.
206 - * @return bool
207 - */
208 - public function auto_preload( string $family, string $weight, string $style ): bool {
209 - global $wpdb;
210 - $table = $this->table();
211 -
212 - // 1. Exact match (family + weight + style), only if not user-set.
213 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
214 - $updated = (int) $wpdb->query(
215 - $wpdb->prepare(
216 - "UPDATE {$table} SET is_preloaded = 1
217 - WHERE family = %s AND weight = %s AND style = %s
218 - AND preload_user_set = 0 AND is_preloaded = 0 AND is_enabled = 1",
219 - $family,
220 - $weight,
221 - $style
222 - )
223 - );
224 -
225 - if ( $updated > 0 ) {
226 - return true;
227 - }
228 -
229 - // 2. The browser's computed weight/style often differs from the hosted
230 - // file (e.g. a heading renders at 700 but only 400 is hosted, or a
231 - // variable font resolves oddly). So if no exact row matched, preload
232 - // the family's single best enabled, not-yet-preloaded, not-user-set
233 - // variant — preferring the requested style, then a 400/normal-ish
234 - // weight. This ensures an above-the-fold family still gets preloaded.
235 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
236 - $id = $wpdb->get_var(
237 - $wpdb->prepare(
238 - "SELECT id FROM {$table}
239 - WHERE family = %s
240 - AND preload_user_set = 0 AND is_preloaded = 0 AND is_enabled = 1
241 - ORDER BY ( style = %s ) DESC,
242 - ABS( CAST( NULLIF(weight,'') AS UNSIGNED ) - 400 ) ASC,
243 - id ASC
244 - LIMIT 1",
245 - $family,
246 - $style
247 - )
248 - );
249 -
250 - if ( ! $id ) {
251 - return false;
252 - }
253 -
254 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
255 - $wpdb->query(
256 - $wpdb->prepare( "UPDATE {$table} SET is_preloaded = 1 WHERE id = %d", (int) $id )
257 - );
258 -
259 - return true;
260 - }
261 -
262 262 public function set_flag_all( string $field, bool $value ): void {
263 263 global $wpdb;
264 264
265 265 if ( ! in_array( $field, array( 'is_enabled', 'is_preloaded' ), true ) ) {
@@ -512,8 +512,56 @@
512 512 LIMIT 1",
513 513 $family,
514 514 $weight,
515 515 $style
516 + )
517 + );
518 +
519 + return $file ? (string) $file : null;
520 + }
521 +
522 + /**
523 + * Best hosted file to PRELOAD for a family, tolerant of weight/style drift.
524 + * The beacon reports the above-the-fold face using the element's COMPUTED
525 + * weight/style, which often differs from the hosted variant (e.g. a
526 + * single-weight family rendered bold reports weight 700, but only 400 is
527 + * hosted). An exact lookup would then return nothing and the font would
528 + * never preload. So we prefer an exact match, then the same style, then the
529 + * page's subset (2.0.4; e.g. cyrillic on a Russian site), then latin, then
530 + * the closest available weight — always returning a real file when the
531 + * (enabled) family is hosted at all.
532 + *
533 + * @param string $family Family.
534 + * @param string $weight Desired weight.
535 + * @param string $style Desired style.
536 + * @param string $subset Preferred subset.
537 + * @return string|null
538 + */
539 + public function resolve_preload_file( string $family, string $weight, string $style, string $subset = 'latin' ): ?string {
540 + global $wpdb;
541 +
542 + $target = (int) preg_replace( '/\D+/', '', $weight );
543 + $target = $target > 0 ? $target : 400;
544 +
545 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery
546 + $file = $wpdb->get_var(
547 + $wpdb->prepare(
548 + "SELECT font_file FROM {$this->table()}
549 + WHERE family = %s AND is_enabled = 1 AND font_file <> ''
550 + ORDER BY
551 + ( weight = %s AND style = %s ) DESC,
552 + ( style = %s ) DESC,
553 + ( subset = %s ) DESC,
554 + ( subset = 'latin' ) DESC,
555 + ABS( CAST( COALESCE( NULLIF(weight,''), '400' ) AS SIGNED ) - %d ) ASC,
556 + id ASC
557 + LIMIT 1",
558 + $family,
559 + $weight,
560 + $style,
561 + $style,
562 + $subset,
563 + $target
516 564 )
517 565 );
518 566
519 567 return $file ? (string) $file : null;