<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

namespace Akeeba\Engine\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use RuntimeException;
use ZipArchive;

/**
 * Class Zipnative
 *
 * This file uses the ZipArchive class to create and add files to existing ZIP
 * archives. For more information on the use of this class, please see:
 * 1. http://devzone.zend.com/article/2105 (tutorial)
 * 2. http://www.php.net/manual/en/class.ziparchive.php (reference)
 *
 * That said, the ZipArchive class is terribly inflexible when it comes down to
 * features already implemented in Akeeba Engine's Zip such as archive
 * splitting, chunked processing of very large files and processing of symlinks.
 * We deem it only suitable for small sites, without large files, running on a
 * decent hosting facility.
 */
class Zipnative extends Base
{
	/** @var string The name of the file holding the ZIP's data, which becomes the final archive */
	private $_dataFileName;

	/** @var ZipArchive An instance of the PHP ZIPArchive class */
	private $zip = null;

	/** @var int Running sum of bytes added to the archive */
	private $runningSum = 0;

	/** @var int Permissions for the backup archive part files */
	protected $permissions = null;

	/**
	 * Class constructor - initializes internal operating parameters
	 *
	 * @throws RuntimeException
	 */
	public function __construct()
	{
		Factory::getLog()->debug(__CLASS__ . " :: New instance");

		if (!class_exists('\ZipArchive'))
		{
			throw new RuntimeException('Your server does not support the ZipArchive extension. Please use a different Archiver Engine and retry backing up your site');
		}

		parent::__construct();
	}

	/**
	 * Common code which gets called on instance creation or wake-up (unserialization)
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function __bootstrap_code()
	{
		parent::__bootstrap_code();

		// So that the first run doesn't crash!
		if (empty($this->_dataFileName))
		{
			return;
		}

		// Try to reopen the ZIP
		$this->zip = new ZipArchive;

		if (!file_exists($this->_dataFileName))
		{
			$res = $this->zip->open($this->_dataFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE);
		}
		else
		{
			$res = $this->zip->open($this->_dataFileName);
		}

		if ($res !== true)
		{
			switch ($res)
			{
				case ZipArchive::ER_EXISTS:
					throw new RuntimeException("The archive {$this->_dataFileName} already exists");
					break;

				case ZipArchive::ER_INCONS:
					throw new RuntimeException("Inconsistent archive {$this->_dataFileName} detected");
					break;

				case ZipArchive::ER_INVAL:
					throw new RuntimeException("Invalid archive {$this->_dataFileName} detected");
					break;

				case ZipArchive::ER_MEMORY:
					throw new RuntimeException("Not enough memory to process archive {$this->_dataFileName}");
					break;

				case ZipArchive::ER_NOENT:
					throw new RuntimeException("Unexpected ZipArchive::ER_NOENT error processing archive {$this->_dataFileName}");
					break;

				case ZipArchive::ER_NOZIP:
					throw new RuntimeException("File {$this->_dataFileName} is not a ZIP archive!");
					break;

				case ZipArchive::ER_OPEN:
					throw new RuntimeException("Could not open archive file {$this->_dataFileName} for writing");
					break;

				case ZipArchive::ER_READ:
					throw new RuntimeException("Could not read from archive file {$this->_dataFileName}");
					break;

				case ZipArchive::ER_SEEK:
					throw new RuntimeException("Could not seek into position while processing archive file {$this->_dataFileName}");
					break;
			}
		}
	}

	/**
	 * Initialises the archiver class, creating the archive from an existent
	 * installer's JPA archive.
	 *
	 * @param   string  $targetArchivePath  Absolute path to the generated archive
	 * @param   array   $options            A named key array of options (optional). This is currently not supported
	 *
	 * @return  void
	 */
	public function initialize($targetArchivePath, $options = [])
	{
		Factory::getLog()->debug(__CLASS__ . " :: initialize - archive $targetArchivePath");

		// Get names of temporary files
		$this->_dataFileName = $targetArchivePath;

		// Try to kill the archive if it exists
		Factory::getLog()->debug(__CLASS__ . " :: Killing old archive");

		$fp = fopen($this->_dataFileName, "w");

		if (!($fp === false))
		{
			ftruncate($fp, 0);
			$this->conditionalFileClose($fp);
		}
		else
		{
			@unlink($this->_dataFileName);
		}

		$this->runningSum = 0;

		// Make sure we open the file
		$this->__bootstrap_code();
	}

	/**
	 * In this engine, we have no finalization, really
	 *
	 * @return  void
	 */
	public function finalize()
	{
		$this->zip->close();

		@chmod($this->_dataFileName, $this->getPermissions());
	}

	/**
	 * Returns a string with the extension (including the dot) of the files produced
	 * by this class.
	 *
	 * @return string
	 */
	public function getExtension()
	{
		return '.zip';
	}

	/**
	 * The most basic file transaction: add a single entry (file or directory) to
	 * the archive.
	 *
	 * @param   bool    $isVirtual         If true, the next parameter contains file data instead of a file name
	 * @param   string  $sourceNameOrData  Absolute file name to read data from or the file data itself is $isVirtual is
	 *                                     true
	 * @param   string  $targetName        The (relative) file name under which to store the file in the archive
	 *
	 * @return bool True on success, false otherwise
	 */
	protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
	{
		if (!is_object($this->zip))
		{
			return false;
		}

		if (!$isVirtual)
		{
			Factory::getLog()->debug(__CLASS__ . " :: Adding $sourceNameOrData");

			if (is_dir($sourceNameOrData))
			{
				$result = $this->zip->addEmptyDir($targetName);
			}
			else
			{
				$this->runningSum += filesize($sourceNameOrData);
				$result           = $this->zip->addFile($sourceNameOrData, $targetName);
			}
		}
		else
		{
			Factory::getLog()->debug('  Virtual add:' . $targetName . ' (' . strlen($sourceNameOrData) . ')');
			$this->runningSum += strlen($sourceNameOrData);

			if (empty($sourceNameOrData))
			{
				$result = $this->zip->addEmptyDir($targetName);
			}
			else
			{
				$result = $this->zip->addFromString($targetName, $sourceNameOrData);
			}
		}

		$this->zip->close();
		$this->__bootstrap_code();

		return true;
	}

	/**
	 * Return the requested permissions for the backup archive file.
	 *
	 * @return  int
	 * @since   8.0.0
	 */
	protected function getPermissions(): int
	{
		if (!is_null($this->permissions))
		{
			return $this->permissions;
		}

		$configuration     = Factory::getConfiguration();
		$permissions       = $configuration->get('engine.archiver.common.permissions', '0666') ?: '0666';
		$this->permissions = octdec($permissions);

		return $this->permissions;
	}
}
