first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<?php
/**
* ProLegacy addon - Unified legacy compatibility system
*
* Name: Duplicator PRO Legacy Support
* Version: 1.0.0
* Description: Provides backward compatibility for deprecated prefixes (hooks, options, temp files)
* Author: Duplicator
* Author URI: https://duplicator.com
* Requires PHP: 7.4
* Requires Duplicator min version: 4.5.24
*
* PHP version 7.4
*
* @category Duplicator
* @package Addons\ProLegacy
* @author Duplicator <support@duplicator.com>
* @copyright 2011-2025 Snap Creek LLC
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
* @link https://duplicator.com/
*/
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
/**
* ProLegacy Addon Class
*
* Provides unified legacy compatibility system:
* - Hook forwarding: old → new hook names (40 public hooks)
* - Options & user meta migration during upgrade
* - Temporary files cleanup (dup_pro_* prefixes)
* - Upgrade coordination and logging
*
* @category Duplicator
* @package Addons\ProLegacy
* @author Duplicator <support@duplicator.com>
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
* @link https://duplicator.com/
*/
class ProLegacy extends \Duplicator\Core\Addons\AbstractAddonCore
{
/**
* Initialize addon
*
* @return void
*/
public function init(): void
{
VersionMigration::init();
BackupDirMigration::init();
LegacyUpgrade::init();
HookForwarding::init();
LegacyFiles::init();
}
/**
* Get addon directory path
*
* @return string
*/
public static function getAddonPath(): string
{
return __DIR__;
}
/**
* Get addon main file path
*
* @return string
*/
public static function getAddonFile(): string
{
return __FILE__;
}
}

View File

@@ -0,0 +1,3 @@
<?php
/* Silence is golden. */

View File

@@ -0,0 +1,96 @@
<?php
/**
* Backup directory migration from backups-dup-pro to duplicator-backups
*
* @package Duplicator\Addons\ProLegacy
* @copyright (c) 2025, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Utils\Logging\DupLog;
use Throwable;
/**
* Handles migration of backup directory from old to new name
*
* Must run BEFORE initEntities (priority 50) which calls initStorageDirectory()
* that creates the backup directory if it doesn't exist.
*
* Race condition handling: During upgrade, DupLog::trace() calls from earlier
* priority hooks (e.g., migrateOptionPrefixes at priority 12) may initialize
* TraceLogMng which creates the new directory structure prematurely. This class
* handles that case by moving any contents from the new directory back to the
* old one before performing the rename.
*/
class BackupDirMigration
{
const OLD_DIR_NAME = 'backups-dup-pro';
const NEW_DIR_NAME = 'duplicator-backups';
/**
* Initialize migration hook
*
* Priority 45: Before initEntities (50) which calls
* StoragesUtil::initDefaultStorage() -> initStorageDirectory()
*
* @return void
*/
public static function init(): void
{
add_action('duplicator_upgrade', [__CLASS__, 'migrate'], 45, 2);
}
/**
* Migrate backup directory from old name to new name
*
* If the new directory already exists (created prematurely by trace logging),
* copies its contents to the old directory first, then performs the rename.
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrate($currentVersion, $newVersion): void
{
// Skip for new installations
if ($currentVersion === false) {
return;
}
try {
$contentDir = untrailingslashit(wp_normalize_path((string) realpath(WP_CONTENT_DIR)));
$oldPath = $contentDir . '/' . self::OLD_DIR_NAME;
$newPath = $contentDir . '/' . self::NEW_DIR_NAME;
if (!is_dir($oldPath)) {
return;
}
if (is_dir($newPath)) {
// If new directory exists, copy its contents into old directory first
if (!SnapIO::rcopy($newPath, $oldPath)) {
DupLog::trace("LEGACY MIGRATION: Failed to copy contents from new directory to old: " . $newPath);
return;
}
if (!SnapIO::rrmdir($newPath)) {
DupLog::trace("LEGACY MIGRATION: Failed to remove new directory after merge: " . $newPath);
return;
}
}
if (SnapIO::rename($oldPath, $newPath)) {
DupLog::trace("LEGACY MIGRATION: Renamed backup directory from " . self::OLD_DIR_NAME . " to " . self::NEW_DIR_NAME);
} else {
DupLog::trace("LEGACY MIGRATION: Failed to rename backup directory");
}
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating backup directory: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
/**
* Hook Forwarding Class
*
* Forwards old hook names (duplicator_pro_*) to new hook names (duplicator_*)
* for backward compatibility with third-party code
*/
class HookForwarding
{
/**
* Version when old hooks were deprecated
*/
private const DEPRECATION_VERSION = '4.5.25';
/**
* Registry of public hooks requiring backward compatibility
* Maps new hook names to array with old hook name and accepted args count
*
* Only 4 public actions are documented and supported for backward compatibility.
* All other hooks are internal use only.
*/
private const HOOKS_REGISTRY = [
'duplicator_build_before_start' => [
'old_hook' => 'duplicator_pro_build_before_start',
'accepted_args' => 1,
],
'duplicator_build_completed' => [
'old_hook' => 'duplicator_pro_build_completed',
'accepted_args' => 1,
],
'duplicator_build_fail' => [
'old_hook' => 'duplicator_pro_build_fail',
'accepted_args' => 1,
],
'duplicator_first_login_after_install' => [
'old_hook' => 'duplicator_pro_first_login_after_install',
'accepted_args' => 1,
],
];
/**
* Initialize hook forwarding system
*
* @return void
*/
public static function init(): void
{
self::registerForwarding();
}
/**
* Register hook forwarding for all registered hooks
*
* When new code fires do_action('duplicator_build_before_start', $package),
* this callback triggers do_action_deprecated() for the old hook name,
* which shows a deprecation warning in WP_DEBUG mode and executes callbacks.
*
* Priority 9999 ensures this runs last, after all other callbacks on the new hook.
*
* @return void
*/
private static function registerForwarding(): void
{
foreach (self::HOOKS_REGISTRY as $newHook => $hookData) {
add_action($newHook, function (...$args) use ($newHook, $hookData) {
do_action_deprecated(
$hookData['old_hook'],
$args,
self::DEPRECATION_VERSION,
$newHook
);
}, 9999, $hookData['accepted_args']);
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Legacy constants mapping for backward compatibility
*
* This file maps old DUPLICATOR_PRO_* constants to new DUPLICATOR_* constants.
* Users who have defined old constants in wp-config.php will still have them work.
*
* IMPORTANT: This file must be loaded BEFORE define.php
*
* @package Duplicator\Addons\ProLegacy
* @copyright (c) 2025, Snap Creek LLC
*/
namespace Duplicator\Addons\ProLegacy;
/**
* Maps legacy DUPLICATOR_PRO_* constants to new DUPLICATOR_* constants
*
* Only includes constants that were overridable in wp-config.php (defined with if (!defined()) pattern)
*/
class LegacyConstants
{
/**
* Mapping of old constant names to new constant names
*
* Only constants that were user-overridable in the original define.php
*
* @var array<string,string>
*/
private const CONSTANTS_MAP = [
'DUPLICATOR_PRO_SSDIR_NAME' => 'DUPLICATOR_SSDIR_NAME',
'DUPLICATOR_PRO_DEBUG_TPL_OUTPUT_INVALID' => 'DUPLICATOR_DEBUG_TPL_OUTPUT_INVALID',
'DUPLICATOR_PRO_DEBUG_TPL_DATA' => 'DUPLICATOR_DEBUG_TPL_DATA',
'DUPLICATOR_PRO_INDEX_INCLUDE_HASH' => 'DUPLICATOR_INDEX_INCLUDE_HASH',
'DUPLICATOR_PRO_INDEX_INCLUDE_INSTALLER_FILES' => 'DUPLICATOR_INDEX_INCLUDE_INSTALLER_FILES',
'DUPLICATOR_PRO_DISALLOW_IMPORT' => 'DUPLICATOR_DISALLOW_IMPORT',
'DUPLICATOR_PRO_DISALLOW_RECOVERY' => 'DUPLICATOR_DISALLOW_RECOVERY',
'DUPLICATOR_PRO_PRIMARY_OAUTH_SERVER' => 'DUPLICATOR_PRIMARY_OAUTH_SERVER',
'DUPLICATOR_PRO_SECONDARY_OAUTH_SERVER' => 'DUPLICATOR_SECONDARY_OAUTH_SERVER',
];
/**
* Apply legacy constant mappings
*
* For each legacy constant that is defined, define the new constant with the same value
* (only if the new constant is not already defined)
*
* @return void
*/
public static function apply(): void
{
foreach (self::CONSTANTS_MAP as $oldName => $newName) {
if (\defined($oldName) && !\defined($newName)) {
\define($newName, \constant($oldName));
}
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Legacy file compatibility for backward compatibility with old installers
*
* @package Duplicator\Addons\ProLegacy
* @copyright (c) 2025, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
/**
* Handles legacy file creation for backward compatibility
*
* Creates legacy-prefixed files alongside new files so that old installers
* can still read the overwrite params files.
*/
class LegacyFiles
{
const LEGACY_OVERWRITE_PARAMS_PREFIX = 'duplicator_pro_params_overwrite';
/**
* Initialize legacy file hooks
*
* @return void
*/
public static function init(): void
{
add_action('duplicator_after_overwrite_params_created', [__CLASS__, 'createLegacyOverwriteParams'], 10, 3);
}
/**
* Create legacy overwrite params file for backward compatibility with old installers
*
* When new plugin creates duplicator_params_overwrite_HASH.json,
* this also creates duplicator_pro_params_overwrite_HASH.json so that
* old installers (which look for the legacy prefix) can still find it.
*
* @param string $filePath Full path to the new overwrite params file
* @param array<string, mixed> $params Parameters written to the file
* @param string $packageHash Package hash used in filename
*
* @return void
*/
public static function createLegacyOverwriteParams(string $filePath, array $params, string $packageHash): void
{
$directory = dirname($filePath);
$legacyFilePath = $directory . '/' . self::LEGACY_OVERWRITE_PARAMS_PREFIX . '_' . $packageHash . '.json';
// Copy the new file to the legacy location
if (file_exists($filePath)) {
copy($filePath, $legacyFilePath);
}
}
}

View File

@@ -0,0 +1,724 @@
<?php
/**
* Legacy upgrade functions for backward compatibility
*
* @package Duplicator\Addons\ProLegacy
* @copyright (c) 2025, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
use Duplicator\Core\CapMng;
use Duplicator\Core\Models\AbstractEntity;
use Duplicator\Core\UniqueId;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Models\ScheduleEntity;
use Duplicator\Models\SecureGlobalEntity;
use Duplicator\Models\StaticGlobal;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\StoragesUtil;
use Duplicator\Package\AbstractPackage;
use Duplicator\Package\DupPackage;
use Duplicator\Utils\Crypt\CryptBlowfish;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Views\AdminNotices;
use Throwable;
/**
* Handles legacy upgrade migrations from old prefixes
*
* Priority scheme:
* - 10: initTables (creates database tables)
* - 20: initCapabilities
* - 50: initEntities (initializes default entities - plugin semi-initialized after this)
* - 100: initSecureKey (new installations only)
* - 200: initUniqueId
* - 1000: updateOptionVersion
* - 1001: setInstallInfo
* - 10000: resaveAllEntities (always last)
*/
class LegacyUpgrade
{
const FIRST_VERSION_WITH_NEW_TABLES = '4.5.14-beta2';
const FIRST_VERSION_WITH_STATIC_GLOBAL_IMPROVED = '4.5.23-beta1';
const FIRST_VERSION_DEFAULT_PURGE = '4.5.20-beta1';
const FIRST_VERSION_WITH_PACKAGE_TYPE = '4.5.22-beta3';
const FIRST_VERSION_WITH_DYNAMIC_GLOBAL_ENTITY = '4.5.22-beta8';
const FIRST_VERSION_WITH_LOGS_SUBFOLDER = '4.5.23-beta2';
const FIRST_VERSION_WITH_WEBSITE_IDENTIFIER_CORE = '4.5.23-RC3';
const FIRST_VERSION_WITH_ACTIVITY_LOG = '4.5.25-beta1';
const FIRST_VERSION_WITH_NEW_OPTION_PREFIX = '4.5.25-beta8';
const LEGACY_TRACE_LOG_ENABLED_OPT = 'duplicator_pro_trace_log_enabled';
const LEGACY_SEND_TRACE_TO_ERROR_LOG_OPT = 'duplicator_pro_send_trace_to_error_log';
const LEGACY_PLUGIN_DATA_OPTION_KEY = 'duplicator_pro_plugin_data_stats';
/**
* List of deprecated WordPress options to be removed during upgrade
*
* @var string[]
*/
const DEPRECATED_OPTIONS = ['duplicator_pro_package_active'];
/**
* Initialize upgrade hooks
*
* Core priorities: 10=initTables, 20=initCapabilities, 50=initEntities, 100+=post-init
*
* @return void
*/
public static function init(): void
{
// AFTER initTables (10), BEFORE initCapabilities (20)
add_action('duplicator_upgrade', [__CLASS__, 'migrateOptionPrefixes'], 12, 2);
add_action('duplicator_upgrade', [__CLASS__, 'migrateUserMetaKeys'], 12, 2);
add_action('duplicator_upgrade', [__CLASS__, 'migrateCapabilities'], 18, 2);
// AFTER initCapabilities (20), BEFORE initEntities (50)
add_action('duplicator_upgrade', [__CLASS__, 'migrateStaticGlobalOptions'], 40, 2);
add_action('duplicator_upgrade', [__CLASS__, 'migrateEntityTypes'], 40, 2);
// AFTER initEntities (50), BEFORE initSecureKey (100)
add_action('duplicator_upgrade', [__CLASS__, 'storeDupSecureKey'], 101, 2);
// BEFORE initUniqueId (200)
add_action('duplicator_upgrade', [__CLASS__, 'migrateWebsiteIdentifier'], 150, 2);
// AFTER initUniqueId (200)
add_action('duplicator_upgrade', [__CLASS__, 'moveDataToDynamicGlobalEntity'], 250, 2);
add_action('duplicator_upgrade', [__CLASS__, 'updatePackageType'], 251, 2);
add_action('duplicator_upgrade', [__CLASS__, 'migrateLogsToSubfolder'], 252, 2);
// Legacy storage fixes
add_action('duplicator_upgrade', [__CLASS__, 'fixDoubleDefaultStorages'], 300, 2);
add_action('duplicator_upgrade', [__CLASS__, 'updateBackupRecordPurgeSettings'], 301, 2);
add_action('duplicator_upgrade', [__CLASS__, 'cleanupDeprecatedOptions'], 302, 2);
add_action('duplicator_upgrade', [__CLASS__, 'notifyActivityLogIntegration'], 303, 2);
}
/**
* Migrate options from old prefixes (duplicator_pro_*, duplicator_expire_) to new prefix (dupli_opt_*)
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateOptionPrefixes($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_NEW_OPTION_PREFIX, '>=')) {
return;
}
/** @var \wpdb $wpdb */
global $wpdb;
try {
// Map of old option names to new option names
$optionMapping = [
'duplicator_pro_capabilities' => 'dupli_opt_capabilities',
'duplicator_pro_notifications' => 'dupli_opt_notifications',
'duplicator_pro_plugin_data_stats' => 'dupli_opt_plugin_data_stats',
'duplicator_pro_recover_point' => 'dupli_opt_recover_point',
'duplicator_pro_ui_view_state' => 'dupli_opt_ui_view_state',
'duplicator_pro_inst_hash_notice' => 'dupli_opt_inst_hash_notice',
'duplicator_pro_activate_plugins_after_installation' => 'dupli_opt_activate_plugins_after_installation',
'duplicator_pro_migration_success' => 'dupli_opt_migration_success',
'duplicator_pro_s3_contents_fetch_fail' => 'dupli_opt_s3_contents_fetch_fail',
'duplicator_pro_quick_fix_notice' => 'dupli_opt_quick_fix_notice',
'duplicator_pro_failed_schedule_notice' => 'dupli_opt_failed_schedule_notice',
'duplicator_pro_failed_backup_notice' => 'dupli_opt_failed_backup_notice',
'duplicator_pro_activity_log_upgrade_notice' => 'dupli_opt_activity_log_upgrade_notice',
'duplicator_pro_first_login_after_install' => 'dupli_opt_first_login_after_install',
'duplicator_pro_migration_data' => 'dupli_opt_migration_data',
'duplicator_pro_clean_install_report' => 'dupli_opt_clean_install_report',
'duplicator_pro_help_docs_expire' => 'dupli_opt_help_docs_expire',
'duplicator_pro_auth_token_auto_active' => 'dupli_opt_auth_token_auto_active',
'duplicator_pro_frotend_delay' => 'dupli_opt_frotend_delay',
'duplicator_pro_pending_cancellations' => 'dupli_opt_pending_cancellations',
'duplicator_pro_exe_safe_mode' => 'dupli_opt_exe_safe_mode',
'duplicator_pro_settings' => 'dupli_opt_settings',
// StaticGlobal options
'duplicator_uninstall_package_option' => 'dupli_opt_uninstall_package',
'duplicator_uninstall_settings_option' => 'dupli_opt_uninstall_settings',
'duplicator_crypt_option' => 'dupli_opt_crypt',
'duplicator_trace_log_enabled_option' => 'dupli_opt_trace_log_enabled',
'duplicator_trace_to_error_log_option' => 'dupli_opt_trace_to_error_log',
// UniqueId, TransferFailureHandler, DupCloud options
'duplicator_unique_id' => 'dupli_opt_unique_id',
'duplicator_failed_transfers' => 'dupli_opt_failed_transfers',
'duplicator_dup_cloud_out_of_space_notice' => 'dupli_opt_dup_cloud_out_of_space_notice',
];
$migratedCount = 0;
foreach ($optionMapping as $oldName => $newName) {
$oldValue = get_option($oldName);
if ($oldValue !== false) {
update_option($newName, $oldValue);
delete_option($oldName);
$migratedCount++;
}
}
if ($migratedCount > 0) {
DupLog::trace("LEGACY MIGRATION: Migrated {$migratedCount} options to new prefix");
}
// Migrate duplicator_expire_* options to dupli_opt_expire_*
$expireOptions = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like('duplicator_expire_') . '%'
),
ARRAY_A
);
$expireMigratedCount = 0;
foreach ($expireOptions as $option) {
$oldName = $option['option_name'];
$newName = str_replace('duplicator_expire_', 'dupli_opt_expire_', $oldName);
update_option($newName, $option['option_value']);
delete_option($oldName);
$expireMigratedCount++;
}
if ($expireMigratedCount > 0) {
DupLog::trace("LEGACY MIGRATION: Migrated {$expireMigratedCount} expire options to new prefix");
}
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating option prefixes: " . $e->getMessage());
}
}
/**
* Migrate user meta keys from old prefixes to new prefix (dupli_opt_*)
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateUserMetaKeys($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_NEW_OPTION_PREFIX, '>=')) {
return;
}
/** @var \wpdb $wpdb */
global $wpdb;
$userMetaMapping = [
'duplicator_pro_created_format' => 'dupli_opt_created_format',
'duplicator_pro_opts_per_page' => 'dupli_opt_opts_per_page',
'duplicator_user_ui_option' => 'dupli_opt_user_ui_option',
'dupli-import-view-mode' => 'dupli_opt_import_view_mode',
'duplicator_recommended_plugin_dismissed' => 'dupli_opt_recommended_plugin_dismissed',
];
try {
foreach ($userMetaMapping as $oldKey => $newKey) {
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->usermeta} SET meta_key = %s WHERE meta_key = %s",
$newKey,
$oldKey
)
);
}
// Bulk migrate any remaining duplicator_pro_* user meta
$wpdb->query(
"UPDATE {$wpdb->usermeta} SET meta_key = REPLACE(meta_key, 'duplicator_pro_', 'dupli_opt_')
WHERE meta_key LIKE 'duplicator\_pro\_%'"
);
DupLog::trace("LEGACY MIGRATION: Migrated user meta keys to new prefix");
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating user meta keys: " . $e->getMessage());
}
}
/**
* Migrate capabilities from old prefix (duplicator_pro_*) to new prefix (duplicator_*)
*
* This migration must run BEFORE CapMng::getInstance() (priority 4) because:
* 1. CapMng reads capabilities from database option
* 2. The option contains capability names as array keys
* 3. If we change CAP_PREFIX without migrating, keys won't match and reset() will be called
*
* Migration steps:
* 1. Read current capabilities from dupli_opt_capabilities option
* 2. Rename array keys from duplicator_pro_* to duplicator_*
* 3. Update WordPress roles: remove old caps, add new caps
* 4. Update WordPress users: remove old caps, add new caps
* 5. Save updated option
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateCapabilities($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_NEW_OPTION_PREFIX, '>=')) {
return;
}
$oldPrefix = 'duplicator_pro_';
$newPrefix = CapMng::CAP_PREFIX;
try {
$capabilities = get_option(CapMng::OPTION_KEY, false);
if ($capabilities === false || !is_array($capabilities)) {
return;
}
DupLog::trace("LEGACY MIGRATION: Starting capability prefix migration");
// Step 1: Remove old capabilities from WordPress roles and users
foreach ($capabilities as $oldCapName => $data) {
if (strpos($oldCapName, $oldPrefix) !== 0) {
continue;
}
foreach ($data['roles'] as $roleName) {
$role = get_role($roleName);
if ($role) {
$role->remove_cap($oldCapName);
}
}
foreach ($data['users'] as $userId) {
$user = get_user_by('id', $userId);
if ($user) {
$user->remove_cap($oldCapName);
}
}
}
// Step 2: Build new capabilities array with renamed keys
$newCapabilities = [];
foreach ($capabilities as $oldCapName => $data) {
if (strpos($oldCapName, $oldPrefix) === 0) {
$newCapName = $newPrefix . substr($oldCapName, strlen($oldPrefix));
} else {
$newCapName = $oldCapName;
}
$newCapabilities[$newCapName] = $data;
}
// Step 3: Save updated option and let CapMng sync with WordPress
update_option(CapMng::OPTION_KEY, $newCapabilities);
CapMng::getInstance()->update($newCapabilities);
$migratedCount = count($newCapabilities);
DupLog::trace("LEGACY MIGRATION: Migrated {$migratedCount} capabilities from {$oldPrefix}* to {$newPrefix}*");
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating capabilities: " . $e->getMessage());
}
}
/**
* Migrate entity type keys from old prefix (DUP_PRO_*) to new format
*
* Updates the `type` column in duplicator_entities table.
* Runs AFTER initTables (priority 10) and BEFORE initEntities (priority 50).
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateEntityTypes($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_NEW_OPTION_PREFIX, '>=')) {
return;
}
/** @var \wpdb $wpdb */
global $wpdb;
$typeMapping = [
'DUP_PRO_Schedule_Entity' => 'Schedule_Entity',
'DUP_PRO_Global_Entity' => 'Global_Entity',
'DUP_PRO_Brand_Entity' => 'Brand_Entity',
'DUP_PRO_Package_Template_Entity' => 'Package_Template_Entity',
'DUP_PRO_Storage_Entity' => 'Storage_Entity',
'DUP_PRO_System_Global_Entity' => 'System_Global_Entity',
'DUP_PRO_Secure_Global_Entity' => 'Secure_Global_Entity',
'DUP_PRO_Staging_Entity' => 'Staging_Entity',
];
try {
$tableName = $wpdb->base_prefix . 'duplicator_entities';
$migratedCount = 0;
foreach ($typeMapping as $oldType => $newType) {
$result = $wpdb->update(
$tableName,
['type' => $newType],
['type' => $oldType],
['%s'],
['%s']
);
if ($result !== false && $result > 0) {
$migratedCount += $result;
}
}
if ($migratedCount > 0) {
DupLog::trace("LEGACY MIGRATION: Migrated {$migratedCount} entity type keys");
}
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating entity types: " . $e->getMessage());
}
}
/**
* Migrate static global options from old prefix
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateStaticGlobalOptions($currentVersion, $newVersion): void
{
if (
$currentVersion === false ||
version_compare($currentVersion, self::FIRST_VERSION_WITH_STATIC_GLOBAL_IMPROVED, '>=')
) {
return;
}
$traceLogEnabled = get_option(self::LEGACY_TRACE_LOG_ENABLED_OPT, false);
$sendTraceToErrorLog = get_option(self::LEGACY_SEND_TRACE_TO_ERROR_LOG_OPT, false);
if ($traceLogEnabled !== false) {
StaticGlobal::setTraceLogEnabledOption($traceLogEnabled);
delete_option(self::LEGACY_TRACE_LOG_ENABLED_OPT);
DupLog::trace("LEGACY MIGRATION: Migrated trace_log_enabled option");
}
if ($sendTraceToErrorLog !== false) {
StaticGlobal::setSendTraceToErrorLogOption($sendTraceToErrorLog);
delete_option(self::LEGACY_SEND_TRACE_TO_ERROR_LOG_OPT);
DupLog::trace("LEGACY MIGRATION: Migrated send_trace_to_error_log option");
}
}
/**
* Remove deprecated WordPress options with old prefix
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function cleanupDeprecatedOptions($currentVersion, $newVersion): void
{
try {
foreach (self::DEPRECATED_OPTIONS as $optionName) {
if (delete_option($optionName)) {
DupLog::trace("LEGACY CLEANUP: Removed deprecated option: {$optionName}");
}
}
} catch (Throwable $e) {
DupLog::trace("LEGACY CLEANUP: Error removing deprecated options: " . $e->getMessage());
}
}
/**
* Save DUP SECURE KEY for legacy upgrades
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function storeDupSecureKey($currentVersion, $newVersion): void
{
if ($currentVersion !== false && SnapUtil::versionCompare($currentVersion, '4.5.0', '<=', 3)) {
CryptBlowfish::createWpConfigSecureKey(true, true);
}
}
/**
* Migrate website identifier from PluginData to UniqueId core class
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateWebsiteIdentifier($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_WEBSITE_IDENTIFIER_CORE, '>=')) {
return;
}
try {
$pluginDataOption = get_option(self::LEGACY_PLUGIN_DATA_OPTION_KEY, false);
if ($pluginDataOption === false) {
return;
}
$pluginDataArray = json_decode($pluginDataOption, true);
if (!is_array($pluginDataArray) || !isset($pluginDataArray['identifier']) || strlen($pluginDataArray['identifier']) === 0) {
return;
}
$oldIdentifier = $pluginDataArray['identifier'];
update_option(UniqueId::OPTION_KEY, $oldIdentifier, true);
DupLog::trace("LEGACY MIGRATION: Copied identifier to new location: " . UniqueId::OPTION_KEY);
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error migrating website identifier: " . $e->getMessage());
}
}
/**
* Update legacy package adding type column if empty
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function updatePackageType($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_PACKAGE_TYPE, '>=')) {
return;
}
/** @var \wpdb $wpdb */
global $wpdb;
$table = DupPackage::getTableName();
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET type = %s WHERE type IS NULL OR type = ''", DupPackage::getBackupType()));
DupLog::trace("LEGACY MIGRATION: Updated package type column");
}
/**
* Move data to dynamic global entity from secure global entity
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function moveDataToDynamicGlobalEntity($currentVersion, $newVersion): void
{
if (
$currentVersion === false ||
version_compare($currentVersion, self::FIRST_VERSION_WITH_DYNAMIC_GLOBAL_ENTITY, '>=')
) {
return;
}
$sGlobal = SecureGlobalEntity::getInstance();
$dGlobal = DynamicGlobalEntity::getInstance();
if (!is_null($sGlobal->lkp) && strlen($sGlobal->lkp) > 0) {
$dGlobal->setValString('license_key_visible_pwd', $sGlobal->lkp);
$sGlobal->lkp = null;
}
if (!is_null($sGlobal->basic_auth_password) && strlen($sGlobal->basic_auth_password) > 0) {
$dGlobal->setValString('basic_auth_password', $sGlobal->basic_auth_password);
$sGlobal->basic_auth_password = null;
}
$sGlobal->save();
$dGlobal->save();
DupLog::trace("LEGACY MIGRATION: Moved data to DynamicGlobalEntity");
}
/**
* Fix double default storages bug from legacy versions
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function fixDoubleDefaultStorages($currentVersion, $newVersion): void
{
if ($currentVersion === false) {
return;
}
try {
$defaultStorageId = StoragesUtil::getDefaultStorageId();
$doubleStorageIds = StoragesUtil::removeDoubleDefaultStorages();
if ($doubleStorageIds === []) {
return;
}
// Auto assign references to the correct default storage
ScheduleEntity::listCallback(
function (ScheduleEntity $schedule) use ($defaultStorageId, $doubleStorageIds): void {
$save = false;
foreach ($schedule->storage_ids as $key => $storageId) {
if (!in_array($storageId, $doubleStorageIds)) {
continue;
}
$schedule->storage_ids[$key] = $defaultStorageId;
$save = true;
}
$schedule->storage_ids = array_values(array_unique($schedule->storage_ids));
if ($save) {
$schedule->save();
}
}
);
DupPackage::dbSelectByStatusCallback(
function (DupPackage $package) use ($defaultStorageId, $doubleStorageIds): void {
$save = false;
if (in_array($package->active_storage_id, $doubleStorageIds)) {
$package->active_storage_id = $defaultStorageId;
$save = true;
}
foreach ($package->upload_infos as $key => $info) {
if (!in_array($info->getStorageId(), $doubleStorageIds)) {
continue;
}
$info->setStorageId($defaultStorageId);
$save = true;
}
if ($save) {
$package->save();
}
},
[
[
'op' => '>=',
'status' => AbstractPackage::STATUS_COMPLETE,
],
]
);
DupLog::trace("LEGACY MIGRATION: Fixed double default storages");
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error fixing double default storages: " . $e->getMessage());
}
}
/**
* Sets the correct backup purge setting based on previous default local storage settings
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function updateBackupRecordPurgeSettings($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_DEFAULT_PURGE, '>=')) {
return;
}
$global = GlobalEntity::getInstance();
if (StoragesUtil::getDefaultStorage()->isPurgeEnabled()) {
$global->setPurgeBackupRecords(AbstractStorageEntity::BACKUP_RECORDS_REMOVE_DEFAULT);
} else {
$global->setPurgeBackupRecords(AbstractStorageEntity::BACKUP_RECORDS_REMOVE_NEVER);
}
$global->save();
DupLog::trace("LEGACY MIGRATION: Updated backup record purge settings");
}
/**
* Migrate existing log files from root directory to logs subfolder
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrateLogsToSubfolder($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_LOGS_SUBFOLDER, '>=')) {
return;
}
try {
DupLog::trace("LEGACY MIGRATION: Moving log files to logs subfolder");
// Ensure logs directory exists
if (!file_exists(DUPLICATOR_LOGS_PATH)) {
SnapIO::mkdirP(DUPLICATOR_LOGS_PATH);
SnapIO::chmod(DUPLICATOR_LOGS_PATH, 'u+rwx');
SnapIO::createSilenceIndex(DUPLICATOR_LOGS_PATH);
}
// Use SnapIO::regexGlob for more robust file discovery
$logFiles = SnapIO::regexGlob(DUPLICATOR_SSDIR_PATH, [
'regexFile' => [
'/.*_log\.txt$/',
'/.*_log_bak\.txt$/',
'/.*\.log$/',
],
'regexFolder' => false,
'recursive' => false,
]);
$migratedCount = 0;
foreach ($logFiles as $oldPath) {
$filename = basename($oldPath);
$newPath = DUPLICATOR_LOGS_PATH . '/' . $filename;
if (SnapIO::rename($oldPath, $newPath)) {
$migratedCount++;
}
}
DupLog::trace("LEGACY MIGRATION: Moved {$migratedCount} log files to logs subfolder");
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error moving log files: " . $e->getMessage());
}
}
/**
* Set transient for Activity Log integration upgrade notice if failed backups exist
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function notifyActivityLogIntegration($currentVersion, $newVersion): void
{
if ($currentVersion === false || version_compare($currentVersion, self::FIRST_VERSION_WITH_ACTIVITY_LOG, '>=')) {
return;
}
try {
$count = DupPackage::countByStatus(
[
[
'op' => '<',
'status' => AbstractPackage::STATUS_PRE_PROCESS,
],
],
[DupPackage::getBackupType()]
);
if ($count > 0) {
set_transient(AdminNotices::ACTIVITY_LOG_UPGRADE_NOTICE, $count, 0);
DupLog::trace("LEGACY MIGRATION: Set Activity Log upgrade notice transient with count: " . $count);
}
} catch (Throwable $e) {
DupLog::trace("LEGACY MIGRATION: Error setting Activity Log upgrade notice: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Version options migration for backward compatibility
*
* @package Duplicator\Addons\ProLegacy
* @copyright (c) 2025, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\ProLegacy;
/**
* Handles migration of version-related options from old to new prefix
*
* This class provides fallback filters so the core can read old option values
* during the transition period before migration runs.
*/
class VersionMigration
{
const OLD_VERSION_OPT_KEY = 'duplicator_pro_plugin_version';
const OLD_INSTALL_INFO_OPT_KEY = 'duplicator_pro_install_info';
const OLD_INSTALL_TIME_OPT_KEY = 'duplicator_pro_install_time';
/**
* Initialize fallback filters and migration hooks
*
* Must be called early in plugin boot, before upgrade checks run.
*
* @return void
*/
public static function init(): void
{
add_filter('duplicator_stored_version', [__CLASS__, 'fallbackVersion']);
add_filter('duplicator_stored_install_info', [__CLASS__, 'fallbackInstallInfo']);
// Priority 3: run very early on duplicator_upgrade, leaving room for future additions
add_action('duplicator_upgrade', [__CLASS__, 'migrate'], 3, 2);
}
/**
* Fallback filter for version option
*
* If new option doesn't exist, return value from old option.
*
* @param string|false $version Version from new option or false
*
* @return string|false
*/
public static function fallbackVersion($version)
{
if ($version === false) {
return get_option(self::OLD_VERSION_OPT_KEY, false);
}
return $version;
}
/**
* Fallback filter for install info option
*
* If new option doesn't exist, return value from old option.
*
* @param array<string,mixed>|false $installInfo Install info from new option or false
*
* @return array<string,mixed>|false
*/
public static function fallbackInstallInfo($installInfo)
{
if ($installInfo === false) {
return get_option(self::OLD_INSTALL_INFO_OPT_KEY, false);
}
return $installInfo;
}
/**
* Migrate old version options to new names
*
* Called during upgrade process. Deletes old options after migration.
* Idempotent - safe to run multiple times.
*
* @param false|string $currentVersion current Duplicator version
* @param string $newVersion new Duplicator version
*
* @return void
*/
public static function migrate($currentVersion = false, $newVersion = ''): void
{
// Version option: just delete old, new will be written by updateOptionVersion()
delete_option(self::OLD_VERSION_OPT_KEY);
// Install info: migrate value if needed
$oldInstallInfo = get_option(self::OLD_INSTALL_INFO_OPT_KEY, false);
$newInstallInfo = get_option(\Duplicator\Core\Upgrade\UpgradePlugin::DUP_INSTALL_INFO_OPT_KEY, false);
if ($oldInstallInfo !== false && $newInstallInfo === false) {
update_option(
\Duplicator\Core\Upgrade\UpgradePlugin::DUP_INSTALL_INFO_OPT_KEY,
$oldInstallInfo,
false
);
}
delete_option(self::OLD_INSTALL_INFO_OPT_KEY);
// Cleanup legacy install time option
delete_option(self::OLD_INSTALL_TIME_OPT_KEY);
}
}