Sindbad~EG File Manager
<?php
namespace Inpsyde\SearchReplace\Database;
use Exception;
use Inpsyde\SearchReplace\Service;
/**
* Class Replace
* runs search & replace on a database
* adapted from: https://github.com/interconnectit/Search-Replace-DB/blob/master/srdb.class.php
*
* @package Inpsyde\SearchReplace\Database
*/
class Replace {
/**
* The Database Interface Object
*
* @var Manager
*/
private $dbm;
/**
* Count of rows to be replaced at a time
*
* @var int
*/
private $page_size = 100;
/**
* @var bool - set if dry run
*/
private $dry_run = true;
/**
* @var Service\MaxExecutionTime
*/
private $max_execution;
/**
* Store csv data
*
* @var array
*/
private $csv_data = [];
/**
* Replace constructor.
*
* @param Manager $dbm
* @param Service\MaxExecutionTime $max_execution
*/
public function __construct( Manager $dbm, Service\MaxExecutionTime $max_execution ) {
$this->dbm = $dbm;
$this->max_execution = $max_execution;
}
/**
* The main loop triggered in step 5. Up here to keep it out of the way of the
* HTML. This walks every table in the db that was selected in step 3 and then
* walks every row and column replacing all occurrences of a string with another.
* We split large tables into blocks (size is set via $page_size)when dealing with them to save
* on memory consumption.
*
* @param string $search What we want to replace.
* @param string $replace What we want to replace it with.
* @param string $tables The name of the table we want to look at.
* @param null $csv
*
* @return array|\WP_Error Collection of information gathered during the run.
* @throws \Throwable
*/
public function run_search_replace( $search, $replace, $tables, $csv = null ) {
if ( $search === $replace && '' !== $search ) {
return new \WP_Error( 'error', esc_html__( 'Search and replace pattern can\'t be the same!', 'search-and-replace' ) );
}
$this->max_execution->set();
$report = [
'errors' => null,
'changes' => [],
'tables' => '0',
'changes_count' => '0',
];
foreach ( (array) $tables as $table ) {
// Count tables.
$report ['tables'] ++;
$table_report = $this->replace_values( $table, $search, $replace, $csv );
// Log changes if any.
if ( 0 !== $table_report['change'] ) {
$report['changes'][ $table ] = $table_report;
$report ['changes_count'] += $table_report['change'];
}
}
$this->max_execution->restore();
return $report;
}
/**
* Replace data values inside the table.
*
* @param string $table
* @param string $search
* @param string $replace
* @param null $csv
*
* @return array
* @throws \Throwable
*/
public function replace_values( $table, $search = '', $replace = '', $csv = null ) {
$table_report = [
'table_name' => $table,
'rows' => 0,
'change' => 0,
'changes' => [],
'updates' => 0,
'start' => microtime(),
'end' => microtime(),
'errors' => [],
];
// Check we have a search string, bail if not.
if ( empty( $search ) && empty( $csv ) ) {
$table_report['errors'][] = 'Search string is empty';
return $table_report;
}
// Grab table structure in order to determine which columns are used to store serialized values in it.
$table_structure = $this->dbm->get_table_structure( $table );
if ( ! $table_structure ) {
return $table_report;
}
$maybe_serialized = [];
foreach ( $table_structure as $struct ) {
// Longtext is used for meta_values as best practice in all of the automatic products.
// @codingStandardsIgnoreLine
if ( 0 === stripos( $struct->Type, 'longtext' ) ) {
// @codingStandardsIgnoreLine
$maybe_serialized[] = strtolower( $struct->Field );
}
}
// Split columns array in primary key string and columns array.
$columns = $this->dbm->get_columns( $table );
list( $primary_key, $columns ) = $columns;
if ( null === $primary_key ) {
array_push(
$table_report['errors'],
"The table \"{$table}\" has no primary key. Changes will have to be made manually.",
'results'
);
return $table_report;
}
$table_report['start'] = microtime();
// Count the number of rows we have in the table if large we'll split into blocks, This is a mod from Simon Wheatley
$row_count = $this->dbm->get_rows( $table );
$page_size = $this->page_size;
$pages = ceil( $row_count / $page_size );
// Prepare CSV data
if ( null !== $csv ) {
$csv_lines = explode( "\n", $csv );
$csv_head = str_getcsv( 'search,replace' );
foreach ( $csv_lines as $line ) {
$this->csv_data[] = array_combine( $csv_head, str_getcsv( $line ) );
}
}
for ( $page = 0; $page < $pages; $page ++ ) {
$start = $page * $page_size;
// Grab the content of the table
$data = $this->dbm->get_table_content( $table, $start, $page_size );
if ( ! $data ) {
$table_report['errors'][] = 'no data in table ' . $table;
}
foreach ( $data as $row ) {
++ $table_report['rows'];
$update_sql = [];
$where_sql = [];
$update = true;
foreach ( $columns as $column ) {
// Skip the GUID column per WordPress Codex.
if ( $column === 'guid' ) {
continue;
}
$data_to_fix = $row[ $column ];
if ( $column === $primary_key ) {
$where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $data_to_fix ) . '"';
continue;
}
// Run a search replace on the data that'll respect the serialisation.
if ( is_serialized( $data_to_fix, false )
&& in_array( strtolower( $column ), $maybe_serialized, true )
) {
// Run a search replace on the data that'll respect the serialisation.
$edited_data = $this->recursive_unserialize_replace( $search, $replace, $data_to_fix );
} else {
$edited_data = str_replace( $search, $replace, $data_to_fix );
}
// Run a search replace by CSV parameters if CSV input present
if ( null !== $csv ) {
foreach ( $this->csv_data as $entry ) {
$edited_data = is_serialized( $edited_data, false ) ?
$this->recursive_unserialize_replace(
$entry['search'],
$entry['replace'],
$edited_data
) : str_replace( $entry['search'], $entry['replace'], $data_to_fix );
}
}
// Something was changed.
if ( $edited_data !== $data_to_fix ) {
++ $table_report['change'];
// log changes
// @todo : does it work with non UTF-8 encodings?
$table_report['changes'][] = [
'row' => $table_report['rows'],
'column' => $column,
'from' => $data_to_fix,
'to' => $edited_data,
];
$update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $edited_data ) . '"';
$update = true;
}
}
// Determine what to do with updates.
if ( true === $this->dry_run ) {
// Don't do anything if a dry run.
continue;
}
if ( $update && ! empty( $where_sql ) && ! empty( $update_sql ) ) {
// If there are changes to make, run the query.
$result = $this->dbm->update( $table, $update_sql, $where_sql );
if ( ! $result ) {
$table_report['errors'][] = sprintf(
/* translators: $1 is the number of rows found in database */
esc_html__( 'Error updating row: %d.', 'search-and-replace' ),
$row
);
} else {
$table_report['updates'] ++;
}
}
}
}
$table_report['end'] = microtime( true );
$this->dbm->flush();
return $table_report;
}
/**
* Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
*
* @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
* @access public
*
* @param array|string $input The string to escape.
*
* @return string
*/
public function mysql_escape_mimic( $input ) {
if ( is_array( $input ) ) {
return array_map( __METHOD__, $input );
}
if ( ! empty( $input ) && is_string( $input ) ) {
return str_replace(
[ '\\', "\0", "\n", "\r", "'", '"', "\x1a" ],
[ '\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z' ],
$input
);
}
return $input;
}
/**
* Recursive unserialize replace
*
* Take a serialised array and unserialize it replacing elements as needed and
* unserializing any subordinate arrays and performing the replace on those too.
*
* Be aware, the method due to his recursive characteristic cannot prevent execution in case the
* data passed to the function isn't a serialized value at the first time.
*
* It's up to you to be sure the value is serialized before call the method.
*
* @param string $from String we're looking to replace.
* @param string $to What we want it to be replaced with.
* @param array|string|object $data Used to pass any subordinate arrays back to in.
* @param bool $serialised Does the array passed via $data need serialising. Default to true.
*
* @throws \Throwable Whatever exception is thrown if WP_DEBUG is true.
*
* @return array The original array with all elements replaced as needed.
*/
public function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialised = true ) {
// Some unserialized data cannot be re-serialised eg. SimpleXMLElements.
try {
$unserialized = is_serialized( $data, false ) ? $this->maybe_unserialize_safe( $data ) : false;
if ( $unserialized !== false && ! is_serialized_string( $data ) ) {
$data = $this->recursive_unserialize_replace( $from, $to, $unserialized, false );
} elseif ( is_array( $data ) ) {
$_tmp = [];
foreach ( (array) $data as $key => $value ) {
$_tmp[ $key ] = $this->recursive_unserialize_replace( $from, $to, $value, false );
}
$data = $_tmp;
unset( $_tmp );
} elseif ( 'object' == gettype( $data ) ) {
if ( $this->is_cloneable( $data ) ) {
$_tmp = clone $data;
$props = get_object_vars( $data );
foreach ( $props as $key => $value ) {
// PHP handles weirdly properties with integer names
if ( is_int( $key ) ) {
continue;
}
// Skip protected properties
if ( is_string( $key ) && preg_match( "/^(\\\\0).+/im", preg_quote( $key ) ) === 1 ) {
continue;
}
$_tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false );
}
$data = $_tmp;
unset( $_tmp );
}
} else {
// Don't process data that isn't a string.
// In this case, just return it because we haven't coverage for this kind of value.
if ( ! is_string( $data ) ) {
return $data;
}
$marker = false;
if ( is_serialized_string( $data ) ) {
// @codingStandardsIgnoreLine
$data = $this->maybe_unserialize_safe( $data );
$marker = true;
}
$tmp_data = $data;
$data = str_replace( $from, $to, $data );
// Do not allow to return valid serialized data,
// If after replacement data is_serialized then add one | to the replacement.
if ( is_serialized( $data, false ) ) {
$data = str_replace( $from, '|' . $to, $tmp_data );
}
if ( $marker ) {
$data = maybe_serialize( $data );
}
}
if ( $serialised ) {
// @codingStandardsIgnoreLine
$data = serialize( $data );
}
} catch ( Exception $throwable ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
throw $throwable;
}
do_action( 'inpsyde.search_and_replace.error', $throwable );
}
return $data;
}
/**
* Returns true, if dry run, false if not
*
* @return bool
*/
public function get_dry_run() {
return $this->dry_run;
}
/**
* Sets the dry run option.
*
* @param bool $state : TRUE for dry run, FALSE for writing changes to DB.
*
* @return bool
*/
public function set_dry_run( $state ) {
if ( is_bool( $state ) ) {
$this->dry_run = $state;
}
return $state;
}
/**
* Unserializes data only if it was serialized.
* unserialize function is called with allowed_classes set to false
* to ensure no object instantiation and magic function calls.
*
* @since 3.2.3
*
* @param string $data Data that might be unserialized.
* @return mixed Unserialized data can be any type.
*/
public function maybe_unserialize_safe( $data ) {
if ( is_serialized( $data ) ) {
return @unserialize( trim( $data ), array('allowed_classes' => false ) );
}
return $data;
}
/**
* Returns true if the object can be cloned.
*
* @param object $object
*
* @return bool
*/
private function is_cloneable( $object ) {
return ( new \ReflectionClass( get_class( $object ) ) )->isCloneable();
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists