shell bypass 403
<?php
/**
* Attachment db renamer class.
*/
/**
* URL Replacer for WordPress
*
* A standalone class to replace URLs in WordPress database,
* including handling image size variations and scaled images.
*
* @since 4.0.0
*/
class Optml_Attachment_Db_Renamer {
/**
* Tables to skip during replacement
*
* @var array
*/
private $skip_tables = [ 'users', 'terms', 'term_relationships', 'term_taxonomy' ];
/**
* Columns to skip during replacement
*
* @var array
*/
private $skip_columns = [ 'user_pass' ];
/**
* Handle image size variations
*
* @var bool
*/
private $handle_image_sizes = false;
/**
* Constructor
*/
public function __construct( $skip_sizes = false ) {
$this->handle_image_sizes = ! $skip_sizes;
}
/**
* Replace URLs in the WordPress database
*
* @param string $old_url The base URL to search for (e.g., http://domain.com/wp-content/uploads/2025/03/image.jpg).
* @param string $new_url The base URL to replace with (e.g., http://domain.com/wp-content/uploads/2025/03/new-name.jpg).
*
* @return int Number of replacements made
*/
public function replace( $old_url, $new_url ) {
if ( $old_url === $new_url ) {
return 0;
}
if ( empty( $old_url ) || empty( $new_url ) ) {
return 0;
}
if ( ! is_string( $old_url ) || ! is_string( $new_url ) ) { // @phpstan-ignore-line docs require a string but it could be empty
return 0;
}
$tables = $this->get_tables();
$total_replacements = 0;
foreach ( $tables as $table ) {
if ( in_array( $table, $this->skip_tables, true ) ) {
continue;
}
list($primary_keys, $columns) = $this->get_columns( $table );
// Skip tables with no primary keys
if ( empty( $primary_keys ) ) {
continue;
}
foreach ( $columns as $column ) {
if ( in_array( $column, $this->skip_columns, true ) ) {
continue;
}
$replacements = $this->process_column( $table, $column, $primary_keys, $old_url, $new_url );
$total_replacements += $replacements;
}
}
return $total_replacements;
}
/**
* Get WordPress tables
*
* @return array Table names
*/
private function get_tables() {
global $wpdb;
return array_values( $wpdb->tables() );
}
/**
* Get columns for a table
*
* @param string $table Table name.
*
* @return array Array containing primary keys and text columns
*/
private function get_columns( $table ) {
global $wpdb;
$primary_keys = [];
$text_columns = [];
// Get table information
$results = $wpdb->get_results( $wpdb->prepare( 'DESCRIBE %i', $table ) );
if ( ! empty( $results ) ) {
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
foreach ( $results as $col ) {
if ( 'PRI' === $col->Key ) {
$primary_keys[] = $col->Field;
}
if ( $this->is_text_col( $col->Type ) ) {
$text_columns[] = $col->Field;
}
}
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
return [ $primary_keys, $text_columns ];
}
/**
* Check if column is text type
*
* @param string $type Column type.
*
* @return bool True if text column
*/
private function is_text_col( $type ) {
foreach ( [ 'text', 'varchar', 'longtext', 'mediumtext', 'char' ] as $token ) {
if ( false !== stripos( $type, $token ) ) {
return true;
}
}
return false;
}
/**
* Process a single column for replacements
*
* @param string $table Table name.
* @param string $column Column name.
* @param array $primary_keys Primary keys.
* @param string $old_url Old URL.
* @param string $new_url New URL.
*
* @return int Number of replacements
*/
private function process_column( $table, $column, $primary_keys, $old_url, $new_url ) {
global $wpdb;
$count = 0;
// Check for serialized data
$has_serialized = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(%i) FROM %i WHERE %i REGEXP '^[aiO]:[1-9]' LIMIT 1",
$column,
$table,
$column
)
);
// Process with PHP if serialized data is found
if ( $has_serialized ) {
$count = $this->php_handle_column( $table, $column, $primary_keys, $old_url, $new_url );
} else {
// Use direct SQL replacement for non-serialized data
$count = $this->sql_handle_column( $table, $column, $old_url, $new_url );
}
return $count;
}
/**
* Handle column using SQL replacement
*
* @param string $table Table name.
* @param string $column Column name.
* @param string $old_url Old URL.
* @param string $new_url New URL.
*
* @return int Number of replacements
*/
private function sql_handle_column( $table, $column, $old_url, $new_url ) {
global $wpdb;
$count = 0;
// Get the filename components
$old_path_parts = parse_url( $old_url );
if ( ! isset( $old_path_parts['path'] ) ) {
return 0;
}
$old_path = $old_path_parts['path'];
$old_file_info = pathinfo( $old_path );
$old_base = $old_file_info['filename'];
$old_dir = dirname( $old_path );
$old_domain = isset( $old_path_parts['host'] ) ? 'http' . ( isset( $old_path_parts['scheme'] ) && $old_path_parts['scheme'] === 'https' ? 's' : '' ) . '://' . $old_path_parts['host'] : '';
// Build pattern to match any URL containing the base filename
$base_url = $old_domain . $old_dir . '/' . $old_base;
// Get rows with regular URLs
$rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM %i WHERE %i LIKE %s',
$table,
$column,
'%' . $wpdb->esc_like( $base_url ) . '%'
)
);
// Also create a pattern for JSON-escaped version
$json_base_url = str_replace( '/', '\/', $base_url );
// Get rows with JSON-escaped URLs
$json_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM %i WHERE %i LIKE %s',
$table,
$column,
'%' . $wpdb->esc_like( $json_base_url ) . '%'
)
);
// Merge results, avoiding duplicates
$processed_ids = [];
$all_rows = array_merge( $rows, $json_rows );
if ( empty( $all_rows ) ) {
return 0;
}
foreach ( $all_rows as $row ) {
$id_field = $row->ID ?? $row->id ?? null;
if ( ! $id_field ) {
foreach ( $row as $field => $value ) {
if ( stripos( $field, 'id' ) !== false ) {
$id_field = $value;
break;
}
}
}
if ( ! $id_field ) {
continue;
}
// Skip if we've already processed this row
if ( isset( $processed_ids[ $id_field ] ) ) {
continue;
}
$processed_ids[ $id_field ] = true;
$content = $row->$column;
$new_content = $this->replace_image_urls( $content, $old_url, $new_url );
if ( $content !== $new_content ) {
$wpdb->update(
$table,
[ $column => $new_content ],
[ 'ID' => $id_field ]
);
++$count;
}
}
return $count;
}
/**
* Handle column using PHP for serialized data
*
* @param string $table Table name.
* @param string $column Column name.
* @param array $primary_keys Primary keys.
* @param string $old_url Old URL.
* @param string $new_url New URL.
*
* @return int Number of replacements
*/
private function php_handle_column( $table, $column, $primary_keys, $old_url, $new_url ) {
global $wpdb;
$count = 0;
$json_old_url = str_replace( '/', '\/', $old_url );
// Build the query and allow processing with multiple primary keys.
$query = 'SELECT ';
foreach ( $primary_keys as $key => $value ) {
$query .= $wpdb->prepare( '%i, ', $value );
}
$query .= '%i FROM %i WHERE %s LIKE %s LIMIT 100';
// Get the rows that need updating - first for regular URLs
$rows = $wpdb->get_results(
$wpdb->prepare(
$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$column,
$table,
$column,
'%' . $wpdb->esc_like( $old_url ) . '%'
)
);
// Also get rows with JSON-escaped URLs
$json_rows = $wpdb->get_results(
$wpdb->prepare(
$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$column,
$table,
$column,
'%' . $wpdb->esc_like( $json_old_url ) . '%'
)
);
// Merge results, avoiding duplicates
$processed_ids = [];
$all_rows = array_merge( $rows, $json_rows );
foreach ( $all_rows as $row ) {
// Generate a unique identifier for this row based on primary keys
$row_id = '';
foreach ( $primary_keys as $key ) {
$row_id .= $row->$key . '|';
}
// Skip if we've already processed this row
if ( isset( $processed_ids[ $row_id ] ) ) {
continue;
}
$processed_ids[ $row_id ] = true;
$value = $row->$column;
// Skip empty values
if ( empty( $value ) ) {
continue;
}
// Replace URLs in the value (handling serialized data)
$new_value = $this->replace_urls_in_value( $value, $old_url, $new_url );
// Skip if no change
if ( $value === $new_value ) {
continue;
}
// Build WHERE clause for this row
$where_conditions = [];
foreach ( $primary_keys as $key ) {
$where_conditions[ $key ] = $row->$key;
}
// Update the row
$updated = $wpdb->update(
$table,
[ $column => $new_value ],
$where_conditions
);
if ( $updated ) {
++$count;
}
}
return $count;
}
/**
* Replace URLs in a value, handling serialized data
*
* @param string $value The value to process.
* @param string $old_url Old URL.
* @param string $new_url New URL.
*
* @return string The processed value
*/
private function replace_urls_in_value( $value, $old_url, $new_url ) {
// Check if the value is serialized
if ( $this->is_serialized( $value ) ) {
$unserialized = @unserialize( $value );
// If unserialize successful, process the data
if ( $unserialized !== false ) {
$replaced = $this->replace_in_data( $unserialized, $old_url, $new_url );
return serialize( $replaced );
}
}
// Handle image sizes for non-serialized content
if ( $this->handle_image_sizes ) {
return $this->replace_image_urls( $value, $old_url, $new_url );
}
// Simple string replacement for non-serialized data
return str_replace( $old_url, $new_url, $value );
}
/**
* Replace image URLs including various WordPress size variations and scaled images
*
* @param string $content The content to process.
* @param string $old_url Old URL pattern.
* @param string $new_url New URL pattern.
*
* @return string The processed content
*/
private function replace_image_urls( $content, $old_url, $new_url ) {
// Get the filename components
$old_path_parts = parse_url( $old_url );
$new_path_parts = parse_url( $new_url );
if ( ! isset( $old_path_parts['path'] ) || ! isset( $new_path_parts['path'] ) ) {
// If we can't parse the URLs, fallback to direct replacement
return str_replace( $old_url, $new_url, $content );
}
// Extract file name info
$old_path = $old_path_parts['path'];
$new_path = $new_path_parts['path'];
$old_file_info = pathinfo( $old_path );
$new_file_info = pathinfo( $new_path );
$old_base = $old_file_info['filename'];
$new_base = $new_file_info['filename'];
$old_ext = isset( $old_file_info['extension'] ) ? $old_file_info['extension'] : '';
$new_ext = isset( $new_file_info['extension'] ) ? $new_file_info['extension'] : $old_ext;
// Define domain parts
$old_domain = isset( $old_path_parts['host'] ) ? 'http' . ( isset( $old_path_parts['scheme'] ) && $old_path_parts['scheme'] === 'https' ? 's' : '' ) . '://' . $old_path_parts['host'] : '';
$new_domain = isset( $new_path_parts['host'] ) ? 'http' . ( isset( $new_path_parts['scheme'] ) && $new_path_parts['scheme'] === 'https' ? 's' : '' ) . '://' . $new_path_parts['host'] : '';
// Replace original URLs
$content = str_replace( $old_url, $new_url, $content );
// Replace JSON-escaped URLs
$json_old_url = str_replace( '/', '\/', $old_url );
$json_new_url = str_replace( '/', '\/', $new_url );
$content = str_replace( $json_old_url, $json_new_url, $content );
// If we have a file with extension, handle variations
if ( ! empty( $old_ext ) && $this->handle_image_sizes ) {
$old_dir = dirname( $old_path );
$new_dir = dirname( $new_path );
// Replace WordPress image size variations (e.g., image-300x200.jpg)
$size_pattern = '/' . preg_quote( $old_domain . $old_dir . '/' . $old_base, '/' ) . '-\d+x\d+\.' . preg_quote( $old_ext, '/' ) . '/';
$content = preg_replace_callback(
$size_pattern,
function ( $matches ) use ( $old_base, $new_base, $old_domain, $new_domain, $old_dir, $new_dir, $old_ext, $new_ext ) {
// Extract the size part (e.g., -300x200)
$size_part = substr( $matches[0], strlen( $old_domain . $old_dir . '/' . $old_base ), -strlen( '.' . $old_ext ) );
// Build the new URL with the same size
return $new_domain . $new_dir . '/' . $new_base . $size_part . '.' . $new_ext;
},
$content
);
// Replace -scaled variations
$scaled_pattern = '/' . preg_quote( $old_domain . $old_dir . '/' . $old_base, '/' ) . '-scaled\.' . preg_quote( $old_ext, '/' ) . '/';
$content = preg_replace_callback(
$scaled_pattern,
function ( $matches ) use ( $new_base, $new_domain, $new_dir, $new_ext ) {
return $new_domain . $new_dir . '/' . $new_base . '-scaled.' . $new_ext;
},
$content
);
// Replace JSON-escaped variations
$json_size_pattern = '/' . preg_quote( str_replace( '/', '\/', $old_domain . $old_dir . '/' . $old_base ), '/' ) . '-\d+x\d+\.' . preg_quote( $old_ext, '/' ) . '/';
$content = preg_replace_callback(
$json_size_pattern,
function ( $matches ) use ( $old_base, $new_base, $old_domain, $new_domain, $old_dir, $new_dir, $old_ext, $new_ext ) {
// Extract the size part (e.g., -300x200)
$size_part = substr( $matches[0], strlen( str_replace( '/', '\/', $old_domain . $old_dir . '/' . $old_base ) ), -strlen( '.' . $old_ext ) );
// Build the new URL with the same size
return str_replace( '/', '\/', $new_domain . $new_dir . '/' . $new_base . $size_part . '.' . $new_ext );
},
$content
);
// Replace JSON-escaped scaled variations
$json_scaled_pattern = '/' . preg_quote( str_replace( '/', '\/', $old_domain . $old_dir . '/' . $old_base ), '/' ) . '-scaled\.' . preg_quote( $old_ext, '/' ) . '/';
$content = preg_replace_callback(
$json_scaled_pattern,
function ( $matches ) use ( $new_base, $new_domain, $new_dir, $new_ext ) {
return str_replace( '/', '\/', $new_domain . $new_dir . '/' . $new_base . '-scaled.' . $new_ext );
},
$content
);
}
return $content;
}
/**
* Recursively replace URLs in data structure
*
* @param mixed $data The data to process.
* @param string $old_url Old URL.
* @param string $new_url New URL.
*
* @return mixed The processed data
*/
private function replace_in_data( $data, $old_url, $new_url ) {
if ( is_array( $data ) ) {
// Process arrays recursively
foreach ( $data as $key => $value ) {
$data[ $key ] = $this->replace_in_data( $value, $old_url, $new_url );
}
} elseif ( is_object( $data ) ) {
// Process objects recursively
foreach ( $data as $key => $value ) {
$data->$key = $this->replace_in_data( $value, $old_url, $new_url );
}
} elseif ( is_string( $data ) ) {
// Replace URLs in strings
if ( $this->handle_image_sizes ) {
$data = $this->replace_image_urls( $data, $old_url, $new_url );
} else {
$data = str_replace( $old_url, $new_url, $data );
}
}
return $data;
}
/**
* Check if a string is serialized
*
* @param string $data String to check.
*
* @return bool True if serialized
*/
private function is_serialized( $data ) {
// If it isn't a string, it isn't serialized
if ( ! is_string( $data ) ) {
return false;
}
$data = trim( $data );
if ( 'N;' === $data ) {
return true;
}
if ( strlen( $data ) < 4 ) {
return false;
}
if ( ':' !== $data[1] ) {
return false;
}
$last_char = substr( $data, -1 );
if ( ';' !== $last_char && '}' !== $last_char ) {
return false;
}
$token = $data[0];
switch ( $token ) {
case 's':
if ( '"' !== substr( $data, -2, 1 ) ) {
return false;
}
// Fall through
case 'a':
case 'O':
case 'i':
case 'd':
return (bool) preg_match( "/^{$token}:[0-9]+:/", $data );
default:
return false;
}
}
}
/**
* Example usage:
*
* $replacer = new Optml_Attachment_Db_Renamer();
*
* // Replace all variations of the image
* $count = $replacer->replace(
* 'http://om-wp.test/wp-content/uploads/2025/03/image.jpg',
* 'http://om-wp.test/wp-content/uploads/2025/03/new-name.jpg'
* );
*
* echo "Replaced $count instances";
*/