This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,421 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The dashboard-specific functionality of the plugin.
*
* Registers styles and scripts, adds the custom administration page,
* and processes user input on the "search/replace" form.
*
* @link https://bettersearchreplace.com
* @since 1.0.0
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class BSR_Admin {
/**
* The ID of this plugin.
*
* @since 1.0.0
* @access private
* @var string $better_search_replace The ID of this plugin.
*/
private $better_search_replace;
/**
* The version of this plugin.
*
* @since 1.0.0
* @access private
* @var string $version The current version of this plugin.
*/
private $version;
/**
* Initialize the class and set its properties.
*
* @since 1.0.0
* @var string $better_search_replace The name of this plugin.
* @var string $version The version of this plugin.
*/
public function __construct( $better_search_replace, $version ) {
$this->better_search_replace = $better_search_replace;
$this->version = $version;
}
/**
* Register any CSS and JS used by the plugin.
* @since 1.0.0
* @access public
* @param string $hook Used for determining which page(s) to load our scripts.
*/
public function enqueue_scripts( $hook ) {
if ( 'tools_page_better-search-replace' === $hook ) {
$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_style( 'better-search-replace', BSR_URL . "assets/css/better-search-replace$min.css", array(), $this->version, 'all' );
wp_enqueue_style( 'jquery-style', BSR_URL . 'assets/css/jquery-ui.min.css', array(), $this->version, 'all' );
wp_enqueue_script( 'jquery-ui-slider' );
wp_enqueue_script( 'better-search-replace', BSR_URL . "assets/js/better-search-replace$min.js", array( 'jquery' ), $this->version, true );
wp_enqueue_style( 'thickbox' );
wp_enqueue_script( 'thickbox' );
wp_localize_script( 'better-search-replace', 'bsr_object_vars', array(
'page_size' => get_option( 'bsr_page_size' ) ? absint( get_option( 'bsr_page_size' ) ) : 20000,
'endpoint' => BSR_AJAX::get_endpoint(),
'ajax_nonce' => wp_create_nonce( 'bsr_ajax_nonce' ),
'no_search' => __( 'No search string was defined, please enter a URL or string to search for.', 'better-search-replace' ),
'no_tables' => __( 'Please select the tables that you want to update.', 'better-search-replace' ),
'unknown' => __( 'An error occurred processing your request. Try decreasing the "Max Page Size", or contact support.', 'better-search-replace' ),
'processing' => __( 'Processing...', 'better-search-replace' )
) );
}
}
/**
* Register any menu pages used by the plugin.
* @since 1.0.0
* @access public
*/
public function bsr_menu_pages() {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy filter name; public API for capability override.
$cap = apply_filters( 'bsr_capability', 'manage_options' );
add_submenu_page( 'tools.php', __( 'Better Search Replace', 'better-search-replace' ), __( 'Better Search Replace', 'better-search-replace' ), $cap, 'better-search-replace', array( $this, 'bsr_menu_pages_callback' ) );
}
/**
* The callback for creating a new submenu page under the "Tools" menu.
* @access public
*/
public function bsr_menu_pages_callback() {
require_once BSR_PATH . 'includes/class-bsr-templates-helper.php';
require_once BSR_PATH . 'templates/bsr-dashboard.php';
}
/**
* Renders the result or error onto the better-search-replace admin page.
*/
public static function render_result() {
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Each GET branch calls BSR_Utils::validate_tools_screen_get() before use.
if ( ! filter_has_var( INPUT_GET, 'result' ) || ! BSR_Utils::validate_tools_screen_get() ) {
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
return;
}
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
$result = get_transient( 'bsr_results' );
// Have results with required fields set with correctly typed data?
if (
empty( $result ) ||
! isset( $result['tables'] ) ||
! is_int( $result['tables'] ) ||
! isset( $result['change'] ) ||
! is_int( $result['change'] ) ||
! isset( $result['updates'] ) ||
! is_int( $result['updates'] )
) {
return;
}
// TB_iframe is required for Thickbox iframe mode. Core thickbox truncates src at "TB_"; bsr_thickbox_fix() in assets/js/better-search-replace.js restores the full URL for action=bsr_view_details only.
$details_url = self::get_view_details_url(
array(
'TB_iframe' => 'true',
'width' => '800',
'height' => '500',
)
);
$result_message_allowed_html = array(
'p' => array(),
'strong' => array(),
'a' => array(
'href' => true,
'class' => true,
'title' => true,
),
);
echo '<div class="updated bsr-updated" style="display: none;">';
if ( isset( $result['dry_run'] ) && $result['dry_run'] === 'on' ) {
$msg = sprintf(
/* translators: 1: number of tables, 2: cells found, 3: changes made, 4: details URL, 5: Thickbox title attribute. */
__(
'<p><strong>DRY RUN:</strong> <strong>%1$d</strong> tables were searched, <strong>%2$d</strong> cells were found that need to be updated, and <strong>%3$d</strong> changes were made.</p><p><a href="%4$s" class="thickbox" title="%5$s">Click here</a> for more details, or use the form below to run the search/replace.</p>',
'better-search-replace'
),
(int) $result['tables'],
(int) $result['change'],
(int) $result['updates'],
esc_url( $details_url ),
esc_attr__( 'Dry Run Details', 'better-search-replace' )
);
echo wp_kses( $msg, $result_message_allowed_html );
} else {
$msg = sprintf(
/* translators: 1: tables searched, 2: cells changed, 3: updates, 4: details URL, 5: Thickbox title attribute. */
__(
'<p>During the search/replace, <strong>%1$d</strong> tables were searched, with <strong>%2$d</strong> cells changed in <strong>%3$d</strong> updates.</p><p><a href="%4$s" class="thickbox" title="%5$s">Click here</a> for more details.</p>',
'better-search-replace'
),
(int) $result['tables'],
(int) $result['change'],
(int) $result['updates'],
esc_url( $details_url ),
esc_attr__( 'Search/Replace Details', 'better-search-replace' )
);
echo wp_kses( $msg, $result_message_allowed_html );
}
echo '</div>';
}
/**
* Prefills the given value on the search/replace page (dry run, live run, from profile).
* @access public
* @param string $value The value to check for.
* @param string $type The type of the value we're filling.
*/
public static function prefill_value( $value, $type = 'text' ) {
if ( ! BSR_Utils::validate_tools_screen_get() ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Tools GET gated by BSR_Utils::validate_tools_screen_get().
if ( filter_has_var( INPUT_GET, 'result' ) && get_transient( 'bsr_results' ) ) {
$values = get_transient( 'bsr_results' );
} else {
$values = array();
}
// Prefill the value.
if ( isset( $values[ $value ] ) ) {
if ( 'checkbox' === $type && 'on' === $values[ $value ] ) {
echo 'checked';
} else {
echo esc_attr( str_replace( '#BSR_BACKSLASH#', '\\', $values[ $value ] ) );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
}
/**
* Loads the tables available to run a search replace, prefilling if already
* selected the tables.
* @access public
*/
public static function load_tables() {
if ( ! BSR_Utils::validate_tools_screen_get() ) {
echo '<select id="bsr-table-select" name="select_tables[]" multiple="multiple" style=""></select>';
return;
}
$tables = BSR_DB::get_tables();
$sizes = BSR_DB::get_sizes();
echo '<select id="bsr-table-select" name="select_tables[]" multiple="multiple" style="">';
$result = null;
$transient_result = get_transient( 'bsr_results' );
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Tools GET gated by BSR_Utils::validate_tools_screen_get().
if ( filter_has_var( INPUT_GET, 'result' ) && $transient_result ) {
$result = $transient_result;
}
foreach ( $tables as $table ) {
$table_size = isset( $sizes[ $table ] ) ? $sizes[ $table ] : '';
$selected = false;
if ( is_array( $result ) && isset( $result['table_reports'][ $table ] ) ) {
$selected = true;
}
printf(
'<option value="%1$s"%2$s>%3$s</option>',
esc_attr( $table ),
$selected ? ' selected="selected"' : '',
esc_html( $table . ' ' . $table_size )
);
}
echo '</select>';
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
}
/**
* admin-post.php URL for bsr_view_details with nonce.
*
* For Thickbox iframe links you may pass TB_iframe=true (and width/height). Core thickbox.js
* truncates iframe src at the substring "TB_"; bsr_thickbox_fix() in assets/js/better-search-replace.js
* restores the full URL when action=bsr_view_details. Do not add other query keys whose names
* contain "TB_".
*
* @param array $extra Query arguments (action is set automatically; _wpnonce is always appended last).
* @return string Raw URL; use esc_url() when printing in HTML.
*/
public static function get_view_details_url( $extra = array() ) {
$args = array_merge( array( 'action' => 'bsr_view_details' ), (array) $extra );
$url = add_query_arg( $args, admin_url( 'admin-post.php' ) );
return add_query_arg( '_wpnonce', wp_create_nonce( 'bsr_view_details' ), $url );
}
/**
* Loads the result details (via Thickbox).
* @access public
*/
public function load_details() {
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce verified below; Thickbox GET must not rely on referer.
if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_REQUEST['_wpnonce'] ) ), 'bsr_view_details' ) ) {
wp_die( esc_html__( 'Security check failed.', 'better-search-replace' ), '', array( 'response' => 403 ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
if ( ! bsr_enabled_for_user() ) {
wp_die( esc_html__( 'Sorry, you are not allowed to view this content.', 'better-search-replace' ), '', array( 'response' => 403 ) );
}
if ( ! get_transient( 'bsr_results' ) ) {
return;
}
$results = get_transient( 'bsr_results' );
$min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_style( 'common' );
wp_enqueue_style(
'better-search-replace',
BSR_URL . 'assets/css/better-search-replace' . $min . '.css',
array( 'common' ),
BSR_VERSION,
'all'
);
wp_print_styles( array( 'common', 'better-search-replace' ) );
$upgrade_url = 'https://deliciousbrains.com/better-search-replace/upgrade/?utm_source=insideplugin&utm_medium=web&utm_content=tooltip&utm_campaign=bsr-to-migrate';
?>
<div style="padding: 32px; background-color: var(--color-white); min-height: 100%;">
<table id="bsr-results-table" class="widefat">
<thead>
<tr>
<th class="bsr-first"><?php esc_html_e( 'Table', 'better-search-replace' ); ?></th>
<th class="bsr-second"><?php esc_html_e( 'Changes Found', 'better-search-replace' ); ?></th>
<th class="bsr-third"><?php esc_html_e( 'Rows Updated', 'better-search-replace' ); ?></th>
<th class="bsr-fourth"><?php esc_html_e( 'Time', 'better-search-replace' ); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $results['table_reports'] as $table_name => $report ) {
$time = $report['end'] - $report['start'];
$change_cell = '';
if ( 0 !== (int) $report['change'] ) {
$change_cell = '<a class="tooltip">' . esc_html( (string) $report['change'] ) . '</a>';
$change_cell .= '<span class="helper-message right">';
$change_cell .= wp_kses_post(
sprintf(
/* translators: %s: URL to the upgrade page. */
__( '<a href="%s" target="_blank">UPGRADE</a> to view details on the exact changes that will be made.', 'better-search-replace' ),
esc_url( $upgrade_url )
)
);
$change_cell .= '</span>';
}
$updates_cell = '0';
if ( 0 !== (int) $report['updates'] ) {
$updates_cell = '<strong>' . esc_html( (string) $report['updates'] ) . '</strong>';
}
printf(
'<tr><td class="bsr-first">%1$s</td><td class="bsr-second">%2$s</td><td class="bsr-third">%3$s</td><td class="bsr-fourth">%4$s%5$s</td></tr>',
esc_html( $table_name ),
wp_kses_post( $change_cell ),
wp_kses_post( $updates_cell ),
esc_html( (string) round( $time, 3 ) ),
esc_html__( ' seconds', 'better-search-replace' )
);
}
?>
</tbody>
</table>
</div>
<?php
}
/**
* Registers our settings in the options table.
* @access public
*/
public function register_option() {
register_setting( 'bsr_settings_fields', 'bsr_page_size', 'absint' );
}
/**
* Downloads the system info file for support.
* @access public
*/
public function download_sysinfo() {
if ( ! BSR_Utils::check_admin_referer( 'bsr_download_sysinfo', 'bsr_sysinfo_nonce', false ) ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce verified via BSR_Utils::check_admin_referer() above; PHPCS does not recognize the wrapper.
if ( ! isset( $_POST['bsr-sysinfo'] ) ) {
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
return;
}
nocache_headers();
header( 'Content-Type: text/plain' );
header( 'Content-Disposition: attachment; filename="bsr-system-info.txt"' );
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified via BSR_Utils::check_admin_referer(); isset() above; plain-text system info for download; tags stripped below.
$sysinfo = wp_unslash( (string) $_POST['bsr-sysinfo'] );
echo esc_html( wp_strip_all_tags( $sysinfo ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
die();
}
/**
* Displays the link to upgrade to BSR Pro
* @access public
* @param array $links The links assigned to the plugin.
*/
public function meta_upgrade_link( $links, $file ) {
$plugin = plugin_basename( BSR_FILE );
if ( $file == $plugin ) {
return array_merge(
$links,
array(
'<a href="' . esc_url( 'https://bettersearchreplace.com/?utm_source=insideplugin&utm_medium=web&utm_content=plugins-page&utm_campaign=pro-upsell' ) . '">' . esc_html__( 'Upgrade to Pro', 'better-search-replace' ) . '</a>',
)
);
}
return $links;
}
}
@@ -0,0 +1,290 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* AJAX-specific functionality for the plugin.
*
* @link https://bettersearchreplace.com
* @since 1.2
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class BSR_AJAX {
/**
* Sanitized `bsr-ajax` action when `is_authenticated_bsr_ajax_request()` passes.
*
* @var string
*/
private $authenticated_ajax_action = '';
/**
* Initiate our custom ajax handlers.
* @access public
*/
public function init() {
add_action( 'init', array( $this, 'define_ajax' ), 1 );
add_action( 'init', array( $this, 'do_bsr_ajax' ), 2 );
$this->add_ajax_actions();
}
/**
* Gets our custom endpoint.
* @access public
* @return string
*/
public static function get_endpoint() {
return esc_url_raw(
add_query_arg(
'page',
'better-search-replace',
admin_url( 'tools.php' )
)
);
}
/**
* Custom admin AJAX requests must include a valid nonce and capability.
*
* @return bool
*/
private function is_authenticated_bsr_ajax_request() {
$this->authenticated_ajax_action = '';
if ( ! isset( $_REQUEST['bsr_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_REQUEST['bsr_ajax_nonce'] ) ), 'bsr_ajax_nonce' ) ) {
return false;
}
if ( ! isset( $_REQUEST['bsr-ajax'] ) ) {
return false;
}
$ajax_action = sanitize_text_field( wp_unslash( (string) $_REQUEST['bsr-ajax'] ) );
if ( empty( $ajax_action ) ) {
return false;
}
if ( ! bsr_enabled_for_user() ) {
return false;
}
$this->authenticated_ajax_action = $ajax_action;
return true;
}
/**
* Set BSR AJAX constant and headers.
* @access public
*/
public function define_ajax() {
if ( ! $this->is_authenticated_bsr_ajax_request() ) {
return;
}
if ( ! defined( 'DOING_AJAX' ) ) {
define( 'DOING_AJAX', true );
}
if ( ! WP_DEBUG || ( WP_DEBUG && ! WP_DEBUG_DISPLAY ) ) {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
@ini_set( 'display_errors', 0 );
}
send_origin_headers();
@header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
@header( 'X-Robots-Tag: noindex' );
send_nosniff_header();
nocache_headers();
}
/**
* Check if we're doing AJAX and fire the related action.
* @access public
*/
public function do_bsr_ajax() {
global $wp_query;
if ( ! $this->is_authenticated_bsr_ajax_request() ) {
return;
}
$wp_query->set( 'bsr-ajax', $this->authenticated_ajax_action );
if ( $action = $wp_query->get( 'bsr-ajax' ) ) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Dynamic bsr_ajax_{$action} hooks; literal prefix bsr_ajax_.
do_action( 'bsr_ajax_' . sanitize_text_field( $action ) );
die();
}
}
/**
* Adds any AJAX-related actions.
* @access public
*/
public function add_ajax_actions() {
$actions = array(
'process_search_replace',
);
foreach ( $actions as $action ) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Action name built from allowlisted values; prefix bsr_ajax_.
add_action( 'bsr_ajax_' . $action, array( $this, $action ) );
}
}
/**
* Processes the search/replace form submitted by the user.
* @access public
*/
public function process_search_replace() {
if ( ! BSR_Utils::check_admin_referer( 'bsr_ajax_nonce', 'bsr_ajax_nonce', false ) ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce and capability verified via BSR_Utils::check_admin_referer(); PHPCS does not recognize the wrapper.
$db = new BSR_DB();
$step = isset( $_REQUEST['bsr_step'] ) ? absint( $_REQUEST['bsr_step'] ) : 0;
$page = isset( $_REQUEST['bsr_page'] ) ? absint( $_REQUEST['bsr_page'] ) : 0;
if ( 0 === $step && 0 === $page ) {
$args = array();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- bsr_data is URL-encoded form body; parsed after nonce; fields validated when applied.
$bsr_data_raw = isset( $_REQUEST['bsr_data'] ) ? wp_unslash( (string) $_REQUEST['bsr_data'] ) : '';
parse_str( $bsr_data_raw, $args );
if ( ! isset( $args['select_tables'] ) || ! is_array( $args['select_tables'] ) ) {
$args['select_tables'] = array();
}
$args = array(
'select_tables' => array_map( 'trim', $args['select_tables'] ),
'case_insensitive' => isset( $args['case_insensitive'] ) ? $args['case_insensitive'] : 'off',
'replace_guids' => isset( $args['replace_guids'] ) ? $args['replace_guids'] : 'off',
'dry_run' => isset( $args['dry_run'] ) ? $args['dry_run'] : 'off',
'search_for' => isset( $args['search_for'] ) ? stripslashes( $args['search_for'] ) : '',
'replace_with' => isset( $args['replace_with'] ) ? stripslashes( $args['replace_with'] ) : '',
'completed_pages' => isset( $args['completed_pages'] ) ? absint( $args['completed_pages'] ) : 0,
);
$args['total_pages'] = isset( $args['total_pages'] ) ? absint( $args['total_pages'] ) : $db->get_total_pages( $args['select_tables'] );
delete_transient( 'bsr_results' );
delete_option( 'bsr_data' );
} else {
$args = get_option( 'bsr_data' );
if ( ! is_array( $args ) ) {
$args = array(
'select_tables' => array(),
'total_pages' => 1,
'completed_pages' => 0,
);
}
}
if ( isset( $args['select_tables'][ $step ] ) ) {
$result = $db->srdb( $args['select_tables'][ $step ], $page, $args );
$this->append_report( $args['select_tables'][ $step ], $result['table_report'], $args );
if ( false === $result['table_complete'] ) {
++$page;
} else {
++$step;
$page = 0;
}
if ( isset( $args['select_tables'][ $step ] ) ) {
$msg_tbl = esc_html( $args['select_tables'][ $step ] );
$message = sprintf(
/* translators: 1: current table number, 2: total number of tables, 3: table name. */
__( 'Processing table %1$d of %2$d: %3$s', 'better-search-replace' ),
$step + 1,
count( $args['select_tables'] ),
$msg_tbl
);
}
++$args['completed_pages'];
$percentage = $args['completed_pages'] / $args['total_pages'] * 100 . '%';
} else {
$db->maybe_update_site_url();
$step = 'done';
$percentage = '100%';
}
update_option( 'bsr_data', $args );
$result = array(
'step' => $step,
'page' => $page,
'percentage' => $percentage,
'url' => esc_url_raw( BSR_Utils::tools_page_url( array( 'tab' => 'bsr_search_replace', 'result' => 'true' ) ) ),
'bsr_data' => build_query( $args ),
);
if ( isset( $message ) ) {
$result['message'] = $message;
}
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
wp_send_json( $result );
}
/**
* Helper function for assembling the BSR Results.
* @access public
* @param string $table The name of the table to append to.
* @param array $report The report for that table.
* @param array $args An array of arguements used for this run.
* @return boolean
*/
public function append_report( $table, $report, $args ) {
if ( ! BSR_Utils::check_admin_referer( 'bsr_ajax_nonce', 'bsr_ajax_nonce', false ) ) {
return false;
}
$results = get_transient( 'bsr_results' ) ? get_transient( 'bsr_results' ) : array();
$results['search_for'] = isset( $args['search_for'] ) ? $args['search_for'] : '';
$results['replace_with'] = isset( $args['replace_with'] ) ? $args['replace_with'] : '';
$results['dry_run'] = isset( $args['dry_run'] ) ? $args['dry_run'] : 'off';
$results['case_insensitive'] = isset( $args['case_insensitive'] ) ? $args['case_insensitive'] : 'off';
$results['replace_guids'] = isset( $args['replace_guids'] ) ? $args['replace_guids'] : 'off';
$results['change'] = isset( $results['change'] ) ? $results['change'] + $report['change'] : $report['change'];
$results['updates'] = isset( $results['updates'] ) ? $results['updates'] + $report['updates'] : $report['updates'];
if ( isset( $results['table_reports'] ) && isset( $results['table_reports'][ $table ] ) ) {
$results['table_reports'][ $table ]['change'] = $results['table_reports'][ $table ]['change'] + $report['change'];
$results['table_reports'][ $table ]['updates'] = $results['table_reports'][ $table ]['updates'] + $report['updates'];
$results['table_reports'][ $table ]['end'] = $report['end'];
} else {
$results['table_reports'][ $table ] = $report;
}
$results['tables'] = count( $results['table_reports'] );
if ( ! set_transient( 'bsr_results', $results, DAY_IN_SECONDS ) ) {
return false;
}
return true;
}
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
$bsr_ajax = new BSR_AJAX();
$bsr_ajax->init();
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
@@ -0,0 +1,128 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Processes compatibility functionality.
* @since 1.0
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class BSR_Compatibility {
/**
* Returns the system info.
* @access public
* @return string
*/
public static function get_sysinfo() {
global $wpdb;
$return = '### Begin System Info ###' . "\n\n";
// Basic site info
$return .= '-- WordPress Configuration' . "\n\n";
$return .= 'Site URL: ' . site_url() . "\n";
$return .= 'Home URL: ' . home_url() . "\n";
$return .= 'Multisite: ' . ( is_multisite() ? 'Yes' : 'No' ) . "\n";
$return .= 'Version: ' . get_bloginfo( 'version' ) . "\n";
$return .= 'Language: ' . get_locale() . "\n";
$return .= 'Table Prefix: ' . 'Length: ' . strlen( $wpdb->prefix ) . "\n";
$return .= 'WP_DEBUG: ' . ( defined( 'WP_DEBUG' ) ? WP_DEBUG ? 'Enabled' : 'Disabled' : 'Not set' ) . "\n";
$return .= 'Memory Limit: ' . WP_MEMORY_LIMIT . "\n";
// Plugin Configuration
$return .= "\n" . '-- Better Search Replace Configuration' . "\n\n";
$return .= 'Plugin Version: ' . BSR_VERSION . "\n";
$db = new BSR_DB();
$return .= 'Max Page Size: ' . $db->get_page_size() . "\n";
// Server Configuration
$return .= "\n" . '-- Server Configuration' . "\n\n";
$os = self::get_os();
$return .= 'Operating System: ' . $os['name'] . "\n";
$return .= 'PHP Version: ' . PHP_VERSION . "\n";
$return .= 'MySQL Version: ' . $wpdb->db_version() . "\n";
$return .= 'Server Software: ' . self::get_server_software_for_display() . "\n";
// PHP configs... now we're getting to the important stuff
$return .= "\n" . '-- PHP Configuration' . "\n\n";
$return .= 'Memory Limit: ' . ini_get( 'memory_limit' ) . "\n";
$return .= 'Post Max Size: ' . ini_get( 'post_max_size' ) . "\n";
$return .= 'Upload Max Filesize: ' . ini_get( 'upload_max_filesize' ) . "\n";
$return .= 'Time Limit: ' . ini_get( 'max_execution_time' ) . "\n";
$return .= 'Max Input Vars: ' . ini_get( 'max_input_vars' ) . "\n";
$return .= 'Display Errors: ' . ( ini_get( 'display_errors' ) ? 'On (' . ini_get( 'display_errors' ) . ')' : 'N/A' ) . "\n";
// WordPress active plugins
$return .= "\n" . '-- WordPress Active Plugins' . "\n\n";
$plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
foreach( $plugins as $plugin_path => $plugin ) {
if( !in_array( $plugin_path, $active_plugins ) )
continue;
$return .= $plugin['Name'] . ': ' . $plugin['Version'] . "\n";
}
// WordPress inactive plugins
$return .= "\n" . '-- WordPress Inactive Plugins' . "\n\n";
foreach( $plugins as $plugin_path => $plugin ) {
if( in_array( $plugin_path, $active_plugins ) )
continue;
$return .= $plugin['Name'] . ': ' . $plugin['Version'] . "\n";
}
if( is_multisite() ) {
// WordPress Multisite active plugins
$return .= "\n" . '-- Network Active Plugins' . "\n\n";
$plugins = wp_get_active_network_plugins();
$active_plugins = get_site_option( 'active_sitewide_plugins', array() );
foreach( $plugins as $plugin_path ) {
$plugin_base = plugin_basename( $plugin_path );
if( !array_key_exists( $plugin_base, $active_plugins ) )
continue;
$plugin = get_plugin_data( $plugin_path );
$return .= $plugin['Name'] . ': ' . $plugin['Version'] . "\n";
}
}
$return .= "\n" . '### End System Info ###';
return $return;
}
/**
* Server software string for system info (safe for display in plain-text export).
*
* @return string
*/
private static function get_server_software_for_display() {
if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
return '';
}
return sanitize_text_field( wp_unslash( (string) $_SERVER['SERVER_SOFTWARE'] ) );
}
/**
* Determines the current operating system.
* @access public
* @return array
*/
public static function get_os() {
$os = array();
$uname = php_uname( 's' );
$os['code'] = strtoupper( substr( $uname, 0, 3 ) );
$os['name'] = $uname;
return $os;
}
}
@@ -0,0 +1,536 @@
<?php
/**
* Processes database-related functionality.
* @since 1.0
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class BSR_DB {
/**
* The page size used throughout the plugin.
* @var int
*/
public $page_size;
/**
* The name of the backup file.
* @var string
*/
public $file;
/**
* The WordPress database class.
* @var WPDB
*/
private $wpdb;
/**
* Initializes the class and its properties.
* @access public
*/
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->page_size = $this->get_page_size();
}
/**
* Returns an array of tables in the database.
* @access public
* @return array
*/
public static function get_tables() {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema discovery; not suitable for long-lived object cache.
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
if ( is_main_site() ) {
$tables = $wpdb->get_col( 'SHOW TABLES' );
} else {
$blog_id = get_current_blog_id();
$tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->base_prefix . absint( $blog_id ) . '\_%' ) );
}
} else {
$tables = $wpdb->get_col( 'SHOW TABLES' );
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
return $tables;
}
/**
* Returns an array containing the size of each database table.
* @access public
* @return array
*/
public static function get_sizes() {
global $wpdb;
$sizes = array();
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Server metadata for admin display only.
$tables = $wpdb->get_results( 'SHOW TABLE STATUS', ARRAY_A );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
if ( is_array( $tables ) && ! empty( $tables ) ) {
foreach ( $tables as $table ) {
$size = round( $table['Data_length'] / 1024 / 1024, 2 );
$sizes[ $table['Name'] ] = sprintf(
/* translators: %s: table size in megabytes. */
__( '(%s MB)', 'better-search-replace' ),
$size
);
}
}
return $sizes;
}
/**
* Returns the current page size.
* @access public
* @return int
*/
public function get_page_size() {
$page_size = get_option( 'bsr_page_size' ) ? get_option( 'bsr_page_size' ) : 20000;
return absint( $page_size );
}
/**
* Returns the number of pages in a table.
* @access public
* @return int
*/
public function get_pages_in_table( $table ) {
if ( false === $this->table_exists( $table ) ) {
return 0;
}
$wpdb = $this->wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Row count on allowlisted table name; not suitable for object cache.
$rows = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$pages = ceil( $rows / $this->page_size );
return absint( $pages );
}
/**
* Gets the total number of pages in the DB.
* @access public
* @return int
*/
public function get_total_pages( $tables ) {
$total_pages = 0;
foreach ( $tables as $table ) {
// Get the number of rows & pages in the table.
$pages = $this->get_pages_in_table( $table );
// Always include 1 page in case we have to create schemas, etc.
if ( $pages == 0 ) {
$pages = 1;
}
$total_pages += $pages;
}
return absint( $total_pages );
}
/**
* Gets the columns in a table and its primary key column names (composite keys supported).
*
* @access public
* @param string $table The table to check.
* @return array First element: list of primary key column names (DESCRIBE order). Second: all column names.
*/
public function get_columns( $table ) {
$primary_keys = null;
$columns = array();
if ( false === $this->table_exists( $table ) ) {
return array( $primary_keys, $columns );
}
$wpdb = $this->wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- DESCRIBE on allowlisted table for SRDB column list.
$fields = $wpdb->get_results( $wpdb->prepare( 'DESCRIBE %i', $table ) );
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
if ( is_array( $fields ) ) {
foreach ( $fields as $column ) {
$columns[] = $column->Field;
if ( 'PRI' == $column->Key ) {
$primary_keys[] = $column->Field;
}
}
}
return array( $primary_keys, $columns );
}
/**
* Adapated from interconnect/it's search/replace script.
*
* Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
* and to be compatible with batch processing via AJAX.
*
* @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
*
* @access public
* @param string $table The table to run the replacement on.
* @param int $page The page/block to begin the query on.
* @param array $args An associative array containing arguements for this run.
* @return array
*/
public function srdb( $table, $page, $args ) {
if ( ! $this->table_exists( $table ) ) {
$table_report = array(
'change' => 0,
'updates' => 0,
'start' => microtime( true ),
'end' => microtime( true ),
'errors' => array(),
'skipped' => true,
);
return array( 'table_complete' => true, 'table_report' => $table_report );
}
$wpdb = $this->wpdb;
$current_page = absint( $page );
$pages = $this->get_pages_in_table( $table );
$done = false;
$args['search_for'] = str_replace( '#BSR_BACKSLASH#', '\\', $args['search_for'] );
$args['replace_with'] = str_replace( '#BSR_BACKSLASH#', '\\', $args['replace_with'] );
$table_report = array(
'change' => 0,
'updates' => 0,
'start' => microtime( true ),
'end' => microtime( true ),
'errors' => array(),
'skipped' => false,
);
// Get a list of columns in this table.
list( $primary_keys, $columns ) = $this->get_columns( $table );
// Bail out early if there isn't a primary key.
if ( empty( $primary_keys ) ) {
$table_report['skipped'] = true;
return array( 'table_complete' => true, 'table_report' => $table_report );
}
$current_row = 0;
$start = $page * $this->page_size;
$end = $this->page_size;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Paged read of allowlisted table for batch search/replace.
$data = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM %i LIMIT %d, %d',
$table,
$start,
$end
),
ARRAY_A
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
foreach ( $data as $row ) {
$current_row++;
$update_sql = array();
$where_sql = array();
$update_values = array();
$where_values = array();
$upd = false;
foreach ( $columns as $column ) {
$data_to_fix = $row[ $column ];
if ( in_array( $column, $primary_keys, true ) ) {
$where_sql[] = '%i = %s';
$where_values[] = $column;
$where_values[] = $data_to_fix;
continue;
}
// Skip GUIDs by default.
if ( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
continue;
}
if ( $wpdb->options === $table ) {
// Skip any BSR options as they may contain the search field.
if ( isset( $should_skip ) && true === $should_skip ) {
$should_skip = false;
continue;
}
// If the Site URL needs to be updated, let's do that last.
if ( isset( $update_later ) && true === $update_later ) {
$update_later = false;
$edited_data = $this->recursive_unserialize_replace( $args['search_for'], $args['replace_with'], $data_to_fix, false, $args['case_insensitive'] );
if ( $edited_data != $data_to_fix ) {
$table_report['change']++;
$table_report['updates']++;
update_option( 'bsr_update_site_url', $edited_data );
continue;
}
}
if ( '_transient_bsr_results' === $data_to_fix || 'bsr_profiles' === $data_to_fix || 'bsr_update_site_url' === $data_to_fix || 'bsr_data' === $data_to_fix ) {
$should_skip = true;
}
if ( 'siteurl' === $data_to_fix && $args['dry_run'] !== 'on' ) {
$update_later = true;
}
}
// Run a search replace on the data that'll respect the serialisation.
$edited_data = $this->recursive_unserialize_replace( $args['search_for'], $args['replace_with'], $data_to_fix, false, $args['case_insensitive'] );
// Something was changed
if ( $edited_data != $data_to_fix ) {
$update_sql[] = '%i = %s';
$update_values[] = $column;
$update_values[] = $edited_data;
$upd = true;
$table_report['change']++;
}
}
// Determine what to do with updates.
if ( $args['dry_run'] === 'on' ) {
// Don't do anything if a dry run
} elseif ( $upd && ! empty( $where_sql ) ) {
// If there are changes to make, run the query.
$sql_template = 'UPDATE %i SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
$prepare_args = array_merge( array( $sql_template, $table ), $update_values, $where_values );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Dynamic UPDATE: table and columns allowlisted; values passed through $wpdb->prepare().
$result = $wpdb->query( call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args ) );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
if ( ! $result ) {
$table_report['errors'][] = sprintf(
/* translators: %d: database row number that failed to update. */
__( 'Error updating row: %d.', 'better-search-replace' ),
$current_row
);
} else {
$table_report['updates']++;
}
}
} // end row loop
if ( $current_page >= $pages - 1 ) {
$done = true;
}
$table_report['end'] = microtime( true );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Clear wpdb result buffers after batch SRDB.
$wpdb->flush();
return array( 'table_complete' => $done, 'table_report' => $table_report );
}
/**
* Adapated from interconnect/it's search/replace script.
*
* @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
*
* Take a serialised array and unserialise it replacing elements as needed and
* unserialising any subordinate arrays and performing the replace on those too.
*
* @access private
* @param string $from String we're looking to replace.
* @param string $to What we want it to be replaced with
* @param array $data Used to pass any subordinate arrays back to in.
* @param boolean $serialised Does the array passed via $data need serialising.
* @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
*
* @return string|array The original array with all elements replaced as needed.
*/
public function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialised = false, $case_insensitive = false ) {
try {
// Exit early if $data is a string but has no search matches.
if ( is_string( $data ) ) {
$has_match = $case_insensitive ? false !== stripos( $data, $from ) : false !== strpos( $data, $from );
if ( ! $has_match ) {
return $data;
}
}
if ( is_string( $data ) && ! is_serialized_string( $data ) && ( $unserialized = $this->unserialize( $data ) ) !== false ) {
$data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
}
elseif ( is_array( $data ) ) {
$_tmp = array( );
foreach ( $data as $key => $value ) {
$_tmp[ $key ] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
}
$data = $_tmp;
unset( $_tmp );
}
// Submitted by Tina Matter
elseif ( 'object' == gettype( $data ) ) {
if($this->is_object_cloneable($data)) {
$_tmp = clone $data;
$props = get_object_vars( $data );
foreach ( $props as $key => $value ) {
// Integer properties are crazy and the best thing we can do is to just ignore them.
// see http://stackoverflow.com/a/10333200
if ( is_int( $key ) ) {
continue;
}
// Skip any representation of a protected property
// https://github.com/deliciousbrains/better-search-replace/issues/71#issuecomment-1369195244
if ( is_string( $key ) && 1 === preg_match( "/^(\\\\0).+/im", preg_quote( $key ) ) ) {
continue;
}
$_tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
}
$data = $_tmp;
unset( $_tmp );
}
}
elseif ( is_serialized_string( $data ) ) {
$unserialized = $this->unserialize( $data );
if ( $unserialized !== false ) {
$data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
}
}
else {
if ( is_string( $data ) ) {
$data = $this->str_replace( $from, $to, $data, $case_insensitive );
}
}
if ( $serialised ) {
return serialize( $data );
}
} catch( Exception $error ) {
}
return $data;
}
/**
* Updates the Site URL if necessary.
* @access public
* @return boolean
*/
public function maybe_update_site_url() {
$option = get_option( 'bsr_update_site_url' );
if ( $option ) {
update_option( 'siteurl', $option );
delete_option( 'bsr_update_site_url' );
return true;
}
return false;
}
/**
* Return unserialized object or array
*
* @param string $serialized_string Serialized string.
* @param string $method The name of the caller method.
*
* @return mixed, false on failure
*/
public static function unserialize( $serialized_string ) {
if ( ! is_serialized( $serialized_string ) ) {
return false;
}
$serialized_string = trim( $serialized_string );
if ( PHP_VERSION_ID >= 70000 ) {
$unserialized_string = @unserialize( $serialized_string, array('allowed_classes' => false ) );
} else {
$unserialized_string = @BSR\Brumann\Polyfill\Unserialize::unserialize( $serialized_string, array( 'allowed_classes' => false ) );
}
return $unserialized_string;
}
/**
* Wrapper for str_replace
*
* @param string $from
* @param string $to
* @param string $data
* @param string|bool $case_insensitive
*
* @return string
*/
public function str_replace( $from, $to, $data, $case_insensitive = false ) {
if ( 'on' === $case_insensitive ) {
$data = str_ireplace( $from, $to, $data );
} else {
$data = str_replace( $from, $to, $data );
}
return $data;
}
/**
* Checks whether a table exists in DB.
*
* @param $table
*
* @return bool
*/
private function table_exists( $table ) {
return in_array( $table, self::get_tables(), true );
}
/**
* Check if a given object can be cloned.
*
* @param object $object
*
* @return bool
*/
private function is_object_cloneable( $object ) {
return ( new \ReflectionClass( get_class( $object ) ) )->isCloneable();
}
}
@@ -0,0 +1,135 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register all actions and filters for the plugin
*
* @link https://bettersearchreplace.com
* @since 1.0.0
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
/**
* Register all actions and filters for the plugin.
*
* Maintain a list of all hooks that are registered throughout
* the plugin, and register them with the WordPress API. Call the
* run function to execute the list of actions and filters.
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
class BSR_Loader {
/**
* The array of actions registered with WordPress.
*
* @since 1.0.0
* @access protected
* @var array $actions The actions registered with WordPress to fire when the plugin loads.
*/
protected $actions;
/**
* The array of filters registered with WordPress.
*
* @since 1.0.0
* @access protected
* @var array $filters The filters registered with WordPress to fire when the plugin loads.
*/
protected $filters;
/**
* Initialize the collections used to maintain the actions and filters.
*
* @since 1.0.0
*/
public function __construct() {
$this->actions = array();
$this->filters = array();
}
/**
* Add a new action to the collection to be registered with WordPress.
*
* @since 1.0.0
* @var string $hook The name of the WordPress action that is being registered.
* @var object $component A reference to the instance of the object on which the action is defined.
* @var string $callback The name of the function definition on the $component.
* @var int Optional $priority The priority at which the function should be fired.
* @var int Optional $accepted_args The number of arguments that should be passed to the $callback.
*/
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* Add a new filter to the collection to be registered with WordPress.
*
* @since 1.0.0
* @var string $hook The name of the WordPress filter that is being registered.
* @var object $component A reference to the instance of the object on which the filter is defined.
* @var string $callback The name of the function definition on the $component.
* @var int Optional $priority The priority at which the function should be fired.
* @var int Optional $accepted_args The number of arguments that should be passed to the $callback.
*/
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
}
/**
* A utility function that is used to register the actions and hooks into a single
* collection.
*
* @since 1.0.0
* @access private
* @var array $hooks The collection of hooks that is being registered (that is, actions or filters).
* @var string $hook The name of the WordPress filter that is being registered.
* @var object $component A reference to the instance of the object on which the filter is defined.
* @var string $callback The name of the function definition on the $component.
* @var int Optional $priority The priority at which the function should be fired.
* @var int Optional $accepted_args The number of arguments that should be passed to the $callback.
* @return type The collection of actions and filters registered with WordPress.
*/
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args
);
return $hooks;
}
/**
* Register the filters and actions with WordPress.
*
* @since 1.0.0
*/
public function run() {
foreach ( $this->filters as $hook ) {
add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
}
foreach ( $this->actions as $hook ) {
add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] );
}
}
}
@@ -0,0 +1,164 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The core plugin class.
*
* This is used to define internationalization, dashboard-specific hooks, and
* public-facing site hooks.
*
* Also maintains the unique identifier of this plugin as well as the current
* version of the plugin.
*
* @since 1.0
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class Better_Search_Replace {
/**
* The loader that's responsible for maintaining and registering all hooks that power
* the plugin.
*
* @since 1.0
* @access protected
* @var BSR_Loader $loader Maintains and registers all hooks for the plugin.
*/
protected $loader;
/**
* The unique identifier of this plugin.
*
* @since 1.0
* @access protected
* @var string $plugin_name The string used to uniquely identify this plugin.
*/
protected $plugin_name;
/**
* The current version of the plugin.
*
* @since 1.0
* @access protected
* @var string $version The current version of the plugin.
*/
protected $version;
/**
* Define the core functionality of the plugin.
*
* Set the plugin name and the plugin version that can be used throughout the plugin.
* Load the dependencies, define the locale, and set the hooks for the Dashboard and
* the public-facing side of the site.
*
* @since 1.0
*/
public function __construct() {
$this->plugin_name = 'better-search-replace';
$this->version = BSR_VERSION;
$this->load_dependencies();
$this->define_admin_hooks();
}
/**
* Load the required dependencies for this plugin.
*
* Create an instance of the loader which will be used to register the hooks
* with WordPress.
*
* @since 1.0
* @access private
*/
private function load_dependencies() {
require_once BSR_PATH . 'includes/class-bsr-loader.php';
require_once BSR_PATH . 'includes/class-bsr-admin.php';
require_once BSR_PATH . 'includes/class-bsr-ajax.php';
require_once BSR_PATH . 'includes/class-bsr-db.php';
require_once BSR_PATH . 'includes/class-bsr-compatibility.php';
require_once BSR_PATH . 'includes/class-bsr-plugin-footer.php';
require_once BSR_PATH . 'includes/class-bsr-utils.php';
if ( PHP_VERSION_ID < 70000 ) {
require_once BSR_PATH . 'vendor/brumann/polyfill-unserialize/src/Unserialize.php';
require_once BSR_PATH . 'vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php';
}
$this->loader = new BSR_Loader();
}
/**
* Register all of the hooks related to the dashboard functionality
* of the plugin.
*
* @since 1.0
* @access private
*/
private function define_admin_hooks() {
// Initialize the admin class.
$plugin_admin = new BSR_Admin( $this->get_plugin_name(), $this->get_version() );
$plugin_footer = new BSR_Plugin_Footer();
/// Register the admin pages and scripts.
$this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
$this->loader->add_action( 'admin_menu', $plugin_admin, 'bsr_menu_pages' );
// Other admin actions.
$this->loader->add_action( 'admin_init', $plugin_admin, 'register_option' );
$this->loader->add_action( 'admin_post_bsr_view_details', $plugin_admin, 'load_details' );
$this->loader->add_action( 'admin_post_bsr_download_sysinfo', $plugin_admin, 'download_sysinfo' );
$this->loader->add_action( 'plugin_row_meta', $plugin_admin, 'meta_upgrade_link', 10, 2 );
// Footer Actions
$this->loader->add_filter( 'update_footer', $plugin_footer, 'update_footer', 20);
$this->loader->add_filter( 'admin_footer_text', $plugin_footer, 'admin_footer_text', 20);
}
/**
* Run the loader to execute all of the hooks with WordPress.
*
* @since 1.0
*/
public function run() {
$this->loader->run();
}
/**
* The name of the plugin used to uniquely identify it within the context of
* WordPress and to define internationalization functionality.
*
* @since 1.0
* @return string The name of the plugin.
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* The reference to the class that orchestrates the hooks with the plugin.
*
* @since 1.0
* @return Better_Search_Replace_Loader Orchestrates the hooks of the plugin.
*/
public function get_loader() {
return $this->loader;
}
/**
* Retrieve the version number of the plugin.
*
* @since 1.0
* @return string The version number of the plugin.
*/
public function get_version() {
return $this->version;
}
}
@@ -0,0 +1,102 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin footer functionality for the plugin
*
* @since 1.4.3
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
class BSR_Plugin_Footer {
/**
* Filter admin footer text for BSR pages
*
* @param string $text
* @return string
* @handles admin_footer_text
**/
public function admin_footer_text( $text ) {
if ( ! BSR_Utils::is_bsr_screen() ) {
return $text;
}
$product_link = BSR_Utils::external_link(
BSR_Utils::bsr_url(
'',
[
'utm_source' => 'bsr_free',
'utm_medium' => 'insideplugin',
'utm_campaign' => 'plugin_footer',
'utm_content' => 'footer_colophon'
]
),
BSR_NAME
);
$wpe_link = BSR_Utils::external_link(
BSR_Utils::wpe_url(
'',
[
'utm_source' => 'bsr_plugin',
'utm_content' => 'bsr_free_plugin_footer_text'
]
),
'WP Engine'
);
return sprintf(
/* translators: %1$s is a link to BSR's website, and %2$s is a link to WP Engine's website. */
__( '%1$s is developed and maintained by %2$s.', 'better-search-replace' ),
$product_link,
$wpe_link
);
}
/**
* Filter update footer text for BSR pages
*
* @param string $content
* @return string
* @handles update_footer
**/
public function update_footer( $content ) {
if ( ! BSR_Utils::is_bsr_screen() ) {
return $content;
}
$utm_params = [
'utm_source' => 'bsr_free',
'utm_campaign' => 'plugin_footer',
'utm_content' => 'footer_navigation'
];
$links[] = BSR_Utils::external_link(
BSR_Utils::bsr_url(
'/docs/',
$utm_params
),
__( 'Documentation', 'better-search-replace' )
);
$links[] = '<a href="' . BSR_Utils::plugin_page_url() . '&tab=bsr_help">' . __( 'Support', 'better-search-replace' ) . '</a>';
$links[] = BSR_Utils::external_link(
BSR_Utils::bsr_url(
'/feedback/',
$utm_params
),
__( 'Feedback', 'better-search-replace' )
);
if ( defined( 'BSR_NAME' ) && defined( 'BSR_VERSION' ) ) {
$links[] = BSR_NAME . ' ' . BSR_VERSION;
}
return join( ' &#8729; ', $links );
}
}
@@ -0,0 +1,29 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) exit;
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Historical class name; used by admin templates.
class BSR_Templates_Helper {
/**
* Returns a fully qualified path for the given active tab name
* if the file name is not supported, the default template path is returned.
*
* @param string $active_tab
* @return string
*/
public static function get_tab_template($active_tab) {
switch($active_tab) {
case 'bsr_settings':
return BSR_PATH . 'templates/bsr-settings.php';
case 'bsr_help':
return BSR_PATH . 'templates/bsr-help.php';
default:
return BSR_PATH . 'templates/bsr-search-replace.php';
}
}
}
@@ -0,0 +1,166 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Utility functionality for the plugin
*
* @since 1.4.3
*
* @package Better_Search_Replace
* @subpackage Better_Search_Replace/includes
*/
// Prevent direct access.
if ( ! defined( 'BSR_PATH' ) ) {
exit;
}
class BSR_Utils {
const BSR_URL = 'https://bettersearchreplace.com';
const WPE_URL = 'https://wpengine.com';
/** Nonce action for tools.php?page=better-search-replace screen query args (redirects, tab links). */
const TOOLS_SCREEN_NONCE_ACTION = 'bsr_tools_screen';
/**
* Create an external link for given URL.
*
* @param string $url
* @param string $text
*
* @return string
*/
public static function external_link( $url, $text ) {
return sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( $url ), esc_html( $text ) );
}
/**
* Generate Better Search Replace site URL with correct UTM tags.
*
* @param string $path
* @param array $args
* @param string $hash
*
* @return string
*/
public static function bsr_url( $path, $args = array(), $hash = '' ) {
$args = wp_parse_args(
$args,
array( 'utm_medium' => 'insideplugin' )
);
$args = array_map( 'urlencode', $args );
$url = trailingslashit( self::BSR_URL ) . ltrim( $path, '/' );
$url = add_query_arg( $args, $url );
if ( $hash ) {
$url .= '#' . $hash;
}
return $url;
}
/**
* Generate WP Engine site URL with correct UTM tags.
*
* @param string $path
* @param array $args
* @param string $hash
*
* @return string
*/
public static function wpe_url( $path = '', $args = array(), $hash = '' ) {
$args = wp_parse_args(
$args,
[
'utm_medium' => 'referral',
'utm_campaign' => 'bx_prod_referral',
]
);
$args = array_map( 'urlencode', $args );
$url = trailingslashit( self::WPE_URL ) . ltrim( $path, '/' );
$url = add_query_arg( $args, $url );
if ( $hash ) {
$url .= '#' . $hash;
}
return $url;
}
/**
* Get the plugin page url
*
* @return string
*/
public static function plugin_page_url() {
return menu_page_url( 'better-search-replace', false );
}
/**
* Tools → Better Search Replace URL with a nonce for GET query args on that screen.
*
* @param array $args Query arguments (merged with page=better-search-replace).
* @return string Unescaped URL; pass through esc_url() for HTML or esc_url_raw() for redirects.
*/
public static function tools_page_url( $args = array() ) {
$url = add_query_arg(
array_merge(
array( 'page' => 'better-search-replace' ),
$args
),
admin_url( 'tools.php' )
);
return add_query_arg( '_wpnonce', wp_create_nonce( self::TOOLS_SCREEN_NONCE_ACTION ), $url );
}
/**
* Whether the current tools screen GET request may use BSR-specific query args (result, import, profile, etc.).
* Missing nonce is allowed for backward compatibility when the user is already authorized.
*
* @return bool
*/
public static function validate_tools_screen_get() {
// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce is verified below when present; capability is always required.
if ( ! bsr_enabled_for_user() ) {
return false;
}
if ( ! isset( $_GET['_wpnonce'] ) ) {
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
return true;
}
$valid = (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ), self::TOOLS_SCREEN_NONCE_ACTION );
// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
return $valid;
}
/**
* Is current admin screen for bsr.
*
* @return bool
*/
public static function is_bsr_screen() {
$screen = get_current_screen();
return $screen->base === 'tools_page_better-search-replace';
}
/**
* Ensures intent by verifying that a user was referred from another admin
* page with the correct security nonce, and that user has the capability
* level to use the plugin.
*
* @param int|string $action The nonce action.
* @param string $query_arg Key to check for nonce in `$_REQUEST`.
* @param bool $die Whether to die on failure (default false so callers can return JSON/redirects).
*
* @return bool
*/
public static function check_admin_referer( $action, $query_arg, $die = false ) {
return check_admin_referer( $action, $query_arg, $die ) && bsr_enabled_for_user();
}
}
@@ -0,0 +1 @@
<?php // Silence is golden