From 5c542e2733a6b879f3c3a453bc6a8b805e10571b Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 24 Aug 2026 12:48:10 +0330 Subject: [PATCH] fix(settings): never plant deploy seed defaults on restore-from-backup Ignore limangold/payping seed URLs from backup or superadmin when filling; keep any real local value untouched unless force-overwrite. Co-authored-by: Cursor --- .../SettingsRestoreFromBackupCommand.php | 145 +++++++++++++----- 1 file changed, 106 insertions(+), 39 deletions(-) diff --git a/app/Console/Commands/SettingsRestoreFromBackupCommand.php b/app/Console/Commands/SettingsRestoreFromBackupCommand.php index 97a4f92..18cae5b 100644 --- a/app/Console/Commands/SettingsRestoreFromBackupCommand.php +++ b/app/Console/Commands/SettingsRestoreFromBackupCommand.php @@ -13,12 +13,10 @@ use Illuminate\Support\Facades\Schema; * بازگرداندن مقادیر تنظیمات can_edit=0 از جدول بکاپ (پیش‌فرض settings_copy1) * به جدول settings — بعد از این‌که مالکیت این کلیدها از سوپرادمین به همین نصب برگشت. * - * قاعده سخت: اگر ردیف لوکال value غیرخالی دارد، هرگز عوض نمی‌شود - * (حتی با --overwrite؛ برای اجبار از --force-overwrite استفاده کن). - * - * پر کردن فقط برای ردیف غایب یا value خالی: - * 1) اگر --fill-from-superadmin و سوپرادمین مقدار دارد → همان - * 2) وگرنه مقدار بکاپ + * قواعد سخت: + * 1) value غیرخالی لوکال هرگز عوض نمی‌شود (مگر --force-overwrite). + * 2) پیش‌فرض‌های seed/deploy (مثل limangold.ir) هرگز به‌عنوان مقدار واقعی نوشته نمی‌شوند. + * 3) با --fill-from-superadmin برای خالی/غایب: اول SA واقعی، بعد بکاپ واقعی. * * کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند. */ @@ -32,10 +30,34 @@ class SettingsRestoreFromBackupCommand extends Command protected $description = 'بازگرداندن مقادیر تنظیمات پیشرفته از جدول بکاپ به settings (بدون دست زدن به value موجود)'; - /** پیش‌فرض‌های deploy که در بکاپ مانده‌اند و نباید بر مقدار واقعی سوپرادمین بچربند */ - private const DEPLOY_DEFAULT_VALUES = [ + /** + * پیش‌فرض‌های INSERT در deploy_settings_const — اگر فقط همین‌ها در بکاپ/SA باشند، + * نوشته نمی‌شوند (خالی می‌ماند تا مقدار واقعی پروژه از بین نرود). + * + * @var array> + */ + private const KEY_SEED_DEFAULTS = [ + 'montzar_base_url' => [ + 'https://limangold.ir', + 'http://limangold.ir', + ], + 'payping_fin_url' => [ + 'https://limangold.ir/kernel/admin/fin', + ], + 'payping_start_url' => [ + 'https://payping.limangold.ir/payment/start', + ], + 'payping_verify_url' => [ + 'https://payping.limangold.ir/payment/verify', + ], + ]; + + /** دامنه/مقدارهایی که در هر کلیدی seed محسوب می‌شوند */ + private const GLOBAL_SEED_MARKERS = [ 'https://limangold.ir', 'http://limangold.ir', + 'https://payping.limangold.ir', + 'http://payping.limangold.ir', ]; public function handle(): int @@ -77,18 +99,29 @@ class SettingsRestoreFromBackupCommand extends Command $hasType = Setting::hasTypeColumn(); $report = []; - $counts = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'kept' => 0]; + $counts = [ + 'inserted' => 0, + 'updated' => 0, + 'skipped' => 0, + 'kept' => 0, + 'seed_ignored' => 0, + ]; foreach ($keys as $key) { $backup = $backupRows[$key] ?? null; $local = $localRows->get($key); - [$value, $source] = $this->resolveFillValue( + [$value, $source, $ignoredSeed] = $this->resolveFillValue( + $key, $backup['value'] ?? null, $superAdminValues[$key] ?? null, $fillFromSuperAdmin ); + if ($ignoredSeed) { + $counts['seed_ignored']++; + } if (! $local) { + // ردیف غایب: فقط اگر مقدار واقعی داریم value می‌گذاریم؛ وگرنه خالی $counts['inserted']++; $report[] = [$key, '—', $this->mask($key, $value), 'INSERT '.($source ?: 'empty')]; if (! $dryRun) { @@ -109,30 +142,38 @@ class SettingsRestoreFromBackupCommand extends Command } $localValue = trim((string) ($local->value ?? '')); - $localIsDeployDefault = $this->isDeployDefault($localValue); + $localIsSeed = $localValue !== '' && $this->isSeedDefault($key, $localValue); + $saReal = isset($superAdminValues[$key]) && ! $this->isSeedDefault($key, $superAdminValues[$key]) + ? $superAdminValues[$key] + : null; - // قاعده سخت: value واقعی موجود دست نخوره - // استثنا: لوکال فقط پیش‌فرض deploy (مثل limangold.ir) است و SA مقدار واقعی دارد - $allowReplaceDefault = $fillFromSuperAdmin - && $localIsDeployDefault - && isset($superAdminValues[$key]) - && ! $this->isDeployDefault($superAdminValues[$key]); + // value واقعی موجود: دست نخوره + // استثنا: لوکال فقط seed است و SA مقدار واقعی دارد → اصلاح اشتباه قبلی + $allowReplaceSeed = $fillFromSuperAdmin && $localIsSeed && $saReal !== null; - if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceDefault) { + if ($localValue !== '' && ! $forceOverwrite && ! $allowReplaceSeed) { $counts['kept']++; continue; } + if ($allowReplaceSeed) { + $value = $saReal; + $source = 'superadmin'; + } + $writeValue = $value !== null && $value !== '' + && ! $this->isSeedDefault($key, $value) && ( $localValue === '' - || $allowReplaceDefault + || $allowReplaceSeed || ($forceOverwrite && $localValue !== (string) $value) ); + // فقط meta — و فقط وقتی value را عوض نمی‌کنیم و لوکال seed/خالی نیست + // برای ردیف با value واقعی، meta هم دست نمی‌زنیم (جلوگیری از side-effect) $metaChanged = false; - if ($backup) { + if ($backup && ($localValue === '' || $allowReplaceSeed || $forceOverwrite)) { $metaChanged = $this->metaDiffers($local, $backup, $hasType); } @@ -157,7 +198,7 @@ class SettingsRestoreFromBackupCommand extends Command if ($writeValue) { $local->value = $value; } - if ($backup) { + if ($backup && $metaChanged) { $local->title_fa = $backup['title_fa'] ?: ($local->title_fa ?: $key); if (($backup['description'] ?? '') !== '') { $local->description = $backup['description']; @@ -174,16 +215,17 @@ class SettingsRestoreFromBackupCommand extends Command } $this->info(sprintf( - '%sinsert=%d update=%d kept=%d skip=%d (backup=%s, superadmin=%d)', + '%sinsert=%d update=%d kept=%d skip=%d seed_ignored=%d (backup=%s, superadmin=%d)', $dryRun ? '[dry-run] ' : '', $counts['inserted'], $counts['updated'], $counts['kept'], $counts['skipped'], + $counts['seed_ignored'], $table, count($superAdminValues) )); - $this->comment('kept = ردیف‌هایی که value غیرخالی داشتند و دست نخورده ماندند.'); + $this->comment('kept = value واقعی لوکال دست نخورده | seed_ignored = پیش‌فرض limangold/... از منبع پر کردن حذف شد'); if ($dryRun) { $this->comment('هیچ چیزی نوشته نشد. برای اجرا همین دستور را بدون --dry-run بزن.'); @@ -193,45 +235,72 @@ class SettingsRestoreFromBackupCommand extends Command } /** - * برای پر کردن خالی/غایب: با --fill-from-superadmin اول SA، بعد بکاپ. - * - * @return array{0: ?string, 1: string} + * @return array{0: ?string, 1: string, 2: bool} [value, source, ignoredSeed] */ - private function resolveFillValue(?string $backupValue, ?string $superAdminValue, bool $preferSuperAdmin): array - { - $backupValue = $backupValue === null ? null : trim($backupValue); - $superAdminValue = $superAdminValue === null ? null : trim($superAdminValue); + private function resolveFillValue( + string $key, + ?string $backupRaw, + ?string $superAdminRaw, + bool $preferSuperAdmin + ): array { + $backupWasSeed = $backupRaw !== null && trim($backupRaw) !== '' && $this->isSeedDefault($key, trim($backupRaw)); + $saWasSeed = $superAdminRaw !== null && trim($superAdminRaw) !== '' && $this->isSeedDefault($key, trim($superAdminRaw)); + $ignoredSeed = $backupWasSeed || $saWasSeed; + + $backupValue = $this->nullIfSeed($key, $backupRaw); + $superAdminValue = $this->nullIfSeed($key, $superAdminRaw); if ($preferSuperAdmin && $superAdminValue !== null && $superAdminValue !== '') { - return [$superAdminValue, 'superadmin']; + return [$superAdminValue, 'superadmin', $ignoredSeed]; } if ($backupValue !== null && $backupValue !== '') { - return [$backupValue, 'backup']; + return [$backupValue, 'backup', $ignoredSeed]; } - return [null, '']; + return [null, '', $ignoredSeed]; } - private function isDeployDefault(string $value): bool + private function nullIfSeed(string $key, ?string $value): ?string + { + if ($value === null) { + return null; + } + $value = trim($value); + if ($value === '') { + return null; + } + if ($this->isSeedDefault($key, $value)) { + return null; + } + + return $value; + } + + private function isSeedDefault(string $key, string $value): bool { $normalized = rtrim(strtolower(trim($value)), '/'); if ($normalized === '') { return false; } - foreach (self::DEPLOY_DEFAULT_VALUES as $default) { + foreach (self::KEY_SEED_DEFAULTS[$key] ?? [] as $default) { if ($normalized === rtrim(strtolower($default), '/')) { return true; } } + foreach (self::GLOBAL_SEED_MARKERS as $marker) { + $m = rtrim(strtolower($marker), '/'); + if ($normalized === $m || str_starts_with($normalized, $m.'/')) { + return true; + } + } + return false; } /** - * ردیف‌های can_edit=0 بکاپ، بدون کلیدهای سوپرادمین و runtime. - * * @return array> */ private function readBackup(string $table): array @@ -264,8 +333,6 @@ class SettingsRestoreFromBackupCommand extends Command } /** - * مقدار غیرخالی کلیدهای غیرسوپرادمینی که هنوز در سوپرادمین مانده‌اند. - * * @return array */ private function superAdminValues(): array