File: /home/h340499/public_html/wp-content/plugins/duplicator-pro/src/Package/AbstractPackage.php
<?php
namespace Duplicator\Package;
use Duplicator\Package\Database\DatabasePkg;
use Duplicator\Models\GlobalEntity;
use Duplicator\Package\Create\PackInstaller;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Package\PackMultisite;
use Duplicator\Models\TemplateEntity;
use Duplicator\Package\Storage\UploadInfo;
use Duplicator\Addons\ProBase\License\License;
use Duplicator\Installer\Package\ArchiveDescriptor;
use Duplicator\Installer\Package\DescriptorDBTableInfo;
use Duplicator\Installer\Package\InstallerDescriptors;
use Duplicator\Libs\DupArchive\Headers\DupArchiveFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveHeader;
use Duplicator\Libs\DupArchive\Processors\DupArchiveProcessingFailure;
use Duplicator\Libs\Index\FileIndexManager;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapOpenBasedir;
use Duplicator\Libs\Snap\SnapOrigFileManager;
use Duplicator\Libs\Snap\SnapServer;
use Duplicator\Libs\Snap\SnapString;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Libs\Snap\SnapWP;
use Duplicator\Libs\WpConfig\WPConfigTransformer;
use Duplicator\Libs\WpUtils\WpArchiveUtils;
use Duplicator\Libs\WpUtils\WpDbUtils;
use Duplicator\Libs\WpUtils\WpUtilsMultisite;
use Duplicator\Models\BrandEntity;
use Duplicator\Models\ScheduleEntity;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\Local\LocalStorage;
use Duplicator\Models\Storages\StoragesUtil;
use Duplicator\Models\SystemGlobalEntity;
use Duplicator\Package\Archive\Filters\ArchiveFitersInfo;
use Duplicator\Package\Archive\Filters\ScopeBase;
use Duplicator\Package\Archive\Filters\ScopeDirectory;
use Duplicator\Package\Archive\Filters\ScopeFile;
use Duplicator\Package\Archive\PackageArchive;
use Duplicator\Package\Create\BuildComponents;
use Duplicator\Package\Create\BuildProgress;
use Duplicator\Package\Database\DatabaseInfo;
use Duplicator\Package\Create\DbBuildProgress;
use Duplicator\Package\Create\DupArchive\PackageDupArchiveCreateState;
use Duplicator\Package\Create\DupArchive\PackageDupArchiveExpandState;
use Duplicator\Package\Create\Scan\Tree\Tree;
use Duplicator\Package\Recovery\RecoveryPackage;
use Duplicator\Package\Recovery\RecoveryStatus;
use Duplicator\Utils\ExpireOptions;
use Exception;
use ReflectionObject;
use Throwable;
use VendorDuplicator\Amk\JsonSerialize\JsonSerialize;
use VendorDuplicator\Amk\JsonSerialize\JsonUnserializeMap;
use WP_Error;
abstract class AbstractPackage
{
use TraitCreateActiviyLog;
use TraitPackageProgress;
const EXEC_TYPE_NOT_SET = -1; // User for legacy packages load, never used for new packages
const EXEC_TYPE_MANUAL = 0;
const EXEC_TYPE_SCHEDULED = 1;
const EXEC_TYPE_RUN_NOW = 2;
const FLAG_MANUAL = 'MANUAL';
const FLAG_SCHEDULE = 'SCHEDULE';
const FLAG_SCHEDULE_RUN_NOW = 'SCHEDULE_RUN_NOW';
const FLAG_DB_ONLY = 'DB_ONLY';
const FLAG_MEDIA_ONLY = 'MEDIA_ONLY';
const FLAG_HAVE_LOCAL = 'HAVE_LOCAL';
const FLAG_HAVE_REMOTE = 'HAVE_REMOTE';
const FLAG_DISASTER_AVAIABLE = 'DISASTER_AVAIABLE';
const FLAG_DISASTER_SET = 'DISASTER_SET';
const FLAG_CREATED_AFTER_RESTORE = 'CREATED_AFTER_RESTORE';
const FLAG_ZIP_ARCHIVE = 'ZIP_ARCHIVE';
const FLAG_DUP_ARCHIVE = 'DUP_ARCHIVE';
const FLAG_ACTIVE = 'ACTIVE'; // For future use
const FLAG_TEMPLATE = 'TEMPLATE'; // For future use
const FLAG_TEMPORARY = 'TEMPORARY'; // Temporary package for creation initial package
const STATUS_REQUIREMENTS_FAILED = -6;
const STATUS_STORAGE_FAILED = -5;
const STATUS_STORAGE_CANCELLED = -4;
const STATUS_PENDING_CANCEL = -3;
const STATUS_BUILD_CANCELLED = -2;
const STATUS_ERROR = -1;
const STATUS_PRE_PROCESS = 0;
const STATUS_SCANNING = 3;
const STATUS_SCAN_VALIDATION = 4;
const STATUS_AFTER_SCAN = 5;
const STATUS_START = 10;
const STATUS_DBSTART = 20;
const STATUS_DBDONE = 39;
const STATUS_ARCSTART = 40;
const STATUS_ARCVALIDATION = 60;
const STATUS_ARCDONE = 65;
const STATUS_COPIEDPACKAGE = 70;
const STATUS_STORAGE_PROCESSING = 75;
const STATUS_COMPLETE = 100;
const FILE_TYPE_INSTALLER = 0;
const FILE_TYPE_ARCHIVE = 1;
const FILE_TYPE_LOG = 3;
const PACKAGE_HASH_DATE_FORMAT = 'YmdHis';
/** @var int<-1,max> */
protected $ID = -1;
/** @var string */
public $VersionWP = '';
/** @var string */
public $VersionDB = '';
/** @var string */
public $VersionPHP = '';
/** @var string */
public $VersionOS = '';
/** @var string */
protected $name = '';
/** @var string */
protected $hash = '';
/** @var int Enum self::EXEC_TYPE_* */
protected $execType = self::EXEC_TYPE_NOT_SET;
/** @var string */
public $notes = '';
/** @var string */
public $StorePath = DUPLICATOR_PRO_SSDIR_PATH_TMP;
/** @var string */
public $StoreURL = DUPLICATOR_PRO_SSDIR_URL . '/';
/** @var string */
public $ScanFile = '';
/** @var float */
public $timer_start = -1;
/** @var string */
public $Runtime = '';
/** @var string */
public $ExeSize = '0';
/** @var string */
public $ZipSize = '0';
/** @var string */
public $Brand = '';
/** @var int<-2,max> */
public $Brand_ID = -2;
/** @var int ENUM PackageArchive::ZIP_MODE_* */
public $ziparchive_mode = PackageArchive::ZIP_MODE_MULTI_THREAD;
/** @var PackageArchive */
public $Archive;
/** @var PackMultisite */
public $Multisite;
/** @var PackInstaller */
public $Installer;
/** @var DatabasePkg */
public $Database;
/** @var string[] */
public $components = [];
/** @var int self::STATUS_* enum */
protected int $status = self::STATUS_PRE_PROCESS;
/** @var int<-1,max> */
protected $schedule_id = -1;
// Schedule ID that created this
// Chunking progress through build and storage uploads
/** @var InstallerDescriptors */
protected $descriptorsMng;
/** @var BuildProgress */
public $build_progress;
/** @var DbBuildProgress */
public $db_build_progress;
/** @var UploadInfo[] */
public $upload_infos = [];
/** @var int<-1,max> */
public $active_storage_id = -1;
/** @var int<-1,max> */
public $template_id = -1;
/** @var bool */
protected $buildEmailSent = false;
/** @var string */
protected $version = DUPLICATOR_PRO_VERSION;
protected string $created = '';
/** @var string */
protected $updated = '';
/** @var string[] list ENUM self::FLAG_* */
protected $flags = [];
/** @var bool */
protected $flagUpdatedAfterLoad = true;
/**
* Class contructor
* The constructor is final to prevent PHP stan error
* Unsafe usage of new static(). See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static
* For now I have solved it this way but if in the future it is necessary to expand the builders there are other ways to handle this
*
* @param int $execType self::EXEC_TYPE_* ENUM
* @param int[] $storageIds Storages id
* @param TemplateEntity $template Template for Backup or null
* @param ScheduleEntity $schedule Schedule for Backup or null
*/
final public function __construct(
$execType = self::EXEC_TYPE_MANUAL,
$storageIds = [],
?TemplateEntity $template = null,
?ScheduleEntity $schedule = null
) {
global $wp_version;
switch ($execType) {
case self::EXEC_TYPE_MANUAL:
$this->execType = self::EXEC_TYPE_MANUAL;
break;
case self::EXEC_TYPE_SCHEDULED:
$this->execType = self::EXEC_TYPE_SCHEDULED;
break;
case self::EXEC_TYPE_RUN_NOW:
$this->execType = self::EXEC_TYPE_RUN_NOW;
break;
default:
throw new Exception("Package type $execType not supported");
}
$this->VersionOS = defined('PHP_OS') ? PHP_OS : 'unknown';
$this->VersionWP = $wp_version;
$this->VersionPHP = phpversion();
$dbversion = WpDbUtils::getVersion();
$this->VersionDB = (empty($dbversion) ? '- unknown -' : $dbversion);
if ($schedule !== null) {
$this->schedule_id = $schedule->getId();
}
$timestamp = time();
$this->created = gmdate("Y-m-d H:i:s", $timestamp);
$this->name = $this->getNameFromFormat($template, $timestamp);
$this->hash = $this->makeHash();
$this->components = BuildComponents::COMPONENTS_DEFAULT;
$this->Database = new DatabasePkg($this);
$this->Archive = new PackageArchive($this);
$this->Multisite = new PackMultisite();
$this->Installer = new PackInstaller($this);
$this->build_progress = new BuildProgress();
$this->db_build_progress = new DbBuildProgress();
$this->build_progress->setBuildMode();
$this->setByTemplate($template);
if (empty($storageIds)) {
$storageIds = [StoragesUtil::getDefaultStorageId()];
}
$this->addUploadInfos($storageIds);
$this->updatePackageFlags();
}
/**
* Clone
*
* @return void
*/
public function __clone()
{
$this->Database = clone $this->Database;
$this->Archive = clone $this->Archive;
$this->Multisite = clone $this->Multisite;
$this->Installer = clone $this->Installer;
$this->build_progress = clone $this->build_progress;
$this->db_build_progress = clone $this->db_build_progress;
$cloneInfo = [];
foreach ($this->upload_infos as $key => $obj) {
$cloneInfo[$key] = clone $obj;
}
$this->upload_infos = $cloneInfo;
}
/**
* Set properties by template
*
* @param TemplateEntity $template template
*
* @return void
*/
protected function setByTemplate(?TemplateEntity $template = null)
{
if ($template === null) {
return;
}
//BRAND
$brand_data = BrandEntity::getByIdOrDefault((int) $template->installer_opts_brand);
$brand_data->prepareAttachmentsInstaller();
$this->Brand = $brand_data->name;
$this->Brand_ID = $brand_data->getId();
$this->notes = $template->notes;
//MULTISITE
$this->Multisite->FilterSites = $template->filter_sites;
//ARCHIVE
$this->components = $template->components;
$this->Archive->FilterOn = $template->archive_filter_on;
$this->Archive->FilterDirs = $template->archive_filter_dirs;
$this->Archive->FilterExts = $template->archive_filter_exts;
$this->Archive->FilterFiles = $template->archive_filter_files;
$this->Archive->FilterNames = $template->archive_filter_names;
//INSTALLER
$this->Installer->OptsDBHost = $template->installer_opts_db_host;
$this->Installer->OptsDBName = $template->installer_opts_db_name;
$this->Installer->OptsDBUser = $template->installer_opts_db_user;
$this->Installer->OptsSecureOn = $template->installer_opts_secure_on;
$this->Installer->passowrd = $template->installerPassowrd;
$this->Installer->OptsSkipScan = $template->installer_opts_skip_scan;
// CPANEL
$this->Installer->OptsCPNLEnable = $template->installer_opts_cpnl_enable;
$this->Installer->OptsCPNLHost = $template->installer_opts_cpnl_host;
$this->Installer->OptsCPNLUser = $template->installer_opts_cpnl_user;
$this->Installer->OptsCPNLDBAction = $template->installer_opts_cpnl_db_action;
$this->Installer->OptsCPNLDBHost = $template->installer_opts_cpnl_db_host;
$this->Installer->OptsCPNLDBName = $template->installer_opts_cpnl_db_name;
$this->Installer->OptsCPNLDBUser = $template->installer_opts_cpnl_db_user;
//DATABASE
$this->Database->FilterOn = $template->database_filter_on;
$this->Database->prefixFilter = $template->databasePrefixFilter;
$this->Database->prefixSubFilter = $template->databasePrefixSubFilter;
$this->Database->FilterTables = $template->database_filter_tables;
$this->Database->Compatible = $template->database_compatibility_modes;
}
/**
* Get package id
*
* @return int
*/
public function getId(): int
{
return $this->ID;
}
/**
* Get package status
*
* @return int self::STATUS_* enum
*/
public function getStatus(): int
{
return $this->status;
}
/**
* Add upload info
*
* @param int[] $storage_ids storage ids
*
* @return void
*/
protected function addUploadInfos($storage_ids)
{
DupLog::traceObject('ADDING UPLOAD INFOS', $storage_ids);
$this->upload_infos = [];
foreach ($storage_ids as $storage_id) {
$this->addUploadInfo($storage_id);
}
DupLog::trace('NUMBER UPLOAD INFOS ADDED: ' . count($this->upload_infos));
}
/**
* Add an upload info
*
* @param int $storageId Storage ID
* @param bool $isDownload Is download
*
* @return WP_Error|true
*/
public function addUploadInfo($storageId, $isDownload = false)
{
$errors = new WP_Error();
if (AbstractStorageEntity::exists($storageId) == false) {
$errors->add('storage_id', sprintf(__('Could not find storage ID %d!', 'duplicator-pro'), $storageId));
DupLog::trace("Storage id {$storageId} not found");
return $errors;
}
$errors = apply_filters('duplicator_validate_upload_info_data', $errors, $this, $storageId, $isDownload);
if ($errors->has_errors()) {
DupLog::trace("Duplicator before add upload info hook returned false");
return $errors;
}
$uploadInfo = new UploadInfo($storageId);
$uploadInfo->setDownloadFromRemote($isDownload);
$this->upload_infos[] = $uploadInfo;
return true;
}
/**
* Check if package can start backup execution
*
* @return bool True if package can start backup execution, false otherwise
*/
protected function canStartBackup(): bool
{
$ids = [];
foreach ($this->upload_infos as $uploadInfo) {
$ids[] = $uploadInfo->getStorageId();
}
return StoragesUtil::hasValidStorage($ids, true, true);
}
/**
* Get backup type
*
* @return string
*/
abstract public static function getBackupType(): string;
/**
* Register package type, in this function must add filter duplicator_package_type_classes_map
*
* @return void
*/
public static function registerType(): void
{
add_filter('duplicator_package_type_classes_map', function (array $classesMap): array {
$classesMap[static::getBackupType()] = static::class;
return $classesMap;
});
}
/**
* Generate a Backup name from a template
*
* @param ?TemplateEntity $template Template to use
* @param int $timestamp Timestamp
*
* @return string
*/
protected function getNameFromFormat(
?TemplateEntity $template = null,
$timestamp = 0
) {
$nameFormat = new NameFormat();
$nameFormat->setTimestamp($timestamp);
$nameFormat->setScheduleId($this->schedule_id);
if ($template instanceof TemplateEntity) {
$nameFormat->setFormat($template->package_name_format);
$nameFormat->setTemplateId($template->getId());
}
return $nameFormat->getName();
}
/**
* Return the package class name by type
*
* @param string $type Backup type
*
* @return class-string<self>
*/
final protected static function getClassNameByType(string $type): string
{
$typesMap = apply_filters('duplicator_package_type_classes_map', []);
if (isset($typesMap[$type])) {
if (!is_subclass_of($typesMap[$type], self::class)) {
throw new Exception("Package type $type is not a subclass of " . self::class);
}
return $typesMap[$type];
} else {
throw new Exception("Package type $type not supported");
}
}
/**
* Return Backup from json
*
* @param string $json json string
* @param class-string $mainClass Main object class name
* @param ?object $rowData Database row data
*
* @return static
*/
protected static function getFromJson(string $json, string $mainClass, ?object $rowData = null)
{
if (!is_subclass_of($mainClass, self::class)) {
throw new Exception("Package type {$mainClass} is not a subclass of " . self::class);
}
$map = new JsonUnserializeMap(
[
'' => 'cl:' . $mainClass,
'Archive' => 'cl:' . PackageArchive::class,
'Archive/Package' => 'rf:',
'Archive/FileIndexManager' => 'cl:' . FileIndexManager::class,
'Archive/FilterInfo' => 'cl:' . ArchiveFitersInfo::class,
'Archive/FilterInfo/Dirs' => '?cl:' . ScopeDirectory::class,
'Archive/FilterInfo/Files' => '?cl:' . ScopeFile::class,
'Archive/FilterInfo/Exts' => '?cl:' . ScopeBase::class,
'Archive/FilterInfo/TreeSize' => '?cl:' . Tree::class,
'Multisite' => 'cl:' . PackMultisite::class,
'Installer' => 'cl:' . PackInstaller::class,
'Installer/Package' => 'rf:',
'Installer/origFileManger' => '?cl:' . SnapOrigFileManager::class,
'Installer/configTransformer' => '?cl:' . WPConfigTransformer::class,
'Installer/archiveDescriptor' => '?cl:' . ArchiveDescriptor::class,
'Database' => 'cl:' . DatabasePkg::class,
'Database/Package' => 'rf:',
'Database/info' => 'cl:' . DatabaseInfo::class,
'Database/info/tablesList/*' => 'cl:' . DescriptorDBTableInfo::class,
'build_progress' => 'cl:' . BuildProgress::class,
'build_progress/dupCreate' => '?cl:' . PackageDupArchiveCreateState::class,
'build_progress/dupCreate/package' => 'rf:',
'build_progress/dupCreate/archiveHeader' => 'cl:' . DupArchiveHeader::class,
'build_progress/dupCreate/failures/*' => 'cl:' . DupArchiveProcessingFailure::class,
'build_progress/dupExpand' => '?cl:' . PackageDupArchiveExpandState::class,
'build_progress/dupExpand/package' => 'rf:',
'build_progress/dupExpand/archiveHeader' => 'cl:' . DupArchiveHeader::class,
'build_progress/dupExpand/currentFileHeader' => '?cl:' . DupArchiveFileHeader::class,
'build_progress/dupExpand/failures/*' => 'cl:' . DupArchiveProcessingFailure::class,
'db_build_progress' => 'cl:' . DbBuildProgress::class,
'upload_infos/*' => 'cl:' . UploadInfo::class,
]
);
/** @var ?static */
$package = JsonSerialize::unserializeWithMap($json, $map);
if (!$package instanceof $mainClass) {
throw new Exception('Can\'t read json object ');
}
// MAKE SURE THIS IS TRUE TO AVOID INFINITE LOOPS
$package->flagUpdatedAfterLoad = true;
if (is_object($rowData)) {
$reflect = new ReflectionObject($package);
$dbValuesToProps = [
'id' => 'ID',
'name' => 'name',
'hash' => 'hash',
'status' => 'status',
'flags' => 'flags',
'version' => 'version',
'created' => 'created',
'updated_at' => 'updated',
];
foreach ($dbValuesToProps as $dbKey => $propName) {
if (
!isset($rowData->{$dbKey}) ||
!property_exists($package, $propName)
) {
continue;
}
$prop = $reflect->getProperty($propName);
if (PHP_VERSION_ID < 80100) {
$prop->setAccessible(true);
}
$prop->setValue($package, $rowData->{$dbKey});
}
}
if ($package->execType) {
if (strlen($package->getVersion()) == 0) {
$tmp = JsonSerialize::unserialize($json);
$package->version = $tmp['Version'];
}
}
// For legacy packages, set execType if not set
if ($package->execType === self::EXEC_TYPE_NOT_SET) {
if ($package->hasFlag(self::FLAG_MANUAL)) {
$package->execType = self::EXEC_TYPE_MANUAL;
} elseif ($package->hasFlag(self::FLAG_SCHEDULE)) {
$package->execType = self::EXEC_TYPE_SCHEDULED;
} elseif ($package->hasFlag(self::FLAG_SCHEDULE_RUN_NOW)) {
$package->execType = self::EXEC_TYPE_RUN_NOW;
}
}
// THIS MUST BE SET AT THE END OF THE FUNCTION TO AVOID INFINITE LOOPS.
// DON'T move this line elsewhere, as it ensures that the package flags
// are updated correctly without causing recursive calls to updatePackageFlags().
$package->flagUpdatedAfterLoad = false;
return $package;
}
/**
* Return Backup flags
*
* @return string[] ENUM self::FLAG_*
*/
protected function getFlags()
{
if ($this->flagUpdatedAfterLoad == false) {
$this->updatePackageFlags();
$this->flagUpdatedAfterLoad = true;
}
return $this->flags;
}
/**
* Check if package have flag
*
* @param string $flag flag to check, ENUM self::FLAG_*
*
* @return bool
*/
public function hasFlag($flag)
{
return in_array($flag, $this->getFlags());
}
/**
* Returns true if this is a DB only Backup
*
* @return bool
*/
public function isDBOnly()
{
return BuildComponents::isDBOnly($this->components) || $this->Archive->ExportOnlyDB;
}
/**
* Returns true if this is a File only Backup
*
* @return bool
*/
public function isDBExcluded()
{
return BuildComponents::isDBExcluded($this->components);
}
/**
* Get execution type
*
* @return int
*/
public function getExecutionType(): int
{
return $this->execType;
}
/**
* Check if package have local storage
*
* @return bool
*/
public function haveLocalStorage(): bool
{
foreach ($this->upload_infos as $upload_info) {
if ($upload_info->isLocal()) {
$filePath = SnapIO::trailingslashit($upload_info->getStorage()->getLocationString()) . $this->getArchiveFilename();
if (file_exists($filePath)) {
return true;
}
}
}
return false;
}
/**
* Check if package have remote storage
*
* @return bool
*/
public function haveRemoteStorage(): bool
{
foreach ($this->upload_infos as $upload_info) {
if (
$upload_info->isRemote() &&
$upload_info->packageExists() &&
$upload_info->hasCompleted(true) &&
!$upload_info->isDownloadFromRemote()
) {
return true;
}
}
return false;
}
/**
* Get all storages in which the package exists.
* This function may also send requests to remote storages if necessary.
*
* @param bool $remoteOnly if true return only remote storages
* @param string $returnType 'obj' or 'id'
*
* @return AbstractStorageEntity[]
*/
public function getValidStorages($remoteOnly = false, string $returnType = 'obj'): array
{
$packageUpdate = false;
$storages = [];
$storagesIds = [];
foreach ($this->upload_infos as $upload_info) {
if (
($remoteOnly && !$upload_info->isRemote()) ||
!$upload_info->packageExists() ||
!$upload_info->hasCompleted(true)
) {
continue;
}
$storage = $upload_info->getStorage();
if ($storage->isValid() === false) {
continue;
}
if (in_array($storage->getId(), $storagesIds)) {
continue;
}
if (!$storage->hasPackage($this)) {
$upload_info->setPackageExists(false);
$packageUpdate = true;
continue;
}
if ($returnType === 'obj') {
$storages[] = $storage;
}
$storagesIds[] = $storage->getId();
}
if ($packageUpdate) {
$this->update();
}
return ($returnType === 'obj' ? $storages : $storagesIds);
}
/**
*
* @param bool $die if true die on error otherwise return true on success and false on error
*
* @return bool
*/
public function save($die = true)
{
if ($this->ID < 1) {
/** @var \wpdb $wpdb */
global $wpdb;
$this->version = DUPLICATOR_PRO_VERSION;
// Created is set in the constructor
$this->updated = gmdate("Y-m-d H:i:s");
$results = $wpdb->insert(
static::getTableName(),
[
'type' => static::getBackupType(),
'name' => $this->name,
'hash' => $this->hash,
'archive_name' => $this->getArchiveFilename(),
'status' => 0,
'flags' => '',
'package' => '',
'version' => $this->version,
'created' => $this->created,
'updated_at' => $this->updated,
],
[
'%s',
'%s',
'%s',
'%s',
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
]
);
if ($results === false) {
DupLog::trace("Problem inserting Backup: {$wpdb->last_error}");
if ($die) {
DupLog::errorAndDie(
"Duplicator is unable to insert a Backup record into the database table.",
"'{$wpdb->last_error}'"
);
}
return false;
}
$this->ID = $wpdb->insert_id;
}
// I run the update in each case even after the insert because the saved object does not have the id
return $this->update($die);
}
/**
* update Backup in database
*
* @param bool $die if true die on error otherwise return true on success and false on error
*
* @return bool
*/
public function update($die = true): bool
{
/** @var \wpdb $wpdb */
global $wpdb;
$this->cleanObjectBeforeSave();
$this->updatePackageFlags();
$this->version = DUPLICATOR_PRO_VERSION;
$this->updated = gmdate("Y-m-d H:i:s");
$packageObj = JsonSerialize::serialize($this, JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME);
if (!$packageObj) {
if ($die) {
DupLog::errorAndDie("Backup SetStatus was unable to serialize Backup object while updating record.");
}
return false;
}
$wpdb->flush();
if (
$wpdb->update(
static::getTableName(),
[
'name' => $this->name,
'hash' => $this->hash,
'archive_name' => $this->getArchiveFilename(),
'status' => (int) $this->status,
'flags' => implode(',', $this->flags),
'package' => $packageObj,
'version' => $this->version,
'created' => $this->created,
'updated_at' => $this->updated,
],
['ID' => $this->ID],
[
'%s',
'%s',
'%s',
'%d',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
],
['%d']
) === false
) {
if ($die) {
DupLog::errorAndDie("Database update error: " . $wpdb->last_error);
} else {
DupLog::infoTrace("Database update error: " . $wpdb->last_error);
}
return false;
}
return true;
}
/**
* Sets the status to log the state of the build and save in database
*
* @param int $status The status self::STATUS_* enum
*
* @return void
*/
final public function setStatus(int $status): void
{
if (
$status < self::STATUS_REQUIREMENTS_FAILED ||
$status > self::STATUS_COMPLETE
) {
throw new Exception("Package SetStatus did not receive a proper code.");
}
$previousStatus = $this->status;
$hasChanged = ($previousStatus != $status);
if ($hasChanged) {
// Execute hooks only if status has changed
do_action('duplicator_pro_package_before_set_status', $this, $status);
$this->status = $status;
}
$this->update(); // Always update Backup
if ($hasChanged) {
do_action('duplicator_pro_package_after_set_status', $this, $status);
// Add log event after update only if status has changed
$this->addLogEvent($previousStatus);
}
}
/**
* Set progress
*
* @param float $progressPercent Progress percentage
*
* @return void
*
* @deprecated This method is deprecated and no longer updates progress.
* Progress is now calculated dynamically via getProgress() from TraitPackageProgress.
* The progressPercent property is kept for backward compatibility but not updated.
*/
public function setProgressPercent(float $progressPercent): void
{
// No-op: Progress is now calculated dynamically via getProgress()
// The progressPercent property is maintained for backward compatibility but not updated
}
/**
* Check if the package has valid storage with completed backups
* This verifies that backup files actually exist in valid storages
*
* @return bool True if backup files exist in valid storages, false otherwise
*/
public function hasValidStorage(): bool
{
return count($this->getValidStorages()) > 0;
}
/**
* Return archive file name
*
* @return string
*/
public function getArchiveFilename(): string
{
$extension = strtolower($this->Archive->Format);
return "{$this->getNameHash()}_archive.{$extension}";
}
/**
* Get the name of the file that contains the database
*
* @return string
*/
public function getDatabaseFilename(): string
{
return $this->getNameHash() . '_database.sql';
}
/**
* Get the name of the file that contains the list of directories
*
* @return string
*/
public function getIndexFileName(): string
{
return $this->getNameHash() . '_index.txt';
}
/**
* Get name
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Get hash
*
* @return string
*/
public function getHash(): string
{
return $this->hash;
}
/**
* Get name hash
*
* @return string
*/
public function getNameHash(): string
{
return $this->name . '_' . $this->hash;
}
/**
* Get Internal archive hash
*
* @return string Backup hash
*/
public function getPrimaryInternalHash()
{
$archiveInfo = ArchiveDescriptor::getArchiveNameParts($this->getArchiveFilename());
return $archiveInfo['packageHash'];
}
/**
* Get secondary Backup hash
*
* @return string Backup hash
*/
public function getSecondaryInternalHash()
{
$newHash = $this->makeHash();
$hashParts = explode('_', $newHash);
$firstPart = substr($hashParts[0], 0, 7);
$hashParts = explode('_', $this->hash);
$secondPart = substr($hashParts[1], -8);
return $firstPart . '-' . $secondPart;
}
/**
* Get the backup's descriptor manager
*
* @return InstallerDescriptors The descriptor manager
*/
public function getDescriptorMng()
{
if (is_null($this->descriptorsMng)) {
$this->descriptorsMng = new InstallerDescriptors(
$this->getPrimaryInternalHash(),
date(self::PACKAGE_HASH_DATE_FORMAT, strtotime($this->created))
);
}
return $this->descriptorsMng;
}
/**
* Get version of Backups stored in DB
*
* @return string
*/
public function getVersion(): string
{
return $this->version;
}
/**
* Get scheudle id
*
* @return int
*/
public function getScheduleId()
{
return $this->schedule_id;
}
/**
* Get Backup storages
*
* @return AbstractStorageEntity[]
*/
public function getStorages(): array
{
$storages = [];
foreach ($this->upload_infos as $upload_info) {
$storage = $upload_info->getStorage();
if ($storage->isValid() === false) {
continue;
}
$storages[] = $storage;
}
return $storages;
}
/**
* Return true if package have storage type
*
* @param int $storage_type storage type
*
* @return bool
*/
public function containsStorageType($storage_type): bool
{
foreach ($this->getStorages() as $storage) {
if ($storage->getSType() == $storage_type) {
return true;
}
}
return false;
}
/**
* Validates the inputs from the UI for correct data input
*
* @return InputValidator
*/
public function validateInputs()
{
$validator = new InputValidator();
if ($this->Archive->FilterOn) {
$validator->explodeFilterCustom(
$this->Archive->FilterDirs,
';',
InputValidator::FILTER_VALIDATE_FOLDER_WITH_COMMENT,
[
'valkey' => 'FilterDirs',
'errmsg' => __(
'Directory: <b>%1$s</b> is an invalid path.
Please remove the value from the Archive > Files Tab > Folders input box and apply only valid paths.',
'duplicator-pro'
),
]
);
$validator->explodeFilterCustom(
$this->Archive->FilterExts,
';',
InputValidator::FILTER_VALIDATE_FILE_EXT,
[
'valkey' => 'FilterExts',
'errmsg' => __(
'File extension: <b>%1$s</b> is an invalid extension name.
Please remove the value from the Archive > Files Tab > File Extensions input box and apply only valid extensions. For example \'jpg\'',
'duplicator-pro'
),
]
);
$validator->explodeFilterCustom(
$this->Archive->FilterFiles,
';',
InputValidator::FILTER_VALIDATE_FILE_WITH_COMMENT,
[
'valkey' => 'FilterFiles',
'errmsg' => __(
'File: <b>%1$s</b> is an invalid file name.
Please remove the value from the Archive > Files Tab > Files input box and apply only valid file names.',
'duplicator-pro'
),
]
);
}
//FILTER_VALIDATE_DOMAIN throws notice message on PHP 5.6
if (defined('FILTER_VALIDATE_DOMAIN')) {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.filter_validate_domainFound
$validator->filterVar($this->Installer->OptsDBHost, FILTER_VALIDATE_DOMAIN, [
'valkey' => 'OptsDBHost',
'errmsg' => __('MySQL Server Host: <b>%1$s</b> isn\'t a valid host', 'duplicator-pro'),
'acc_vals' => [
'',
'localhost',
],
]);
}
return $validator;
}
/**
* Get created date
*
* @return string
*/
public function getCreated(): string
{
return $this->created;
}
/**
* Retur the Backup flags
*
* @return void
*/
protected function updatePackageFlags()
{
if (empty($this->flags)) {
switch ($this->getExecutionType()) {
case self::EXEC_TYPE_MANUAL:
$this->flags[] = self::FLAG_MANUAL;
break;
case self::EXEC_TYPE_SCHEDULED:
$this->flags[] = self::FLAG_SCHEDULE;
break;
case self::EXEC_TYPE_RUN_NOW:
$this->flags[] = self::FLAG_SCHEDULE_RUN_NOW;
break;
}
$this->flags[] = $this->Archive->Format == 'ZIP' ? self::FLAG_ZIP_ARCHIVE : self::FLAG_DUP_ARCHIVE;
if ($this->isDBOnly()) {
$this->flags[] = self::FLAG_DB_ONLY;
}
if (BuildComponents::isMediaOnly($this->components)) {
$this->flags[] = self::FLAG_MEDIA_ONLY;
}
}
$this->flags = array_diff(
$this->flags,
[
self::FLAG_HAVE_LOCAL,
self::FLAG_HAVE_REMOTE,
self::FLAG_DISASTER_SET,
self::FLAG_DISASTER_AVAIABLE,
]
);
if ($this->status == self::STATUS_COMPLETE) {
// ONLY for complete Backups
if ($this->haveLocalStorage()) {
$this->flags[] = self::FLAG_HAVE_LOCAL;
}
if ($this->haveRemoteStorage()) {
$this->flags[] = self::FLAG_HAVE_REMOTE;
}
if (RecoveryPackage::getRecoverPackageId() === $this->ID) {
$this->flags[] = self::FLAG_DISASTER_SET;
} else {
$status = new RecoveryStatus($this);
if ($status->isRecoveable()) {
$this->flags[] = self::FLAG_DISASTER_AVAIABLE;
}
}
}
}
/**
* Clean object before save
*
* @return void
*/
protected function cleanObjectBeforeSave()
{
if ($this->status == self::STATUS_COMPLETE || $this->status < self::STATUS_PRE_PROCESS) {
// If complete clean build progress, to clean temp data
$this->build_progress->reset();
$this->db_build_progress->reset();
$this->Archive->FilterInfo->reset();
}
}
/**
*
* @param int $id Backup ID
*
* @return false|static false if fail
*/
public static function getById($id)
{
if ($id < 0) {
return false;
}
global $wpdb;
$table = static::getTableName();
$sql = $wpdb->prepare("SELECT * FROM `{$table}` where ID = %d", $id);
$row = $wpdb->get_row($sql);
// DupLog::traceObject('Object row', $row);
if ($row) {
return static::packageFromRow($row);
} else {
return false;
}
}
/**
* Get the next active Backup
*
* @return ?AbstractPackage
*/
public static function getNextActive(): ?AbstractPackage
{
$result = static::getPackagesByStatus([
'relation' => 'AND',
[
'op' => '>=',
'status' => self::STATUS_PRE_PROCESS,
],
[
'op' => '<',
'status' => self::STATUS_COMPLETE,
]
], 1, 0, '`id` ASC');
if (count($result) > 0) {
return $result[0];
} else {
return null;
}
}
/**
* Get schedule if is set
*
* @return ?ScheduleEntity
*/
public function getSchedule(): ?ScheduleEntity
{
if ($this->schedule_id === -1) {
return null;
}
if (($schedule = ScheduleEntity::getById($this->schedule_id)) === false) {
DupLog::traceBacktrace("No ScheduleEntity found: id {$this->schedule_id}");
return null;
}
return $schedule;
}
/**
* Post scheduled build failure
*
* @param array<string, mixed> $tests Tests results
*
* @return void
*/
public function postScheduledBuildFailure(array $tests = []): void
{
$this->postScheduledBuildProcessing(0, false, $tests);
}
/**
* Post scheduled storage failure
*
* @return void
*/
public function postScheduledStorageFailure(): void
{
$this->postScheduledBuildProcessing(1, false);
}
/**
* Processes the Backup after the build
*
* @param int $stage 0 for failure at build, 1 for failure during storage phase
* @param bool $success true if build was successful
* @param array<string, mixed> $tests Tests results
*
* @return void
*/
protected function postScheduledBuildProcessing(int $stage, bool $success, array $tests = []): void
{
try {
if ($this->schedule_id == -1) {
return;
}
if (($schedule = $this->getSchedule()) === null) {
throw new Exception("Couldn't get schedule by ID {$this->schedule_id} to start post scheduled build processing.");
}
$system_global = SystemGlobalEntity::getInstance();
$system_global->schedule_failed = !$success;
$system_global->save();
$schedule->times_run++;
$schedule->last_run_time = time();
$schedule->last_run_status = ($success ? ScheduleEntity::RUN_STATUS_SUCCESS : ScheduleEntity::RUN_STATUS_FAILURE);
$schedule->save();
if (!empty($tests) && $tests['RES']['INSTALL'] == 'Fail') {
$system_global->addQuickFix(
__('Backup was cancelled because installer files from a previous migration were found.', 'duplicator-pro'),
__(
'Click the button to remove all installer files.',
'duplicator-pro'
),
[
'special' => ['remove_installer_files' => 1],
]
);
}
} catch (Exception $ex) {
DupLog::trace($ex->getMessage());
}
}
/**
* Starts the Backup build process
*
* @param bool $closeOnEnd if true the function will close the log and die on error
*
* @return void
*/
public function runBuild($closeOnEnd = true): void
{
try {
DupLog::trace('Main build step');
// START LOGGING
DupLog::open($this->getNameHash());
$global = GlobalEntity::getInstance();
$this->build_progress->startTimer();
if ($this->build_progress->initialized == false) {
$this->runBuildStart();
}
// At one point having this as an else as not part of the main logic prevented failure emails from getting sent.
// Note2: Think that by putting has_completed() at top of check will prevent archive from continuing to build after a failure has hit.
if ($this->build_progress->hasCompleted()) {
$this->runBuildComplete();
} elseif (!$this->build_progress->database_script_built) {
//START BUILD
//PHPs serialze method will return the object, but the ID above is not passed
//for one reason or another so passing the object back in seems to do the trick
try {
if ((!$global->package_mysqldump) && ($global->package_phpdump_mode == WpDbUtils::PHPDUMP_MODE_MULTI)) {
$this->Database->buildInChunks();
} else {
$this->Database->build();
$this->build_progress->database_script_built = true;
$this->update();
}
} catch (Exception $e) {
do_action('duplicator_pro_build_database_fail', $this);
DupLog::infoTrace("Runtime error in database dump Message: " . $e->getMessage());
throw $e;
}
DupLog::trace("Done building database");
if ($this->build_progress->database_script_built) {
DupLog::trace("Set db built for Backup $this->ID");
}
} elseif (!$this->build_progress->archive_built) {
$this->Archive->buildFile($this);
$this->update();
} elseif (!$this->build_progress->installer_built) {
// Note: Duparchive builds installer within the main build flow not here
$this->Installer->build($this->build_progress);
$this->update();
if ($this->build_progress->failed) {
throw new Exception('ERROR: Problem adding installer to archive.');
}
}
if ($this->build_progress->failed) {
throw new Exception('Build progress fail');
}
} catch (Exception $e) {
DupLog::infoTraceException($e, 'Build failed');
$message = "Backup creation failed.\n"
. " EXCEPTION message: " . $e->getMessage() . "\n";
$message .= $e->getFile() . ' LINE: ' . $e->getLine() . "\n";
$message .= $e->getTraceAsString();
$this->buildFail($message, $closeOnEnd);
}
if ($closeOnEnd) {
DupLog::close();
}
}
/**
* Run build start
*
* @return void
*/
protected function runBuildStart(): void
{
global $wp_version;
$global = GlobalEntity::getInstance();
DupLog::trace("**** START OF BUILD: " . $this->getNameHash());
do_action('duplicator_pro_build_before_start', $this);
$this->timer_start = microtime(true);
$this->ziparchive_mode = $global->ziparchive_mode;
if (!License::can(License::CAPABILITY_MULTISITE_PLUS)) {
$this->Multisite->FilterSites = [];
}
$php_max_time = @ini_get("max_execution_time");
$php_max_memory = @ini_get('memory_limit');
$php_max_time = ($php_max_time == 0) ? "(0) no time limit imposed" : "[{$php_max_time}] not allowed";
$php_max_memory = ($php_max_memory === false) ? "Unable to set php memory_limit" : WP_MAX_MEMORY_LIMIT . " ({$php_max_memory} default)";
$architecture = SnapUtil::getArchitectureString();
$clientkickoffstate = $global->clientside_kickoff ? 'on' : 'off';
$archive_engine = $global->getArchiveEngine();
$serverSoftware = SnapUtil::sanitizeTextInput(INPUT_SERVER, 'SERVER_SOFTWARE', 'unknown');
$info = "********************************************************************************\n";
$info .= "********************************************************************************\n";
$info .= "DUPLICATOR PRO PACKAGE-LOG: " . @date("Y-m-d H:i:s") . "\n";
$info .= "NOTICE: Do NOT post to public sites or forums \n";
$info .= "PACKAGE CREATION START\n";
$info .= "********************************************************************************\n";
$info .= "********************************************************************************\n";
$info .= "VERSION:\t" . DUPLICATOR_PRO_VERSION . "\n";
$info .= "WORDPRESS:\t{$wp_version}\n";
$info .= "PHP INFO:\t" . phpversion() . ' | ' . 'SAPI: ' . php_sapi_name() . "\n";
$info .= "SERVER:\t\t{$serverSoftware} \n";
$info .= "ARCHITECTURE:\t{$architecture} \n";
$info .= "CLIENT KICKOFF: {$clientkickoffstate} \n";
$info .= "PHP TIME LIMIT: {$php_max_time} \n";
$info .= "PHP MAX MEMORY: {$php_max_memory} \n";
$info .= "RUN TYPE:\t" . PackageUtils::getExecTypeString($this->getExecutionType(), $this->template_id) . "\n";
$info .= "MEMORY STACK:\t" . SnapServer::getPHPMemory() . "\n";
$info .= "ARCHIVE ENGINE: {$archive_engine}\n";
$info .= "PACKAGE COMPONENTS:\n\t" . BuildComponents::displayComponentsList($this->components, ",\n\t");
DupLog::infoTrace($info);
// CREATE DB RECORD
$this->build_progress->setBuildMode();
if ($this->Archive->isArchiveEncrypt() && !SettingsUtils::isArchiveEncryptionAvailable()) {
throw new Exception("Archive encryption isn't available.");
}
$this->build_progress->initialized = true;
$this->setStatus(self::STATUS_START);
do_action('duplicator_pro_build_start', $this);
if (
$this->getExecutionType() === self::EXEC_TYPE_SCHEDULED &&
!License::can(License::CAPABILITY_SCHEDULE)
) {
// Prevent scheduled backups from running if the license doesn't support it
throw new Exception("Can't process package schedule " . $this->ID . " because Duplicator isn't licensed");
}
// Validate that at least one valid storage exists before starting backup
if (!$this->canStartBackup()) {
$errorMessage = __('Cannot start backup: There are invalid storages. Please check your storage configurations.', 'duplicator-pro');
DupLog::error(__('Backup validation failed', 'duplicator-pro'), $errorMessage);
throw new Exception($errorMessage);
}
}
/**
* Run build complete
*
* @return void
*/
protected function runBuildComplete(): void
{
DupLog::info("\n********************************************************************************");
DupLog::info("STORAGE:");
DupLog::info("********************************************************************************");
foreach ($this->upload_infos as $upload_info) {
$storage = $upload_info->getStorage();
if ($storage->isValid() === false) {
continue;
}
// Protection against deleted storage
$storage_type_string = strtoupper($storage->getStypeName());
$storage_path = $storage->getLocationString();
DupLog::info($storage_type_string . ": " . $storage->getName() . ', ' . $storage_path);
}
if (!$this->build_progress->failed) {
// Only makees sense to perform build integrity check on completed archives
$this->buildIntegrityCheck();
}
$timerEnd = microtime(true);
$timerSum = SnapString::formattedElapsedTime($timerEnd, $this->timer_start);
$this->Runtime = $timerSum;
// FINAL REPORT
$info = "\n********************************************************************************\n";
$info .= "RECORD ID:[{$this->ID}]\n";
$info .= "TOTAL PROCESS RUNTIME: {$timerSum}\n";
$info .= "PEAK PHP MEMORY USED: " . SnapServer::getPHPMemory(true) . "\n";
$info .= "DONE PROCESSING => {$this->name} " . @date("Y-m-d H:i:s") . "\n";
DupLog::info($info);
DupLog::trace("Done Backup building");
if ($this->build_progress->failed) {
throw new Exception("Backup creation failed.");
} else {
//File Cleanup
$this->buildCleanup();
do_action('duplicator_pro_build_completed', $this);
}
}
/**
* Set Backup for cancellation
*
* @return void
*/
public function setForCancel(): void
{
$pending_cancellations = static::getPendingCancellations();
if (!in_array($this->ID, $pending_cancellations)) {
array_push($pending_cancellations, $this->ID);
ExpireOptions::set(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT, $pending_cancellations, DUPLICATOR_PRO_PENDING_CANCELLATION_TIMEOUT);
}
}
/**
* Clear all pending cancellations
*
* @return void
*/
public static function clearPendingCancellations(): void
{
if (ExpireOptions::delete(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT) == false) {
DupLog::traceError("Couldn't remove pending cancel transient");
}
}
/**
*
* @param boolean $delete_temp Deprecated, always true
*
* @return boolean
*/
public function delete($delete_temp = false)
{
$ret_val = false;
global $wpdb;
$tblName = static::getTableName();
$getResult = $wpdb->get_results($wpdb->prepare("SELECT name, hash FROM `{$tblName}` WHERE id = %d", $this->ID), ARRAY_A);
if ($getResult) {
$row = $getResult[0];
$name_hash = "{$row['name']}_{$row['hash']}";
$delResult = $wpdb->query($wpdb->prepare("DELETE FROM `{$tblName}` WHERE id = %d", $this->ID));
if ($delResult != 0) {
$ret_val = true;
static::deleteDefaultLocalFiles($name_hash, $delete_temp);
$this->deleteLocalStorageFiles();
}
}
return $ret_val;
}
/**
* Get log filename
*
* @return string
*/
public function getLogFilename()
{
return $this->getNameHash() . '_log.txt';
}
/**
* Delete local storage files
*
* @return void
*/
protected function deleteLocalStorageFiles()
{
$storages = $this->getStorages();
$archive_filename = $this->getArchiveFilename();
$installer_filename = $this->Installer->getInstallerLocalName();
$log_filename = $this->getLogFilename();
$index_filename = $this->getIndexFileName();
foreach ($storages as $storage) {
if ($storage->getSType() !== LocalStorage::getSType()) {
continue;
}
$path = $storage->getLocationString();
$archive_filepath = $path . "/" . $archive_filename;
$installer_filepath = $path . "/" . $installer_filename;
$log_filepath = $path . "/" . $log_filename;
$index_filepath = $path . "/" . $index_filename;
@unlink($archive_filepath);
@unlink($installer_filepath);
@unlink($log_filepath);
@unlink($index_filepath);
}
}
/**
* Return list of local storages
*
* @return AbstractStorageEntity[]
*/
public function getLocalStorages(): array
{
$storages = [];
foreach ($this->upload_infos as $upload_info) {
if (!$upload_info->isLocal()) {
continue;
}
$storages[] = $upload_info->getStorage();
}
return $storages;
}
/**
* Build cleanup
*
* @return void
*/
protected function buildCleanup(): void
{
$files = SnapIO::regexGlob(DUPLICATOR_PRO_SSDIR_PATH_TMP);
if (count($files) > 0) {
$filesToStore = [
$this->Installer->getInstallerLocalName(),
$this->Archive->getFileName(),
];
$newPath = DUPLICATOR_PRO_SSDIR_PATH;
foreach ($files as $file) {
$fileName = basename($file);
if (!strstr($fileName, $this->getNameHash())) {
continue;
}
if (in_array($fileName, $filesToStore)) {
if (function_exists('rename')) {
rename($file, "{$newPath}/{$fileName}");
} elseif (function_exists('copy')) {
copy($file, "{$newPath}/{$fileName}");
} else {
throw new Exception('copy and rename function don\'t found');
}
}
if (file_exists($file)) {
unlink($file);
}
}
}
$this->setStatus(self::STATUS_COPIEDPACKAGE);
}
/**
* Integriry check for the build process
*
* @return void
*/
protected function buildIntegrityCheck()
{
//INTEGRITY CHECKS
//We should not rely on data set in the serlized object, we need to manually check each value
//indepentantly to have a true integrity check.
DupLog::info("\n********************************************************************************");
DupLog::info("INTEGRITY CHECKS:");
DupLog::info("********************************************************************************");
//------------------------
//SQL CHECK: File should be at minimum 5K. A base WP install with only Create tables is about 9K
$sql_temp_path = $this->Database->getTempSafeFilePath();
$sql_temp_size = @filesize($sql_temp_path);
$sql_easy_size = SnapString::byteSize($sql_temp_size);
$sql_done_txt = SnapIO::tailFile($sql_temp_path, 3);
// Note: Had to add extra size check of 800 since observed bad sql when filter was on
if (
in_array(BuildComponents::COMP_DB, $this->components) &&
(!strstr($sql_done_txt, (string) DUPLICATOR_PRO_DB_EOF_MARKER) ||
(!$this->Database->FilterOn && $sql_temp_size < DUPLICATOR_PRO_MIN_SIZE_DBFILE_WITHOUT_FILTERS) ||
($this->Database->FilterOn && $this->Database->info->tablesFinalCount > 0 && $sql_temp_size < DUPLICATOR_PRO_MIN_SIZE_DBFILE_WITH_FILTERS))
) {
$this->build_progress->failed = true;
$error_text = "ERROR: SQL file not complete.
The file looks too small ($sql_temp_size bytes) or the end of file marker was not found.";
$system_global = SystemGlobalEntity::getInstance();
if ($this->Database->DBMode == 'MYSQLDUMP') {
$fix_text = __('Click button to switch database engine to PHP', 'duplicator-pro');
$system_global->addQuickFix(
$error_text,
$fix_text,
[
'global' => [
'package_mysqldump' => 0,
'package_mysqldump_qrylimit' => 32768,
],
]
);
} else {
$fix_text = __('Click button to switch database engine to MySQLDump', 'duplicator-pro');
$system_global->addQuickFix($error_text, $fix_text, [
'global' => [
'package_mysqldump' => 1,
'package_mysqldump_path' => '',
],
]);
}
DupLog::error("$error_text **RECOMMENDATION: $fix_text", '');
throw new Exception($error_text);
}
DupLog::info("SQL FILE: {$sql_easy_size}");
//------------------------
//INSTALLER CHECK:
$exe_temp_path = SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH_TMP . '/' . $this->Installer->getInstallerLocalName());
$exe_temp_size = @filesize($exe_temp_path);
$exe_easy_size = SnapString::byteSize($exe_temp_size);
$exe_done_txt = SnapIO::tailFile($exe_temp_path, 10);
if (!strstr($exe_done_txt, 'DUPLICATOR_PRO_INSTALLER_EOF') && !$this->build_progress->failed) {
throw new Exception("ERROR: Installer file not complete. The end of file marker was not found. Please try to re-create the Backup.");
}
DupLog::info("INSTALLER FILE: {$exe_easy_size}");
//------------------------
//ARCHIVE CHECK:
// Only performs check if we were able to obtain the count
DupLog::trace("Archive file count is " . $this->Archive->file_count);
if ($this->Archive->file_count != -1) {
$zip_easy_size = SnapString::byteSize($this->Archive->Size);
if (!($this->Archive->Size)) {
throw new Exception("ERROR: The archive file contains no size. Archive Size: {$zip_easy_size}");
}
$scan_filepath = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/{$this->getNameHash()}_scan.json";
$json = '';
DupLog::trace("***********Does $scan_filepath exist?");
if (file_exists($scan_filepath)) {
$json = file_get_contents($scan_filepath);
} else {
$error_message = sprintf(
__(
"Can't find Scanfile %s. Please ensure there no non-English characters in the Backup or schedule name.",
'duplicator-pro'
),
$scan_filepath
);
throw new Exception($error_message);
}
$scanReport = json_decode($json);
$expected_filecount = (int) ($scanReport->ARC->UDirCount + $scanReport->ARC->UFileCount);
DupLog::info("ARCHIVE FILE: {$zip_easy_size} ");
DupLog::info(sprintf(__('EXPECTED FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($expected_filecount)));
DupLog::info(sprintf(__('ACTUAL FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($this->Archive->file_count)));
$this->ExeSize = $exe_easy_size;
$this->ZipSize = $zip_easy_size;
/* ------- ZIP Filecount Check -------- */
// Any zip of over 500 files should be within 2% - this is probably too loose but it will catch gross errors
DupLog::trace("Expected filecount = $expected_filecount and archive filecount=" . $this->Archive->file_count);
if ($expected_filecount > 500) {
$straight_ratio = ($this->Archive->file_count > 0 ? (float) $expected_filecount / (float) $this->Archive->file_count : 0);
// RSR NEW
$warning_count = $scanReport->ARC->UnreadableFileCount + $scanReport->ARC->UnreadableDirCount;
DupLog::trace("Unread counts) unreadfile:{$scanReport->ARC->UnreadableFileCount} unreaddir:{$scanReport->ARC->UnreadableDirCount}");
$warning_ratio = ((float) ($expected_filecount + $warning_count)) / (float) $this->Archive->file_count;
DupLog::trace(
"Straight ratio is $straight_ratio and warning ratio is $warning_ratio.
# Expected=$expected_filecount # Warning=$warning_count and #Archive File {$this->Archive->file_count}"
);
// Allow the real file count to exceed the expected by 10% but only allow 1% the other way
if (($straight_ratio < 0.90) || ($straight_ratio > 1.01)) {
// Has to exceed both the straight as well as the warning ratios
if (($warning_ratio < 0.90) || ($warning_ratio > 1.01)) {
$zip_file_count = $this->Archive->file_count;
$error_message = sprintf(
'ERROR: File count in archive vs expected suggests a bad archive (%1$d vs %2$d).',
$zip_file_count,
$expected_filecount
);
if ($this->build_progress->current_build_mode == PackageArchive::BUILD_MODE_SHELL_EXEC) {
// $fix_text = "Go to: Settings > Packages Tab > Archive Engine to ZipArchive.";
$fix_text = __("Click on button to set archive engine to DupArchive.", 'duplicator-pro');
$system_global = SystemGlobalEntity::getInstance();
$system_global->addQuickFix(
$error_message,
$fix_text,
[
'global' => ['archive_build_mode' => 3],
]
);
$error_message .= ' **' . sprintf(__("RECOMMENDATION: %s", 'duplicator-pro'), $fix_text);
}
DupLog::trace($error_message);
throw new Exception($error_message);
}
}
}
}
}
/**
* Backup build fail, this method die the process and set the Backup status to error
*
* @param string $message Error message
* @param bool $die If true, the process will die
*
* @return void
*/
public function buildFail(string $message, bool $die = true): void
{
$this->build_progress->failed = true;
$this->setStatus(self::STATUS_ERROR);
$this->postScheduledBuildProcessing(0, false);
do_action('duplicator_pro_build_fail', $this);
if ($die) {
DupLog::errorAndDie($message);
} else {
DupLog::error($message);
}
}
/**
*
* @param string $hash Hash
*
* @return false|static false if fail
*/
public static function getByHash($hash)
{
global $wpdb;
$table = static::getTableName();
$sql = $wpdb->prepare("SELECT * FROM `{$table}` where hash = %s", $hash);
$row = $wpdb->get_row($sql);
if ($row) {
return static::packageFromRow($row);
} else {
return false;
}
}
/**
* Get hash from backup archive filename
*
* @param string $archiveName Archive filename
*
* @return ?static Return Backup or null on failure
*/
public static function getByArchiveName($archiveName)
{
global $wpdb;
if (!preg_match(DUPLICATOR_PRO_ARCHIVE_REGEX_PATTERN, $archiveName, $matches)) {
return null;
}
$table = static::getTableName();
$sql = $wpdb->prepare("SELECT * FROM `{$table}` where archive_name = %s", $archiveName);
$row = $wpdb->get_row($sql);
if ($row) {
return static::packageFromRow($row);
} else {
return null;
}
}
/**
* Process storage upload
*
* @return bool
*/
public function processStorages()
{
//START LOGGING
DupLog::open($this->getNameHash());
DupLog::info("-----------------------------------------");
DupLog::info("STORAGE PROCESSING THREAD INITIATED");
$complete = (count($this->upload_infos) == 0);
// Indicates if all storages have finished (succeeded or failed all-together)
$error_present = false;
$local_default_present = false;
if (!$complete) {
$complete = true;
$latest_upload_infos = $this->getLatestUploadInfos();
foreach ($latest_upload_infos as $upload_info) {
if ($upload_info->isDefaultStorage()) {
$local_default_present = true;
}
if ($upload_info->isFailed()) {
DupLog::trace("The following Upload Info is marked as failed");
DupLog::traceObject('upload_info var:', $upload_info);
$error_present = true;
} elseif ($upload_info->hasCompleted() == false) {
$complete = false;
$storage = $upload_info->getStorage();
if ($storage->isValid() === false) {
throw new Exception('Storage ' . $storage->getName() . '[' . $storage->getId() . '] Isn\'t Valid');
}
if ($upload_info->hasStarted() === false) {
DupLog::trace("Upload Info hasn't started yet, starting it");
$upload_info->start();
}
// Process a bit of work then let the next cron take care of if it's completed or not.
StoragesUtil::processPackage($this, $upload_info);
break;
} else {
$storage = $upload_info->getStorage();
if ($storage->isValid() === false) {
DupLog::trace('Storage ' . $storage->getName() . '[' . $storage->getId() . '] Isn\'t Valid bug is complete so skip');
continue;
}
$storage_type_string = strtoupper($storage->getStypeName());
DupLog::trace(
"Upload Info already completed for storage id: " . $upload_info->getStorageId() .
", type: " . $storage_type_string . ", name: " . $storage->getName()
);
}
}
} else {
DupLog::trace("No storage ids defined for Backup $this->ID!");
$error_present = true;
}
DupLog::info("STORAGE PROCESSING THREAD FINISHED");
DupLog::info("-----------------------------------------");
if ($complete) {
DupLog::info("STORAGE PROCESSING COMPLETED");
if ($error_present) {
DupLog::trace("Storage error is present");
$this->setStatus(self::STATUS_COMPLETE);
$this->postScheduledBuildProcessing(1, false);
if ($local_default_present == false) {
DupLog::trace("Deleting Backup files from default location.");
static::deleteDefaultLocalFiles($this->getNameHash(), true, false);
}
} else {
if ($local_default_present == false) {
DupLog::trace("Deleting Backup files from default location.");
static::deleteDefaultLocalFiles($this->getNameHash(), true, false);
} else {
$default_local_storage = StoragesUtil::getDefaultStorage();
DupLog::trace('Purge old default local storage Backups');
$default_local_storage->purgeOldPackages();
}
$this->setStatus(self::STATUS_COMPLETE);
$this->postScheduledBuildProcessing(1, true);
}
do_action('duplicator_pro_package_transfer_completed', $this);
}
return $complete;
}
/**
* Get all Backups marked for cancellation
*
* @return int[] array of Backup ids
*/
public static function getPendingCancellations()
{
$pending_cancellations = ExpireOptions::get(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT);
if ($pending_cancellations === false) {
$pending_cancellations = [];
}
return $pending_cancellations;
}
/**
* Check if the Backup is marked for cancellation
*
* @return bool
*/
public function isCancelPending()
{
$pending_cancellations = static::getPendingCancellations();
return in_array($this->ID, $pending_cancellations);
}
/**
* Removes all files related to the namehash from the directory
*
* @param string $nameHash Package namehash
* @param string $dir path to dir
* @param bool $deleteLogFiles if set to true will delete log files too
*
* @return void
*/
public static function deletePackageFilesInDir($nameHash, $dir, $deleteLogFiles = false): void
{
$globFiles = glob(SnapIO::safePath(SnapIO::untrailingslashit($dir) . "/" . $nameHash . "_*"));
foreach ($globFiles as $globFile) {
if (!$deleteLogFiles && SnapString::endsWith($globFile, '_log.txt')) {
DupLog::trace("Skipping purge of $globFile because deleteLogFiles is false.");
continue;
}
if (SnapIO::unlink($globFile)) {
DupLog::trace("Successful purge of $globFile.");
} else {
DupLog::trace("Failed purge of $globFile.");
}
}
}
/**
* Delete default local files
*
* @param string $name_hash Package namehash
* @param bool $delete_temp if set to true will delete temp files too
* @param bool $delete_log_files if set to true will delete log files too
*
* @return void
*/
public static function deleteDefaultLocalFiles($name_hash, $delete_temp, $delete_log_files = true): void
{
if ($delete_temp) {
static::deletePackageFilesInDir($name_hash, DUPLICATOR_PRO_SSDIR_PATH_TMP, true);
}
static::deletePackageFilesInDir($name_hash, DUPLICATOR_PRO_SSDIR_PATH, $delete_log_files);
if ($delete_log_files) {
static::deletePackageFilesInDir($name_hash, DUPLICATOR_PRO_PATH_LOGS, $delete_log_files);
}
}
/**
* Get upload infos
*
* @return array<int,UploadInfo>
*/
public function getLatestUploadInfos(): array
{
$upload_infos = [];
// Just save off the latest per the storage id
foreach ($this->upload_infos as $upload_info) {
$upload_infos[$upload_info->getStorageId()] = $upload_info;
}
return $upload_infos;
}
/**
* Select Backups from database
*
* @param string $where where conditions
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string $resultType ids => int[], row => row without Backup blob, fullRow => row with Backup blob, objs => DUP_Package objects[]
* @param string[] $backupTypes backup types to include, is empty all types are included
* @param bool $includeTemporary if true include temporary packages
*
* @return self[]|object[]|int[]
*/
public static function dbSelect(
string $where,
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
string $resultType = 'objs',
array $backupTypes = [],
bool $includeTemporary = false
): array {
global $wpdb;
$table = static::getTableName();
$where = ' WHERE ' . (strlen($where) > 0 ? $where : '1');
if (!$includeTemporary) {
$where .= $wpdb->prepare(' AND FIND_IN_SET(%s, `flags`) = 0', AbstractPackage::FLAG_TEMPORARY);
}
if (count($backupTypes) > 0) {
$placeholders = implode(',', array_fill(0, count($backupTypes), '%s'));
$where .= $wpdb->prepare(" AND `type` IN ($placeholders)", ...$backupTypes);
}
$packages = [];
$offsetStr = $wpdb->prepare(' OFFSET %d', $offset);
$limitStr = $wpdb->prepare(' LIMIT %d', ($limit > 0 ? $limit : PHP_INT_MAX));
$orderByStr = empty($orderBy) ? '' : ' ORDER BY ' . $orderBy . ' ';
switch ($resultType) {
case 'ids':
$cols = '`id`';
break;
case 'row':
$cols = '`id`,`type`,`name`,`hash`,`archive_name`,`status`,`flags`,`version`,`created`,`updated_at`';
break;
case 'fullRow':
case 'objs':
default:
$cols = '*';
break;
}
$rows = $wpdb->get_results('SELECT ' . $cols . ' FROM `' . $table . '` ' . $where . $orderByStr . $limitStr . $offsetStr);
if ($rows != null) {
switch ($resultType) {
case 'ids':
foreach ($rows as $row) {
$packages[] = $row->id;
}
break;
case 'row':
case 'fullRow':
$packages = $rows;
break;
case 'objs':
default:
foreach ($rows as $row) {
$package = static::packageFromRow($row);
if ($package != null) {
$packages[] = $package;
}
}
}
}
return $packages;
}
/**
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions
*
* @return string
*/
protected static function statusContitionsToWhere($conditions = [])
{
$accepted_op = [
'<',
'>',
'=',
'<>',
'>=',
'<=',
];
$relation = (isset($conditions['relation']) && strtoupper($conditions['relation']) == 'OR') ? ' OR ' : ' AND ';
unset($conditions['relation']);
$where = '';
if (!empty($conditions)) {
$str_conds = [];
foreach ($conditions as $cond) {
$op = (isset($cond['op']) && in_array($cond['op'], $accepted_op)) ? $cond['op'] : '=';
$status = isset($cond['status']) ? (int) $cond['status'] : 0;
$str_conds[] = 'status ' . $op . ' ' . $status;
}
$where = implode($relation, $str_conds) . ' ';
} else {
$where = '1 ';
}
return $where;
}
/**
* Execute $callback function foreach Backup result
*
* @param callable $callback function callback(self $package)
* @param string $where where conditions
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return void
*/
public static function dbSelectCallback(
callable $callback,
string $where,
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
array $backupTypes = []
): void {
$ids = static::dbSelect($where, $limit, $offset, $orderBy, 'ids', $backupTypes);
foreach ($ids as $id) {
if (($package = static::getById($id)) == false) {
continue;
}
call_user_func($callback, $package);
unset($package);
}
}
/**
* Get Backups with status conditions and/or pagination
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string $resultType ids => int[], row => row without Backup blob,
* fullRow => row with Backup blob, objs =>
* DUP_Package objects[]
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return DupPackage[]|object[]|int[]
*/
public static function getPackagesByStatus(
array $conditions = [],
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
string $resultType = 'objs',
array $backupTypes = []
) {
return static::dbSelect(static::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, $resultType, $backupTypes);
}
/**
* Get Backups row db with status conditions and/or pagination
*
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return object[] // return row database without Backup blob
*/
public static function getRowByStatus(
array $conditions = [],
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
array $backupTypes = []
) {
return static::dbSelect(static::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, 'row', $backupTypes);
}
/**
* Get Backups ids with status conditions and/or pagination
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return int[] return row database without Backup blob
*/
public static function getIdsByStatus(
array $conditions = [],
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
array $backupTypes = []
): array {
return static::dbSelect(static::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, 'ids', $backupTypes);
}
/**
* count Backup with status condition
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return int
*/
public static function countByStatus(array $conditions = [], array $backupTypes = [])
{
$where = static::statusContitionsToWhere($conditions);
$ids = static::dbSelect($where, 0, 0, '', 'ids', $backupTypes);
return count($ids);
}
/**
* Execute $callback function foreach Backup result
* For each iteration the memory is released
* Conditions Example
* [
* relation = 'AND',
* [ 'op' => '>=' , 'status' => self::STATUS_START ]
* [ 'op' => '<' , 'status' => self::STATUS_COMPLETE ]
* ]
*
* @param callable $callback function callback(self $package)
* @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
* @param int $limit max row numbers if 0 the limit is PHP_INT_MAX
* @param int $offset offset 0 is at begin
* @param string $orderBy default `id` ASC if empty no order
* @param string[] $backupTypes backup types to include, is empty all types are included
*
* @return void
*/
public static function dbSelectByStatusCallback(
callable $callback,
array $conditions = [],
int $limit = 0,
int $offset = 0,
string $orderBy = '`id` ASC',
array $backupTypes = []
): void {
static::dbSelectCallback($callback, static::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, $backupTypes);
}
/**
*
* @param object $row Database row
*
* @return ?static
*/
protected static function packageFromRow($row)
{
$package = null;
if (strlen($row->hash) == 0) {
DupLog::trace("Hash is 0 for the Backup $row->id...");
return null;
}
if (property_exists($row, 'id')) {
$row->id = (int) $row->id;
}
if (property_exists($row, 'type')) {
$row->type = (string) $row->type;
}
if (property_exists($row, 'status')) {
$row->status = (int) $row->status;
}
if (property_exists($row, 'flags')) {
$row->flags = strlen($row->flags) == 0 ? [] : explode(',', $row->flags);
}
try {
$class = static::getClassNameByType($row->type);
$package = static::getFromJson($row->package, $class, $row);
} catch (Throwable $ex) {
DupLog::infoTraceException($ex, "Problem getting Backup from json.");
return null;
}
return $package;
}
/**
* Generates a scan report
*
* @return array<string,mixed> of scan results
*/
public function createScanReport(): array
{
global $wpdb;
$report = [];
DupLog::trace('Scanning');
try {
$global = GlobalEntity::getInstance();
do_action('duplicator_before_scan_report', $this);
//Set tree filters
$this->Archive->setTreeFilters();
//Load scan data necessary for report
$db = $this->Database->getScanData();
$timerStart = microtime(true);
$this->ScanFile = "{$this->getNameHash()}_scan.json";
$report['RPT']['ScanTime'] = "0";
$report['RPT']['ScanFile'] = $this->ScanFile;
//FILES
$scanPath = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/{$this->ScanFile}";
$dirCount = $this->Archive->DirCount;
$fileCount = $this->Archive->FileCount;
$fullCount = $dirCount + $fileCount;
$unreadable = array_merge($this->Archive->FilterInfo->Files->Unreadable, $this->Archive->FilterInfo->Dirs->Unreadable);
$site_warning_size = $global->archive_build_mode === PackageArchive::BUILD_MODE_ZIP_ARCHIVE ?
DUPLICATOR_PRO_SCAN_SITE_ZIP_ARCHIVE_WARNING_SIZE : DUPLICATOR_PRO_SCAN_SITE_WARNING_SIZE;
$filteredTables = ($this->Database->FilterOn ? explode(',', $this->Database->FilterTables) : []);
$subsites = WpUtilsMultisite::getSubsites($this->Multisite->FilterSites, $filteredTables);
$hasImportableSites = SnapUtil::inArrayExtended($subsites, fn($subsite): bool => count($subsite->filteredTables) === 0);
$hasNotImportableSites = SnapUtil::inArrayExtended($subsites, fn($subsite): bool => count($subsite->filteredTables) > 0);
$hasFilteredSiteTables = $this->Database->info->tablesBaseCount !== $this->Database->info->tablesFinalCount;
$pathsOutOpenbaseDir = array_filter($this->Archive->FilterInfo->Dirs->Unknown, fn($path): bool => !SnapOpenBasedir::isPathValid($path));
// Filtered subsites
$filteredSites = [];
if (is_multisite() && License::can(License::CAPABILITY_MULTISITE_PLUS)) {
$filteredSites = array_map(
fn($siteId) => get_blog_details(['blog_id' => $siteId]),
$this->Multisite->FilterSites
);
}
// Check if the user has the privileges to show the CREATE FUNCTION and CREATE PROCEDURE statements
$privileges_to_show_create_func = true;
$query = $wpdb->prepare("SHOW PROCEDURE STATUS WHERE `Db` = %s", $wpdb->dbname);
$procedures = $wpdb->get_col($query, 1);
if (count($procedures)) {
$create = $wpdb->get_row("SHOW CREATE PROCEDURE `" . $procedures[0] . "`", ARRAY_N);
$privileges_to_show_create_func = isset($create[2]);
}
$query = $wpdb->prepare("SHOW FUNCTION STATUS WHERE `Db` = %s", $wpdb->dbname);
$functions = $wpdb->get_col($query, 1);
if (count($functions)) {
$create = $wpdb->get_row("SHOW CREATE FUNCTION `" . $functions[0] . "`", ARRAY_N);
$privileges_to_show_create_func = $privileges_to_show_create_func && isset($create[2]);
}
$privileges_to_show_create_func = apply_filters('duplicator_privileges_to_show_create_func', $privileges_to_show_create_func);
//Add info to report to
$report = [
'Status' => 1,
'ARC' => [
'Size' => SnapString::byteSize($this->Archive->Size),
'DirCount' => number_format($dirCount),
'FileCount' => number_format($fileCount),
'FullCount' => number_format($fullCount),
'USize' => $this->Archive->Size,
'UDirCount' => $dirCount,
'UFileCount' => $fileCount,
'UFullCount' => $fullCount,
'UnreadableDirCount' => $this->Archive->FilterInfo->Dirs->getUnreadableCount(),
'UnreadableFileCount' => $this->Archive->FilterInfo->Files->getUnreadableCount(),
'FilterDirsAll' => $this->Archive->FilterDirsAll,
'FilterFilesAll' => $this->Archive->FilterFilesAll,
'FilterExtsAll' => $this->Archive->FilterExtsAll,
'FilteredCoreDirs' => $this->Archive->filterWpCoreFoldersList(),
'RecursiveLinks' => $this->Archive->RecursiveLinks,
'UnreadableItems' => $unreadable,
'PathsOutOpenbaseDir' => $pathsOutOpenbaseDir,
'FilteredSites' => $filteredSites,
'Subsites' => $subsites,
'Status' => [
'Size' => $this->Archive->Size <= $site_warning_size && $this->Archive->Size >= 0,
'Big' => count($this->Archive->FilterInfo->Files->Size) <= 0,
'AddonSites' => count($this->Archive->FilterInfo->Dirs->AddonSites) <= 0,
'UnreadableItems' => empty($this->Archive->RecursiveLinks) && empty($unreadable) && empty($pathsOutOpenbaseDir),
'showCreateFuncStatus' => $privileges_to_show_create_func,
'showCreateFunc' => $privileges_to_show_create_func,
'HasImportableSites' => $hasImportableSites,
'HasNotImportableSites' => $hasNotImportableSites,
'HasFilteredCoreFolders' => $this->Archive->hasWpCoreFolderFiltered(),
'HasFilteredSiteTables' => $hasFilteredSiteTables,
'HasFilteredSites' => !empty($filteredSites),
'IsDBOnly' => $this->isDBOnly(),
'Network' => !$hasNotImportableSites && empty($filteredSites),
'PackageIsNotImportable' => !(
(!$hasFilteredSiteTables || $hasImportableSites) &&
(!$hasNotImportableSites || License::can(License::CAPABILITY_MULTISITE_PLUS))
),
],
],
'DB' => [
'Status' => $db['Status'],
'SizeInBytes' => $db['Size'],
'Size' => SnapString::byteSize($db['Size']),
'Rows' => number_format($db['Rows']),
'TableCount' => $db['TableCount'],
'TableList' => $db['TableList'],
'FilteredTables' => ($this->Database->FilterOn ? explode(',', $this->Database->FilterTables) : []),
'DBExcluded' => BuildComponents::isDBExcluded($this->components),
],
'SRV' => BuildRequirements::getChecks($this)['SRV'],
'RPT' => [
'ScanCreated' => @date("Y-m-d H:i:s"),
'ScanTime' => SnapString::formattedElapsedTime(microtime(true), $timerStart),
'ScanPath' => $scanPath,
'ScanFile' => $this->ScanFile,
],
];
if (($json = JsonSerialize::serialize($report, JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME)) === false) {
throw new Exception('Problem encoding json');
}
if (@file_put_contents($scanPath, $json) === false) {
throw new Exception('Problem writing scan file');
}
//Safe to clear at this point only JSON
//report stores the full directory and file lists
$this->Archive->Dirs = [];
$this->Archive->Files = [];
/**
* don't save filter info in report scan json.
*/
$report['ARC']['FilterInfo'] = $this->Archive->FilterInfo;
DupLog::trace("TOTAL SCAN TIME = " . SnapString::formattedElapsedTime(microtime(true), $timerStart));
} catch (Exception $ex) {
DupLog::trace("SCAN ERROR: " . $ex->getMessage());
DupLog::trace("SCAN ERROR: " . $ex->getTraceAsString());
DupLog::errorAndDie("An error has occurred scanning the file system.", $ex->getMessage());
}
do_action('duplicator_after_scan_report', $this, $report);
return $report;
}
/**
* Check if backup transfer is interrupted
*
* @return bool returns true if Backup transfer was canceled or failed
*/
public function transferWasInterrupted(): bool
{
$recentUploadInfos = static::getRecentUploadInfos();
foreach ($recentUploadInfos as $recentUploadInfo) {
if ($recentUploadInfo->isFailed() || $recentUploadInfo->isCancelled()) {
return true;
}
}
return false;
}
/**
* Get recent unique $uploadInfos with giving highest priority to the latest one uploadInfo
* if two or more uploadInfo of the same storage type exists
*
* @return UploadInfo[]
*/
protected function getRecentUploadInfos(): array
{
$uploadInfos = [];
$tempStorageIds = [];
foreach (array_reverse($this->upload_infos) as $upload_info) {
if (!in_array($upload_info->getStorageId(), $tempStorageIds)) {
$tempStorageIds[] = $upload_info->getStorageId();
$uploadInfos[] = $upload_info;
}
}
return $uploadInfos;
}
/**
* Adds file and dirs lists to scan report.
*
* @param string $json_path string The path to the json file
* @param bool $includeLists Include the file and dir lists in the report
*
* @return mixed The scan report
*/
public function getScanReportFromJson($json_path, $includeLists = false)
{
if (!file_exists($json_path)) {
$message = sprintf(
__(
"ERROR: Can't find Scanfile %s. Please ensure there no non-English characters in the Backup or schedule name.",
'duplicator-pro'
),
$json_path
);
throw new Exception($message);
}
$json_contents = file_get_contents($json_path);
$report = json_decode($json_contents);
if ($report === null) {
throw new Exception("Couldn't decode scan file.");
}
if ($includeLists) {
$targetRootPath = WpArchiveUtils::getTargetRootPath();
$indexManager = $this->Archive->getIndexManager();
$report->ARC->Dirs = $indexManager->getPathArray(FileIndexManager::LIST_TYPE_DIRS, $targetRootPath);
$report->ARC->Files = $indexManager->getPathArray(FileIndexManager::LIST_TYPE_FILES, $targetRootPath);
}
return $report;
}
/**
* Makes the hashkey for the Backup files
*
* @return string A unique hashkey
*/
final protected function makeHash()
{
// IMPORTANT! Be VERY careful in changing this format - the FTP delete logic requires 3 segments with the last segment to be the date in YmdHis format.
try {
$date = date(self::PACKAGE_HASH_DATE_FORMAT, strtotime($this->created));
if (function_exists('random_bytes')) {
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_bytesFound
$rand = (string) random_bytes(8);
return bin2hex($rand) . mt_rand(1000, 9999) . '_' . $date;
} else {
return strtolower(md5(uniqid((string) random_int(0, mt_getrandmax()), true))) . '_' . $date;
}
} catch (Exception $exc) {
return strtolower(md5(uniqid((string) random_int(0, mt_getrandmax()), true))) . '_' . $date;
}
}
/**
* Get Backup table name
*
* @return string
*/
public static function getTableName()
{
global $wpdb;
return $wpdb->base_prefix . "duplicator_backups";
}
/**
* Init entity table
*
* @return string[] Strings containing the results of the various update queries.
*/
final public static function initTable()
{
/** @var \wpdb $wpdb */
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$tableName = static::getTableName();
$flags = [
self::FLAG_MANUAL,
self::FLAG_SCHEDULE,
self::FLAG_SCHEDULE_RUN_NOW,
self::FLAG_DB_ONLY,
self::FLAG_MEDIA_ONLY,
self::FLAG_HAVE_LOCAL,
self::FLAG_HAVE_REMOTE,
self::FLAG_DISASTER_AVAIABLE,
self::FLAG_DISASTER_SET,
self::FLAG_CREATED_AFTER_RESTORE,
self::FLAG_ACTIVE,
self::FLAG_TEMPLATE,
self::FLAG_ZIP_ARCHIVE,
self::FLAG_DUP_ARCHIVE,
self::FLAG_TEMPORARY,
];
$flagsStr = array_map(fn($flag): string => "'{$flag}'", $flags);
$flagsStr = implode(',', $flagsStr);
// PRIMARY KEY must have 2 spaces before for dbDelta to work
// Mysql 5.5 can't have more than 1 DEFAULT CURRENT_TIMESTAMP
$sql = <<<SQL
CREATE TABLE `{$tableName}` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`type` varchar(100) NOT NULL,
`name` varchar(250) NOT NULL,
`hash` varchar(50) NOT NULL,
`archive_name` varchar(350) NOT NULL DEFAULT '',
`status` int(11) NOT NULL,
`flags` set({$flagsStr}) NOT NULL DEFAULT '',
`package` longtext NOT NULL,
`version` varchar(30) NOT NULL DEFAULT '',
`created` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `type_idx` (`type`),
KEY `hash` (`hash`),
KEY `flags` (`flags`),
KEY `version` (`version`),
KEY `created` (`created`),
KEY `updated_at` (`updated_at`),
KEY `status` (`status`),
KEY `name` (`name`(191)),
KEY `archive_name` (`archive_name`(191))
) {$charset_collate};
SQL;
return SnapWP::dbDelta($sql);
}
}