| @@ -530,8 +530,95 @@ | ||
| 530 | 530 | delete_transient( 'wdkit_registered_widgets_' . $widget_type ); |
| 531 | 531 | } |
| 532 | 532 | } |
| 533 | 533 | |
| 534 | +if ( ! function_exists( 'wdesignkit_widget_registry_option_prefix' ) ) { | |
| 535 | + /** | |
| 536 | + * Shared prefix of every versioned widget-registry cache option. | |
| 537 | + * | |
| 538 | + * One constant-ish source of truth for the three places that need to match these rows by | |
| 539 | + * pattern rather than by exact name: the orphan sweep below, and uninstall.php. Keep it in | |
| 540 | + * step with wdesignkit_widget_registry_cache_key(), which builds the full name. | |
| 541 | + * | |
| 542 | + * @since 2.6.5 | |
| 543 | + * | |
| 544 | + * @return string | |
| 545 | + */ | |
| 546 | + function wdesignkit_widget_registry_option_prefix() { | |
| 547 | + return 'wdkit_widget_registry_'; | |
| 548 | + } | |
| 549 | +} | |
| 550 | + | |
| 551 | +if ( ! function_exists( 'wdesignkit_purge_stale_widget_registries' ) ) { | |
| 552 | + /** | |
| 553 | + * Deletes widget-registry cache options left behind by OTHER plugin versions. | |
| 554 | + * | |
| 555 | + * wdesignkit_widget_registry_cache_key() embeds WDKIT_VERSION in the option name, which is | |
| 556 | + * deliberate — it is what makes an update or a rollback orphan the old entry so the registry | |
| 557 | + * rebuilds itself instead of trusting a stale cache (ClickUp 86d41cd1z). What was missing is | |
| 558 | + * the other half: nothing ever deleted the orphan. There was no cleanup path anywhere in the | |
| 559 | + * codebase, so every release left one row per builder in wp_options permanently. Observed on | |
| 560 | + * a site upgraded twice — twelve rows across three versions, the largest 12 KB: | |
| 561 | + * | |
| 562 | + * wdkit_widget_registry_elementor_2_6_3 wdkit_widget_registry_gutenberg_2_6_3 | |
| 563 | + * wdkit_widget_registry_elementor_2_6_4 wdkit_widget_registry_gutenberg_2_6_4 … | |
| 564 | + * | |
| 565 | + * These are written with autoload = false, so there is no per-request cost and this is | |
| 566 | + * housekeeping rather than a performance fix — but it grows without bound, and a row nothing | |
| 567 | + * will ever read again is exactly what an uninstall is expected to leave clean. | |
| 568 | + * | |
| 569 | + * Matches by prefix and keeps only the CURRENT version's keys, so it also collects rows from | |
| 570 | + * a build whose version this code cannot know about (a rollback, or a downgrade). The LIKE | |
| 571 | + * pattern is escaped with $wpdb->esc_like(); the option names are plugin-generated and | |
| 572 | + * contain no wildcards today, but the escape is what keeps that true if the prefix changes. | |
| 573 | + * | |
| 574 | + * @since 2.6.5 | |
| 575 | + * | |
| 576 | + * @return int Number of orphaned options deleted. | |
| 577 | + */ | |
| 578 | + function wdesignkit_purge_stale_widget_registries() { | |
| 579 | + global $wpdb; | |
| 580 | + | |
| 581 | + $prefix = wdesignkit_widget_registry_option_prefix(); | |
| 582 | + $pattern = $wpdb->esc_like( $prefix ) . '%'; | |
| 583 | + | |
| 584 | + $names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- option names are only discoverable by pattern; no caching API covers a LIKE sweep. | |
| 585 | + $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $pattern ) | |
| 586 | + ); | |
| 587 | + | |
| 588 | + if ( empty( $names ) ) { | |
| 589 | + return 0; | |
| 590 | + } | |
| 591 | + | |
| 592 | + // Build the set of names that are still live, rather than parsing the version out of each | |
| 593 | + // row: the builder slug can itself contain underscores (gutenberg_core), so splitting on | |
| 594 | + // '_' to find the version suffix is ambiguous. Comparing against what the current version | |
| 595 | + // WOULD be named has no such ambiguity. | |
| 596 | + $keep = array(); | |
| 597 | + foreach ( array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ) as $builder ) { | |
| 598 | + $key = wdesignkit_widget_registry_cache_key( $builder ); | |
| 599 | + if ( '' !== $key ) { | |
| 600 | + $keep[ $key ] = true; | |
| 601 | + } | |
| 602 | + } | |
| 603 | + | |
| 604 | + $deleted = 0; | |
| 605 | + foreach ( $names as $name ) { | |
| 606 | + if ( isset( $keep[ $name ] ) ) { | |
| 607 | + continue; | |
| 608 | + } | |
| 609 | + | |
| 610 | + // delete_option() rather than a bulk DELETE: it clears the option cache and fires the | |
| 611 | + // documented hooks, which a raw query would silently skip. | |
| 612 | + if ( delete_option( $name ) ) { | |
| 613 | + ++$deleted; | |
| 614 | + } | |
| 615 | + } | |
| 616 | + | |
| 617 | + return $deleted; | |
| 618 | + } | |
| 619 | +} | |
| 620 | + | |
| 534 | 621 | if ( ! function_exists( 'wdesignkit_widget_folder_name' ) ) { |
| 535 | 622 | /** |
| 536 | 623 | * Canonical widget folder name: "<Title-With-Hyphens>_<widget_id>". |
| 537 | 624 | * |