Better Search Replace (v1.4.10) — Architecture Summary
Overview
Better Search Replace is a pure admin-utility WordPress plugin that performs serialization-aware search/replace operations across all database tables. It has zero public-facing surface: no shortcodes, no Gutenberg blocks, no REST routes, and no frontend assets. All functionality is gated behind the WordPress admin panel under Tools → Better Search Replace.
Auth Model
The plugin uses a two-layer defense-in-depth auth model:
Bootstrap gate (
bsr_enabled_for_user()onafter_setup_theme): The entire plugin — including all hook registrations — is conditionally instantiated only when the current user passescurrent_user_can(apply_filters('bsr_capability', 'manage_options')). This means unauthenticated or lower-privileged users never see any registered hooks.Per-handler gate (
BSR_Utils::check_admin_referer()): Every write/destructive handler additionally verifies both a WordPress nonce (check_admin_referer()) and re-checks the capability (bsr_enabled_for_user()). Exception:BSR_Admin::load_details()(admin_post_bsr_view_details) has no explicit nonce or capability check — it relies solely on WordPress core'sadmin-post.phplogged-in requirement.
The bsr_capability filter is the single most important extension point and risk: any plugin can hook it to lower the required capability below manage_options, exposing full database write access to lower-privileged roles.
Custom AJAX Transport
The plugin does not use WordPress's standard wp-admin/admin-ajax.php. Instead it hooks init (priority 1/2) and dispatches on $_GET['bsr-ajax'], sanitized via sanitize_text_field(). The resolved URL is <admin_url>/tools.php?page=better-search-replace&bsr-ajax=<action>. This fires on every WordPress page load (including frontend) when the parameter is present, though the auth gate prevents exploitation.
Batch Processing Architecture
The core operation is a client-driven pagination loop: JavaScript POSTs to the custom AJAX endpoint with incrementing bsr_step (table index) and bsr_page (row-block offset) until the server responds with step='done'. Batch state is persisted in wp_options['bsr_data'] between calls. The siteurl option is specially deferred to avoid mid-run URL breakage.
Database Access
All DB access is via $wpdb. Table names are validated via table_exists() (SHOW TABLES whitelist) and esc_sql(). UPDATE queries are constructed by hand using a custom mysql_escape_mimic() function rather than $wpdb->prepare(). Column names come from DESCRIBE output (schema-controlled). The search/replace strings flow through PHP str_replace/str_ireplace and are never directly interpolated into SQL.
Serialization Safety
Deserialization is centralized in BSR_DB::unserialize() which enforces allowed_classes=false (PHP 7+) or a namespaced polyfill (BSR\Brumann\Polyfill\Unserialize) for PHP < 7. This prevents PHP object injection attacks.
Persistence
Three wp_options keys: bsr_page_size (config), bsr_data (run state including raw search/replace strings), bsr_update_site_url (deferred siteurl). One transient: bsr_results (24h TTL, holds per-table stats plus verbatim search/replace strings). No uninstall hook — all options persist after plugin removal.
No External Dependencies
Zero outbound HTTP calls at runtime. No AI integrations. No telemetry. All hardcoded external URLs are static HTML anchor tags only. Updates flow through WordPress.org infrastructure.
Multisite
Declared Network: true. bsr_enabled_for_user() uses current_user_can() which evaluates against the current blog's capabilities — a sub-site admin with manage_options gains full search/replace access. No is_super_admin() floor is enforced.
