HEX
Server: LiteSpeed
System: Linux s787.bom1.mysecurecloudhost.com 4.18.0-477.13.1.lve.el8.x86_64 #1 SMP Thu Jun 1 16:40:47 EDT 2023 x86_64
User: mobilech (5348)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/backup/src/JetBackup/Snapshot/Snapshot.php
<?php

namespace JetBackup\Snapshot;

use Exception;
use JetBackup\Archive\Archive;
use JetBackup\BackupJob\BackupJob;
use JetBackup\CLI\CLI;
use JetBackup\Cron\Task\Task;
use JetBackup\Data\Engine;
use JetBackup\Data\SleekStore;
use JetBackup\Destination\Destination;
use JetBackup\Destination\Vendors\Imported\Imported;
use JetBackup\Entities\Util;
use JetBackup\Exception\AjaxException;
use JetBackup\Exception\ArchiveException;
use JetBackup\Exception\DBException;
use JetBackup\Exception\GzipException;
use JetBackup\Exception\IOException;
use JetBackup\Exception\JBException;
use JetBackup\Exception\QueueException;
use JetBackup\Exception\SnapshotMetaException;
use JetBackup\Export\Vendor\Vendor;
use JetBackup\Factory;
use JetBackup\JetBackup;
use JetBackup\Log\LogController;
use JetBackup\Queue\Queue;
use JetBackup\Queue\QueueItem;
use JetBackup\Queue\QueueItemDownload;
use JetBackup\Queue\QueueItemExport;
use JetBackup\Queue\QueueItemExtract;
use JetBackup\Queue\QueueItemRestore;
use JetBackup\ResumableTask\ResumableTask;
use JetBackup\Schedule\Schedule;
use JetBackup\UserInput\UserInput;
use JetBackup\Wordpress\Wordpress;
use SleekDB\Exceptions\InvalidArgumentException;
use SleekDB\QueryBuilder;

if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');

class Snapshot extends Engine {

	const COLLECTION = 'snapshots';
	
	const UNIQUE_ID = 'unique_id';
	const NAME = 'name';
	const VERSION = 'version';
	const BACKUP_TYPE = 'backup_type';
	const NOTES = 'notes';
	const CREATED = 'created';
	const CONTAINS = 'contains';
	const STRUCTURE = 'structure';
	const SIZE = 'size';
	const REINDEX = 'reindex';
	const LOCKED = 'locked';
	const DELETED = 'deleted';
	const SCHEDULES = 'schedules';
	const SCHEDULES_NAMES = 'schedules_names';
	const DESTINATION_ID = 'destination_id';
	const DESTINATION_NAME = 'destination_name';
	const JOB_IDENTIFIER = 'job_identifier';
	const ITEMS = 'items';
	const PARAMS = 'params';

	const SKELETON_DATABASE_DIRNAME = 'database';
	const SKELETON_LOG_DIRNAME = 'log';
	const SKELETON_LOG_FILENAME = 'task.log.gz';
	const SKELETON_TEMP_DIRNAME = 'tmp';
	const SKELETON_META_DIRNAME = 'meta';
	const SKELETON_CONFIG_DIRNAME = 'config';
	const SKELETON_FILES_DIRNAME = 'files';
	const SKELETON_FILES_ARCHIVE_NAME = 'files' . Archive::ARCHIVE_EXT;
	const SKELETON_META_FILENAME = 'meta.json';
	const SKELETON_FILES_LIST_FILENAME = 'files.list';

	const META_MIN_VERSION = '1.0.3';
	const META_VERSION = '1.0.3';
	const META_FILEPATH = '%s' . JetBackup::SEP . self::SKELETON_META_DIRNAME . JetBackup::SEP . self::SKELETON_META_FILENAME;
	
	const LOG_FILEPATH = '%s' . JetBackup::SEP . self::SKELETON_LOG_DIRNAME . JetBackup::SEP . self::SKELETON_LOG_FILENAME;

	const SNAPSHOT_NAME_PATTERN = 'snap_%s_%s';
	const SNAPSHOT_NAME_REGEX = "/^snap_([\d]{4}-[\d]{2}-[\d]{2})_([\d]{6})_(.*)$/";

	const PARAM_MULTISITE = 'multisite';
	const PARAM_SITE_URL = 'site_url';
	const PARAM_DB_PREFIX = 'db_prefix';
	const PARAM_DB_EXCLUDED = 'db_excluded';
	const PARAM_WP_CONTENT_ONLY_BACKUP = 'wp_content_only_backup';

	/**
	 * @var SnapshotItem[]
	 */
	private array $_items=[];
	
	public function __construct($_id=null) {
		parent::__construct(self::COLLECTION);
		if($_id) $this->_loadById((int) $_id);
	}

	public function setUniqueId($id) { $this->set(self::UNIQUE_ID, $id); }
	public function getUniqueId():string { return $this->get(self::UNIQUE_ID); }

	public function setCreated(int $created) { $this->set(self::CREATED, $created); }
	public function getCreated():int { return $this->get(self::CREATED, 0); }

	public function setName(string $name) { $this->set(self::NAME, $name); }
	public function getName():string { return $this->get(self::NAME); }

	public function setBackupType(int $type) { $this->set(self::BACKUP_TYPE, $type); }
	public function getBackupType() : int { return $this->get(self::BACKUP_TYPE, 0); }

	public function setDestinationId(int $_id) { $this->set(self::DESTINATION_ID, $_id); }
	public function getDestinationId():int { return $this->get(self::DESTINATION_ID, 0); }

	public function getDestinationName():string {return (new Destination($this->getDestinationId()))->getName();}

	public function setJobIdentifier(string $identifier) { $this->set(self::JOB_IDENTIFIER, $identifier); }
	public function getJobIdentifier():string { return $this->get(self::JOB_IDENTIFIER); }

	public function setSize(int $size) { $this->set(self::SIZE, $size); }
	public function getSize():int { return (int) $this->get(self::SIZE, 0); }

	public function setStructure(int $structure) { $this->set(self::STRUCTURE, $structure); }
	public function getStructure():int { return (int) $this->get(self::STRUCTURE, 0); }

	public function setContains(int $contains) { $this->set(self::CONTAINS, $contains); }
	public function getContains():int { return (int) $this->get(self::CONTAINS, 0); }

	public function setNotes(string $notes) { $this->set(self::NOTES, $notes); }
	public function getNotes():string { return $this->get(self::NOTES); }

	public function setSchedules(array $schedules) { $this->set(self::SCHEDULES, $schedules); }
	public function getSchedules(): array {return $this->get(self::SCHEDULES, []);}

	/**
	 * @return array
	 */
	public function getSchedulesNamesByType(): array {
		$schedules = $this->getSchedules();
		$output = [];
		if (!empty($schedules)) {
			foreach ($schedules as $schedule_type) {
				$output[] = [
					'schedule_id' => $schedule_type,
					'schedule_name' => Schedule::TYPE_NAMES[$schedule_type] ?? 'Unknown',
				];
			}
		}
		return $output;
	}


	public function setReindex(bool $reindex) { $this->set(self::REINDEX, $reindex); }
	public function isReindex():bool { return $this->get(self::REINDEX, false); }

	public function setLocked(bool $locked) { $this->set(self::LOCKED, $locked); }
	public function isLocked():bool { return $this->get(self::LOCKED, false); }

	public function setDeleted(int $deleted) { $this->set(self::DELETED, $deleted); }
	public function getDeleted():int { return $this->get(self::DELETED, 0); }
	public function isDeleted():bool { return $this->getDeleted() > 0 && $this->getDeleted() <= time(); }

	public function setParams(array $params) { $this->set(self::PARAMS, $params); }
	public function getParams():array { return $this->get(self::PARAMS, []); }
	public function getParam(string $key, $default=null) {
		$params = $this->getParams();
		return $params[$key] ?? $default;
	}
	public function addParam(string $key, $value):void {
		$params = $this->getParams();
		$params[$key] = $value;
		$this->setParams($params);
	}

	public function setItems(array $items) { $this->_items = $items; }
	public function addItem(SnapshotItem $item) { $this->_items[] = $item; }

	/**
	 * @return SnapshotItem[]
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws InvalidArgumentException|DBException
	 */
	public function getItems():array {
		
		if($this->_items) return $this->_items;

		$output = [];
		
		$list = SnapshotItem::query()
			->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
            ->orderBy([SnapshotItem::NAME =>'asc'])
			->getQuery()
			->fetch();

		foreach($list as $item_details) $output[] = new SnapshotItem($item_details[JetBackup::ID_FIELD]);

		return $output; 
	}

	/**
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public function removeItems() {
		SnapshotItem::query()
			->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
			->getQuery()
			->delete();
	}

	/**
	 * @param $source
	 * @param LogController|null $logController
	 * @param callable|null $callback
	 * @param array $excludes
	 * @param array $includes
	 *
	 * @return void
	 * @throws ArchiveException
	 * @throws DBException
	 * @throws GzipException
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public function extract($source, ?LogController $logController=null, ?callable $callback=null, array $excludes=[],  array $includes=[]) {

		if(!($items = $this->getItems())) return;

		if(!$logController) $logController = new LogController();

		foreach($items as $item) $item->extract($source, $logController, $callback, $excludes, $includes);
	}

	/**
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public function delete():void {
		parent::delete();
		
		SnapshotItem::query()
			->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
			->getQuery()
			->delete();
	}
	
	public function isCompressed():bool { return $this->getStructure() == BackupJob::STRUCTURE_COMPRESSED; }
	
	public static function generateName(): string {
		return sprintf(self::SNAPSHOT_NAME_PATTERN, Util::date('Y-m-d_His'), Util::generateUniqueId());
	}
	
	public function save():void {
		if(!$this->getUniqueId()) $this->setUniqueId(Util::generateUniqueId());
		parent::save();
		
		foreach($this->_items as $item) {
			$item->setParentId($this->getId());
			$item->save();
		}
	}

	public static function db():SleekStore {
		return new SleekStore(self::COLLECTION);
	}

	public static function query():QueryBuilder {
		return self::db()->createQueryBuilder();
	}

	/**
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws InvalidArgumentException
	 */
	public static function getTotalSnapshots():int {
		return count(self::query()
			->select([JetBackup::ID_FIELD])
			->getQuery()
			->fetch());
	}

	/**
	 * @param int $type
	 *
	 * @return void
	 */
	public function addScheduleByType(int $type):void {
		$types = $this->getSchedules();
		if(in_array($type, $types)) return;
		$types[] = $type;
		$this->setSchedules($types);
	}

	/**
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws QueueException
	 * @throws InvalidArgumentException
	 */
	public function addToDownloadQueue() {
		$download = new QueueItemDownload();
		$download->setSnapshotId($this->getId());

		$queue_item = QueueItem::prepare();
		$queue_item->setType(Queue::QUEUE_TYPE_DOWNLOAD);
		$queue_item->setItemId($this->getId());
		$queue_item->setItemData($download);

		Queue::addToQueue($queue_item);
	}

    /**
     * @throws \SleekDB\Exceptions\IOException
     * @throws QueueException
     * @throws InvalidArgumentException
     */

    public function addToDownloadLogQueue() {
        $download = new QueueItemDownload();
        $download->setSnapshotId($this->getId());

        $queue_item = QueueItem::prepare();
        $queue_item->setType(Queue::QUEUE_TYPE_DOWNLOAD_BACKUP_LOG);
        $queue_item->setItemId($this->getId());
        $queue_item->setItemData($download);

        Queue::addToQueue($queue_item);
    }

	/**
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws QueueException
	 * @throws InvalidArgumentException
	 */
	private static function _addToRestoreQueue(int $id=0, string $path='', int $options=0, array $exclude=[],  array $include=[], array $exclude_db=[], array $include_db=[], array $filemanager=[]) {
		$restore = new QueueItemRestore();

		// these file cannot be restored, we can cut our own branch
		$exclude[] = 'wp-config.php';
		$exclude[] = '.htaccess';

		if($id) $restore->setSnapshotId($id);
		elseif($path) {
			if(!file_exists($path) || !is_readable($path)) throw new QueueException("The provided snapshot path not exists");
			if( !Archive::isGzCompressed($path) && !Archive::isTar($path)) throw new QueueException("The provided snapshot path is not a tar.gz file");
			$restore->setSnapshotPath($path);
		} else throw new QueueException("You must provide snapshot id or path");

		// Check if backup was created with wp-content-only restriction
		$isWpContentOnlyBackup = false;
		if ($id) {
			$snapshot = new Snapshot($id);
			$isWpContentOnlyBackup = $snapshot->getParam(self::PARAM_WP_CONTENT_ONLY_BACKUP, false);
		}

		// Apply wp-content-only restriction if:
		// 1. Current setting is enabled, OR
		// 2. Backup was originally created with this restriction (WP Cloud or setting was enabled)
		// Note: in multisite restore, when restoring subsites $includes will already have values
		if ((Factory::getSettingsRestore()->isRestoreWpContentOnlyEnabled() || $isWpContentOnlyBackup) && empty($include)) {
			$exclude = [];
			$include[] = Wordpress::WP_CONTENT . JetBackup::SEP . '*';
		}

		$restore->setOptions($options);
		$restore->setExcludes($exclude);
		$restore->setIncludes($include);
		$restore->setExcludedDatabases($exclude_db);
		$restore->setIncludedDatabases($include_db);
		$restore->setFileManager($filemanager);
		
		$queue_item = QueueItem::prepare();
		$queue_item->setType(Queue::QUEUE_TYPE_RESTORE);
		$queue_item->setItemData($restore);

		Queue::addToQueue($queue_item);
	}

	/**
	 * @param $path
	 * @param int $options
	 * @param array $exclude
	 * @param array $exclude_db
	 * @param array $include_db
	 *
	 * @return void
	 * @throws InvalidArgumentException
	 * @throws QueueException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public static function addToRestoreQueueByPath($path, int $options= QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE | QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE, array $exclude=[], array $exclude_db=[], array $include_db=[]) {
		$include = []; // placeholder for future implementation
		self::_addToRestoreQueue(0, $path, $options, $exclude, $include, $exclude_db, $include_db);
	}

	/**
	 * Import a backup file from a given path and save it to the database.
	 * This allows the backup to be indexed and restored later without re-uploading.
	 *
	 * @param string $path Path to the backup file (tar or tar.gz)
	 * @param bool $crossDomain Allow cross-domain restores
	 *
	 * @return Snapshot The imported and saved snapshot
	 * @throws QueueException
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws DBException
	 * @throws \JetBackup\Exception\DestinationException
	 */
	public static function importFromPath(string $path, bool $crossDomain = true): Snapshot {
		// 1. Validate backup file
		if (!file_exists($path) || !is_readable($path)) {
			throw new QueueException("The provided backup file doesn't exist or is not readable");
		}

		$isCompressed = Archive::isGzCompressed($path);
		if (!$isCompressed && !Archive::isTar($path)) {
			throw new QueueException("The provided file is not a valid backup file (tar or tar.gz)");
		}

		// 2. Extract only meta.json to a temp location
		$tempDir = Factory::getLocations()->getTempDir() . JetBackup::SEP . 'import_' . Util::generateUniqueId();
		if (!file_exists($tempDir)) {
			mkdir($tempDir, 0700, true);
		}

		$extractPath = $path;

		// If compressed, decompress to temp location first
		if ($isCompressed) {
			$decompressedPath = $tempDir . JetBackup::SEP . basename($path, '.gz');
			\JetBackup\Archive\Gzip::decompress($path, $decompressedPath);
			$extractPath = $decompressedPath;
		}

		// Extract only the meta directory from the tar
		try {
			$archive = new Archive($extractPath);
			$archive->setExcludeCallback(function($path, $isDir) {
				// Exclude everything EXCEPT meta directory files
				$metaDir = self::SKELETON_META_DIRNAME;
				return !str_starts_with($path, $metaDir . JetBackup::SEP) && $path !== $metaDir;
			});
			$archive->extract($tempDir);
		} catch (ArchiveException $e) {
			Util::rm($tempDir);
			throw new QueueException("Failed to extract backup meta: " . $e->getMessage());
		}

		// Remove decompressed temp file if we created one
		if ($isCompressed && file_exists($extractPath)) {
			unlink($extractPath);
		}

		// 3. Parse meta.json
		$metaFile = sprintf(self::META_FILEPATH, $tempDir);
		if (!file_exists($metaFile)) {
			Util::rm($tempDir);
			throw new QueueException("The provided backup doesn't contain a valid meta file");
		}

		$snapshot = new Snapshot();
		try {
			$snapshot->importMeta($metaFile, $crossDomain);
		} catch (SnapshotMetaException $e) {
			Util::rm($tempDir);
			throw new QueueException("Can't import backup: " . $e->getMessage());
		}

		// Clean up temp meta extraction
		Util::rm($tempDir);

		// 4. Get or create Imported destination
		$destination = Destination::createImportedDestination();

		// 5. Generate job identifier for imported backup
		$jobIdentifier = 'imported_' . Util::generateUniqueId();

		// 6. Check for existing snapshot with same name to avoid duplicates
		$existing = self::query()
			->where([self::NAME, '=', $snapshot->getName()])
			->where([self::DESTINATION_ID, '=', $destination->getId()])
			->getQuery()
			->first();

		if ($existing) {
			// Return existing snapshot, no need to re-import
			return new Snapshot($existing[JetBackup::ID_FIELD]);
		}

		// 7. Move backup file to permanent location
		$destination->connect();
		$permanentDir = JetBackup::SEP . $jobIdentifier;

		if (!$destination->dirExists($permanentDir)) {
			$destination->createDir($permanentDir);
		}

		$backupFilename = $snapshot->getName() . ($isCompressed ? '.tar.gz' : '.tar');
		$permanentPath = $permanentDir . JetBackup::SEP . $backupFilename;

		// Copy file to permanent location (use copy to preserve original until confirmed)
		$destination->getInstance()->copyFileToRemote($path, $permanentPath);
		$destination->disconnect();

        $dataDir = Factory::getLocations()->getDataDir();

        $archiveFilename = basename($permanentPath);

        $extractFolderName = $snapshot->getName();

        $archivePath = $dataDir . JetBackup::SEP. Imported::IMPORTS_DIR . JetBackup::SEP.$jobIdentifier. JetBackup::SEP. $archiveFilename;
        $extractDir  = $dataDir . JetBackup::SEP. Imported::IMPORTS_DIR . JetBackup::SEP .$jobIdentifier. JetBackup::SEP. $extractFolderName;

        if (!is_dir($extractDir)) {
            mkdir($extractDir, 0755, true);
        }
        try {
            $archive = new Archive($archivePath);
            $archive->extract($extractDir);
        } catch (\Exception $e) {
            throw new QueueException("Failed to extract imported backup: " . $e->getMessage());
        }
        unlink($archivePath);
        // Remove original uploaded file
		unlink($path);

		// If the file was decompressed from a .gz file, the original .gz parent dir may now be empty
		$parentDir = dirname($path);
		if (is_dir($parentDir) && count(scandir($parentDir)) == 2) {
			rmdir($parentDir);
		}

		// 8. Set snapshot fields
		$snapshot->setDestinationId($destination->getId());
		$snapshot->setJobIdentifier($jobIdentifier);
		$snapshot->addParam('imported', true);
		$snapshot->addParam('import_time', time());
		$snapshot->addParam('original_filename', basename($path));

		// Add import schedule type for retention tracking
		$snapshot->addScheduleByType(Schedule::TYPE_IMPORTED);

		// 9. Save snapshot to database
		$snapshot->save();

		return $snapshot;
	}

	/**
	 * @return void
	 * @throws InvalidArgumentException
	 * @throws QueueException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public function addToRestoreQueue(int $options=QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE | QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE, array $exclude=[], array $include=[], array $exclude_db=[], array $include_db=[], array $filemanager=[]) {
		self::_addToRestoreQueue($this->getId(), '', $options, $exclude, $include, $exclude_db, $include_db, $filemanager);
	}

	/**
	 * @param int $type
	 *
	 * @return void
	 * @throws InvalidArgumentException
	 * @throws QueueException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public function addToExportQueue(int $type) {

		if (!$type || !in_array($type, Vendor::ALL_VENDORS)) throw new QueueException("Invalid Panel type");

		$export = new QueueItemExport();
		$export->setType($type);
		$export->setSnapshotId($this->getId());

		$queue_item = QueueItem::prepare();
		$queue_item->setType(Queue::QUEUE_TYPE_EXPORT);
		$queue_item->setItemId($this->getId());
		$queue_item->setItemData($export);

		Queue::addToQueue($queue_item);
	}

	/**
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws QueueException
	 */
	public function addToExtractQueue($extract_path=null): void {
		$extract = new QueueItemExtract();
		$extract->setSnapshotId($this->getId());
		if($extract_path) $extract->setExtractPath($extract_path);

		$queue_item = QueueItem::prepare();
		$queue_item->setType(Queue::QUEUE_TYPE_EXTRACT);
		$queue_item->setItemId($this->getId());
		$queue_item->setItemData($extract);

		Queue::addToQueue($queue_item);
	}
	
	/**
	 * @param int $type
	 *
	 * @return void
	 */
	public function removeSchedule(int $type):void {
		if($type < 1) return;
		$types = $this->getSchedules();
		$offset = array_search($type, $types);
		if($offset === false) return;
		array_splice($types, $offset, 1);
		$this->setSchedules($types);
	}

	/**
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public static function getTotalSnapshotsSize():int {
		$list = self::query()
			->where([Engine::ENGINE, '=', Engine::ENGINE_WP])
			->select([self::SIZE])
			->getQuery()
			->fetch();

		$size = 0;
		foreach ($list as $item) $size += $item[self::SIZE];
		return $size;
	}

	/**
	 * @param int $destinationID
	 *
	 * @return void
	 * @throws AjaxException
	 * @throws DBException
	 * @throws InvalidArgumentException
	 * @throws \SleekDB\Exceptions\IOException
	 */
	public static function deleteByDestinationID(int $destinationID):void {

		$snapshots = Snapshot::query()
		                     ->select([JetBackup::ID_FIELD])
		                     ->where([Snapshot::DESTINATION_ID, '=', $destinationID])
		                     ->getQuery()
		                     ->fetch();

		if(empty($snapshots)) return;

		foreach ($snapshots as $snapshotID) {
			$snapshot = new Snapshot($snapshotID[JetBackup::ID_FIELD]);
			if(!$snapshot->getId()) throw new AjaxException("Invalid snapshot id provided");
			$snapshot->delete();
		}

	}

	/**
	 * @return array
	 */
	public function getDisplay(): array {
		return [
			JetBackup::ID_FIELD     => $this->getId(),
			self::UNIQUE_ID         => $this->getUniqueId(),
			self::DESTINATION_ID    => $this->getDestinationId(),
			self::DESTINATION_NAME  => $this->getDestinationName(),
			self::NAME              => $this->getName(),
			self::BACKUP_TYPE       => $this->getBackupType(),
			Engine::ENGINE          => $this->getEngine(),
			self::CONTAINS          => $this->getContains(),
			self::STRUCTURE         => $this->getStructure(),
			self::SCHEDULES         => $this->getSchedules(),
			self::SCHEDULES_NAMES   => $this->getSchedulesNamesByType(),
			self::NOTES             => $this->getNotes(),
			self::PARAMS            => $this->getParams(),
			self::CREATED           => $this->getCreated(),
			self::SIZE              => Util::bytesToHumanReadable($this->getSize()),
			self::LOCKED            => $this->isLocked() ? 1 : 0,
			self::DELETED           => $this->getDeleted()
		];
	}

	/**
	 * @return array
	 */
	public function getDisplayCLI(): array {
		return [
			'ID'     => $this->getId(),
			'Destination ID'    => $this->getDestinationId(),
			'Name'              => $this->getName(),
			'Backup Type'       => $this->getBackupType(),
			'Engine'            => $this->getEngineName(),
			'Contains'          => $this->getContains(),
			'Structure'         => $this->getStructure(),
			'Schedules'         => $this->getSchedules(),
			'Notes'             => $this->getNotes(),
			'Created'           => CLI::date($this->getCreated()),
			'Size'              => $this->getSize(),
			'Locked'            => $this->isLocked() ? 'Yes' : 'No',
			'Deleted'           => $this->getDeleted(),
		];
	}

	public function exportMeta($path):void {

		if($this->getEngine() != Engine::ENGINE_WP) 
			throw new SnapshotMetaException("Only snapshot of type WordPress is allowed to be exported");
			
		$items = [];
		foreach($this->_items as $item) $items[] = $item->exportMeta();
		$umask = umask(077);

		file_put_contents(sprintf(self::META_FILEPATH, $path), json_encode([
			self::VERSION           => self::META_VERSION,
			self::NAME              => $this->getName(),
			self::BACKUP_TYPE       => $this->getBackupType(),
			Engine::ENGINE          => $this->getEngine(),
			self::CONTAINS          => $this->getContains(),
			self::STRUCTURE         => $this->getStructure(),
			self::SCHEDULES         => $this->getSchedules(),
			self::JOB_IDENTIFIER    => $this->getJobIdentifier(),
			self::CREATED           => $this->getCreated(),
			self::SIZE              => $this->getSize(),
			self::PARAMS            => $this->getParams(),
			self::ITEMS             => $items,
		]));
		umask($umask);
	}

	/**
	 * @param string $file
	 * @param bool $cross_domain
	 *
	 * @return void
	 * @throws SnapshotMetaException
	 */
	public function importMeta(string $file, bool $cross_domain=false):void {


		$meta = json_decode(file_get_contents($file));

		$current_site_url = Wordpress::getSiteDomain();

		if(isset($meta->version) && version_compare($meta->version, self::META_MIN_VERSION, '>=')) {

			$this->setEngine($meta->{Engine::ENGINE});
			$this->setBackupType($meta->{self::BACKUP_TYPE});
			$this->setCreated($meta->{self::CREATED});
			$this->setName($meta->{self::NAME});
			$this->setContains($meta->{self::CONTAINS});
			$this->setParams((array) $meta->{self::PARAMS});
			$this->setStructure($meta->{self::STRUCTURE});
			$this->setSchedules($meta->{self::SCHEDULES});
			$this->setSize($meta->{self::SIZE});

			$backup_site_url = preg_replace("#^http(s?)://#", "", $this->getParam(self::PARAM_SITE_URL));

			if( !$cross_domain && $backup_site_url != $current_site_url)
				throw new SnapshotMetaException("This snapshot isn't owned by this site, you need to allow cross-domain restores");

			foreach($meta->{self::ITEMS} as $item_meta) {
				$item = new SnapshotItem();
				$item->importMeta($item_meta);
				$this->addItem($item);
			}

		} elseif(isset($meta->meta_version)) {
			// Support meta file with version 1.0.2 and below

			$backup_site_url = preg_replace("#^http(s?)://#", "", $meta->domain_backup);

			if( !$cross_domain && $backup_site_url != $current_site_url)
				throw new SnapshotMetaException("This snapshot isn't owned by this site, you need to allow cross-domain restores");

			$contains = 0;

			if(isset($meta->job_settings->backup_contains)) {
				$backup_contains = (array) $meta->job_settings->backup_contains;
				if(in_array('homedir', $backup_contains)) $contains |= BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR;
				if(in_array('database', $backup_contains)) $contains |= BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE;
			}

			$this->setEngine(isset($meta->queue_item->engine) ? (Wordpress::strContains($meta->queue_item->engine, '5.303,13.236,5.301,20.758 C91.384,64.08,82.732,78.016,69.871,85.516z') ? Engine::ENGINE_WP : Engine::ENGINE_JB) : Engine::ENGINE_WP);
			$this->setBackupType(BackupJob::TYPE_ACCOUNT);
			$this->setCreated($meta->queue_item->created ?? 0);
			$this->setName(isset($meta->queue_item->snap) && $meta->queue_item->snap ? 'snap_' . $meta->queue_item->snap : '');
			$this->setContains($contains);
			if(isset($meta->backup_structure_name)) $this->setStructure($meta->backup_structure_name == 'Compressed' ? BackupJob::STRUCTURE_COMPRESSED : BackupJob::STRUCTURE_ARCHIVED);
			$this->setSize($meta->size ?? 0);

			$this->addParam(Snapshot::PARAM_MULTISITE, $meta->multisite_details);
			$this->addParam(Snapshot::PARAM_SITE_URL, $meta->domain_backup);
			
			$schedules = [];
			if(isset($meta->queue_item->parent_schedules)) foreach($meta->queue_item->parent_schedules as $schedule_id) $schedules[] = (int) $schedule_id;
			$this->setSchedules($schedules);

			if($this->getContains() & BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR) {
				$item = new SnapshotItem();
				$item->setBackupType(BackupJob::TYPE_ACCOUNT);
				$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR);
				$item->setCreated($meta->queue_item->created);
				$item->setName('');
				$item->setPath('files/files.tar' . ($this->isCompressed() ? '.gz' : ''));
				$item->setSize($meta->size - $meta->db_size);

				$this->addItem($item);
			}

			if(isset($meta->db_items) && is_array($meta->db_items)) {
				foreach ($meta->db_items as $db_item) {
					$item = new SnapshotItem();
					$item->setName($db_item->name ?? '');
					$item->setPath('db_dumps/' . $db_item->name . '.sql' . ($db_item->compressed ? '.gz' : ''));
					$item->setSize($db_item->size ?? 0);
					$item->setCreated($db_item->mtime);
					$item->setBackupType(BackupJob::TYPE_ACCOUNT);
					$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE);

					$item->addParam(Snapshot::PARAM_DB_PREFIX, $meta->db_prefix);
					$item->addParam(Snapshot::PARAM_DB_EXCLUDED,  in_array($db_item->name, $meta->job_settings->exDatabase));
					
					$this->addItem($item);
				}
			}

			$item = new SnapshotItem();
			$item->setBackupType(BackupJob::TYPE_ACCOUNT);
			$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_FULL);
			$item->setCreated($meta->queue_item->created);
			$item->setName('');
			$item->setPath('');
			$item->setSize(0);

			$item->addParam(Snapshot::PARAM_MULTISITE, $meta->multisite_details);
			$item->addParam(Snapshot::PARAM_SITE_URL, $meta->domain_backup);

			$this->addItem($item);

		} else throw new SnapshotMetaException("Can't find meta file version");

		$this->_validateImportMeta();
	}

	/**
	 * @return void
	 * @throws SnapshotMetaException
	 */
	private function _validateImportMeta():void {
		if(!$this->getName()) throw new SnapshotMetaException("Can't find name in meta file");
		if(!$this->getEngine()) throw new SnapshotMetaException("Can't find engine in meta file");
		if(!$this->getBackupType()) throw new SnapshotMetaException("Can't find backup type in meta file");
		if(!$this->getCreated()) throw new SnapshotMetaException("Can't find creation date in meta file");
		if(!$this->getContains()) throw new SnapshotMetaException("Can't find backup contains in meta file");
		if(!$this->getStructure()) throw new SnapshotMetaException("Can't find backup structure in meta file");
		// Schedules are optional - manual/on-demand backups may not have schedules
		if(!$this->_items) throw new SnapshotMetaException("Can't find backup items in meta file");
	}
}