Как восстановить сериализованные данные после редактирования дампа MySQL в текстовом редакторе


Иногда возникает задача переноса сайта под управлением WordPress с одного хостинга на другой со сменой доменного имени. Дело в том, что WordPress, записывая данные в базу использует абсолютные пути для всех медиаданных. Это значит, что при загрузке изображения, в базу записывается путь http://mysite.ru/wp-content/uploads/2010/01/file.jpgвместо /wp-content/uploads/2010/01/file.jpg. Поэтому при смене доменного имени логично предположить, что все медиа данные отображены не будут.

Для того чтобы перенести сайт под управлением WordPress на новый доменсоблюдаем следующую последовательность:

  1. Создаем дамп базы MySQL (лучше использовать программу dbForge Studio for MySQL)
  2. Открываем полученный дамп в текстовом редакторе
  3. Заменяем все вхождения одной строки со старым доменом на новую строку с новым доменом. Например http://site1.ru на http://site2.ru
  4. Сохраняем изменённый файл
  5. Восстанавливаем базу из отредактированного файла

Все бы хорошо, но при таком способе могут поломаться сериализованные данные. Речь вот о чем. некоторые плагины или функции записывают данные в базу в виде сериализованных строк. Например, вот в таком виде:

a:1:{s:4:"Test";s:17:"unserialize here!";}

При считывании из базы происходит как бы распаковка сериализованных строк. Например, строка выше предстает в виде:

array (
'Test' => 'unserialize here!',
)

Как вы уже поняли, если в таких строках вы замените одну запись на другую, то это вызовет ошибку, поскольку значение поля изменится, а его длина не будет соответствовать новому значению, и данные не будут прочитаны. Например, возьмем часть сериализованной строки, где значение строки в таком виде имеет вид s:4:"Test". Это значит строка из 4 символов. Заменив Test на New мы должны изменить и длину строки, нов дампе где сотни и тысячи строк это очень проблематично.

На практике я столкнулся с подобной проблемой при переносе данных на новый домен сайта с плагином WooCommerce. Вернее для этого плагина стояло дополнение YIKES Custom Product Tabs for WooCommerce. Это дополнение создает кастомные вкладки на странице товара, и в эти вкладки можно добавить текст или изображения. Ну а хранятся эти вкладки в базе в виде сериализованных строк, которые и были изменены при переносе сайта с одного домена на другой.

Немного поразмыслив, я понял, что нужно приводить сериализованные строки в порядок. Изменить значения длинны новых строк в сериализованных данных я решил при помощи регулярных выражений, точнее вот такого:

$string = preg_replace('!s:(\d+):([\\\\]?"[\\\\]?"|[\\\\]?"((.*?)[^\\\\])[\\\\]?");!e', "'s:'.strlen(unescape_mysql('$3')).':\"'.unescape_quotes('$3').'\";'", $string);

Где соответственно функции:

// Unescape to avoid dump-text issues
function unescape_mysql($value) {
	return str_replace(array("\\\\", "\\0", "\\n", "\\r", "\Z", "\'", '\"'),
					 array("\\", "\0", "\n", "\r", "\x1a", "'", '"'), 
					 $value);
}	
// Fix strange behaviour if you have escaped quotes in your replacement
function unescape_quotes($value) {
	return str_replace('\"', '"', $value);
}

За основу был взят плагин FG Fix Serialized Stringsкоторый исправляет сериализованные строки в таблице Options, и немного переделан.

<?php
/**
* Plugin Name: FG Fix Serialized Strings
* Plugin Uri: http://wordpress.org/extend/plugins/fg-fix-serialized-strings
* Description: Fix the broken serialized strings in the options table
* Version: 1.1.1
* Author: Frédéric GILLES
*/

// Exit if accessed directly
if ( !defined( 'ABSPATH' ) ) exit;

add_action( 'plugins_loaded', 'fgfss_load', 20 );

if ( !function_exists( 'fgfss_load' ) ) {
	function fgfss_load() {
		new fgfss();
	}
}

if ( !class_exists('fg-fix-serialized-strings', false) ) {
	class fgfss {

		private $strings_fixed_count = 0;
		private $restored_widgets_count = 0;

		public function __construct() {
			load_plugin_textdomain( 'fg-fix-serialized-strings', null, basename(dirname( __FILE__ )) . '/languages' );
			add_action( 'admin_menu', array(&$this, 'add_menu') );
		}

		/**
		 * Add the menu in WP backend
		 */
		public function add_menu() {
			$menu_title = __('Fix Serialized Strings', 'fg-fix-serialized-strings');
			add_submenu_page( 'tools.php', $menu_title, $menu_title, 'manage_options', 'fg-fix-serialized-strings', array(&$this, 'build_page') );
		}

		/**
		 * Build the tools page
		 */
		public function build_page() {
			if ( isset( $_POST['fix-serialized-strings'] ) ) {
				check_admin_referer('fgfss-fix');
				$this->fix_options_table();
				//$this->restore_lost_widgets();
			}
			include(dirname(__FILE__) . '/fg-fix-serialized-strings.tpl.php');
		}

		/**
		 * Fix the options table
		 */
		private function fix_options_table() {
			global $wpdb;
			$sql = "SELECT meta_id, meta_value FROM $wpdb->postmeta";
			$postmeta = $wpdb->get_results($sql);
			foreach ( $postmeta as $option ) {

				$fixed_string = $this->fix_serialized($option->meta_value);
				if ( $fixed_string != $option->meta_value ) {

					//echo $fixed_string;
					// update the table
					
					$wpdb->update(
						$wpdb->postmeta,
						array('meta_value' => $fixed_string), // data
						array('meta_id'	=> $option->meta_id) // where
					);
					
					$this->strings_fixed_count++;
				}
			}
			$this->display_admin_notice(sprintf(_n('%d string fixed', '%d strings fixed', $this->strings_fixed_count, 'fg-fix-serialized-strings'), $this->strings_fixed_count));
		}

		/**
		 * Restore the lost text widgets
		 */
		private function restore_lost_widgets() {
			$sidebars_widgets = get_option('sidebars_widgets');
			$text_widgets = get_option('widget_text');
			if ( is_array($text_widgets ) ) {
				foreach ( $text_widgets as $text_id => $text_widget ) {
					if ( is_array($text_widget) ) {
						// test if the text widget exists in the sidebars widgets
						$widget_is_found = false;
						$text_widget_id = 'text-' . $text_id;
						foreach ( $sidebars_widgets as $sidebar_widgets ) {
							if ( is_array($sidebar_widgets) ) {
								if ( array_search($text_widget_id, $sidebar_widgets) !== false ) {
									// the widget is found
									$widget_is_found = true;
									break;
								}
							}
						}
						if ( !$widget_is_found ) {
							// Restore the widget into the inactive widgets
							$sidebars_widgets['wp_inactive_widgets'][] = $text_widget_id;
							update_option('sidebars_widgets', $sidebars_widgets);
							$this->restored_widgets_count++;
						}
					}
				}
			}
			$this->display_admin_notice(sprintf(_n('%d widget restored', '%d widgets restored', $this->restored_widgets_count, 'fg-fix-serialized-strings'), $this->restored_widgets_count));
			if ( $this->restored_widgets_count > 0 ) {
				$this->display_admin_notice(__('The restored widgets will be found in the inactive widgets section.', 'fg-fix-serialized-strings'));
			}
		}
		
		/**
		 * Display admin notice
		 *
		 * @param string $message Message to display as a notice
		 */
		protected function display_admin_notice( $message )	{
			echo '<div class="updated"><p>[fg-fix-serialized-strings] '.$message.'</p></div>';
		}


		/**
		 * Fix a serialized string
		 */
		private function fix_serialized($string) {
			if ( !preg_match('/^[aOs]:/', $string) ) {
				return $string;
				breack;
			}
			if ( @unserialize($string) !== false ) {
				return $string;
				breack;
			}
			$string = preg_replace('!s:(\d+):([\\\\]?"[\\\\]?"|[\\\\]?"((.*?)[^\\\\])[\\\\]?");!e', "'s:'.strlen(unescape_mysql('$3')).':\"'.unescape_quotes('$3').'\";'", $string);
			//$string = preg_replace_callback('/\bs:(\d+):"(.*?)"/', array($this, 'fix_str_length'), $string);
			return $string;
		}

		/**
		 * Callback function for replacing the string
		 */
		private function fix_str_length($matches) {
			$string = trim($matches[2]);
			$right_length = strlen(($string));
			return 's:' . $right_length . ':"' . ($string) . '"';
		}


	}
}		
// Unescape to avoid dump-text issues
function unescape_mysql($value) {
	return str_replace(array("\\\\", "\\0", "\\n", "\\r", "\Z", "\'", '\"'),
					 array("\\", "\0", "\n", "\r", "\x1a", "'", '"'), 
					 $value);
}	
// Fix strange behaviour if you have escaped quotes in your replacement
function unescape_quotes($value) {
	return str_replace('\"', '"', $value);
}	

Измененный код производит восстановление поломанных сериализованных строк в таблице postmeta.

На этом пока все. Надеюсь вам будет полезен этот материал

Кстати, можно исправлять сразу в дампе SQL, используя PHP скрипт Fix-Serialization. Для этого нужно выполнить команду /usr/bin/php fix-serialization.php my-sql-file.sql

#!/usr/bin/php
<?php

/*
* Blogestudio Fix Serialization	1.2
* Fixer script of length attributes for serialized strings (e.g. WordPress databases)
* License: GPL version 3 or later - http://www.gnu.org/licenses/gpl.txt
* By Pau Iglesias
* http://blogestudio.com
* 
* Inspiration and regular expression code base from David Coveney:
* http://davidcoveney.com/575/php-serialization-fix-for-wordpress-migrations/
* 
* Usage:
*
* 	/usr/bin/php fix-serialization.php my-sql-file.sql
*
* Versions:
* 
* 	1.0 2011-08-03 Initial release
* 	1.1 2011-08-18 Support for backslashed quotes, added some code warnings
* 	1.2 2011-09-29 Support for null or zero length strings after preg_replace is called, and explain how to handle these errors
* 
* Knowed errors:
*
* - Memory size exhausted
* Allowed memory size of 67108864 bytes exhausted (tried to allocate 35266489 bytes)
* How to fix: update php.ini memory_limit to 512M or more, and restart cgi service or web server
*
* - Function preg_replace returns null or 0 length string
* If preg_last_error = PREG_BACKTRACK_LIMIT_ERROR (value 2), increase pcre.backtrack_limit in php.ini (by default 100k, change to 2M by example)
* Same way for others preg_last_error codes: http://www.php.net/manual/en/function.preg-last-error.php
* 
* TODO next versions
* 
* - Check if needed UTF-8 support detecting and adding u PCRE modifier
* 
*/



// Unescape to avoid dump-text issues
function unescape_mysql($value) {
	return str_replace(array("\\\\", "\\0", "\\n", "\\r", "\Z", "\'", '\"'),
					 array("\\", "\0", "\n", "\r", "\x1a", "'", '"'), 
					 $value);
}



// Fix strange behaviour if you have escaped quotes in your replacement
function unescape_quotes($value) {
	return str_replace('\"', '"', $value);
}	



// Check command line arguments
if (!(isset($argv) && isset($argv[1]))) {
	
	// Error
	echo 'Error: no input file specified'."\n\n";

// With arguments
} else {
	
	// Compose path from argument
	$path = dirname(__FILE__).'/'.$argv[1];
	if (!file_exists($path)) {
	
		// Error
		echo 'Error: input file does not exists'."\n";
		echo $path."\n\n";
	
	// File exists
	} else {
	
		// Get file contents
		if (!($fp = fopen($path, 'r'))) {
			
			// Error
			echo 'Error: can`t open input file for read'."\n";
			echo $path."\n\n";
		
		// File opened for read
		} else {
			
			// Initializations
			$do_preg_replace = false;
		
			// Copy data
			if (!($data = fread($fp, filesize($path)))) {

				// Error
				echo 'Error: can`t read entire data from input file'."\n";
				echo $path."\n\n";
			
			// Check data
			} elseif (!(isset($data) && strlen($data) > 0)) {

				// Warning
				echo "Warning: the file is empty or can't read contents\n";
				echo $path."\n\n";
			
			// Data ok
			} else {

				// Tag context
				$do_preg_replace = true;

				// Replace serialized string values
				$data = preg_replace('!s:(\d+):([\\\\]?"[\\\\]?"|[\\\\]?"((.*?)[^\\\\])[\\\\]?");!e', "'s:'.strlen(unescape_mysql('$3')).':\"'.unescape_quotes('$3').'\";'", $data);
			}

			// Close file
			fclose($fp);
			
			// Check data
			if (!(isset($data) && strlen($data) > 0)) {
				
				// Check origin
				if ($do_preg_replace) {

					// Error
					echo "Error: preg_replace returns nothing\n";
					if (function_exists('preg_last_error')) echo "preg_last_error() = ".preg_last_error()."\n";
					echo $path."\n\n";
				}
			
			// Data Ok
			} else {

				// And finally write data
				if (!($fp = fopen($path, 'w'))) {

					// Error
					echo "Error: can't open input file for writing\n";
					echo $path."\n\n";
					
				// Open for write
				} else {
					
					// Write file data
					if (!fwrite($fp, $data)) {
						
						// Error
						echo "Error: can't write input file\n";
						echo $path."\n\n";
					}
					
					// Close file
					fclose($fp);
				}
			}
		}
	}
}



?>

UPD. Нашел еще один вариант исправления сериализованных строк (Repair a Serialized Array):

/**
* Extract what remains from an unintentionally truncated serialized string
*
* Example Usage:
* 
* the native unserialize() function returns false on failure
* $data = @unserialize($serialized); // @ silences the default PHP failure notice
* if ($data === false) // could not unserialize
* { 
* $data = repairSerializedArray($serialized); // salvage what we can
* }
*
* $data contains your original array (or what remains of it).

* @param string The serialized array
*/
public function repairSerializedArray($serialized)
{
$tmp = preg_replace('/^a:\d+:\{/', '', $serialized);
return $this->repairSerializedArray_R($tmp); // operates on and whittles down the actual argument
}

/**
* The recursive function that does all of the heavy lifing. Do not call directly.
* @param string The broken serialzized array
* @return string Returns the repaired string
*/
private function repairSerializedArray_R(&$broken)
{
// array and string length can be ignored
// sample serialized data
// a:0:{}
// s:4:"four";
// i:1;
// b:0;
// N;
$data = array();
$index = null;
$len = strlen($broken);
$i = 0;

while(strlen($broken))
{
$i++;
if ($i > $len)
{
break;
}

if (substr($broken, 0, 1) == '}') // end of array
{
$broken = substr($broken, 1);
return $data;
}
else
{
$bite = substr($broken, 0, 2);
switch($bite)
{ 
case 's:': // key or value
$re = '/^s:\d+:"([^\"]*)";/';
if (preg_match($re, $broken, $m))
{
if ($index === null)
{
$index = $m[1];
}
else
{
$data[$index] = $m[1];
$index = null;
}
$broken = preg_replace($re, '', $broken);
}
break;

case 'i:': // key or value
$re = '/^i:(\d+);/';
if (preg_match($re, $broken, $m))
{
if ($index === null)
{
$index = (int) $m[1];
}
else
{
$data[$index] = (int) $m[1];
$index = null;
}
$broken = preg_replace($re, '', $broken);
}
break;

case 'b:': // value only
$re = '/^b:[01];/';
if (preg_match($re, $broken, $m))
{
$data[$index] = (bool) $m[1];
$index = null;
$broken = preg_replace($re, '', $broken);
}
break;

case 'a:': // value only
$re = '/^a:\d+:\{/';
if (preg_match($re, $broken, $m))
{
$broken = preg_replace('/^a:\d+:\{/', '', $broken);
$data[$index] = $this->repairSerializedArray_R($broken);
$index = null;
}
break;

case 'N;': // value only
$broken = substr($broken, 2);
$data[$index] = null;
$index = null;
break;
}
}
}

return $data;
}




Реклама
Поделиться
Качественные премиум темы и шаблоны для Вашего сайта:

Смотреть полный каталог качественных тем и шаблонов

Здесь Вы можете выбрать из более чем 46 000 готовых дизайнов. Шаблоны сайтов + установка + хостинг + персонализация + поисковая оптимизация + копирайтинг — все эти услуги вы всегда можете получить от профессионалов мирового уровня!

Один отзыв

  1. Вадим29/07/2016

Добавить коментарий

восемнадцать + 10 =