پرش به محتوا
پیکربندی

جست‌وجو و جایگزینی آدرس سایت در دیتابیس وردپرس بدون خراب‌کردن داده‌های سریالایزشده

آدرس سایت وردپرس را در دیتابیس عوض کنید بدون اینکه ابزارک‌ها، منوها و تنظیمات قالب خالی شوند. دستور WP-CLI که ساختار داده‌های سریالایزشده را سالم نگه می‌دارد

منتشرشده

هرگز یک UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') ساده را روی کل دیتابیس وردپرس اجرا نکنید. این دستور متن را درست عوض می‌کند و همزمان تنظیمات شما را نابود می‌کند، چون وردپرس آرایه‌ها و آبجکت‌ها را به‌صورت رشته‌های سریالایزشده PHP ذخیره می‌کند و این قالب، طول بایتی هر رشته‌ای را که در خود دارد ثبت می‌کند. یک جایگزینی خام در SQL متن را بازنویسی می‌کند اما هیچ‌وقت به طول ثبت‌شده دست نمی‌زند و مقدار دیگر قابل خواندن نیست.

به‌جایش از ابزاری استفاده کنید که داده سریالایزشده را می‌شناسد. در خط فرمان این ابزار wp search-replace است. اول از دیتابیس بکاپ بگیرید، بعد یک اجرای آزمایشی بگیرید و در نهایت واقعی اجرایش کنید.

دقیقاً چه چیزی خراب می‌شود

یک رشته سریالایزشده PHP این شکلی است:

s:18:"http://example.com"

عدد 18 طول بایتی متنی است که بعد از آن می‌آید. بشمارید — http://example.com دقیقاً ۱۸ کاراکتر است.

حالا یک تنظیم ذخیره‌شده قالب را تصور کنید:

a:1:{s:4:"logo";s:18:"http://example.com";}

یک جایگزینی ساده SQL از http:// به https:// بزنید و انتظار دارید به این برسید:

a:1:{s:4:"logo";s:19:"https://example.com";}

اما نمی‌رسید. چیزی که می‌گیرید این است:

a:1:{s:4:"logo";s:18:"https://example.com";}

رشته حالا ۱۹ بایت است اما هنوز ادعا می‌کند ۱۸ بایت است. PHP طول اعلام‌شده را می‌خواند، ۱۸ بایت جلو می‌رود، کوتیشن پایانی و سمی‌کالن را جایی که انتظار دارد پیدا نمی‌کند و کار را رها می‌کند. کل آرایه در باز شدن شکست می‌خورد.

چرا بی‌صدا شکست می‌خورد

این همان بخشی است که هزینه این باگ را بالا می‌برد. وردپرس خطا نمی‌دهد و متوقف نمی‌شود. وقتی یک گزینه را می‌خواند، مقدار را از تابع کمکی unserialize خودش رد می‌کند، PHP برای داده خراب مقدار false برمی‌گرداند و وردپرس مقداری به کد شما تحویل می‌دهد که انگار آن گزینه هیچ‌وقت ذخیره نشده بوده. افزونه یا قالب هم به پیش‌فرض خودش برمی‌گردد.

پس نشانه، صفحه خطا نیست. نشانه‌ها این‌ها هستند:

  • ابزارک‌ها از ستون کناری ناپدید می‌شوند
  • تنظیمات سفارشی‌ساز به پیش‌فرض قالب برمی‌گردد
  • داده چیدمان صفحه‌ساز خالی برمی‌گردد و صفحه بدون محتوا رندر می‌شود
  • صفحات تنظیمات افزونه‌ها مثل نصب تازه به نظر می‌رسند
  • منوها جایگاه اختصاص‌یافته‌شان را از دست می‌دهند

هیچ‌جای پیشخوان نمی‌گوید مشکلی هست. سطر همچنان در دیتابیس است و همچنان پر از متنی است که درست به نظر می‌رسد. فقط پیشوند طول یکی دو بایت اشتباه است. PHP هنگام شکست unserialize یک اعلان صادر می‌کند، اما روی سایت واقعی که نمایش خطاها خاموش است کسی آن را نمی‌بیند. اگر WP_DEBUG را با WP_DEBUG_LOG فعال کنید و WP_DEBUG_DISPLAY را روی false بگذارید، این اعلان‌ها در یک فایل لاگ می‌نشینند که می‌توانید بخوانیدشان.

آسیب هم یکنواخت پخش نمی‌شود. مقادیر ساده — siteurl، home، محتوای معمولی نوشته، یا آدرسی که در ستون خودش نشسته — از یک جایگزینی خام سالم بیرون می‌آیند. فقط مقادیر سریالایزشده خراب می‌شوند. به همین دلیل است که یک جایگزینی بد در نگاه اول اغلب موفق به نظر می‌رسد.

قبل از دست‌زدن به هر چیزی، wp-config.php را چک کنید

اگر wp-config.php ثابت‌های WP_HOME یا WP_SITEURL را تعریف کرده باشد، این ثابت‌ها بر هر چیزی که در دیتابیس است اولویت دارند. تغییر wp_options انگار اصلاً هیچ کاری نمی‌کند. قبل از اینکه نتیجه بگیرید مشکل از دیتابیس است، دنبال خط‌هایی مثل این‌ها بگردید:

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

یا آن‌ها را به آدرس جدید به‌روز کنید، یا حذفشان کنید تا دیتابیس تصمیم‌گیرنده باشد.

اول بکاپ بگیرید، جدی

برگرداندن یک شکست سریالایز همیشه آسان نیست. وقتی پیشوند طول با محتوا نخواند، مقدار اصلی از روی مقدار خراب قابل بازیابی نیست — بدون داشتن رشته اصلی نمی‌شود فهمید طول درست چقدر بوده. بکاپ تنها راه واقعی برگشت است.

wp db export backup-before-replace.sql

یا اگر WP-CLI در دسترس نیست، با mysqldump:

mysqldump -u USER -p DBNAME > backup-before-replace.sql

اگر می‌توانید، بکاپ را بیرون از سرور نگه دارید. یک فایل بکاپ که در ریشه وب نشسته، هم نقطه بازگردانی است و هم یک مشکل امنیتی.

دستور امن

دستور search-replace در WP-CLI هر مقدار را باز می‌کند، ساختار رمزگشایی‌شده را می‌پیماید، جایگزینی را داخل آن انجام می‌دهد و دوباره با طول‌های درست سریالایز می‌کند. اول اجرای آزمایشی:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

گزارش را بخوانید. به شما می‌گوید در هر جدول و ستون چند جایگزینی انجام خواهد شد. اگر فهرست جدول‌ها غیرمنتظره است، یا جدولی که نمی‌شناسید هزاران مورد تطابق دارد، متوقف شوید و قبل از اجرای واقعی نگاه کنید.

بعد اجرایش کنید:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

چند نکته صادقانه درباره سوئیچ‌ها:

  • --all-tables جدول‌هایی را هم پوشش می‌دهد که خود وردپرس ثبتشان نکرده، و داده افزونه‌ها اغلب همان‌جاست. بدون این گزینه، بعضی جدول‌های افزونه کاملاً نادیده گرفته می‌شوند.
  • --skip-columns=guid مهم است. مقدار guid یک شناسه دائمی برای خوانندگان فید است، نه یک آدرس زنده. بازنویسی‌اش می‌تواند باعث شود مشترکان فید کل بایگانی شما را نوشته جدید ببینند.
  • بدون اسلش انتهایی جست‌وجو کنید و بدون آن نسخه‌هایی از پروتکل که نمی‌خواهید دست بخورند. جایگزینی example.com خالی، آدرس‌های ایمیل و هر اشاره‌ای در متن نوشته‌ها را هم می‌گیرد.

اگر نمی‌توانید از خط فرمان استفاده کنید، افزونه‌های مهاجرتی سازگار با داده سریالایزشده همین کار رمزگشایی را از پیشخوان انجام می‌دهند. رفتاری که دنبالش هستید صراحتاً با عنوان پشتیبانی از داده سریالایزشده توضیح داده می‌شود؛ اگر ابزاری این را نگفته، فرض کنید پشتیبانی نمی‌کند.

کدام جدول‌ها و ستون‌ها درگیرند

جدولستونخطر سریالایز
wp_optionsoption_valueزیاد — ابزارک‌ها، تنظیمات قالب، ترنزینت‌ها، بیشتر تنظیمات افزونه‌ها
wp_postmetameta_valueزیاد — چیدمان صفحه‌سازها، آرایه‌های فیلد سفارشی
wp_usermetameta_valueزیاد — سطوح دسترسی، ترجیحات صفحه و پیشخوان
wp_termmetameta_valueمتوسط — بستگی به افزونه‌ها دارد
wp_postspost_content, guidکم برای محتوا، اما ویژگی‌های شورت‌کد و نشانه‌گذاری بلوک‌ها آدرس دارند
wp_commentscomment_content, comment_author_urlکم
wp_linkslink_url, link_imageکم، و معمولاً بلااستفاده

در حالت چندسایتی، wp_blogs و wp_site و wp_sitemeta و جدول‌های هر سایت مثل wp_2_options و wp_3_options را هم اضافه کنید. تغییر آدرس در چندسایتی قواعد اضافه‌ای فراتر از یک جست‌وجو و جایگزینی دارد — آن را یک کار جداگانه در نظر بگیرید.

مواردی که search-replace کامل حلشان نمی‌کند

صادقانه درباره محدودیت‌ها:

داده JSON ذخیره‌شده در دیتابیس. بعضی صفحه‌سازها چیدمان را به‌شکل JSON با اسلش‌های اسکیپ‌شده ذخیره می‌کنند، پس آدرس شما به‌جای https://example.com به شکل https:\/\/example.com ظاهر می‌شود. جست‌وجو برای شکل عادی آن را پیدا نمی‌کند. ممکن است لازم باشد یک پاس دوم روی رشته اسکیپ‌شده بزنید. روی یک کپی تست کنید.

مقادیر رمزگذاری‌شده با Base64. اگر افزونه‌ای تنظیماتش را قبل از ذخیره انکد کند، هیچ جایگزینی متنی اصلاً آدرس را نمی‌بیند. آن تنظیمات معمولاً باید از صفحه خود افزونه دوباره وارد شوند.

آدرس‌های هاردکدشده در فایل‌ها. هر چیزی که داخل functions.php، یک قالب فرزند یا یک مسیر ثابت فایل نوشته شده، در دیتابیس نیست و دست نمی‌خورد. پوشه قالب را جداگانه grep کنید.

خروجی کش‌شده و تولیدشده. کش آبجکت، کش صفحه و نسخه‌های CDN بعد از یک جایگزینی درست هم آدرس‌های قدیمی را سرو می‌کنند. پاکشان کنید و بعد دوباره چک کنید، قبل از اینکه فرض کنید جایگزینی شکست خورده.

اگر کوئری بد را از قبل اجرا کرده‌اید

بکاپ را برگردانید. جواب همین است و بهتر است رک گفته شود تا اینکه یک روش تعمیر پیشنهاد شود که در بیشتر موارد جواب نمی‌دهد.

اگر بکاپی وجود ندارد، مسیر بازیابی باریک است: مقادیری که اصلاً سریالایز نشده بودند سالم‌اند، و مقادیر سریالایزشده خراب باید دستی بازسازی شوند یا با ذخیره دوباره صفحه تنظیمات مربوطه ریست شوند. ذخیره دوباره یک ناحیه ابزارک، انتخاب دوباره جایگاه منو یا وارد کردن دوباره تنظیمات یک افزونه، مقدار تازه و درست‌سریالایزشده را روی مقدار خراب می‌نویسد. خسته‌کننده است، اما جواب می‌دهد و بهتر از این است که سطرهای خراب سر جایشان بمانند و نفر بعدی را که به سایت نگاه می‌کند سردرگم کنند.

برای ساختن دستورهای درست پیش از اجرای هر چیزی، آن‌ها را با ابزار ساخت کوئری جست‌وجو و جایگزینی وردپرس بسازید — نشان می‌دهد کدام ستون‌ها را می‌شود با SQL ساده دست زد و کدام‌ها به رسیدگی سازگار با سریالایز نیاز دارند، تا این تفکیک را قبل از تعهد به یک دستور ببینید.

FAQ

پرسش‌ها

چرا یک REPLACE ساده در SQL سایت وردپرس را خراب می‌کند؟

چون وردپرس آرایه‌ها و آبجکت‌ها را به‌صورت متن سریالایزشده PHP ذخیره می‌کند و این قالب، طول بایتی هر رشته را هم داخل خودش ثبت می‌کند. یک REPLACE خام متن را عوض می‌کند اما عدد طول قدیمی سر جایش باقی می‌ماند. بعد PHP از باز کردن مقدار سر باز می‌زند، وردپرس به مقدار پیش‌فرض برمی‌گردد و تنظیمات به‌جای اینکه غلط باشند، خالی به نظر می‌رسند.

امن‌ترین راه تغییر آدرس سایت وردپرس در دیتابیس چیست؟

اول یک بکاپ کامل از دیتابیس بگیرید، بعد دستور wp search-replace را با WP-CLI و ابتدا به‌صورت آزمایشی اجرا کنید. WP-CLI هر مقدار را باز می‌کند، جایگزینی را داخل ساختار رمزگشایی‌شده انجام می‌دهد و دوباره با طول‌های اصلاح‌شده سریالایز می‌کند. اگر به خط فرمان دسترسی ندارید، افزونه‌های مهاجرتی که با داده سریالایزشده سازگارند همین کار را از پیشخوان انجام می‌دهند.

کدام جدول‌ها آدرس قدیمی را نگه داشته‌اند؟

عمدتاً wp_options و wp_posts و wp_postmeta و wp_usermeta و wp_termmeta و wp_comments. خطر سریالایز در wp_options و در هر ستون meta_value است، چون ابزارک‌ها، تنظیمات قالب، ترنزینت‌ها و داده‌های صفحه‌ساز به‌شکل آرایه ذخیره می‌شوند. در حالت چندسایتی، جدول‌های wp_blogs و wp_site و wp_sitemeta هم دامنه را در خود دارند.

اگر آدرس در wp-config.php تعریف شده باشد، تغییر دیتابیس مشکل را حل می‌کند؟

نه. اگر wp-config.php ثابت‌های WP_HOME یا WP_SITEURL را تعریف کرده باشد، این ثابت‌ها بر هر چیزی که siteurl و home در wp_options دارند اولویت پیدا می‌کنند. تا وقتی این ثابت‌ها اصلاح یا حذف نشوند، ویرایش دیتابیس هیچ تغییر قابل‌مشاهده‌ای ندارد. اول wp-config.php را ببینید؛ همین یک خط، دلیل خیلی از مهاجرت‌های شکست‌خورده است.

ستون guid را هم باید جایگزین کنم؟

نه. مقدار guid یک شناسه دائمی برای خوانندگان فید است، نه یک آدرس کارآمد، و بازنویسی آن می‌تواند باعث شود مشترکان فید تمام نوشته‌های قدیمی را دوباره به‌عنوان نوشته جدید ببینند. هنگام اجرای WP-CLI گزینه skip-columns را با guid بدهید. تنها استثنای رایج، سایتی است که هیچ‌وقت فید عمومی یا مشترک نداشته.

چطور بفهمم بعد از یک جایگزینی اشتباه، مقداری خراب شده است؟

مقادیر سریالایزشده خراب در دیتابیس کاملاً عادی به نظر می‌رسند اما در وردپرس خالی برمی‌گردند. ابزارک‌ها ناپدید می‌شوند، تنظیمات سفارشی‌ساز ریست می‌شود و گزینه‌های افزونه بدون هیچ پیام خطایی به پیش‌فرض برمی‌گردند. فعال‌کردن WP_DEBUG و WP_DEBUG_LOG اعلان مربوط به شکست unserialize را نمایان می‌کند. مقایسه طول اعلام‌شده رشته با تعداد بایت واقعی هم مستقیماً موضوع را تأیید می‌کند.