| @@ -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; |