feat(deployed): add Akeeba Backup 6.1.1 (no-source, vetted live)
Akeeba Backup Core (Akeeba Ltd) + FOF 2.4.3/FOF30 3.3.4/FEF libraries; exact 6.1.1 not publicly downloadable -> deployed files (code only, no backup archives). Signed-off-by: LÁZÁR Imre <imre@illusion.hu> Assisted-by: claude-code@claude-opus-4-8
This commit is contained in:
parent
eadf605020
commit
9c9a0366e8
@ -0,0 +1,830 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Base\BaseObject as BaseObject;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Akeeba\Engine\Util\FileSystem;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Abstract parent class of all archiver engines
|
||||
*/
|
||||
abstract class Base extends BaseObject
|
||||
{
|
||||
/** @var Filesystem Filesystem utilities object */
|
||||
protected $fsUtils = null;
|
||||
|
||||
/** @var string The archive's comment. It's currently used ONLY in the ZIP file format */
|
||||
protected $_comment;
|
||||
|
||||
/** @var resource JPA transformation source handle */
|
||||
private $_xform_fp;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return Base
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakeup (unserialization) function
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the engine on the backup comment and converts it to plain text for
|
||||
* inclusion in the archive file, if applicable.
|
||||
*
|
||||
* @param string $comment The archive's comment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
// First, sanitize the comment in a text-only format
|
||||
$comment = str_replace("\n", " ", $comment); // Replace newlines with spaces
|
||||
$comment = str_replace("<br>", "\n", $comment); // Replace HTML4 <br> with single newlines
|
||||
$comment = str_replace("<br/>", "\n", $comment); // Replace HTML4 <br> with single newlines
|
||||
$comment = str_replace("<br />", "\n", $comment); // Replace HTML <br /> with single newlines
|
||||
$comment = str_replace("</p>", "\n\n", $comment); // Replace paragraph endings with double newlines
|
||||
$comment = str_replace("<b>", "*", $comment); // Replace bold with star notation
|
||||
$comment = str_replace("</b>", "*", $comment); // Replace bold with star notation
|
||||
$comment = str_replace("<i>", "_", $comment); // Replace italics with underline notation
|
||||
$comment = str_replace("</i>", "_", $comment); // Replace italics with underline notation
|
||||
$this->_comment = strip_tags($comment, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single file in the archive
|
||||
*
|
||||
* @param string $file The absolute path to the file to add
|
||||
* @param string $removePath Path to remove from $file
|
||||
* @param string $addPath Path to prepend to $file
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function addFile($file, $removePath = '', $addPath = '')
|
||||
{
|
||||
$storedName = $this->_addRemovePaths($file, $removePath, $addPath);
|
||||
|
||||
return $this->addFileRenamed($file, $storedName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the archive, given the stored name and its contents
|
||||
*
|
||||
* @param string $fileName The base file name
|
||||
* @param string $addPath The relative path to prepend to file name
|
||||
* @param string $virtualContent The contents of the file to be archived
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function addVirtualFile($fileName, $addPath = '', &$virtualContent)
|
||||
{
|
||||
$mb_encoding = '8bit';
|
||||
|
||||
$storedName = $this->_addRemovePaths($fileName, '', $addPath);
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
$mb_encoding = mb_internal_encoding();
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $this->_addFile(true, $virtualContent, $storedName);
|
||||
}
|
||||
catch (WarningException $e)
|
||||
{
|
||||
$this->setWarning($e->getMessage());
|
||||
$ret = false;
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
$this->setError($e->getMessage());
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
mb_internal_encoding($mb_encoding);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the archive, with a name that's different from the source
|
||||
* filename
|
||||
*
|
||||
* @param string $sourceFile Absolute path to the source file
|
||||
* @param string $targetFile Relative filename to store in archive
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function addFileRenamed($sourceFile, $targetFile)
|
||||
{
|
||||
$mb_encoding = '8bit';
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
$mb_encoding = mb_internal_encoding();
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $this->_addFile(false, $sourceFile, $targetFile);
|
||||
}
|
||||
catch (WarningException $e)
|
||||
{
|
||||
$this->setWarning($e->getMessage());
|
||||
$ret = false;
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
$this->setError($e->getMessage());
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
if (function_exists('mb_internal_encoding'))
|
||||
{
|
||||
mb_internal_encoding($mb_encoding);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of files into the archive, removing $removePath from the
|
||||
* file names and adding $addPath to them.
|
||||
*
|
||||
* @param array $fileList A simple string array of filepaths to include
|
||||
* @param string $removePath Paths to remove from the filepaths
|
||||
* @param string $addPath Paths to add in front of the filepaths
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function addFileList(&$fileList, $removePath = '', $addPath = '')
|
||||
{
|
||||
if (!is_array($fileList))
|
||||
{
|
||||
$this->setWarning('addFileList called without a file list array');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($fileList as $file)
|
||||
{
|
||||
if (!$this->addFile($file, $removePath, $addPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the archiver class, creating the archive from an existent
|
||||
* installer's JPA archive. MUST BE OVERRIDEN BY CHILDREN CLASSES.
|
||||
*
|
||||
* @param string $targetArchivePath Absolute path to the generated archive
|
||||
* @param array $options A named key array of options (optional)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function initialize($targetArchivePath, $options = array());
|
||||
|
||||
/**
|
||||
* Makes whatever finalization is needed for the archive to be considered
|
||||
* complete and useful (or, generally, clean up)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function finalize();
|
||||
|
||||
/**
|
||||
* Transforms a JPA archive (containing an installer) to the native archive format
|
||||
* of the class. It actually extracts the source JPA in memory and instructs the
|
||||
* class to include each extracted file.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param integer $index The index in the source JPA archive's list currently in use
|
||||
* @param integer $offset The source JPA archive's offset to use
|
||||
*
|
||||
* @return array|bool False if an error occurred, return array otherwise
|
||||
*/
|
||||
public function transformJPA($index, $offset)
|
||||
{
|
||||
static $totalSize = 0;
|
||||
|
||||
// Do we have to open the file?
|
||||
if (!$this->_xform_fp)
|
||||
{
|
||||
// Get the source path
|
||||
$registry = Factory::getConfiguration();
|
||||
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
|
||||
|
||||
// Fetch the name of the installer image
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
$xform_source = Platform::getInstance()->get_installer_images_path() .
|
||||
'/foobar.jpa'; // We need this as a "safe fallback"
|
||||
|
||||
// Try to find a sane default if we are not given a valid embedded installer
|
||||
if (!array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$embedded_installer = 'angie';
|
||||
|
||||
if (!array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$allInstallers = array_keys($installerDescriptors);
|
||||
|
||||
foreach ($allInstallers as $anInstaller)
|
||||
{
|
||||
if ($anInstaller == 'none')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$embedded_installer = $anInstaller;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($embedded_installer, $installerDescriptors))
|
||||
{
|
||||
$packages = $installerDescriptors[$embedded_installer]['package'];
|
||||
$langPacks = $installerDescriptors[$embedded_installer]['language'];
|
||||
|
||||
if (empty($packages))
|
||||
{
|
||||
// No installer package specified. Pretend we are done!
|
||||
$retArray = array(
|
||||
"filename" => '', // File name extracted
|
||||
"data" => '', // File data
|
||||
"index" => 0, // How many source JPA files I have
|
||||
"offset" => 0, // Offset in JPA file
|
||||
"skip" => false, // Skip this?
|
||||
"done" => true, // Are we done yet?
|
||||
"filesize" => 0,
|
||||
);
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
$packages = explode(',', $packages);
|
||||
$langPacks = explode(',', $langPacks);
|
||||
$totalSize = 0;
|
||||
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
|
||||
|
||||
foreach ($packages as $package)
|
||||
{
|
||||
$filePath = $pathPrefix . $package;
|
||||
$totalSize += (int) @filesize($filePath);
|
||||
}
|
||||
|
||||
foreach ($langPacks as $langPack)
|
||||
{
|
||||
$filePath = $pathPrefix . $langPack;
|
||||
|
||||
if (!is_file($filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$packages[] = $langPack;
|
||||
$totalSize += (int) @filesize($filePath);
|
||||
}
|
||||
|
||||
if (count($packages) < $index)
|
||||
{
|
||||
$this->setError(__CLASS__ . ":: Installer package index $index not found for embedded installer $embedded_installer");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$package = $packages[$index];
|
||||
|
||||
// A package is specified, use it!
|
||||
$xform_source = $pathPrefix . $package;
|
||||
}
|
||||
|
||||
// 2.3: Try to use sane default if the indicated installer doesn't exist
|
||||
if (!file_exists($xform_source) && (basename($xform_source) != 'angie.jpa'))
|
||||
{
|
||||
$this->setError(__CLASS__ . ":: Installer package $xform_source of embedded installer $embedded_installer not found. Please go to the configuration page, select an Embedded Installer, save the configuration and try backing up again.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try opening the file
|
||||
if (file_exists($xform_source))
|
||||
{
|
||||
$this->_xform_fp = @fopen($xform_source, 'r');
|
||||
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
$this->setError(__CLASS__ . ":: Can't seed archive with installer package " . $xform_source);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setError(__CLASS__ . ":: Installer package " . $xform_source . " does not exist!");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$headerDataLength = 0;
|
||||
|
||||
if (!$offset)
|
||||
{
|
||||
// First run detected!
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Initializing with JPA package ' . $xform_source);
|
||||
|
||||
// Skip over the header and check no problem exists
|
||||
$offset = $this->_xformReadHeader();
|
||||
|
||||
if ($offset === false)
|
||||
{
|
||||
$this->setError('JPA package file was not read');
|
||||
|
||||
return false; // Oops! The package file doesn't exist or is corrupt
|
||||
}
|
||||
|
||||
$headerDataLength = $offset;
|
||||
}
|
||||
|
||||
$ret = $this->_xformExtract($offset);
|
||||
|
||||
$ret['index'] = $index;
|
||||
|
||||
if (is_array($ret))
|
||||
{
|
||||
$ret['chunkProcessed'] = $headerDataLength + $ret['offset'] - $offset;
|
||||
$offset = $ret['offset'];
|
||||
|
||||
if (!$ret['skip'] && !$ret['done'])
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, ' Adding ' . $ret['filename'] . '; Next offset:' . $offset);
|
||||
$this->addVirtualFile($ret['filename'], '', $ret['data']);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
elseif ($ret['done'])
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
$packages = $installerDescriptors[$embedded_installer]['package'];
|
||||
$packages = explode(',', $packages);
|
||||
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
|
||||
$langPacks = $installerDescriptors[$embedded_installer]['language'];
|
||||
$langPacks = explode(',', $langPacks);
|
||||
|
||||
foreach ($langPacks as $langPack)
|
||||
{
|
||||
$filePath = $pathPrefix . $langPack;
|
||||
|
||||
if (!is_file($filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$packages[] = $langPack;
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, ' Done with package ' . $packages[$index]);
|
||||
|
||||
if (count($packages) > ($index + 1))
|
||||
{
|
||||
$ret['done'] = false;
|
||||
$ret['index'] = $index + 1;
|
||||
$ret['offset'] = 0;
|
||||
$this->_xform_fp = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, ' Done with installer seeding.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$reason = ' Skipping ' . $ret['filename'];
|
||||
Factory::getLog()->log(LogLevel::DEBUG, $reason);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setError('JPA extraction returned FALSE. The installer image is corrupt.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ret['done'])
|
||||
{
|
||||
// We are finished! Close the file
|
||||
fclose($this->_xform_fp);
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Initializing with JPA package has finished');
|
||||
}
|
||||
|
||||
$ret['filesize'] = $totalSize;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getExtension();
|
||||
|
||||
/**
|
||||
* Common code which gets called on instance creation or wake-up (unserialization)
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
$this->fsUtils = Factory::getFilesystemTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the $p_remove_dir from $p_filename, while prepending it with $p_add_dir.
|
||||
* Largely based on code from the pclZip library.
|
||||
*
|
||||
* @param string $p_filename The absolute file name to treat
|
||||
* @param string $p_remove_dir The path to remove
|
||||
* @param string $p_add_dir The path to prefix the treated file name with
|
||||
*
|
||||
* @return string The treated file name
|
||||
*/
|
||||
private function _addRemovePaths($p_filename, $p_remove_dir, $p_add_dir)
|
||||
{
|
||||
$p_filename = $this->fsUtils->TranslateWinPath($p_filename);
|
||||
$p_remove_dir = ($p_remove_dir == '') ? '' :
|
||||
$this->fsUtils->TranslateWinPath($p_remove_dir); //should fix corrupt backups, fix by nicholas
|
||||
|
||||
$v_stored_filename = $p_filename;
|
||||
|
||||
if (!($p_remove_dir == ""))
|
||||
{
|
||||
if (substr($p_remove_dir, -1) != '/')
|
||||
{
|
||||
$p_remove_dir .= "/";
|
||||
}
|
||||
|
||||
if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./"))
|
||||
{
|
||||
if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./"))
|
||||
{
|
||||
$p_remove_dir = "./" . $p_remove_dir;
|
||||
}
|
||||
|
||||
if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./"))
|
||||
{
|
||||
$p_remove_dir = substr($p_remove_dir, 2);
|
||||
}
|
||||
}
|
||||
|
||||
$v_compare = $this->_PathInclusion($p_remove_dir, $p_filename);
|
||||
|
||||
if ($v_compare > 0)
|
||||
{
|
||||
if ($v_compare == 2)
|
||||
{
|
||||
$v_stored_filename = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename =
|
||||
substr($p_filename, (function_exists('mb_strlen') ? mb_strlen($p_remove_dir, '8bit') :
|
||||
strlen($p_remove_dir)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename = $p_filename;
|
||||
}
|
||||
|
||||
if (!($p_add_dir == ""))
|
||||
{
|
||||
if (substr($p_add_dir, -1) == "/")
|
||||
{
|
||||
$v_stored_filename = $p_add_dir . $v_stored_filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
$v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
|
||||
}
|
||||
}
|
||||
|
||||
return $v_stored_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function indicates if the path $p_path is under the $p_dir tree. Or,
|
||||
* said in an other way, if the file or sub-dir $p_path is inside the dir
|
||||
* $p_dir.
|
||||
* The function indicates also if the path is exactly the same as the dir.
|
||||
* This function supports path with duplicated '/' like '//', but does not
|
||||
* support '.' or '..' statements.
|
||||
*
|
||||
* Copied verbatim from pclZip library
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $p_dir Source tree
|
||||
* @param string $p_path Check if this is part of $p_dir
|
||||
*
|
||||
* @return integer 0 if $p_path is not inside directory $p_dir,
|
||||
* 1 if $p_path is inside directory $p_dir
|
||||
* 2 if $p_path is exactly the same as $p_dir
|
||||
*/
|
||||
private function _PathInclusion($p_dir, $p_path)
|
||||
{
|
||||
$v_result = 1;
|
||||
|
||||
// ----- Explode dir and path by directory separator
|
||||
$v_list_dir = explode("/", $p_dir);
|
||||
$v_list_dir_size = sizeof($v_list_dir);
|
||||
$v_list_path = explode("/", $p_path);
|
||||
$v_list_path_size = sizeof($v_list_path);
|
||||
|
||||
// ----- Study directories paths
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
|
||||
while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result))
|
||||
{
|
||||
// ----- Look for empty dir (path reduction)
|
||||
if ($v_list_dir[$i] == '')
|
||||
{
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($v_list_path[$j] == '')
|
||||
{
|
||||
$j++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ----- Compare the items
|
||||
if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != ''))
|
||||
{
|
||||
$v_result = 0;
|
||||
}
|
||||
|
||||
// ----- Next items
|
||||
$i++;
|
||||
$j++;
|
||||
}
|
||||
|
||||
// ----- Look if everything seems to be the same
|
||||
if ($v_result)
|
||||
{
|
||||
// ----- Skip all the empty items
|
||||
while (($j < $v_list_path_size) && ($v_list_path[$j] == ''))
|
||||
{
|
||||
$j++;
|
||||
}
|
||||
|
||||
while (($i < $v_list_dir_size) && ($v_list_dir[$i] == ''))
|
||||
{
|
||||
$i++;
|
||||
}
|
||||
|
||||
if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size))
|
||||
{
|
||||
// ----- There are exactly the same
|
||||
$v_result = 2;
|
||||
}
|
||||
else if ($i < $v_list_dir_size)
|
||||
{
|
||||
// ----- The path is shorter than the dir
|
||||
$v_result = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Return
|
||||
return $v_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The most basic file transaction: add a single entry (file or directory) to
|
||||
* the archive.
|
||||
*
|
||||
* @param boolean $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 boolean True on success, false otherwise. DEPRECATED: Use exceptions instead.
|
||||
*
|
||||
* @throws WarningException When there's a warning (the backup integrity is NOT compromised)
|
||||
* @throws ErrorException When there's an error (the backup integrity is compromised – backup dead)
|
||||
*/
|
||||
abstract protected function _addFile($isVirtual, &$sourceNameOrData, $targetName);
|
||||
|
||||
/**
|
||||
* Skips over the JPA header entry and returns the offset file data starts from
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return boolean|integer False on failure, offset otherwise
|
||||
*/
|
||||
private function _xformReadHeader()
|
||||
{
|
||||
// Fail for unreadable files
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go to the beggining of the file
|
||||
rewind($this->_xform_fp);
|
||||
|
||||
// Read the signature
|
||||
$sig = fread($this->_xform_fp, 3);
|
||||
|
||||
// Not a JPA Archive?
|
||||
if ($sig != 'JPA')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and parse header length
|
||||
$header_length_array = unpack('v', fread($this->_xform_fp, 2));
|
||||
$header_length = $header_length_array[1];
|
||||
|
||||
// Read and parse the known portion of header data (14 bytes)
|
||||
$bin_data = fread($this->_xform_fp, 14);
|
||||
$header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data);
|
||||
|
||||
// Load any remaining header data (forward compatibility)
|
||||
$rest_length = $header_length - 19;
|
||||
|
||||
if ($rest_length > 0)
|
||||
{
|
||||
$junk = fread($this->_xform_fp, $rest_length);
|
||||
}
|
||||
|
||||
return ftell($this->_xform_fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a file from the JPA archive and returns an in-memory array containing it
|
||||
* and its file data. The data returned is an array, consisting of the following keys:
|
||||
* "filename" => relative file path stored in the archive
|
||||
* "data" => file data
|
||||
* "offset" => next offset to use
|
||||
* "skip" => if this is not a file, just skip it...
|
||||
* "done" => No more files left in archive
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param integer $offset The absolute data offset from archive's header
|
||||
*
|
||||
* @return array|bool See description for more information
|
||||
*/
|
||||
private function &_xformExtract($offset)
|
||||
{
|
||||
$false = false; // Used to return false values in case an error occurs
|
||||
|
||||
// Generate a return array
|
||||
$retArray = array(
|
||||
"filename" => '', // File name extracted
|
||||
"data" => '', // File data
|
||||
"offset" => 0, // Offset in ZIP file
|
||||
"skip" => false, // Skip this?
|
||||
"done" => false // Are we done yet?
|
||||
);
|
||||
|
||||
// If we can't open the file, return an error condition
|
||||
if ($this->_xform_fp === false)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
// Go to the offset specified
|
||||
if (!fseek($this->_xform_fp, $offset) == 0)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
// Get and decode Entity Description Block
|
||||
$signature = fread($this->_xform_fp, 3);
|
||||
|
||||
// Check signature
|
||||
if ($signature == 'JPF')
|
||||
{
|
||||
// This a JPA Entity Block. Process the header.
|
||||
|
||||
// Read length of EDB and of the Entity Path Data
|
||||
$length_array = unpack('vblocksize/vpathsize', fread($this->_xform_fp, 4));
|
||||
// Read the path data
|
||||
$file = fread($this->_xform_fp, $length_array['pathsize']);
|
||||
// Read and parse the known data portion
|
||||
$bin_data = fread($this->_xform_fp, 14);
|
||||
$header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data);
|
||||
// Read any unknwon data
|
||||
$restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']);
|
||||
|
||||
if ($restBytes > 0)
|
||||
{
|
||||
$junk = fread($this->_xform_fp, $restBytes);
|
||||
}
|
||||
|
||||
$compressionType = $header_data['compression'];
|
||||
|
||||
// Populate the return array
|
||||
$retArray['filename'] = $file;
|
||||
$retArray['skip'] = ($header_data['compsize'] == 0); // Skip over directories
|
||||
|
||||
switch ($header_data['type'])
|
||||
{
|
||||
case 0:
|
||||
// directory
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// file
|
||||
switch ($compressionType)
|
||||
{
|
||||
case 0: // No compression
|
||||
if ($header_data['compsize'] > 0) // 0 byte files do not have data to be read
|
||||
{
|
||||
$retArray['data'] = fread($this->_xform_fp, $header_data['compsize']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // GZip compression
|
||||
$zipData = fread($this->_xform_fp, $header_data['compsize']);
|
||||
$retArray['data'] = gzinflate($zipData);
|
||||
break;
|
||||
|
||||
case 2: // BZip2 compression
|
||||
$zipData = fread($this->_xform_fp, $header_data['compsize']);
|
||||
$retArray['data'] = bzdecompress($zipData);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is not a file header. This means we are done.
|
||||
$retArray['done'] = true;
|
||||
}
|
||||
|
||||
$retArray['offset'] = ftell($this->_xform_fp);
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,708 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
if (!defined('AKEEBA_CHUNK'))
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$chunksize = $configuration->get('engine.archiver.common.chunk_size', 1048576);
|
||||
define('AKEEBA_CHUNK', $chunksize);
|
||||
}
|
||||
|
||||
if (!function_exists('aksubstr'))
|
||||
{
|
||||
/**
|
||||
* Attempt to use mbstring for getting parts of strings
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int|null $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function aksubstr($string, $start, $length = null)
|
||||
{
|
||||
return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') :
|
||||
substr($string, $start, $length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for custom archiver implementations
|
||||
*/
|
||||
abstract class BaseArchiver extends BaseFileManagement
|
||||
{
|
||||
/** @var array The last part which has been finalized and waits to be post-processed */
|
||||
public $finishedPart = array();
|
||||
|
||||
/** @var resource File pointer to the archive being currently written to */
|
||||
protected $fp = null;
|
||||
|
||||
/** @var resource File pointer to the archive's central directory file (for ZIP) */
|
||||
protected $cdfp = null;
|
||||
|
||||
/** @var string The name of the file holding the archive's data, which becomes the final archive */
|
||||
protected $_dataFileName;
|
||||
|
||||
/** @var string Archive full path without extension */
|
||||
protected $dataFileNameWithoutExtension = '';
|
||||
|
||||
/** @var bool Should I store symlinks as such (no dereferencing?) */
|
||||
protected $storeSymlinkTarget = false;
|
||||
|
||||
/** @var int Part size for split archives, in bytes */
|
||||
protected $partSize = 0;
|
||||
|
||||
/** @var bool Should I use Split ZIP? */
|
||||
protected $useSplitArchive = false;
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being serialized
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function _onSerialize()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being destroyed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new archive part file (but does NOT open it for writing)
|
||||
*
|
||||
* @param bool $finalPart True if this is the final part
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function createNewPartFile($finalPart = false);
|
||||
|
||||
/**
|
||||
* Create a new part file and open it for writing
|
||||
*
|
||||
* @param bool $finalPart Is this the final part?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function createAndOpenNewPart($finalPart = false)
|
||||
{
|
||||
@$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
|
||||
// Not enough space on current part, create new part
|
||||
if (!$this->createNewPartFile($finalPart))
|
||||
{
|
||||
$extension = $this->getExtension();
|
||||
$extension = ltrim(strtoupper($extension), '.');
|
||||
|
||||
throw new ErrorException("Could not create new $extension part file " . basename($this->_dataFileName));
|
||||
}
|
||||
|
||||
$this->openArchiveForOutput(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new backup archive
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
protected function createNewBackupArchive()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Killing old archive");
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, "wb");
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
if (file_exists($this->_dataFileName))
|
||||
{
|
||||
@unlink($this->_dataFileName);
|
||||
}
|
||||
|
||||
@touch($this->_dataFileName);
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
chmod($this->_dataFileName, 0666);
|
||||
}
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, "wb");
|
||||
|
||||
if ($this->fp !== false)
|
||||
{
|
||||
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
|
||||
}
|
||||
}
|
||||
|
||||
@ftruncate($this->fp, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the backup archive file for output. Returns false if the archive file cannot be opened in binary append
|
||||
* mode.
|
||||
*
|
||||
* @param bool $force Should I forcibly reopen the file? If false, I'll only open the file if the current
|
||||
* file pointer is null.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function openArchiveForOutput($force = false)
|
||||
{
|
||||
if (is_null($this->fp) || $force)
|
||||
{
|
||||
$this->fp = $this->fopen($this->_dataFileName, "ab");
|
||||
}
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
$this->fp = null;
|
||||
|
||||
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human formatted size to integer representation of bytes,
|
||||
* e.g. 1M to 1024768
|
||||
*
|
||||
* @param string $setting The value in human readable format, e.g. "1M"
|
||||
*
|
||||
* @return integer The value in bytes
|
||||
*/
|
||||
protected function humanToIntegerBytes($setting)
|
||||
{
|
||||
$val = trim($setting);
|
||||
$last = strtolower($val{strlen($val) - 1});
|
||||
|
||||
if (is_numeric($last))
|
||||
{
|
||||
return $setting;
|
||||
}
|
||||
|
||||
switch ($last)
|
||||
{
|
||||
case 't':
|
||||
$val *= 1024;
|
||||
case 'g':
|
||||
$val *= 1024;
|
||||
case 'm':
|
||||
$val *= 1024;
|
||||
case 'k':
|
||||
$val *= 1024;
|
||||
}
|
||||
|
||||
return (int) $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PHP memory limit in bytes
|
||||
*
|
||||
* @return int|null Memory limit in bytes or null if we can't figure it out.
|
||||
*/
|
||||
protected function getMemoryLimit()
|
||||
{
|
||||
if (!function_exists('ini_get'))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$memLimit = ini_get("memory_limit");
|
||||
|
||||
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
|
||||
{
|
||||
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
|
||||
}
|
||||
|
||||
$memLimit = $this->humanToIntegerBytes($memLimit);
|
||||
|
||||
return $memLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable storing of symlink target if we are not on Windows
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function enableSymlinkTargetStorage()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$dereferenceSymlinks = $configuration->get('engine.archiver.common.dereference_symlinks', true);
|
||||
|
||||
if ($dereferenceSymlinks)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We are told not to dereference symlinks. Are we on Windows?
|
||||
if (function_exists('php_uname'))
|
||||
{
|
||||
$isWindows = stristr(php_uname(), 'windows');
|
||||
}
|
||||
else
|
||||
{
|
||||
$isWindows = (DIRECTORY_SEPARATOR == '\\');
|
||||
}
|
||||
|
||||
// If we are not on Windows, enable symlink target storage
|
||||
$this->storeSymlinkTarget = !$isWindows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file size and last modification time (also works on virtual files and symlinks)
|
||||
*
|
||||
* @param string $sourceNameOrData File path to the source file or source data (if $isVirtual is true)
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getFileSizeAndModificationTime(&$sourceNameOrData, $isVirtual, $isSymlink, $isDir)
|
||||
{
|
||||
// Get real size before compression
|
||||
if ($isVirtual)
|
||||
{
|
||||
$fileSize = akstrlen($sourceNameOrData);
|
||||
$fileModTime = time();
|
||||
|
||||
return array($fileSize, $fileModTime);
|
||||
}
|
||||
|
||||
|
||||
if ($isSymlink)
|
||||
{
|
||||
$fileSize = akstrlen(@readlink($sourceNameOrData));
|
||||
$fileModTime = 0;
|
||||
|
||||
return array($fileSize, $fileModTime);
|
||||
}
|
||||
|
||||
// Is the file readable?
|
||||
if (!is_readable($sourceNameOrData) && !$isDir)
|
||||
{
|
||||
// Really, REALLY check if it is readable (PHP sometimes lies, dammit!)
|
||||
$myFP = @$this->fopen($sourceNameOrData, 'rb');
|
||||
|
||||
if ($myFP === false)
|
||||
{
|
||||
// Unreadable file, skip it.
|
||||
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions');
|
||||
}
|
||||
|
||||
@$this->fclose($myFP);
|
||||
}
|
||||
|
||||
// Get the file size
|
||||
$fileSize = $isDir ? 0 : @filesize($sourceNameOrData);
|
||||
$fileModTime = $isDir ? 0 : @filemtime($sourceNameOrData);
|
||||
|
||||
return array($fileSize, $fileModTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred compression method for a file
|
||||
*
|
||||
* @param int $fileSize File size in bytes
|
||||
* @param int $memLimit Memory limit in bytes
|
||||
* @param bool $isDir Is it a directory?
|
||||
* @param bool $isSymlink Is it a symlink?
|
||||
*
|
||||
* @return int Compression method to use: 0 (uncompressed) or 1 (gzip deflate)
|
||||
*/
|
||||
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
|
||||
{
|
||||
// If we don't have gzip installed we can't compress anything
|
||||
if (!function_exists("gzcompress"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Don't compress directories or symlinks
|
||||
if ($isDir || $isSymlink)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Do not compress files over the compression threshold
|
||||
if ($fileSize >= _AKEEBA_COMPRESSION_THRESHOLD)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// No memory limit, file smaller than the compression threshold: always compress.
|
||||
if (is_numeric($memLimit) && ($memLimit == 0))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Non-zero memory limit, PHP can report memory usage, see if there's enough memory.
|
||||
if (is_numeric($memLimit) && function_exists("memory_get_usage"))
|
||||
{
|
||||
$availableRAM = $memLimit - memory_get_usage();
|
||||
// Conservative approach: if the file size is over 40% of the available memory we won't compress.
|
||||
$compressionMethod = (($availableRAM / 2.5) >= $fileSize) ? 1 : 0;
|
||||
|
||||
return $compressionMethod;
|
||||
}
|
||||
|
||||
// Non-zero memory limit, PHP can't report memory usage, compress only files up to 512Kb (very conservative)
|
||||
return ($fileSize <= 524288) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists and is readable
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws WarningException
|
||||
*/
|
||||
protected function testIfFileExists(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
|
||||
{
|
||||
if ($isVirtual || $isDir)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!@file_exists($sourceNameOrData))
|
||||
{
|
||||
if ($isSymlink)
|
||||
{
|
||||
throw new WarningException('The symlink ' . $sourceNameOrData . ' points to a file or folder that no longer exists and will NOT be backed up.');
|
||||
}
|
||||
|
||||
throw new WarningException('The file ' . $sourceNameOrData . ' no longer exists and will NOT be backed up. Are you backing up temporary or cache data?');
|
||||
}
|
||||
|
||||
if (!@is_readable($sourceNameOrData))
|
||||
{
|
||||
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get the compressed data for a file
|
||||
*
|
||||
* @param string $sourceNameOrData
|
||||
* @param bool $isVirtual
|
||||
* @param int $compressionMethod
|
||||
* @param string $zdata
|
||||
* @param int $unc_len
|
||||
* @param int $c_len
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function getZData(&$sourceNameOrData, &$isVirtual, &$compressionMethod, &$zdata, &$unc_len, &$c_len)
|
||||
{
|
||||
// Get uncompressed data
|
||||
$udata =& $sourceNameOrData;
|
||||
|
||||
if (!$isVirtual)
|
||||
{
|
||||
$udata = @file_get_contents($sourceNameOrData);
|
||||
}
|
||||
|
||||
// If the compression fails, we will let it behave like no compression was available
|
||||
$c_len = $unc_len;
|
||||
$compressionMethod = 0;
|
||||
|
||||
// Proceed with compression
|
||||
$zdata = @gzcompress($udata);
|
||||
|
||||
if ($zdata !== false)
|
||||
{
|
||||
// The compression succeeded
|
||||
unset($udata);
|
||||
$compressionMethod = 1;
|
||||
$zdata = aksubstr($zdata, 2, -4);
|
||||
$c_len = akstrlen($zdata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bytes available for writing data to the current part file (i.e. part size minus current offset)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getPartFreeSize()
|
||||
{
|
||||
clearstatcache();
|
||||
$current_part_size = @filesize($this->_dataFileName);
|
||||
|
||||
return (int) $this->partSize - ($current_part_size === false ? 0 : $current_part_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable split archive creation where possible
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function enableSplitArchives()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$partSize = $configuration->get('engine.archiver.common.part_size', 0);
|
||||
|
||||
// If the part size is less than 64Kb we won't enable split archives
|
||||
if ($partSize < 65536)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = $this->getExtension();
|
||||
$altExtension = substr($extension, 0, 2) . '01';
|
||||
$archiveTypeUppercase = strtoupper(substr($extension, 1));
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Split $archiveTypeUppercase creation enabled");
|
||||
|
||||
$this->useSplitArchive = true;
|
||||
$this->partSize = $partSize;
|
||||
$this->dataFileNameWithoutExtension =
|
||||
dirname($this->_dataFileName) . '/' . basename($this->_dataFileName, $extension);
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . $altExtension;
|
||||
|
||||
// Indicate that we have at least 1 part
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file's GZip compressed data to the archive, taking into account archive splitting
|
||||
*
|
||||
* @param string $zdata The compressed data to write to the archive
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function putRawDataIntoArchive(&$zdata)
|
||||
{
|
||||
// Single part archive. Just dump the compressed data.
|
||||
if (!$this->useSplitArchive)
|
||||
{
|
||||
$this->fwrite($this->fp, $zdata);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Split JPA. Check if we need to split the part in the middle of the data.
|
||||
$freeSpaceInPart = $this->getPartFreeSize();
|
||||
|
||||
// Nope. We have enough space to write all of the data in this part.
|
||||
if ($freeSpaceInPart >= akstrlen($zdata))
|
||||
{
|
||||
$this->fwrite($this->fp, $zdata);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$bytesLeftInData = akstrlen($zdata);
|
||||
|
||||
while ($bytesLeftInData > 0)
|
||||
{
|
||||
// Try to write to the archive. We can only write as much bytes as the free space in the backup archive OR
|
||||
// the total data bytes left, whichever is lower.
|
||||
$bytesWritten = $this->fwrite($this->fp, $zdata, min($bytesLeftInData, $freeSpaceInPart));
|
||||
|
||||
// Since we may have written fewer bytes than anticipated we use the real bytes written for calculations
|
||||
$freeSpaceInPart -= $bytesWritten;
|
||||
$bytesLeftInData -= $bytesWritten;
|
||||
|
||||
// If we still have data to write, remove the part already written and keep the rest
|
||||
if ($bytesLeftInData > 0)
|
||||
{
|
||||
$zdata = aksubstr($zdata, -$bytesLeftInData);
|
||||
}
|
||||
|
||||
// If the part file is full create a new one
|
||||
if ($freeSpaceInPart <= 0)
|
||||
{
|
||||
// Create new part
|
||||
$this->createAndOpenNewPart();
|
||||
|
||||
// Get its free space
|
||||
$freeSpaceInPart = $this->getPartFreeSize();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell PHP to free up some memory
|
||||
$zdata = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin or resume adding an uncompressed file into the archive.
|
||||
*
|
||||
* IMPORTANT! Only this case can be spanned across steps: uncompressed, non-virtual data
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file we are reading from.
|
||||
* @param int $fileLength The file size we are supposed to read, in bytes.
|
||||
* @param int $resumeOffset Offset in the file to resume reading from
|
||||
*
|
||||
* @return bool True to indicate more processing is required in the next step
|
||||
*/
|
||||
protected function putUncompressedFileIntoArchive(&$sourceNameOrData, $fileLength = 0, $resumeOffset = null)
|
||||
{
|
||||
// Copy the file contents, ignore directories
|
||||
$sourceFilePointer = @fopen($sourceNameOrData, "rb");
|
||||
|
||||
if ($sourceFilePointer === false)
|
||||
{
|
||||
// If we have already written the file header and can't read the data your archive is busted.
|
||||
throw new ErrorException('Unreadable file ' . $sourceNameOrData . '. Check permissions. Your archive is corrupt!');
|
||||
}
|
||||
|
||||
// Seek to the resume point if required
|
||||
if (!is_null($resumeOffset))
|
||||
{
|
||||
// Seek to new offset
|
||||
$seek_result = @fseek($sourceFilePointer, $resumeOffset);
|
||||
|
||||
if ($seek_result === -1)
|
||||
{
|
||||
// What?! We can't resume!
|
||||
@fclose($sourceFilePointer);
|
||||
|
||||
throw new ErrorException(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));
|
||||
}
|
||||
|
||||
// Change the uncompressed size to reflect the remaining data
|
||||
$fileLength -= $resumeOffset;
|
||||
}
|
||||
|
||||
$mustBreak = $this->putDataFromFileIntoArchive($sourceFilePointer, $fileLength);
|
||||
|
||||
@fclose($sourceFilePointer);
|
||||
|
||||
return $mustBreak;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put up to $fileLength bytes of the file pointer $sourceFilePointer into the backup archive. Returns true if we
|
||||
* ran out of time and need to perform a step break. Returns false when the whole quantity of data has been copied.
|
||||
* Throws an ErrorException if soemthing really bad happens.
|
||||
*
|
||||
* @param resource $sourceFilePointer The pointer to the input file
|
||||
* @param int $fileLength How many bytes to copy
|
||||
*
|
||||
* @return bool True to indicate we need to resume packing the file in the next step
|
||||
*/
|
||||
private function putDataFromFileIntoArchive(&$sourceFilePointer, &$fileLength)
|
||||
{
|
||||
// Get references to engine objects we're going to be using
|
||||
$configuration = Factory::getConfiguration();
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Quick copy data into the archive, AKEEBA_CHUNK bytes at a time
|
||||
while (!feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
|
||||
{
|
||||
// Normally I read up to AKEEBA_CHUNK bytes at a time
|
||||
$chunkSize = AKEEBA_CHUNK;
|
||||
|
||||
// Do I have a split ZIP?
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// I must only read up to the free space in the part file if it's less than AKEEBA_CHUNK.
|
||||
$free_space = $this->getPartFreeSize();
|
||||
$chunkSize = min($free_space, AKEEBA_CHUNK);
|
||||
|
||||
// If I ran out of free space I have to create a new part file.
|
||||
if ($free_space <= 0)
|
||||
{
|
||||
$this->createAndOpenNewPart();
|
||||
|
||||
// We have created the part. If the user asked for immediate post-proc, break step now.
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
$resumeOffset = @ftell($sourceFilePointer);
|
||||
@fclose($sourceFilePointer);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', true);
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
|
||||
// Always close the open part when immediate post-processing is requested
|
||||
@$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// No immediate post-proc. Recalculate the optimal chunk size.
|
||||
$free_space = $this->getPartFreeSize();
|
||||
$chunkSize = min($free_space, AKEEBA_CHUNK);
|
||||
}
|
||||
}
|
||||
|
||||
// Read some data and write it to the backup archive part file
|
||||
$data = fread($sourceFilePointer, $chunkSize);
|
||||
$bytesWritten = $this->fwrite($this->fp, $data, akstrlen($data));
|
||||
|
||||
// Subtract the written bytes from the bytes left to write
|
||||
$fileLength -= $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* According to the file size we read when we were writing the file header we have more data to write. However,
|
||||
* we reached the end of the file. This means the file went away or shrunk. We can't reliably go back and
|
||||
* change the file header since it may be in a previous part file that's already been post-processed. All we can
|
||||
* do is try to warn the user.
|
||||
*/
|
||||
while (feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
|
||||
{
|
||||
throw new ErrorException('The file shrunk or went away while putting it in the backup archive. Your archive is damaged! If this is a temporary or cache file we advise you to exclude the contents of the temporary / cache folder it is contained in.');
|
||||
}
|
||||
|
||||
// WARNING!!! The extra $unc_len != 0 check is necessary as PHP won't reach EOF for 0-byte files.
|
||||
if (!feof($sourceFilePointer) && ($fileLength != 0))
|
||||
{
|
||||
// We have to break, or we'll time out!
|
||||
$resumeOffset = @ftell($sourceFilePointer);
|
||||
@fclose($sourceFilePointer);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
if (!function_exists('akstrlen'))
|
||||
{
|
||||
/**
|
||||
* Attempt to use mbstring for calculating the binary string length.
|
||||
*
|
||||
* @param $string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function akstrlen($string)
|
||||
{
|
||||
return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for an archiver using managed file pointers
|
||||
*/
|
||||
abstract class BaseFileManagement extends Base
|
||||
{
|
||||
/** @var resource File pointer to the archive being currently written to */
|
||||
protected $fp = null;
|
||||
|
||||
/** @var resource File pointer to the archive's central directory file (for ZIP) */
|
||||
protected $cdfp = null;
|
||||
|
||||
/** @var array An array of open file pointers */
|
||||
private $filePointers = array();
|
||||
|
||||
/** @var array An array of the last open files for writing and their last written to offsets */
|
||||
private $fileOffsets = array();
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being serialized
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function _onSerialize()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release file pointers when the object is being destroyed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->_closeAllFiles();
|
||||
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file, if it's not already open, or returns its cached file pointer if it's already open
|
||||
*
|
||||
* @param string $file The filename to open
|
||||
* @param string $mode File open mode, defaults to binary write
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
protected function fopen($file, $mode = 'wb')
|
||||
{
|
||||
if (!array_key_exists($file, $this->filePointers))
|
||||
{
|
||||
//Factory::getLog()->log(LogLevel::DEBUG, "Opening backup archive $file with mode $mode");
|
||||
$this->filePointers[$file] = @fopen($file, $mode);
|
||||
|
||||
// If we open a file for append we have to seek to the correct offset
|
||||
if (substr($mode, 0, 1) == 'a')
|
||||
{
|
||||
if (isset($this->fileOffsets[$file]))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Truncating backup archive file $file to " . $this->fileOffsets[$file] . " bytes");
|
||||
@ftruncate($this->filePointers[$file], $this->fileOffsets[$file]);
|
||||
}
|
||||
|
||||
fseek($this->filePointers[$file], 0, SEEK_END);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filePointers[$file];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes an already open file
|
||||
*
|
||||
* @param resource $fp The file pointer to close
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function fclose(&$fp)
|
||||
{
|
||||
$offset = array_search($fp, $this->filePointers, true);
|
||||
|
||||
$result = @fclose($fp);
|
||||
|
||||
if ($offset !== false)
|
||||
{
|
||||
unset($this->filePointers[$offset]);
|
||||
}
|
||||
|
||||
$fp = null;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to file, defeating magic_quotes_runtime settings (pure binary write)
|
||||
*
|
||||
* @param resource $fp Handle to a file
|
||||
* @param string $data The data to write to the file
|
||||
* @param integer $p_len Maximum length of data to write
|
||||
*
|
||||
* @return int The number of bytes written
|
||||
*
|
||||
* @throws ErrorException When writing to the file is not possible
|
||||
*/
|
||||
protected function fwrite($fp, $data, $p_len = null)
|
||||
{
|
||||
static $lastFp = null;
|
||||
static $filename = null;
|
||||
|
||||
if ($fp !== $lastFp)
|
||||
{
|
||||
$lastFp = $fp;
|
||||
$filename = array_search($fp, $this->filePointers, true);
|
||||
}
|
||||
|
||||
$len = is_null($p_len) ? (akstrlen($data)) : $p_len;
|
||||
$ret = fwrite($fp, $data, $len);
|
||||
|
||||
if (($ret === false) || (abs(($ret - $len)) >= 1))
|
||||
{
|
||||
// Log debug information about the archive file's existence and current size. This helps us figure out if
|
||||
// there is a server-imposed maximum file size limit.
|
||||
clearstatcache();
|
||||
$fileExists = @file_exists($filename) ? 'exists' : 'does NOT exist';
|
||||
$currentSize = @filesize($filename);
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . "::_fwrite() ERROR!! Cannot write to archive file $filename. The file $fileExists. File size $currentSize bytes after writing $ret of $len bytes. Please check the output directory permissions and make sure you have enough disk space available. If this does not help, please set up a Part Size for Split Archives LOWER than this size and retry backing up.");
|
||||
|
||||
throw new ErrorException('Couldn\'t write to the archive file; check the output directory permissions and make sure you have enough disk space available.' . "[len=$ret / $len]");
|
||||
}
|
||||
|
||||
if ($filename !== false)
|
||||
{
|
||||
$this->fileOffsets[$filename] = @ftell($fp);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file path from the list of resumable offsets
|
||||
*
|
||||
* @param $filename
|
||||
*/
|
||||
protected function removeFromOffsetsList($filename)
|
||||
{
|
||||
if (isset($this->fileOffsets[$filename]))
|
||||
{
|
||||
unset($this->fileOffsets[$filename]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all open files known to this archiver object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _closeAllFiles()
|
||||
{
|
||||
if (!empty($this->filePointers))
|
||||
{
|
||||
foreach ($this->filePointers as $file => $fp)
|
||||
{
|
||||
@fclose($fp);
|
||||
|
||||
unset($this->filePointers[$file]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,569 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* JPA creation class
|
||||
*
|
||||
* JPA Format 1.2 implemented, minus BZip2 compression support
|
||||
*/
|
||||
class Jpa extends BaseArchiver
|
||||
{
|
||||
/** @var integer How many files are contained in the archive */
|
||||
private $totalFilesCount = 0;
|
||||
|
||||
/** @var integer The total size of files contained in the archive as they are stored */
|
||||
private $totalCompressedSize = 0;
|
||||
|
||||
/** @var integer The total size of files contained in the archive when they are extracted to disk. */
|
||||
private $totalUncompressedSize = 0;
|
||||
|
||||
/** @var string Standard Header signature */
|
||||
private $archiveSignature = "\x4A\x50\x41";
|
||||
|
||||
/** @var string Entity Block signature */
|
||||
private $fileHeaderSignature = "\x4A\x50\x46";
|
||||
|
||||
/** @var string Marks the split archive's extra header */
|
||||
private $splitArchiveExtraHeader = "\x4A\x50\x01\x01"; //
|
||||
|
||||
/** @var int Current part file number */
|
||||
private $currentPartNumber = 1;
|
||||
|
||||
/** @var int Total number of part files */
|
||||
private $totalParts = 1;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize($targetArchivePath, $options = array())
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: new instance - archive $targetArchivePath");
|
||||
$this->_dataFileName = $targetArchivePath;
|
||||
|
||||
try
|
||||
{
|
||||
// Should we enable Split ZIP feature?
|
||||
$this->enableSplitArchives();
|
||||
|
||||
// Should I use Symlink Target Storage?
|
||||
$this->enableSymlinkTargetStorage();
|
||||
|
||||
// Try to kill the archive if it exists
|
||||
$this->createNewBackupArchive();
|
||||
|
||||
// Write the initial instance of the archive header
|
||||
$this->_writeArchiveHeader();
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Standard Header with current information
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalize()
|
||||
{
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
$this->_closeAllFiles();
|
||||
|
||||
// If Spanned JPA and there is no .jpa file, rename the last fragment to .jpa
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$extension = substr($this->_dataFileName, -4);
|
||||
|
||||
if ($extension != '.jpa')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Renaming last JPA part to .JPA extension');
|
||||
|
||||
$newName = $this->dataFileNameWithoutExtension . '.jpa';
|
||||
|
||||
if (!@rename($this->_dataFileName, $newName))
|
||||
{
|
||||
$this->setError('Could not rename last JPA part to .JPA extension.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_dataFileName = $newName;
|
||||
}
|
||||
|
||||
// Finally, point to the first part so that we can re-write the correct header information
|
||||
if ($this->totalParts > 1)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j01';
|
||||
}
|
||||
}
|
||||
|
||||
// Re-write the archive header
|
||||
try
|
||||
{
|
||||
$this->_writeArchiveHeader();
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return '.jpa';
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a Standard Header at the top of the file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _writeArchiveHeader()
|
||||
{
|
||||
if (!is_null($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
}
|
||||
|
||||
$this->fp = $this->fopen($this->_dataFileName, 'cb');
|
||||
|
||||
if ($this->fp === false)
|
||||
{
|
||||
throw new ErrorException('Could not open ' . $this->_dataFileName . ' for writing. Check permissions and open_basedir restrictions.');
|
||||
}
|
||||
|
||||
// Calculate total header size
|
||||
$headerSize = 19; // Standard Header
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Spanned JPA header
|
||||
$headerSize += 8;
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, $this->archiveSignature); // ID string (JPA)
|
||||
$this->fwrite($this->fp, pack('v', $headerSize)); // Header length; fixed to 19 bytes
|
||||
$this->fwrite($this->fp, pack('C', _JPA_MAJOR)); // Major version
|
||||
$this->fwrite($this->fp, pack('C', _JPA_MINOR)); // Minor version
|
||||
$this->fwrite($this->fp, pack('V', $this->totalFilesCount)); // File count
|
||||
$this->fwrite($this->fp, pack('V', $this->totalUncompressedSize)); // Size of files when extracted
|
||||
$this->fwrite($this->fp, pack('V', $this->totalCompressedSize)); // Size of files when stored
|
||||
|
||||
// Do I need to add a split archive's header too?
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$this->fwrite($this->fp, $this->splitArchiveExtraHeader); // Signature
|
||||
$this->fwrite($this->fp, pack('v', 4)); // Extra field length
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts)); // Number of parts
|
||||
}
|
||||
|
||||
$this->fclose($this->fp);
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
@chmod($this->_dataFileName, 0644);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the bootstrap code to add some define's used by the JPA format engine
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
|
||||
|
||||
/**
|
||||
* Akeeba Backup and JPA Format version change chart:
|
||||
* Akeeba Backup 3.0: JPA Format 1.1 is used
|
||||
* Akeeba Backup 3.1: JPA Format 1.2 with file modification timestamp is used
|
||||
*/
|
||||
define('_JPA_MAJOR', 1); // JPA Format major version number
|
||||
define('_JPA_MINOR', 2); // JPA Format minor version number
|
||||
|
||||
}
|
||||
parent::__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 boolean True on success, false otherwise
|
||||
*
|
||||
* @since 1.2.1
|
||||
*/
|
||||
protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
|
||||
{
|
||||
// Get references to engine objects we're going to be using
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Is this a virtual file?
|
||||
$isVirtual = (bool) $isVirtual;
|
||||
|
||||
// Open data file for output
|
||||
$this->openArchiveForOutput();
|
||||
|
||||
// Should I continue backing up a file from the previous step?
|
||||
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
|
||||
// definition we have an uncompressed, non-virtual file. Hence the default values.
|
||||
$isDir = false;
|
||||
$isSymlink = false;
|
||||
$compressionMethod = 0;
|
||||
$zdata = null;
|
||||
// If we are continuing file packing we have an uncompressed, non-virtual file.
|
||||
$isVirtual = $continueProcessingFile ? false : $isVirtual;
|
||||
$resume = $continueProcessingFile ? 0 : null;
|
||||
|
||||
if ( !$continueProcessingFile)
|
||||
{
|
||||
// Log the file being added
|
||||
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "-- Adding $targetName to archive $messageSource");
|
||||
|
||||
// Write a file header
|
||||
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir, $compressionMethod, $zdata, $unc_len);
|
||||
}
|
||||
else
|
||||
{
|
||||
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
|
||||
$unc_len = $configuration->get('volatile.engine.archiver.unc_len', 0);
|
||||
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
|
||||
|
||||
// Log the file we continue packing
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
|
||||
}
|
||||
|
||||
/* "File data" segment. */
|
||||
if ($compressionMethod == 1)
|
||||
{
|
||||
// Compressed data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($zdata);
|
||||
}
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
// Virtual data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($sourceNameOrData);
|
||||
}
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
// Symlink. Just put the link target into the archive.
|
||||
$this->fwrite($this->fp, @readlink($sourceNameOrData));
|
||||
}
|
||||
elseif ((!$isDir) && (!$isSymlink))
|
||||
{
|
||||
// Uncompressed file.
|
||||
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
|
||||
{
|
||||
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
|
||||
// true here to avoid running the final bit of code which uncaches the file resume data.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, "DEBUG -- Added $targetName to archive");
|
||||
|
||||
// Uncache data
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
$configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
$configuration->set('volatile.engine.archiver.resume', null);
|
||||
$configuration->set('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// ... and return TRUE = success
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the file header to the backup archive.
|
||||
*
|
||||
* Only the first three parameters are input. All other are ignored for input and are overwritten.
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param string $targetName The target path to be stored inside the archive
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
* @param int $compressionMethod The compression method chosen for this file
|
||||
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
|
||||
* We return that from this method to avoid having to compress the same data
|
||||
* twice (once to write the compressed data length in the header and once to
|
||||
* write the compressed data to the archive).
|
||||
* @param int $unc_len The uncompressed size of the file / source data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function writeFileHeader(&$sourceNameOrData, &$targetName, &$isVirtual, &$isSymlink, &$isDir, &$compressionMethod, &$zdata, &$unc_len)
|
||||
{
|
||||
static $memLimit = null;
|
||||
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Uncache data -- WHY DO THAT?!
|
||||
/**
|
||||
* $configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
* $configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
* $configuration->set('volatile.engine.archiver.resume', null);
|
||||
* $configuration->set('volatile.engine.archiver.processingfile',false);
|
||||
* /**/
|
||||
|
||||
// See if it's a directory
|
||||
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
|
||||
|
||||
// See if it's a symlink (w/out dereference)
|
||||
$isSymlink = false;
|
||||
|
||||
if ($this->storeSymlinkTarget && !$isVirtual)
|
||||
{
|
||||
$isSymlink = is_link($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Get real size before compression
|
||||
list($fileSize, $fileModTime) =
|
||||
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
|
||||
|
||||
// Decide if we will compress
|
||||
$compressionMethod = $this->getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
|
||||
|
||||
$storedName = $targetName;
|
||||
|
||||
/* "Entity Description Block" segment. */
|
||||
$unc_len = $fileSize; // File size
|
||||
$storedName .= ($isDir) ? "/" : "";
|
||||
|
||||
/**
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !!!! WARNING!!! DO NOT MOVE THIS BLOCK OF CODE AFTER THE testIfFileExists OR getZData!!!! !!!!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*
|
||||
* PHP 5.6.3 IS BROKEN. Possibly the same applies for all old versions of PHP. If you try to get the file
|
||||
* permissions after reading its contents PHP segfaults.
|
||||
*/
|
||||
// Get file permissions
|
||||
$perms = 0644;
|
||||
|
||||
if (!$isVirtual)
|
||||
{
|
||||
$perms = @fileperms($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Test for non-existing or unreadable files
|
||||
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
|
||||
$c_len = $unc_len;
|
||||
|
||||
if ($compressionMethod == 1)
|
||||
{
|
||||
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
|
||||
}
|
||||
|
||||
$this->totalCompressedSize += $c_len; // Update global data
|
||||
$this->totalUncompressedSize += $fileSize; // Update global data
|
||||
$this->totalFilesCount++;
|
||||
|
||||
// Calculate Entity Description Block length
|
||||
$blockLength = 21 + akstrlen($storedName);
|
||||
|
||||
// If we need to store the file mod date
|
||||
if ($fileModTime > 0)
|
||||
{
|
||||
$blockLength += 8;
|
||||
}
|
||||
|
||||
// Get file type
|
||||
$fileType = 1;
|
||||
|
||||
if ($isSymlink)
|
||||
{
|
||||
$fileType = 2;
|
||||
}
|
||||
elseif ($isDir)
|
||||
{
|
||||
$fileType = 0;
|
||||
}
|
||||
|
||||
// If it's a split JPA file, we've got to make sure that the header can fit in the part
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Compare to free part space
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if ($free_space <= $blockLength)
|
||||
{
|
||||
// Not enough space on current part, create new part
|
||||
$this->createAndOpenNewPart();
|
||||
}
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, $this->fileHeaderSignature); // Entity Description Block header
|
||||
$this->fwrite($this->fp, pack('v', $blockLength)); // Entity Description Block header length
|
||||
$this->fwrite($this->fp, pack('v', akstrlen($storedName))); // Length of entity path
|
||||
$this->fwrite($this->fp, $storedName); // Entity path
|
||||
$this->fwrite($this->fp, pack('C', $fileType)); // Entity type
|
||||
$this->fwrite($this->fp, pack('C', $compressionMethod)); // Compression method
|
||||
$this->fwrite($this->fp, pack('V', $c_len)); // Compressed size
|
||||
$this->fwrite($this->fp, pack('V', $unc_len)); // Uncompressed size
|
||||
$this->fwrite($this->fp, pack('V', $perms)); // Entity permissions
|
||||
|
||||
// Timestamp Extra Field, only for files
|
||||
if ($fileModTime > 0)
|
||||
{
|
||||
$this->fwrite($this->fp, "\x00\x01"); // Extra Field Identifier
|
||||
$this->fwrite($this->fp, pack('v', 8)); // Extra Field Length
|
||||
$this->fwrite($this->fp, pack('V', $fileModTime)); // Timestamp
|
||||
}
|
||||
|
||||
// Cache useful information about the file
|
||||
if (!$isDir && !$isSymlink && !$isVirtual)
|
||||
{
|
||||
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new part for the spanned archive
|
||||
*
|
||||
* @param bool $finalPart Is this the final archive part?
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
protected function createNewPartFile($finalPart = false)
|
||||
{
|
||||
// Close any open file pointers
|
||||
if (!is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
// Remove the just finished part from the list of resumable offsets
|
||||
$this->removeFromOffsetsList($this->_dataFileName);
|
||||
|
||||
// Set the file pointers to null
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
// Push the previous part if we have to post-process it immediately
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
// The first part needs its header overwritten during archive
|
||||
// finalization. Skip it from immediate processing.
|
||||
if ($this->currentPartNumber != 1)
|
||||
{
|
||||
$this->finishedPart[] = $this->_dataFileName;
|
||||
}
|
||||
}
|
||||
|
||||
$this->totalParts++;
|
||||
$this->currentPartNumber = $this->totalParts;
|
||||
|
||||
if ($finalPart)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.jpa';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j' . sprintf('%02d', $this->currentPartNumber);
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, 'Creating new JPA part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart($this->totalParts);
|
||||
|
||||
// Try to remove any existing file
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
// Touch the new file
|
||||
$result = @touch($this->_dataFileName);
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
chmod($this->_dataFileName, 0666);
|
||||
}
|
||||
|
||||
// Try to write 6 bytes to it
|
||||
if ($result)
|
||||
{
|
||||
$result = @file_put_contents($this->_dataFileName, 'AKEEBA') == 6;
|
||||
}
|
||||
|
||||
if ($result)
|
||||
{
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
$result = @touch($this->_dataFileName);
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
chmod($this->_dataFileName, 0666);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,908 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Archiver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Exceptions\ErrorException;
|
||||
use Akeeba\Engine\Base\Exceptions\WarningException;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Util\CRC32;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class Zip extends BaseArchiver
|
||||
{
|
||||
/** @var string Beginning of central directory record. */
|
||||
private $centralDirectoryRecordStartSignature = "\x50\x4b\x01\x02";
|
||||
|
||||
/** @var string End of central directory record. */
|
||||
private $centralDirectoryRecordEndSignature = "\x50\x4b\x05\x06";
|
||||
|
||||
/** @var string Beginning of file contents. */
|
||||
private $fileHeaderSignature = "\x50\x4b\x03\x04";
|
||||
|
||||
/** @var string The name of the temporary file holding the ZIP's Central Directory */
|
||||
private $centralDirectoryFilename;
|
||||
|
||||
/** @var integer The total number of files and directories stored in the ZIP archive */
|
||||
private $totalFilesCount;
|
||||
|
||||
/** @var integer The total size of data in the archive. Note: On 32-bit versions of PHP, this will overflow for archives over 2Gb! */
|
||||
private $totalCompressedSize = 0;
|
||||
|
||||
/** @var integer The chunk size for CRC32 calculations */
|
||||
private $AkeebaPackerZIP_CHUNK_SIZE;
|
||||
|
||||
/** @var int Current part file number */
|
||||
private $currentPartNumber = 1;
|
||||
|
||||
/** @var int Total number of part files */
|
||||
private $totalParts = 1;
|
||||
|
||||
/** @var CRC32 The CRC32 calculations object */
|
||||
private $crcCalculator = null;
|
||||
|
||||
/**
|
||||
* Class constructor - initializes internal operating parameters
|
||||
*
|
||||
* @return Zip
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
|
||||
// Find the optimal chunk size for ZIP archive processing
|
||||
$this->findOptimalChunkSize();
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Chunk size for CRC is now " . $this->AkeebaPackerZIP_CHUNK_SIZE . " bytes");
|
||||
|
||||
// Should I use Symlink Target Storage?
|
||||
$this->enableSymlinkTargetStorage();
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the archiver class, creating the archive from an existent
|
||||
* installer's JPA archive.
|
||||
*
|
||||
* @param string $sourceJPAPath Absolute path to an 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 = array())
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: initialize - archive $targetArchivePath");
|
||||
|
||||
// Get names of temporary files
|
||||
$this->_dataFileName = $targetArchivePath;
|
||||
|
||||
// Should we enable split archive feature?
|
||||
$this->enableSplitArchives();
|
||||
|
||||
// Create the Central Directory temporary file
|
||||
$this->createCentralDirectoryTempFile();
|
||||
|
||||
// Try to kill the archive if it exists
|
||||
$this->createNewBackupArchive();
|
||||
|
||||
// On split archives, include the "Split ZIP" header, for PKZIP 2.50+ compatibility
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$this->openArchiveForOutput();
|
||||
$this->fwrite($this->fp, "\x50\x4b\x07\x08");
|
||||
}
|
||||
}
|
||||
|
||||
public function finalize()
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->finalizeZIPFile();
|
||||
}
|
||||
catch (ErrorException $e)
|
||||
{
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Glues the Central Directory of the ZIP file to the archive and takes care about the differences between single
|
||||
* and multipart archives.
|
||||
*
|
||||
* Official ZIP file format: http://www.pkware.com/appnote.txt
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalizeZIPFile()
|
||||
{
|
||||
// 1. Get size of central directory
|
||||
clearstatcache();
|
||||
$cdOffset = @filesize($this->_dataFileName);
|
||||
$this->totalCompressedSize += $cdOffset;
|
||||
$cdSize = @filesize($this->centralDirectoryFilename);
|
||||
|
||||
// 2. Append Central Directory to data file and remove the CD temp file afterwards
|
||||
if (!is_null($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (!is_null($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
$this->openArchiveForOutput(true);
|
||||
$this->cdfp = $this->fopen($this->centralDirectoryFilename, "rb");
|
||||
|
||||
if ($this->cdfp === false)
|
||||
{
|
||||
// Already glued, return
|
||||
$this->fclose($this->fp);
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Comment length (I need it before I start gluing the archive)
|
||||
$comment_length = akstrlen($this->_comment);
|
||||
|
||||
// Special consideration for split ZIP files
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Calculate size of Central Directory + EOCD records
|
||||
$total_cd_eocd_size = $cdSize + 22 + $comment_length;
|
||||
|
||||
// Free space on the part
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if (($free_space < $total_cd_eocd_size) && ($total_cd_eocd_size > 65536))
|
||||
{
|
||||
// Not enough space on archive for CD + EOCD, will go on separate part
|
||||
$this->createAndOpenNewPart(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the CD record
|
||||
while (!feof($this->cdfp))
|
||||
{
|
||||
/**
|
||||
* Why not split the Central Directory between parts?
|
||||
*
|
||||
* APPNOTE.TXT §8.5.2 "The central directory may span segment boundaries, but no single record in the
|
||||
* central directory should be split across segments."
|
||||
*
|
||||
* This would require parsing the CD temp file to prevent any CD record from spanning across two parts.
|
||||
* But how many bytes is each CD record? It's about 100 bytes per file which gives us about 10,400 files
|
||||
* per MB. Even a 2MB part size holds more than 20,000 file records. A typical 10Mb part size holds more
|
||||
* files than the largest backup I've ever seen. Therefore there is no need to waste computational power
|
||||
* to see if we need to span the Central Directory between parts.
|
||||
*/
|
||||
$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
|
||||
$this->fwrite($this->fp, $chunk);
|
||||
}
|
||||
|
||||
unset($chunk);
|
||||
|
||||
// Delete the temporary CD file
|
||||
$this->fclose($this->cdfp);
|
||||
$this->cdfp = null;
|
||||
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->centralDirectoryFilename);
|
||||
|
||||
// 3. Write the rest of headers to the end of the ZIP file
|
||||
$this->fwrite($this->fp, $this->centralDirectoryRecordEndSignature);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Split ZIP files, enter relevant disk number information
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Number of this disk. */
|
||||
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Disk with central directory start. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-split ZIP files, the disk number MUST be 0
|
||||
$this->fwrite($this->fp, pack('V', 0));
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries "on this disk". */
|
||||
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries overall. */
|
||||
$this->fwrite($this->fp, pack('V', $cdSize)); /* Size of central directory. */
|
||||
$this->fwrite($this->fp, pack('V', $cdOffset)); /* Offset to start of central dir. */
|
||||
|
||||
// 2.0.b2 -- Write a ZIP file comment
|
||||
$this->fwrite($this->fp, pack('v', $comment_length)); /* ZIP file comment length. */
|
||||
$this->fwrite($this->fp, $this->_comment);
|
||||
$this->fclose($this->fp);
|
||||
|
||||
// If Split ZIP and there is no .zip file, rename the last fragment to .ZIP
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$extension = substr($this->_dataFileName, -3);
|
||||
|
||||
if ($extension != '.zip')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Renaming last ZIP part to .ZIP extension');
|
||||
|
||||
$newName = $this->dataFileNameWithoutExtension . '.zip';
|
||||
|
||||
if (!@rename($this->_dataFileName, $newName))
|
||||
{
|
||||
$this->setError('Could not rename last ZIP part to .ZIP extension.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_dataFileName = $newName;
|
||||
}
|
||||
|
||||
// If Split ZIP and only one fragment, change the signature
|
||||
if ($this->totalParts == 1)
|
||||
{
|
||||
$this->fp = $this->fopen($this->_dataFileName, 'r+b');
|
||||
$this->fwrite($this->fp, "\x50\x4b\x30\x30");
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
@chmod($this->_dataFileName, 0644);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the extension (including the dot) of the files produced
|
||||
* by this class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return '.zip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the bootstrap code to add some define's used by the ZIP format engine
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function __bootstrap_code()
|
||||
{
|
||||
if ( !defined('_AKEEBA_COMPRESSION_THRESHOLD'))
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
|
||||
define("_AKEEBA_DIRECTORY_READ_CHUNK", $config->get('engine.archiver.zip.cd_glue_chunk_size')); // How much data to read at once when finalizing ZIP archives
|
||||
}
|
||||
|
||||
$this->crcCalculator = Factory::getCRC32Calculator();
|
||||
|
||||
parent::__bootstrap_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Note down the starting disk number for Split ZIP archives
|
||||
$starting_disk_number_for_this_file = 0;
|
||||
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
$starting_disk_number_for_this_file = $this->currentPartNumber - 1;
|
||||
}
|
||||
|
||||
// Open data file for output
|
||||
$this->openArchiveForOutput();
|
||||
|
||||
// Should I continue backing up a file from the previous step?
|
||||
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
|
||||
// definition we have an uncompressed, non-virtual file. Hence the default values.
|
||||
$isDir = false;
|
||||
$isSymlink = false;
|
||||
$compressionMethod = 1;
|
||||
$zdata = null;
|
||||
// If we are continuing file packing we have an uncompressed, non-virtual file.
|
||||
$isVirtual = $continueProcessingFile ? false : $isVirtual;
|
||||
$resume = $continueProcessingFile ? 0 : null;
|
||||
|
||||
if (!$continueProcessingFile)
|
||||
{
|
||||
// Log the file being added
|
||||
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "-- Adding $targetName to archive $messageSource");
|
||||
|
||||
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir,
|
||||
$compressionMethod, $zdata, $unc_len,
|
||||
$storedName , $crc, $c_len, $hexdtime, $old_offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since we are continuing archiving, it's an uncompressed regular file. Set up the variables.
|
||||
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
|
||||
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
|
||||
$unc_len = $configuration->get('volatile.engine.archiver.unc_len');
|
||||
$storedName = $configuration->get('volatile.engine.archiver.storedName');
|
||||
$crc = $configuration->get('volatile.engine.archiver.crc');
|
||||
$c_len = $configuration->get('volatile.engine.archiver.c_len');
|
||||
$hexdtime = $configuration->get('volatile.engine.archiver.hexdtime');
|
||||
$old_offset = $configuration->get('volatile.engine.archiver.old_offset');
|
||||
|
||||
// Log the file we continue packing
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
|
||||
}
|
||||
|
||||
/* "File data" segment. */
|
||||
if ($compressionMethod == 8)
|
||||
{
|
||||
$this->putRawDataIntoArchive($zdata);
|
||||
}
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
// Virtual data. Put into the archive.
|
||||
$this->putRawDataIntoArchive($sourceNameOrData);
|
||||
}
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
$this->fwrite($this->fp, @readlink($sourceNameOrData));
|
||||
}
|
||||
elseif ((!$isDir) && (!$isSymlink))
|
||||
{
|
||||
// Uncompressed file.
|
||||
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
|
||||
{
|
||||
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
|
||||
// true here to avoid running the final bit of code which writes the central directory record and
|
||||
// uncaches the file resume data.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the central directory file for append
|
||||
if (is_null($this->cdfp))
|
||||
{
|
||||
$this->cdfp = @$this->fopen($this->centralDirectoryFilename, "ab");
|
||||
}
|
||||
|
||||
if ($this->cdfp === false)
|
||||
{
|
||||
throw new ErrorException("Could not open Central Directory temporary file for append!");
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, $this->centralDirectoryRecordStartSignature);
|
||||
|
||||
if (!$isSymlink)
|
||||
{
|
||||
$this->fwrite($this->cdfp, "\x14\x00"); /* Version made by (always set to 2.0). */
|
||||
$this->fwrite($this->cdfp, "\x14\x00"); /* Version needed to extract */
|
||||
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
|
||||
$this->fwrite($this->cdfp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// Symlinks get special treatment
|
||||
$this->fwrite($this->cdfp, "\x14\x03"); /* Version made by (version 2.0 with UNIX extensions). */
|
||||
$this->fwrite($this->cdfp, "\x0a\x03"); /* Version needed to extract */
|
||||
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
|
||||
$this->fwrite($this->cdfp, "\x00\x00"); /* Compression method. */
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, $hexdtime); /* Last mod time/date. */
|
||||
$this->fwrite($this->cdfp, pack('V', $crc)); /* CRC 32 information. */
|
||||
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Compressed filesize. */
|
||||
|
||||
if ($compressionMethod == 0)
|
||||
{
|
||||
// When we are not compressing, $unc_len is being reduced to 0 while backing up.
|
||||
// With this trick, we always store the correct length, as in this case the compressed
|
||||
// and uncompressed length is always the same.
|
||||
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Uncompressed filesize. */
|
||||
}
|
||||
else
|
||||
{
|
||||
// When compressing, the uncompressed length differs from compressed length
|
||||
// and this line writes the correct value.
|
||||
$this->fwrite($this->cdfp, pack('V', $unc_len)); /* Uncompressed filesize. */
|
||||
}
|
||||
|
||||
$fn_length = akstrlen($storedName);
|
||||
$this->fwrite($this->cdfp, pack('v', $fn_length)); /* Length of filename. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* Extra field length. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* File comment length. */
|
||||
$this->fwrite($this->cdfp, pack('v', $starting_disk_number_for_this_file)); /* Disk number start. */
|
||||
$this->fwrite($this->cdfp, pack('v', 0)); /* Internal file attributes. */
|
||||
|
||||
/* External file attributes */
|
||||
if (!$isSymlink)
|
||||
{
|
||||
// Archive bit set
|
||||
$this->fwrite($this->cdfp, pack('V', $isDir ? 0x41FF0010 : 0xFE49FFE0));
|
||||
}
|
||||
else
|
||||
{
|
||||
// For SymLinks we store UNIX file attributes
|
||||
$this->fwrite($this->cdfp, "\x20\x80\xFF\xA1");
|
||||
}
|
||||
|
||||
$this->fwrite($this->cdfp, pack('V', $old_offset)); /* Relative offset of local header. */
|
||||
$this->fwrite($this->cdfp, $storedName); /* File name. */
|
||||
|
||||
/* Optional extra field, file comment goes here. */
|
||||
|
||||
// Finally, increase the file counter by one
|
||||
$this->totalFilesCount++;
|
||||
|
||||
// Uncache data
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
$configuration->set('volatile.engine.archiver.unc_len', null);
|
||||
$configuration->set('volatile.engine.archiver.resume', null);
|
||||
$configuration->set('volatile.engine.archiver.hexdtime', null);
|
||||
$configuration->set('volatile.engine.archiver.crc', null);
|
||||
$configuration->set('volatile.engine.archiver.c_len', null);
|
||||
$configuration->set('volatile.engine.archiver.fn_length', null);
|
||||
$configuration->set('volatile.engine.archiver.old_offset', null);
|
||||
$configuration->set('volatile.engine.archiver.storedName', null);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
|
||||
|
||||
$configuration->set('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
// ... and return TRUE = success
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the file header before putting the file data into the archive
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param string $targetName The target path to be stored inside the archive
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
* @param int $compressionMethod The compression method chosen for this file
|
||||
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
|
||||
* We return that from this method to avoid having to compress the same data
|
||||
* twice (once to write the compressed data length in the header and once to
|
||||
* write the compressed data to the archive).
|
||||
* @param int $unc_len The uncompressed size of the file / source data
|
||||
*
|
||||
* @param string $storedName The file path stored in the archive
|
||||
* @param string $crc CRC-32 for the file
|
||||
* @param int $c_len Compressed data length
|
||||
* @param string $hexdtime ZIP's hexadecimal notation if the file's modification date
|
||||
* @param int $old_offset Offset of the file header in the part file
|
||||
*/
|
||||
protected function writeFileHeader(&$sourceNameOrData, $targetName, &$isVirtual, &$isSymlink, &$isDir,
|
||||
&$compressionMethod, &$zdata, &$unc_len, &$storedName, &$crc, &$c_len,
|
||||
&$hexdtime, &$old_offset)
|
||||
{
|
||||
static $memLimit = null;
|
||||
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// See if it's a directory
|
||||
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
|
||||
|
||||
// See if it's a symlink (w/out dereference)
|
||||
$isSymlink = false;
|
||||
|
||||
if ($this->storeSymlinkTarget && !$isVirtual)
|
||||
{
|
||||
$isSymlink = is_link($sourceNameOrData);
|
||||
}
|
||||
|
||||
// Get real size before compression
|
||||
list($unc_len, $fileModTime) =
|
||||
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
|
||||
|
||||
// Decide if we will compress
|
||||
$compressionMethod = $this->getCompressionMethod($unc_len, $memLimit, $isDir, $isSymlink);
|
||||
|
||||
if ($isVirtual)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, ' Virtual add:' . $targetName . ' (' . $unc_len . ') - ' . $compressionMethod);
|
||||
}
|
||||
|
||||
/* "Local file header" segment. */
|
||||
|
||||
$crc = $this->getCRCForEntity($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
$storedName = $targetName;
|
||||
|
||||
if (!$isSymlink && $isDir)
|
||||
{
|
||||
$storedName .= "/";
|
||||
$unc_len = 0;
|
||||
}
|
||||
|
||||
// Test for non-existing or unreadable files
|
||||
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
|
||||
|
||||
// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
|
||||
$c_len = $unc_len;
|
||||
|
||||
// If we have to compress, read the data in memory and compress it
|
||||
if ($compressionMethod == 8)
|
||||
{
|
||||
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
|
||||
|
||||
// The method modifies $compressionMethod to 0 (uncompressed) or 1 (Deflate) but the ZIP format needs it
|
||||
// to be 0 (uncompressed) or 8 (Deflate). So I just multiply by 8.
|
||||
$compressionMethod *= 8;
|
||||
}
|
||||
|
||||
// Get the hex time.
|
||||
$dtime = dechex($this->unix2DOSTime($fileModTime));
|
||||
|
||||
if (akstrlen($dtime) < 8)
|
||||
{
|
||||
$dtime = "00000000";
|
||||
}
|
||||
|
||||
$hexdtime = chr(hexdec($dtime[6] . $dtime[7])) .
|
||||
chr(hexdec($dtime[4] . $dtime[5])) .
|
||||
chr(hexdec($dtime[2] . $dtime[3])) .
|
||||
chr(hexdec($dtime[0] . $dtime[1]));
|
||||
|
||||
// If it's a split ZIP file, we've got to make sure that the header can fit in the part
|
||||
if ($this->useSplitArchive)
|
||||
{
|
||||
// Get header size, taking into account any extra header necessary
|
||||
$header_size = 30 + akstrlen($storedName);
|
||||
|
||||
// Compare to free part space
|
||||
$free_space = $this->getPartFreeSize();
|
||||
|
||||
if ($free_space <= $header_size)
|
||||
{
|
||||
// Not enough space on current part, create new part
|
||||
$this->createAndOpenNewPart();
|
||||
}
|
||||
}
|
||||
|
||||
$old_offset = @ftell($this->fp);
|
||||
|
||||
if ($this->useSplitArchive && ($old_offset == 0))
|
||||
{
|
||||
// Because in split ZIPs we have the split ZIP marker in the first four bytes.
|
||||
@fseek($this->fp, 4);
|
||||
$old_offset = @ftell($this->fp);
|
||||
}
|
||||
|
||||
// Get the file name length in bytes
|
||||
$fn_length = akstrlen($storedName);
|
||||
|
||||
$this->fwrite($this->fp, $this->fileHeaderSignature); /* Begin creating the ZIP data. */
|
||||
|
||||
/* Version needed to extract. */
|
||||
if (!$isSymlink)
|
||||
{
|
||||
$this->fwrite($this->fp, "\x14\x00");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->fwrite($this->fp, "\x0a\x03");
|
||||
}
|
||||
|
||||
$this->fwrite($this->fp, pack('v', 2048)); /* General purpose bit flag. Bit 11 set = use UTF-8 encoding for filenames & comments */
|
||||
$this->fwrite($this->fp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
|
||||
$this->fwrite($this->fp, $hexdtime); /* Last modification time/date. */
|
||||
$this->fwrite($this->fp, pack('V', $crc)); /* CRC 32 information. */
|
||||
$this->fwrite($this->fp, pack('V', $c_len)); /* Compressed filesize. */
|
||||
$this->fwrite($this->fp, pack('V', $unc_len)); /* Uncompressed filesize. */
|
||||
$this->fwrite($this->fp, pack('v', $fn_length)); /* Length of filename. */
|
||||
$this->fwrite($this->fp, pack('v', 0)); /* Extra field length. */
|
||||
$this->fwrite($this->fp, $storedName); /* File name. */
|
||||
|
||||
// Cache useful information about the file
|
||||
if (!$isDir && !$isSymlink && !$isVirtual)
|
||||
{
|
||||
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
|
||||
$configuration->set('volatile.engine.archiver.hexdtime', $hexdtime);
|
||||
$configuration->set('volatile.engine.archiver.crc', $crc);
|
||||
$configuration->set('volatile.engine.archiver.c_len', $c_len);
|
||||
$configuration->set('volatile.engine.archiver.fn_length', $fn_length);
|
||||
$configuration->set('volatile.engine.archiver.old_offset', $old_offset);
|
||||
$configuration->set('volatile.engine.archiver.storedName', $storedName);
|
||||
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred compression method for a file
|
||||
*
|
||||
* @param int $fileSize File size in bytes
|
||||
* @param int $memLimit Memory limit in bytes
|
||||
* @param bool $isDir Is it a directory?
|
||||
* @param bool $isSymlink Is it a symlink?
|
||||
*
|
||||
* @return int Compression method to use
|
||||
*/
|
||||
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
|
||||
{
|
||||
// ZIP uses 0 for uncompressed and 8 for GZip Deflate whereas the parent method returns 0 and 1 respectively
|
||||
return 8 * parent::getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the CRC-32 checksum
|
||||
*
|
||||
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
|
||||
* @param bool $isVirtual Is this a virtual file?
|
||||
* @param bool $isSymlink Is this a symlink?
|
||||
* @param bool $isDir Is this a directory?
|
||||
*
|
||||
* @return int The CRC-32
|
||||
*/
|
||||
protected function getCRCForEntity(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
|
||||
{
|
||||
if (!$isSymlink && $isDir)
|
||||
{
|
||||
// Dummy CRC for dirs
|
||||
$crc = 0;
|
||||
|
||||
return $crc;
|
||||
}
|
||||
|
||||
if ($isSymlink)
|
||||
{
|
||||
$crc = \crc32(@readlink($sourceNameOrData));
|
||||
|
||||
return $crc;
|
||||
}
|
||||
|
||||
if ($isVirtual)
|
||||
{
|
||||
$crc = \crc32($sourceNameOrData);
|
||||
|
||||
return $crc;
|
||||
}
|
||||
|
||||
// This is supposed to be the fast way to calculate CRC32 of a (large) file.
|
||||
$crc = $this->crcCalculator->crc32_file($sourceNameOrData, $this->AkeebaPackerZIP_CHUNK_SIZE);
|
||||
|
||||
// If the file was unreadable, $crc will be false, so we skip the file
|
||||
if ($crc === false)
|
||||
{
|
||||
throw new WarningException('Could not calculate CRC32 for ' . $sourceNameOrData . '. Looks like it is an unreadable file.');
|
||||
}
|
||||
|
||||
return $crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UNIX timestamp to a 4-byte DOS date and time format
|
||||
* (date in high 2-bytes, time in low 2-bytes allowing magnitude
|
||||
* comparison).
|
||||
*
|
||||
* @param integer $unixtime The current UNIX timestamp.
|
||||
*
|
||||
* @return integer The current date in a 4-byte DOS format.
|
||||
*/
|
||||
protected function unix2DOSTime($unixtime = null)
|
||||
{
|
||||
$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
|
||||
|
||||
if ($timearray['year'] < 1980)
|
||||
{
|
||||
$timearray['year'] = 1980;
|
||||
$timearray['mon'] = 1;
|
||||
$timearray['mday'] = 1;
|
||||
$timearray['hours'] = 0;
|
||||
$timearray['minutes'] = 0;
|
||||
$timearray['seconds'] = 0;
|
||||
}
|
||||
|
||||
return (($timearray['year'] - 1980) << 25) |
|
||||
($timearray['mon'] << 21) |
|
||||
($timearray['mday'] << 16) |
|
||||
($timearray['hours'] << 11) |
|
||||
($timearray['minutes'] << 5) |
|
||||
($timearray['seconds'] >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new part for the spanned archive
|
||||
*
|
||||
* @param bool $finalPart Is this the final archive part?
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
protected function createNewPartFile($finalPart = false)
|
||||
{
|
||||
// Close any open file pointers
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
$this->fclose($this->fp);
|
||||
}
|
||||
|
||||
if (is_resource($this->cdfp))
|
||||
{
|
||||
$this->fclose($this->cdfp);
|
||||
}
|
||||
|
||||
// Remove the just finished part from the list of resumable offsets
|
||||
$this->removeFromOffsetsList($this->_dataFileName);
|
||||
|
||||
// Set the file pointers to null
|
||||
$this->fp = null;
|
||||
$this->cdfp = null;
|
||||
|
||||
// Push the previous part if we have to post-process it immediately
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0))
|
||||
{
|
||||
$this->finishedPart[] = $this->_dataFileName;
|
||||
}
|
||||
|
||||
// Add the part's size to our rolling sum
|
||||
clearstatcache();
|
||||
$this->totalCompressedSize += filesize($this->_dataFileName);
|
||||
$this->totalParts++;
|
||||
$this->currentPartNumber = $this->totalParts;
|
||||
|
||||
if ($finalPart)
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.zip';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.z' . sprintf('%02d', $this->currentPartNumber);
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, 'Creating new ZIP part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
|
||||
|
||||
// Inform the backup engine that we have changed the multipart number
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->updateMultipart($this->totalParts);
|
||||
|
||||
// Try to remove any existing file
|
||||
@unlink($this->_dataFileName);
|
||||
|
||||
// Touch the new file
|
||||
$result = @touch($this->_dataFileName);
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
chmod($this->_dataFileName, 0666);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the optimal chunk size for CRC32 calculations and file processing
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function findOptimalChunkSize()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// The user has entered their own preference
|
||||
if ($configuration->get('engine.archiver.common.chunk_size', 0) > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = AKEEBA_CHUNK;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the PHP memory limit
|
||||
$memLimit = $this->getMemoryLimit();
|
||||
|
||||
// Can't get a PHP memory limit? Use 2Mb chunks (fairly large, right?)
|
||||
if (is_null($memLimit))
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!function_exists("memory_get_usage"))
|
||||
{
|
||||
// PHP can't report memory usage, use a conservative 512Kb
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 524288;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// PHP *can* report memory usage, see if there's enough available memory
|
||||
$availableRAM = $memLimit - memory_get_usage();
|
||||
|
||||
if ($availableRAM > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = $availableRAM * 0.5;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// NEGATIVE AVAILABLE MEMORY?!! Some borked PHP implementations also return the size of the httpd footprint.
|
||||
if (($memLimit - 6291456) > 0)
|
||||
{
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = $memLimit - 6291456;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If all else fails, use 2Mb and cross your fingers
|
||||
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Central Directory temporary file
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
private function createCentralDirectoryTempFile()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$this->centralDirectoryFilename = tempnam($configuration->get('akeeba.basic.output_directory'), 'akzcd');
|
||||
$this->centralDirectoryFilename = basename($this->centralDirectoryFilename);
|
||||
$pos = strrpos($this->centralDirectoryFilename, '/');
|
||||
|
||||
if ($pos !== false)
|
||||
{
|
||||
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
|
||||
}
|
||||
|
||||
$pos = strrpos($this->centralDirectoryFilename, '\\');
|
||||
|
||||
if ($pos !== false)
|
||||
{
|
||||
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
|
||||
}
|
||||
|
||||
$this->centralDirectoryFilename = Factory::getTempFiles()->registerTempFile($this->centralDirectoryFilename);
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: CntDir Tempfile = " . $this->centralDirectoryFilename);
|
||||
|
||||
// Create temporary file
|
||||
if (!@touch($this->centralDirectoryFilename))
|
||||
{
|
||||
throw new ErrorException("Could not open temporary file for ZIP archiver. Please check your temporary directory's permissions!");
|
||||
}
|
||||
|
||||
if (function_exists('chmod'))
|
||||
{
|
||||
chmod($this->centralDirectoryFilename, 0666);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
; Akeeba JPA archiver engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: jpa.ini 522 2011-03-28 17:26:13Z nikosdion $
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title = COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_DESCRIPTION
|
||||
|
||||
; Dereference symlinks?
|
||||
[engine.archiver.common.dereference_symlinks]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION
|
||||
|
||||
; Part size setting. Common between archive engines.
|
||||
; Note: some writers may explicitly override this setting!
|
||||
[engine.archiver.common.part_size]
|
||||
default = 0
|
||||
type = integer
|
||||
min = 0
|
||||
max = 2147483648
|
||||
shortcuts = "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_PARTSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION
|
||||
|
||||
; Chunk size for processing large files. Common between archive engines.
|
||||
[engine.archiver.common.chunk_size]
|
||||
default = 1048576
|
||||
type = integer
|
||||
min = 65536
|
||||
max = 10485760
|
||||
shortcuts = "65536|131072|262144|524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION
|
||||
|
||||
; Do not compress files over this size (in bytes). Common between archive engines.
|
||||
[engine.archiver.common.big_file_threshold]
|
||||
default = 1048576
|
||||
type = integer
|
||||
min = 65536
|
||||
max = 10485760
|
||||
shortcuts = "65536|131072|262144|524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION
|
||||
|
||||
; Obviously, there are no JPA-specific parameters. The common parameters
|
||||
; cover all usage cases.
|
||||
@ -0,0 +1,64 @@
|
||||
; Akeeba ZIP archiver engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: zip.ini 738 2011-06-15 13:11:38Z nikosdion $
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title = COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_DESCRIPTION
|
||||
|
||||
; Dereference symlinks?
|
||||
[engine.archiver.common.dereference_symlinks]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION
|
||||
|
||||
; Part size setting. Common between archive engines.
|
||||
; Note: some writers may explicitly override this setting!
|
||||
[engine.archiver.common.part_size]
|
||||
default = 0
|
||||
type = integer
|
||||
min = 0
|
||||
max = 2147483648
|
||||
shortcuts = "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_PARTSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION
|
||||
|
||||
; Chunk size for processing large files. Common between archive engines.
|
||||
[engine.archiver.common.chunk_size]
|
||||
default = 1048576
|
||||
type = integer
|
||||
min = 65536
|
||||
max = 10485760
|
||||
shortcuts = "65536|131072|262144|524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION
|
||||
|
||||
; Do not compress files over this size (in bytes). Common between archive engines.
|
||||
[engine.archiver.common.big_file_threshold]
|
||||
default = 1048576
|
||||
type = integer
|
||||
min = 65536
|
||||
max = 10485760
|
||||
shortcuts = "65536|131072|262144|524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION
|
||||
|
||||
; Maximum chunk size for ZIP Central Directory gluing
|
||||
[engine.archiver.zip.cd_glue_chunk_size]
|
||||
default = 1048576
|
||||
type = integer
|
||||
min = 65536
|
||||
max = 10485760
|
||||
shortcuts = "65536|131072|262144|524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_DESCRIPTION
|
||||
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* The main class autoloader for AkeebaEngine
|
||||
*/
|
||||
class Autoloader
|
||||
{
|
||||
/**
|
||||
* An instance of this autoloader
|
||||
*
|
||||
* @var Autoloader
|
||||
*/
|
||||
public static $autoloader = null;
|
||||
|
||||
/**
|
||||
* The path to the Akeeba Engine root directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $enginePath = null;
|
||||
|
||||
/**
|
||||
* The directories where Akeeba Engine platforms are stored
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $platformDirs = null;
|
||||
|
||||
/**
|
||||
* Initialise this autoloader
|
||||
*
|
||||
* @return Autoloader
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
if (self::$autoloader == null)
|
||||
{
|
||||
self::$autoloader = new self;
|
||||
}
|
||||
|
||||
return self::$autoloader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor. Registers the autoloader with PHP.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
self::$enginePath = __DIR__;
|
||||
|
||||
spl_autoload_register(array($this, 'autoload_akeeba_engine'));
|
||||
spl_autoload_register(array($this, 'autoload_psr3'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual autoloader
|
||||
*
|
||||
* @param string $className The name of the class to load
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function autoload_akeeba_engine($className)
|
||||
{
|
||||
// Trim the trailing backslash
|
||||
$className = ltrim($className, '\\');
|
||||
|
||||
// Make sure the class has an Akeeba\Engine prefix
|
||||
if (substr($className, 0, 13) != 'Akeeba\\Engine')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the prefix and explode on backslashes
|
||||
$className = substr($className, 14);
|
||||
$class = explode('\\', $className);
|
||||
|
||||
// Do we have a list of platform directories?
|
||||
if (is_null(self::$platformDirs) && class_exists('\\Akeeba\\Engine\\Platform', false))
|
||||
{
|
||||
self::$platformDirs = Platform::getPlatformDirectories();
|
||||
|
||||
if (!is_array(self::$platformDirs))
|
||||
{
|
||||
self::$platformDirs = array();
|
||||
}
|
||||
}
|
||||
|
||||
$rootPaths = array(self::$enginePath);
|
||||
|
||||
if (is_array(self::$platformDirs))
|
||||
{
|
||||
$rootPaths = array_merge(
|
||||
self::$platformDirs, array(self::$enginePath)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($rootPaths as $rootPath)
|
||||
{
|
||||
// First try finding in structured directory format (preferred)
|
||||
$path = $rootPath . '/' . implode('/', $class) . '.php';
|
||||
|
||||
if (@file_exists($path))
|
||||
{
|
||||
include_once $path;
|
||||
}
|
||||
|
||||
// Then try the duplicate last name structured directory format (not recommended)
|
||||
if (!class_exists($className, false))
|
||||
{
|
||||
reset($class);
|
||||
$lastPart = end($class);
|
||||
$path = $rootPath . '/' . implode('/', $class) . '/' . $lastPart . '.php';
|
||||
|
||||
if (@file_exists($path))
|
||||
{
|
||||
include_once $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An autoloader for our copy of PSR-3
|
||||
*
|
||||
* @param string $className The name of the class to load
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function autoload_psr3($className)
|
||||
{
|
||||
// Trim the trailing backslash
|
||||
$className = ltrim($className, '\\');
|
||||
|
||||
// Make sure the class has an Akeeba\Engine prefix
|
||||
if (substr($className, 0, 7) != 'Psr\\Log')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the prefix and explode on backslashes
|
||||
$className = substr($className, 7);
|
||||
$class = explode('\\', $className);
|
||||
|
||||
$rootPath = self::$enginePath . '/Psr/Log';
|
||||
|
||||
// First try finding in structured directory format (preferred)
|
||||
$path = $rootPath . '/' . implode('/', $class) . '.php';
|
||||
|
||||
if (@file_exists($path))
|
||||
{
|
||||
include_once $path;
|
||||
}
|
||||
|
||||
// Then try the duplicate last name structured directory format (not recommended)
|
||||
if (!class_exists($className, false))
|
||||
{
|
||||
reset($class);
|
||||
$lastPart = end($class);
|
||||
$path = $rootPath . '/' . implode('/', $class) . '/' . $lastPart . '.php';
|
||||
|
||||
if (@file_exists($path))
|
||||
{
|
||||
include_once $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the Akeeba Engine autoloader
|
||||
Autoloader::init();
|
||||
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Base;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* The base class of Akeeba Engine objects. Allows for error and warnings logging
|
||||
* and propagation. Largely based on the Joomla! 1.5 JObject class.
|
||||
*/
|
||||
abstract class BaseObject
|
||||
{
|
||||
/** @var array An array of errors */
|
||||
private $_errors = array();
|
||||
|
||||
/** @var array The queue size of the $_errors array. Set to 0 for infinite size. */
|
||||
protected $_errors_queue_size = 0;
|
||||
|
||||
/** @var array An array of warnings */
|
||||
private $_warnings = array();
|
||||
|
||||
/** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */
|
||||
protected $_warnings_queue_size = 0;
|
||||
|
||||
/**
|
||||
* This method should be overridden by descendant classes. It is called when the factory is being
|
||||
* serialized and can be used to perform any necessary cleanup steps.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function _onSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent error message
|
||||
*
|
||||
* @param integer $i Optional error index
|
||||
*
|
||||
* @return string Error message
|
||||
*/
|
||||
public function getError($i = null)
|
||||
{
|
||||
return $this->getItemFromArray($this->_errors, $i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all errors, if any
|
||||
*
|
||||
* @return array Array of error messages
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->_errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error message
|
||||
*
|
||||
* @param string $error Error message
|
||||
* @param bool $log Should I log the message automatically?
|
||||
*/
|
||||
public function setError($error, $log = true)
|
||||
{
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->error($error);
|
||||
}
|
||||
|
||||
if ($this->_errors_queue_size > 0)
|
||||
{
|
||||
if (count($this->_errors) >= $this->_errors_queue_size)
|
||||
{
|
||||
array_shift($this->_errors);
|
||||
}
|
||||
}
|
||||
|
||||
array_push($this->_errors, $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all error messages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetErrors()
|
||||
{
|
||||
$this->_errors = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent warning message
|
||||
*
|
||||
* @param integer $i Optional warning index
|
||||
*
|
||||
* @return string Error message
|
||||
*/
|
||||
public function getWarning($i = null)
|
||||
{
|
||||
return $this->getItemFromArray($this->_warnings, $i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all warnings, if any
|
||||
*
|
||||
* @return array Array of error messages
|
||||
*/
|
||||
public function getWarnings()
|
||||
{
|
||||
return $this->_warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning message
|
||||
*
|
||||
* @param string $warning Warning message
|
||||
* @param bool $log Should I log the message automatically?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setWarning($warning, $log = true)
|
||||
{
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->warning($warning);
|
||||
}
|
||||
|
||||
if ($this->_warnings_queue_size > 0)
|
||||
{
|
||||
if (count($this->_warnings) >= $this->_warnings_queue_size)
|
||||
{
|
||||
array_shift($this->_warnings);
|
||||
}
|
||||
}
|
||||
|
||||
array_push($this->_warnings, $warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all warning messages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetWarnings()
|
||||
{
|
||||
$this->_warnings = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates errors and warnings to a foreign object. Propagated items will be removed from our own instance.
|
||||
*
|
||||
* @param Object $object The object to propagate errors and warnings to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function propagateToObject(&$object)
|
||||
{
|
||||
// Skip non-objects
|
||||
if (!is_object($object))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (method_exists($object, 'setError'))
|
||||
{
|
||||
if (!empty($this->_errors))
|
||||
{
|
||||
foreach ($this->_errors as $error)
|
||||
{
|
||||
$object->setError($error);
|
||||
}
|
||||
|
||||
$this->_errors = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($object, 'setWarning'))
|
||||
{
|
||||
if (!empty($this->_warnings))
|
||||
{
|
||||
foreach ($this->_warnings as $warning)
|
||||
{
|
||||
$object->setWarning($warning);
|
||||
}
|
||||
|
||||
$this->_warnings = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates errors and warnings from a foreign object. Each propagated list is
|
||||
* then cleared on the foreign object, as long as it implements resetErrors() and/or
|
||||
* resetWarnings() methods.
|
||||
*
|
||||
* @param self $object The object to propagate errors and warnings from
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function propagateFromObject(&$object)
|
||||
{
|
||||
if (method_exists($object, 'getErrors'))
|
||||
{
|
||||
$errors = $object->getErrors();
|
||||
|
||||
if (!empty($errors))
|
||||
{
|
||||
foreach ($errors as $error)
|
||||
{
|
||||
$this->setError($error, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($object, 'resetErrors'))
|
||||
{
|
||||
$object->resetErrors();
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($object, 'getWarnings'))
|
||||
{
|
||||
$warnings = $object->getWarnings();
|
||||
|
||||
if (!empty($warnings))
|
||||
{
|
||||
foreach ($warnings as $warning)
|
||||
{
|
||||
$this->setWarning($warning, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($object, 'resetWarnings'))
|
||||
{
|
||||
$object->resetWarnings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the error queue (acts like a LIFO buffer)
|
||||
*
|
||||
* @param int $newSize The new queue size. Set to 0 for infinite length.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setErrorsQueueSize($newSize = 0)
|
||||
{
|
||||
$this->_errors_queue_size = (int)$newSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the warnings queue (acts like a LIFO buffer)
|
||||
*
|
||||
* @param int $newSize The new queue size. Set to 0 for infinite length.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setWarningsQueueSize($newSize = 0)
|
||||
{
|
||||
$this->_warnings_queue_size = (int)$newSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last item of a LIFO string message queue, or a specific item
|
||||
* if so specified.
|
||||
*
|
||||
* @param array $array An array of strings, holding messages
|
||||
* @param int $i Optional message index
|
||||
*
|
||||
* @return mixed The message string, or false if the key doesn't exist
|
||||
*/
|
||||
protected function getItemFromArray($array, $i = null)
|
||||
{
|
||||
// Find the item
|
||||
if ($i === null)
|
||||
{
|
||||
// Default, return the last item
|
||||
$item = end($array);
|
||||
}
|
||||
elseif (!array_key_exists($i, $array))
|
||||
{
|
||||
// If $i has been specified but does not exist, return false
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$item = $array[$i];
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Base\Exceptions;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An exception which leads to an error (and complete halt) in the backup process
|
||||
*/
|
||||
class ErrorException extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Base\Exceptions;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An exception which leads to a warning in the backup process
|
||||
*/
|
||||
class WarningException extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
@ -0,0 +1,539 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Base;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* The superclass of all Akeeba Engine parts. The "parts" are intelligent stateful
|
||||
* classes which perform a single procedure and have preparation, running and
|
||||
* finalization phases. The transition between phases is handled automatically by
|
||||
* this superclass' tick() final public method, which should be the ONLY public API
|
||||
* exposed to the rest of the Akeeba Engine.
|
||||
*/
|
||||
abstract class Part extends BaseObject
|
||||
{
|
||||
/**
|
||||
* Indicates whether this part has finished its initialisation cycle
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $isPrepared = false;
|
||||
|
||||
/**
|
||||
* Indicates whether this part has more work to do (it's in running state)
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $isRunning = false;
|
||||
|
||||
/**
|
||||
* Indicates whether this part has finished its finalization cycle
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $isFinished = false;
|
||||
|
||||
/**
|
||||
* Indicates whether this part has finished its run cycle
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $hasRan = false;
|
||||
|
||||
/**
|
||||
* The name of the engine part (a.k.a. Domain), used in return table
|
||||
* generation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $active_domain = "";
|
||||
|
||||
/**
|
||||
* The step this engine part is in. Used verbatim in return table and
|
||||
* should be set by the code in the _run() method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $active_step = "";
|
||||
|
||||
/**
|
||||
* A more detailed description of the step this engine part is in. Used
|
||||
* verbatim in return table and should be set by the code in the _run()
|
||||
* method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $active_substep = "";
|
||||
|
||||
/**
|
||||
* Any configuration variables, in the form of an array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_parametersArray = array();
|
||||
|
||||
/** @var string The database root key */
|
||||
protected $databaseRoot = array();
|
||||
|
||||
/** @var int Last reported warning's position in array */
|
||||
private $warnings_pointer = -1;
|
||||
|
||||
/** @var bool Should we log the step nesting? */
|
||||
protected $nest_logging = false;
|
||||
|
||||
/** @var object Embedded installer preferences */
|
||||
protected $installerSettings;
|
||||
|
||||
/** @var int How much milliseconds should we wait to reach the min exec time */
|
||||
protected $waitTimeMsec = 0;
|
||||
|
||||
/** @var bool Should I ignore the minimum execution time altogether? */
|
||||
protected $ignoreMinimumExecutionTime = false;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @return Part
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Fetch the installer settings
|
||||
$this->installerSettings = (object)array(
|
||||
'installerroot' => 'installation',
|
||||
'sqlroot' => 'installation/sql',
|
||||
'databasesini' => 1,
|
||||
'readme' => 1,
|
||||
'extrainfo' => 1,
|
||||
'password' => 0,
|
||||
);
|
||||
|
||||
$config = Factory::getConfiguration();
|
||||
$installerKey = $config->get('akeeba.advanced.embedded_installer');
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
|
||||
if (array_key_exists($installerKey, $installerDescriptors))
|
||||
{
|
||||
// The selected installer exists, use it
|
||||
$this->installerSettings = (object)$installerDescriptors[$installerKey];
|
||||
}
|
||||
elseif (array_key_exists('angie', $installerDescriptors))
|
||||
{
|
||||
// The selected installer doesn't exist, but ANGIE exists; use that instead
|
||||
$this->installerSettings = (object)$installerDescriptors['angie'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the preparation for this part. Should set _isPrepared
|
||||
* to true
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function _prepare();
|
||||
|
||||
/**
|
||||
* Runs the finalisation process for this part. Should set
|
||||
* _isFinished to true.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function _finalize();
|
||||
|
||||
/**
|
||||
* Runs the main functionality loop for this part. Upon calling,
|
||||
* should set the _isRunning to true. When it finished, should set
|
||||
* the _hasRan to true. If an error is encountered, setError should
|
||||
* be used.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function _run();
|
||||
|
||||
/**
|
||||
* Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately,
|
||||
* in fear of timing out.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setBreakFlag()
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$registry->set('volatile.breakflag', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the engine part's internal state, in an easy to use manner
|
||||
*
|
||||
* @param string $state One of init, prepared, running, postrun, finished, error
|
||||
* @param string $errorMessage The reported error message, should the state be set to error
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setState($state = 'init', $errorMessage = 'Invalid setState argument')
|
||||
{
|
||||
switch ($state)
|
||||
{
|
||||
case 'init':
|
||||
$this->isPrepared = false;
|
||||
$this->isRunning = false;
|
||||
$this->isFinished = false;
|
||||
$this->hasRan = false;
|
||||
break;
|
||||
|
||||
case 'prepared':
|
||||
$this->isPrepared = true;
|
||||
$this->isRunning = false;
|
||||
$this->isFinished = false;
|
||||
$this->hasRan = false;
|
||||
break;
|
||||
|
||||
case 'running':
|
||||
$this->isPrepared = true;
|
||||
$this->isRunning = true;
|
||||
$this->isFinished = false;
|
||||
$this->hasRan = false;
|
||||
break;
|
||||
|
||||
case 'postrun':
|
||||
$this->isPrepared = true;
|
||||
$this->isRunning = false;
|
||||
$this->isFinished = false;
|
||||
$this->hasRan = true;
|
||||
break;
|
||||
|
||||
case 'finished':
|
||||
$this->isPrepared = true;
|
||||
$this->isRunning = false;
|
||||
$this->isFinished = true;
|
||||
$this->hasRan = false;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
default:
|
||||
$this->setError($errorMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public interface to an engine part. This method takes care for
|
||||
* calling the correct method in order to perform the initialisation -
|
||||
* run - finalisation cycle of operation and return a proper response array.
|
||||
*
|
||||
* @param int $nesting
|
||||
*
|
||||
* @return array A response array
|
||||
*/
|
||||
public function tick($nesting = 0)
|
||||
{
|
||||
$this->waitTimeMsec = 0;
|
||||
$configuration = Factory::getConfiguration();
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Call the right action method, depending on engine part state
|
||||
switch ($this->getState())
|
||||
{
|
||||
case "init":
|
||||
$this->_prepare();
|
||||
break;
|
||||
|
||||
case "prepared":
|
||||
$this->_run();
|
||||
break;
|
||||
|
||||
case "running":
|
||||
$this->_run();
|
||||
break;
|
||||
|
||||
case "postrun":
|
||||
$this->_finalize();
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is still time, we are not finished and there is no break flag set, re-run the tick()
|
||||
// method.
|
||||
$breakFlag = $configuration->get('volatile.breakflag', false);
|
||||
|
||||
if (
|
||||
!in_array($this->getState(), array('finished', 'error')) &&
|
||||
($timer->getTimeLeft() > 0) &&
|
||||
!$breakFlag &&
|
||||
($nesting < 20) &&
|
||||
($this->nest_logging)
|
||||
)
|
||||
{
|
||||
// Nesting is only applied if $this->nest_logging == true (currently only Kettenrad has this)
|
||||
$nesting++;
|
||||
|
||||
if ($this->nest_logging)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "*** Batching successive steps (nesting level $nesting)");
|
||||
}
|
||||
|
||||
$out = $this->tick($nesting);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return the output array
|
||||
$out = $this->_makeReturnTable();
|
||||
|
||||
// Things to do for nest-logged parts (currently, only Kettenrad is)
|
||||
if ($this->nest_logging)
|
||||
{
|
||||
if ($breakFlag)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "*** Engine steps batching: Break flag detected.");
|
||||
}
|
||||
|
||||
// Reset the break flag
|
||||
$configuration->set('volatile.breakflag', false);
|
||||
|
||||
// Log that we're breaking the step
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "*** Batching of engine steps finished. I will now return control to the caller.");
|
||||
|
||||
// Do I need client-side sleep?
|
||||
$serverSideSleep = true;
|
||||
|
||||
if (method_exists($this, 'getTag'))
|
||||
{
|
||||
$tag = $this->getTag();
|
||||
$clientSideSleep = Factory::getConfiguration()->get('akeeba.basic.clientsidewait', 0);
|
||||
|
||||
if (in_array($tag, array('backend', 'restorepoint')) && $clientSideSleep)
|
||||
{
|
||||
$serverSideSleep = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce minimum execution time
|
||||
if (!$this->ignoreMinimumExecutionTime)
|
||||
{
|
||||
$timer = Factory::getTimer();
|
||||
$this->waitTimeMsec = (int)$timer->enforce_min_exec_time(true, $serverSideSleep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send a Return Table back to the caller
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the class's status array
|
||||
*
|
||||
* @return array The response array
|
||||
*/
|
||||
public function getStatusArray()
|
||||
{
|
||||
return $this->_makeReturnTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends any kind of setup information to the engine part. Using this,
|
||||
* we avoid passing parameters to the constructor of the class. These
|
||||
* parameters should be passed as an indexed array and should be taken
|
||||
* into account during the preparation process only. This function will
|
||||
* set the error flag if it's called after the engine part is prepared.
|
||||
*
|
||||
* @param array $parametersArray The parameters to be passed to the engine part.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setup($parametersArray)
|
||||
{
|
||||
if ($this->isPrepared)
|
||||
{
|
||||
$this->setState('error', get_class($this) . ":: Can't modify configuration after the preparation of " . $this->active_domain);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_parametersArray = $parametersArray;
|
||||
|
||||
if (array_key_exists('root', $parametersArray))
|
||||
{
|
||||
$this->databaseRoot = $parametersArray['root'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state of this engine part.
|
||||
*
|
||||
* @return string The state of this engine part. It can be one of error, init, prepared, running, postrun,
|
||||
* finished.
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
if ($this->getError())
|
||||
{
|
||||
return "error";
|
||||
}
|
||||
|
||||
if (!($this->isPrepared))
|
||||
{
|
||||
return "init";
|
||||
}
|
||||
|
||||
if (!($this->isFinished) && !($this->isRunning) && !($this->hasRan) && ($this->isPrepared))
|
||||
{
|
||||
return "prepared";
|
||||
}
|
||||
|
||||
if (!($this->isFinished) && $this->isRunning && !($this->hasRan))
|
||||
{
|
||||
return "running";
|
||||
}
|
||||
|
||||
if (!($this->isFinished) && !($this->isRunning) && $this->hasRan)
|
||||
{
|
||||
return "postrun";
|
||||
}
|
||||
|
||||
if ($this->isFinished)
|
||||
{
|
||||
return "finished";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Response Array based on the engine part's state.
|
||||
*
|
||||
* @return array The Response Array for the current state
|
||||
*/
|
||||
protected function _makeReturnTable()
|
||||
{
|
||||
// Get a list of warnings
|
||||
$warnings = $this->getWarnings();
|
||||
|
||||
// Report only new warnings if there is no warnings queue size
|
||||
if ($this->_warnings_queue_size == 0)
|
||||
{
|
||||
if (($this->warnings_pointer > 0) && ($this->warnings_pointer < (count($warnings))))
|
||||
{
|
||||
$warnings = array_slice($warnings, $this->warnings_pointer + 1);
|
||||
$this->warnings_pointer += count($warnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->warnings_pointer = count($warnings);
|
||||
}
|
||||
}
|
||||
|
||||
$out = array(
|
||||
'HasRun' => (!($this->isFinished)),
|
||||
'Domain' => $this->active_domain,
|
||||
'Step' => $this->active_step,
|
||||
'Substep' => $this->active_substep,
|
||||
'Error' => $this->getError(),
|
||||
'Warnings' => $warnings
|
||||
);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current domain of the engine
|
||||
*
|
||||
* @param string $new_domain The domain to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setDomain($new_domain)
|
||||
{
|
||||
$this->active_domain = $new_domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current domain of the engine
|
||||
*
|
||||
* @return string The current domain
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->active_domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current step of the engine
|
||||
*
|
||||
* @param string $new_step The step to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setStep($new_step)
|
||||
{
|
||||
$this->active_step = $new_step;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current step of the engine
|
||||
*
|
||||
* @return string The current step
|
||||
*/
|
||||
public function getStep()
|
||||
{
|
||||
return $this->active_step;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current sub-step of the engine
|
||||
*
|
||||
* @param string $new_substep The sub-step to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setSubstep($new_substep)
|
||||
{
|
||||
$this->active_substep = $new_substep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current sub-step of the engine
|
||||
*
|
||||
* @return string The current sub-step
|
||||
*/
|
||||
public function getSubstep()
|
||||
{
|
||||
return $this->active_substep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this if your Engine Part can return the percentage of its work already complete
|
||||
*
|
||||
* @return float A number from 0 (nothing done) to 1 (all done)
|
||||
*/
|
||||
public function getProgress()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isIgnoreMinimumExecutionTime()
|
||||
{
|
||||
return $this->ignoreMinimumExecutionTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $ignoreMinimumExecutionTime
|
||||
*/
|
||||
public function setIgnoreMinimumExecutionTime($ignoreMinimumExecutionTime)
|
||||
{
|
||||
$this->ignoreMinimumExecutionTime = $ignoreMinimumExecutionTime;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,671 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Util\ParseIni;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* The Akeeba Engine configuration registry class
|
||||
*/
|
||||
class Configuration
|
||||
{
|
||||
/** @var string Default NameSpace */
|
||||
private $defaultNameSpace = 'global';
|
||||
|
||||
/** @var array Array keys which may contain stock directory definitions */
|
||||
private $directory_containing_keys = array(
|
||||
'akeeba.basic.output_directory'
|
||||
);
|
||||
|
||||
/** @var array Keys whose default values should never be overridden */
|
||||
private $protected_nodes = array();
|
||||
|
||||
/** @var array The registry data */
|
||||
private $registry = array();
|
||||
|
||||
/** @var integer The currently loaded profile */
|
||||
public $activeProfile = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return Configuration
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Create the default namespace
|
||||
$this->makeNameSpace($this->defaultNameSpace);
|
||||
|
||||
// Create a default configuration
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a namespace
|
||||
*
|
||||
* @param string $namespace Name of the namespace to create
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function makeNameSpace($namespace)
|
||||
{
|
||||
$this->registry[$namespace] = array('data' => new \stdClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of namespaces
|
||||
*
|
||||
* @return array List of namespaces
|
||||
*/
|
||||
public function getNameSpaces()
|
||||
{
|
||||
return array_keys($this->registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registry value
|
||||
*
|
||||
* @param string $regpath Registry path (e.g. global.directory.temporary)
|
||||
* @param mixed $default Optional default value
|
||||
* @param boolean $process_special_vars Optional. If true (default), it processes special variables, e.g.
|
||||
* [SITEROOT] in folder names
|
||||
*
|
||||
* @return mixed Value of entry or null
|
||||
*/
|
||||
public function get($regpath, $default = null, $process_special_vars = true)
|
||||
{
|
||||
// Cache the platform-specific stock directories
|
||||
static $stock_directories = array();
|
||||
|
||||
if (empty($stock_directories))
|
||||
{
|
||||
$stock_directories = Platform::getInstance()->get_stock_directories();
|
||||
}
|
||||
|
||||
$result = $default;
|
||||
|
||||
// Explode the registry path into an array
|
||||
if ($nodes = explode('.', $regpath))
|
||||
{
|
||||
// Get the namespace
|
||||
$count = count($nodes);
|
||||
|
||||
if ($count < 2)
|
||||
{
|
||||
$namespace = $this->defaultNameSpace;
|
||||
$nodes[1] = $nodes[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
$namespace = $nodes[0];
|
||||
}
|
||||
|
||||
if (isset($this->registry[$namespace]))
|
||||
{
|
||||
$ns = $this->registry[$namespace]['data'];
|
||||
$pathNodes = $count - 1;
|
||||
|
||||
for ($i = 1; $i < $pathNodes; $i++)
|
||||
{
|
||||
if ((isset($ns->{$nodes[$i]})))
|
||||
{
|
||||
$ns = $ns->{$nodes[$i]};
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($ns->{$nodes[$i]}))
|
||||
{
|
||||
$result = $ns->{$nodes[$i]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process certain directory-containing variables
|
||||
if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
|
||||
{
|
||||
if (!empty($stock_directories))
|
||||
{
|
||||
foreach ($stock_directories as $tag => $content)
|
||||
{
|
||||
$result = str_replace($tag, $content, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a registry value
|
||||
*
|
||||
* @param string $regpath Registry Path (e.g. global.directory.temporary)
|
||||
* @param mixed $value Value of entry
|
||||
* @param bool $process_special_vars Optional. If true (default), it processes special variables, e.g.
|
||||
* [SITEROOT] in folder names
|
||||
*
|
||||
* @return mixed Value of old value or boolean false if operation failed
|
||||
*/
|
||||
public function set($regpath, $value, $process_special_vars = true)
|
||||
{
|
||||
// Cache the platform-specific stock directories
|
||||
static $stock_directories = array();
|
||||
|
||||
if (empty($stock_directories))
|
||||
{
|
||||
$stock_directories = Platform::getInstance()->get_stock_directories();
|
||||
}
|
||||
|
||||
if (in_array($regpath, $this->protected_nodes))
|
||||
{
|
||||
return $this->get($regpath);
|
||||
}
|
||||
|
||||
// Explode the registry path into an array
|
||||
$nodes = explode('.', $regpath);
|
||||
|
||||
// Get the namespace
|
||||
$count = count($nodes);
|
||||
|
||||
if ($count < 2)
|
||||
{
|
||||
$namespace = $this->defaultNameSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
$namespace = array_shift($nodes);
|
||||
$count--;
|
||||
}
|
||||
|
||||
if (!isset($this->registry[$namespace]))
|
||||
{
|
||||
$this->makeNameSpace($namespace);
|
||||
}
|
||||
|
||||
$ns = $this->registry[$namespace]['data'];
|
||||
|
||||
$pathNodes = $count - 1;
|
||||
|
||||
if ($pathNodes < 0)
|
||||
{
|
||||
$pathNodes = 0;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $pathNodes; $i++)
|
||||
{
|
||||
// If any node along the registry path does not exist, create it
|
||||
if (!isset($ns->{$nodes[$i]}))
|
||||
{
|
||||
$ns->{$nodes[$i]} = new \stdClass();
|
||||
}
|
||||
$ns = $ns->{$nodes[$i]};
|
||||
}
|
||||
|
||||
// Set the new values
|
||||
if (is_string($value))
|
||||
{
|
||||
if (substr($value, 0, 10) == '###json###')
|
||||
{
|
||||
$value = json_decode(substr($value, 10));
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process certain directory-containing variables
|
||||
if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
|
||||
{
|
||||
if (!empty($stock_directories))
|
||||
{
|
||||
$data = $value;
|
||||
|
||||
foreach ($stock_directories as $tag => $content)
|
||||
{
|
||||
$data = str_replace($tag, $content, $data);
|
||||
}
|
||||
|
||||
$ns->{$nodes[$i]} = $data;
|
||||
|
||||
return $ns->{$nodes[$i]};
|
||||
}
|
||||
}
|
||||
|
||||
// This is executed if any of the previous two if's is false
|
||||
if (empty($nodes[$i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$ns->{$nodes[$i]} = $value;
|
||||
|
||||
return $ns->{$nodes[$i]};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset (remove) a registry value
|
||||
*
|
||||
* @param string $regpath Registry Path (e.g. global.directory.temporary)
|
||||
*
|
||||
* @return boolean True if the node was removed
|
||||
*/
|
||||
public function remove($regpath)
|
||||
{
|
||||
// Explode the registry path into an array
|
||||
$nodes = explode('.', $regpath);
|
||||
|
||||
// Get the namespace
|
||||
$count = count($nodes);
|
||||
|
||||
if ($count < 2)
|
||||
{
|
||||
$namespace = $this->defaultNameSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
$namespace = array_shift($nodes);
|
||||
$count--;
|
||||
}
|
||||
|
||||
if (!isset($this->registry[$namespace]))
|
||||
{
|
||||
$this->makeNameSpace($namespace);
|
||||
}
|
||||
|
||||
$ns = $this->registry[$namespace]['data'];
|
||||
|
||||
$pathNodes = $count - 1;
|
||||
|
||||
if ($pathNodes < 0)
|
||||
{
|
||||
$pathNodes = 0;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $pathNodes; $i++)
|
||||
{
|
||||
// If any node along the registry path does not exist, return false
|
||||
if (!isset($ns->{$nodes[$i]}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$ns = $ns->{$nodes[$i]};
|
||||
}
|
||||
|
||||
unset($ns->{$nodes[$i]});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the registry to the default values
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
// Load the Akeeba Engine INI files
|
||||
$root_path = __DIR__;
|
||||
|
||||
$paths = array(
|
||||
$root_path . '/Core',
|
||||
$root_path . '/Archiver',
|
||||
$root_path . '/Dump',
|
||||
$root_path . '/Scan',
|
||||
$root_path . '/Writer',
|
||||
$root_path . '/Proc',
|
||||
$root_path . '/Platform/Filter/Stack',
|
||||
$root_path . '/Filter/Stack',
|
||||
);
|
||||
|
||||
$platform_paths = Platform::getInstance()->getPlatformDirectories();
|
||||
|
||||
foreach ($platform_paths as $p)
|
||||
{
|
||||
$paths[] = $p . '/Filter/Stack';
|
||||
$paths[] = $p . '/Config';
|
||||
}
|
||||
|
||||
foreach ($paths as $root)
|
||||
{
|
||||
if (!(is_dir($root) || is_link($root)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_readable($root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$di = new \DirectoryIterator($root);
|
||||
|
||||
/** @var \DirectoryIterator $file */
|
||||
foreach ($di as $file)
|
||||
{
|
||||
if (!$file->isFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// PHP 5.3.5 and earlier do not support getExtension
|
||||
// if ($file->getExtension() == 'ini')
|
||||
if (substr($file->getBasename(), -4) == '.ini')
|
||||
{
|
||||
$this->mergeEngineINI($file->getRealPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges an associative array of key/value pairs into the registry.
|
||||
* If noOverride is set, only non set or null values will be applied.
|
||||
*
|
||||
* @param array $array An associative array. Its keys are registry paths.
|
||||
* @param bool $noOverride [optional] Do not override pre-set values.
|
||||
* @param bool $process_special_vars Optional. If true (default), it processes special variables, e.g.
|
||||
* [SITEROOT] in folder names
|
||||
*/
|
||||
public function mergeArray($array, $noOverride = false, $process_special_vars = true)
|
||||
{
|
||||
if (!$noOverride)
|
||||
{
|
||||
foreach ($array as $key => $value)
|
||||
{
|
||||
$this->set($key, $value, $process_special_vars);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($array as $key => $value)
|
||||
{
|
||||
if (is_null($this->get($key, null)))
|
||||
{
|
||||
$this->set($key, $value, $process_special_vars);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges an INI-style file into the registry. Its sections are registry paths,
|
||||
* keys are appended to the section-defined paths and then set equal to the
|
||||
* values. If noOverride is set, only non set or null values will be applied.
|
||||
* Sections beginning with an underscore will be ignored.
|
||||
*
|
||||
* @param string $inifile The full path to the INI file to load
|
||||
* @param boolean $noOverride [optional] Do not override pre-set values.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mergeINI($inifile, $noOverride = false)
|
||||
{
|
||||
if (!file_exists($inifile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$inidata = ParseIni::parse_ini_file($inifile, true);
|
||||
|
||||
foreach ($inidata as $rootkey => $rootvalue)
|
||||
{
|
||||
if (!is_array($rootvalue))
|
||||
{
|
||||
if (!$noOverride)
|
||||
{
|
||||
$this->set($rootkey, $rootvalue);
|
||||
}
|
||||
elseif (is_null($this->get($rootkey, null)))
|
||||
{
|
||||
$this->set($rootkey, $rootvalue);
|
||||
}
|
||||
}
|
||||
elseif (substr($rootkey, 0, 1) != '_')
|
||||
{
|
||||
foreach ($rootvalue as $key => $value)
|
||||
{
|
||||
if (!$noOverride)
|
||||
{
|
||||
$this->set($rootkey . '.' . $key, $rootvalue);
|
||||
}
|
||||
elseif (is_null($this->get($rootkey . '.' . $key, null)))
|
||||
{
|
||||
$this->set($rootkey . '.' . $key, $rootvalue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges an engine INI file to the configuration. Each section defines a full
|
||||
* registry path (section.subsection.key). It searches each section for the
|
||||
* key named "default" and merges its value to the configuration. The other keys
|
||||
* are simply ignored.
|
||||
*
|
||||
* @param string $inifile The absolute path to an INI file
|
||||
* @param bool $noOverride [optional] If true, values from the INI will not override the configuration
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mergeEngineINI($inifile, $noOverride = false)
|
||||
{
|
||||
if (!file_exists($inifile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$inidata = ParseIni::parse_ini_file($inifile, true);
|
||||
|
||||
foreach ($inidata as $section => $nodes)
|
||||
{
|
||||
if (is_array($nodes))
|
||||
{
|
||||
if (substr($section, 0, 1) != '_')
|
||||
{
|
||||
// Is this a protected node?
|
||||
$protected = false;
|
||||
|
||||
if (array_key_exists('protected', $nodes))
|
||||
{
|
||||
$protected = $nodes['protected'];
|
||||
}
|
||||
|
||||
// If overrides are allowed, unprotect until we can set the value
|
||||
if (!$noOverride)
|
||||
{
|
||||
if (in_array($section, $this->protected_nodes))
|
||||
{
|
||||
$pnk = array_search($section, $this->protected_nodes);
|
||||
unset($this->protected_nodes[$pnk]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('remove', $nodes))
|
||||
{
|
||||
// Remove a node if it has "remove" set
|
||||
$this->remove($section);
|
||||
}
|
||||
elseif (isset($nodes['default']))
|
||||
{
|
||||
if (!$noOverride)
|
||||
{
|
||||
// Update the default value if No Override is set
|
||||
$this->set($section, $nodes['default']);
|
||||
}
|
||||
elseif (is_null($this->get($section, null)))
|
||||
{
|
||||
// Set the default value if it does not exist
|
||||
$this->set($section, $nodes['default']);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if it's a protected node, enable the protection
|
||||
if ($protected)
|
||||
{
|
||||
$this->protected_nodes[] = $section;
|
||||
}
|
||||
else
|
||||
{
|
||||
$idx = array_search($section, $this->protected_nodes);
|
||||
|
||||
if ($idx !== false)
|
||||
{
|
||||
unset($this->protected_nodes[$idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the current registry snapshot as an INI file. Each namespace is
|
||||
* placed in a section of its own.
|
||||
*
|
||||
* @return string INI representation of the registry
|
||||
*/
|
||||
public function exportAsINI()
|
||||
{
|
||||
$inidata = '';
|
||||
$namespaces = $this->getNameSpaces();
|
||||
foreach ($namespaces as $namespace)
|
||||
{
|
||||
if ($namespace == 'volatile')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$inidata .= "[$namespace]\n";
|
||||
$ns = $this->registry[$namespace]['data'];
|
||||
$inidata .= $this->dumpObject($ns);
|
||||
}
|
||||
|
||||
return $inidata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to dump an object as INI-formatted data
|
||||
*
|
||||
* @param object $object The object to dump
|
||||
* @param string $prefix [optional] The prefix to use for the exported data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function dumpObject($object, $prefix = '')
|
||||
{
|
||||
$data = '';
|
||||
$vars = get_object_vars($object);
|
||||
|
||||
foreach ($vars as $key => $value)
|
||||
{
|
||||
if (!is_object($value))
|
||||
{
|
||||
if (is_array($value))
|
||||
{
|
||||
$value = '###json###' . json_encode($value);
|
||||
}
|
||||
$data .= (empty($prefix) ? '' : $prefix . '.') . $key .
|
||||
'="' . addcslashes($value, "\n\r\t\"") . "\"\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
$data .= $this->dumpObject($value, (empty($prefix) ? '' : $prefix . '.') . $key);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the protection status for a specific configuration key
|
||||
*
|
||||
* @param string|array $node The node to protect/unprotect
|
||||
* @param boolean $protect True to protect, false to unprotect
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setKeyProtection($node, $protect = false)
|
||||
{
|
||||
if (is_array($node))
|
||||
{
|
||||
foreach ($node as $k)
|
||||
{
|
||||
$this->setKeyProtection($k, $protect);
|
||||
}
|
||||
}
|
||||
elseif (is_string($node))
|
||||
{
|
||||
if (is_array($this->protected_nodes))
|
||||
{
|
||||
$protected = in_array($node, $this->protected_nodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->protected_nodes = array();
|
||||
$protected = false;
|
||||
}
|
||||
|
||||
if ($protect)
|
||||
{
|
||||
if (!$protected)
|
||||
{
|
||||
$this->protected_nodes[] = $node;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($protected)
|
||||
{
|
||||
$pnk = array_search($node, $this->protected_nodes);
|
||||
unset($this->protected_nodes[$pnk]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of protected keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProtectedKeys()
|
||||
{
|
||||
return $this->protected_nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the protected keys
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetProtectedKeys()
|
||||
{
|
||||
$this->protected_nodes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the protected keys
|
||||
*
|
||||
* @param array $keys A list of keys to protect
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setProtectedKeys($keys)
|
||||
{
|
||||
$this->protected_nodes = $keys;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@ -0,0 +1,63 @@
|
||||
; Akeeba core engine configuration values
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id$
|
||||
|
||||
; ======================================================================
|
||||
; Basic core engine configuration
|
||||
; ======================================================================
|
||||
|
||||
[_group]
|
||||
description = COM_AKEEBA_CONFIG_HEADER_BASIC
|
||||
|
||||
; Output directory
|
||||
[akeeba.basic.output_directory]
|
||||
default = "[DEFAULT_OUTPUT]"
|
||||
type = browsedir
|
||||
title = COM_AKEEBA_CONFIG_OUTDIR_TITLE
|
||||
description = COM_AKEEBA_CONFIG_OUTDIR_DESCRIPTION
|
||||
|
||||
; Log level
|
||||
[akeeba.basic.log_level]
|
||||
default = 4
|
||||
type = enum
|
||||
enumkeys = "COM_AKEEBA_CONFIG_LOGLEVEL_NONE|COM_AKEEBA_CONFIG_LOGLEVEL_WARNING|COM_AKEEBA_CONFIG_LOGLEVEL_DEBUG"
|
||||
enumvalues = "0|2|4"
|
||||
title = COM_AKEEBA_CONFIG_LOGLEVEL_TITLE
|
||||
description = COM_AKEEBA_CONFIG_LOGLEVEL_DESCRIPTION
|
||||
|
||||
; Archive name (template name, no extension, no path!)
|
||||
[akeeba.basic.archive_name]
|
||||
default = "site-[HOST]-[DATE]-[TIME_TZ]"
|
||||
type = string
|
||||
title = COM_AKEEBA_CONFIG_ARCHIVENAME_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ARCHIVENAME_DESCRIPTION
|
||||
|
||||
; Backup type
|
||||
[akeeba.basic.backup_type]
|
||||
default = full
|
||||
type = enum
|
||||
enumkeys = "COM_AKEEBA_CONFIG_BACKUPTYPE_FULL|COM_AKEEBA_CONFIG_BACKUPTYPE_DBONLY"
|
||||
enumvalues = "full|dbonly"
|
||||
title = COM_AKEEBA_CONFIG_BACKUPTYPE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BACKUPTYPE_DESCRIPTION
|
||||
|
||||
; Client-side wait
|
||||
[akeeba.basic.clientsidewait]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_CLIENTSIDEWAIT_TITLE
|
||||
description = COM_AKEEBA_CONFIG_CLIENTSIDEWAIT_DESCRIPTION
|
||||
|
||||
; Client-server communications
|
||||
[akeeba.basic.useiframe]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_USEIFRAMES_TITLE
|
||||
description = COM_AKEEBA_CONFIG_USEIFRAMES_DESCRIPTION
|
||||
|
||||
; Use database storage instead of temporary files
|
||||
[akeeba.core.usedbstorage]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_USEDBSTORAGE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_USEDBSTORAGE_DESCRIPTION
|
||||
@ -0,0 +1,53 @@
|
||||
; Akeeba core engine configuration values
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id$
|
||||
|
||||
; ======================================================================
|
||||
; Advanced configuration
|
||||
; ======================================================================
|
||||
|
||||
[_group]
|
||||
description = COM_AKEEBA_CONFIG_ADVANCED
|
||||
|
||||
; Database dump engine
|
||||
[akeeba.advanced.dump_engine]
|
||||
default = native
|
||||
type = engine
|
||||
subtype = dump
|
||||
protected = 1
|
||||
title = COM_AKEEBA_CONFIG_DUMPENGINE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_DUMPENGINE_DESCRIPTION
|
||||
|
||||
; File scanner engine
|
||||
[akeeba.advanced.scan_engine]
|
||||
default = smart
|
||||
type = engine
|
||||
subtype = scan
|
||||
protected = 1
|
||||
title = COM_AKEEBA_CONFIG_SCANENGINE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_SCANENGINE_DESCRIPTION
|
||||
|
||||
; Archiver engine
|
||||
[akeeba.advanced.archiver_engine]
|
||||
default = jpa
|
||||
type = engine
|
||||
subtype = archiver
|
||||
title = COM_AKEEBA_CONFIG_ARCHIVERENGINE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ARCHIVERENGINE_DESCRIPTION
|
||||
|
||||
; Post processing engine (could also be used for site-to-cloud backup)
|
||||
[akeeba.advanced.postproc_engine]
|
||||
default = "none"
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
; Embedded installer
|
||||
[akeeba.advanced.embedded_installer]
|
||||
default = angie
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[engine.installer.angie.key]
|
||||
default = ""
|
||||
type = "none"
|
||||
protected = "1"
|
||||
@ -0,0 +1,72 @@
|
||||
; Akeeba core engine configuration values
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id$
|
||||
|
||||
[_group]
|
||||
description = COM_AKEEBA_CONFIG_HEADER_QUOTA
|
||||
|
||||
; ======================================================================
|
||||
; Quota management
|
||||
; ======================================================================
|
||||
|
||||
; Remote quotas toggle
|
||||
[akeeba.quota.remote]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
; Maximum backup age quotas
|
||||
[akeeba.quota.maxage.enable]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
; Obsolete records quota
|
||||
[akeeba.quota.obsolete_quota]
|
||||
default = 50
|
||||
type = integer
|
||||
min = 0
|
||||
max = 500
|
||||
shortcuts = "1|10|20|30|40|50"
|
||||
scale = 1
|
||||
uom = items
|
||||
title = COM_AKEEBA_CONFIG_OBSOLETEQUOTA_ENABLE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_OBSOLETEQUOTA_ENABLE_DESCRIPTION
|
||||
|
||||
; Enable size quota
|
||||
[akeeba.quota.enable_size_quota]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_SIZEQUOTA_ENABLE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_SIZEQUOTA_ENABLE_DESCRIPTION
|
||||
|
||||
; Size quota in bytes
|
||||
[akeeba.quota.size_quota]
|
||||
default = 15728640
|
||||
type = integer
|
||||
min = 1
|
||||
max = 4604090368
|
||||
shortcuts = "15728640|52428800|104857600|268435456|536870912|1073741824|2147483648"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_SIZEQUOTA_VALUE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_SIZEQUOTA_VALUE_DESCRIPTION
|
||||
|
||||
; Enable count quota
|
||||
[akeeba.quota.enable_count_quota]
|
||||
default = 1
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_COUNTQUOTA_ENABLE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_COUNTQUOTA_ENABLE_DESCRIPTION
|
||||
|
||||
; Size quota in MB
|
||||
[akeeba.quota.count_quota]
|
||||
default = 3
|
||||
type = integer
|
||||
min = 1
|
||||
max = 200
|
||||
shortcuts = "1|5|10|50|100|200"
|
||||
scale = 1
|
||||
uom = ""
|
||||
title = COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_DESCRIPTION
|
||||
@ -0,0 +1,108 @@
|
||||
; Akeeba core engine configuration values
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id$
|
||||
|
||||
[_group]
|
||||
description = COM_AKEEBA_CONFIG_HEADER_TUNING
|
||||
|
||||
; ======================================================================
|
||||
; Tuning configuration
|
||||
; ======================================================================
|
||||
|
||||
; Minimum execution time per step
|
||||
[akeeba.tuning.min_exec_time]
|
||||
default = 2000
|
||||
type = integer
|
||||
min = 0
|
||||
max = 20000
|
||||
shortcuts = "0|250|500|1000|2000|3000|4000|5000|7500|10000|15000|20000"
|
||||
scale = 1000
|
||||
uom = s
|
||||
title = COM_AKEEBA_CONFIG_MINEXECTIME_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MINEXECTIME_DESCRIPTION
|
||||
|
||||
; Maximum execution time per step
|
||||
[akeeba.tuning.max_exec_time]
|
||||
default = 14
|
||||
type = integer
|
||||
min = 0
|
||||
max = 180
|
||||
shortcuts = "1|2|3|5|7|10|14|15|20|23|25|30|45|60|90|120|180"
|
||||
scale = 1
|
||||
uom = s
|
||||
title = COM_AKEEBA_CONFIG_MAXEXECTIME_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MAXEXECTIME_DESCRIPTION
|
||||
|
||||
; Run-time bias
|
||||
[akeeba.tuning.run_time_bias]
|
||||
default = 75
|
||||
type = integer
|
||||
min = 10
|
||||
max = 100
|
||||
shortcuts = "10|20|25|30|40|50|60|75|80|90|100"
|
||||
scale = 1
|
||||
uom = %
|
||||
title = COM_AKEEBA_CONFIG_RUNTIMEBIAS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_RUNTIMEBIAS_DESCRIPTION
|
||||
|
||||
; Resume backup after an AJAX error has occurred
|
||||
[akeeba.advanced.autoresume]
|
||||
default=1
|
||||
type=bool
|
||||
title=COM_AKEEBA_CONFIG_AUTORESUME_TITLE
|
||||
description=COM_AKEEBA_CONFIG_AUTORESUME_DESCRIPTION
|
||||
|
||||
; Wait period before retrying the backup step
|
||||
[akeeba.advanced.autoresume_timeout]
|
||||
default=10
|
||||
type=integer
|
||||
min=1
|
||||
max=36000
|
||||
scale=1
|
||||
uom="s"
|
||||
shortcuts="3|5|10|15|20|30|45|60|90|120|300|600|900|1800|3600"
|
||||
title=COM_AKEEBA_CONFIG_AUTORESUME_TIMEOUT_TITLE
|
||||
description=COM_AKEEBA_CONFIG_AUTORESUME_TIMEOUT_DESCRIPTION
|
||||
|
||||
; Maximum retries of a backup step after an AJAX error
|
||||
[akeeba.advanced.autoresume_maxretries]
|
||||
default=3
|
||||
type=integer
|
||||
min=1
|
||||
max=1000
|
||||
scale=1
|
||||
shortcuts="1|3|5|7|10|15|20|30|50|100"
|
||||
title=COM_AKEEBA_CONFIG_AUTORESUME_MAXRETRIES_TITLE
|
||||
description=COM_AKEEBA_CONFIG_AUTORESUME_MAXRETRIES_DESCRIPTION
|
||||
|
||||
;; These are the ultra advanced options for speed devils. WARNING: THEY CAN KILL THE BACKUP PROCESS WHEN ENABLED!
|
||||
|
||||
[akeeba.tuning.nobreak.beforelargefile]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[akeeba.tuning.nobreak.afterlargefile]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[akeeba.tuning.nobreak.proactive]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[akeeba.tuning.nobreak.domains]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[akeeba.tuning.nobreak.finalization]
|
||||
default = 0
|
||||
type = "none"
|
||||
protected = 1
|
||||
|
||||
[akeeba.tuning.settimelimit]
|
||||
default = 1
|
||||
type = "none"
|
||||
protected = 1
|
||||
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject;
|
||||
use Akeeba\Engine\Driver\Base as DriverBase;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* A utility class to return a database connection object
|
||||
*/
|
||||
class Database extends BaseObject
|
||||
{
|
||||
private static $instances = array();
|
||||
|
||||
/**
|
||||
* Returns a database connection object. It caches the created objects for future use.
|
||||
*
|
||||
* @param array $options Options to use when instantiating the database connection
|
||||
*
|
||||
* @return DriverBase
|
||||
*/
|
||||
public static function &getDatabase($options, $unset = false)
|
||||
{
|
||||
if (!is_array(self::$instances))
|
||||
{
|
||||
self::$instances = array();
|
||||
}
|
||||
|
||||
$signature = serialize($options);
|
||||
|
||||
if ($unset)
|
||||
{
|
||||
if (!empty(self::$instances[$signature]))
|
||||
{
|
||||
$db = self::$instances[$signature];
|
||||
$db = null;
|
||||
unset(self::$instances[$signature]);
|
||||
}
|
||||
$null = null;
|
||||
|
||||
return $null;
|
||||
}
|
||||
|
||||
if (empty(self::$instances[$signature]))
|
||||
{
|
||||
$driver = array_key_exists('driver', $options) ? $options['driver'] : '';
|
||||
$select = array_key_exists('select', $options) ? $options['select'] : true;
|
||||
$database = array_key_exists('database', $options) ? $options['database'] : null;
|
||||
|
||||
$driver = preg_replace('/[^A-Z0-9_\\\.-]/i', '', $driver);
|
||||
|
||||
if (empty($driver))
|
||||
{
|
||||
// No driver specified; try to guess
|
||||
$default_signature = serialize(Platform::getInstance()->get_platform_database_options());
|
||||
if ($signature == $default_signature)
|
||||
{
|
||||
$driver = Platform::getInstance()->get_default_database_driver(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
$driver = Platform::getInstance()->get_default_database_driver(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure a full driver name was given
|
||||
if ((substr($driver, 0, 7) != '\\Akeeba') && substr($driver, 0, 7) != 'Akeeba\\')
|
||||
{
|
||||
$driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver);
|
||||
}
|
||||
}
|
||||
|
||||
// Useful for PHP 7 which does NOT have the ancient mysql adapter
|
||||
if (($driver == '\\Akeeba\\Engine\\Driver\\Mysql') && !function_exists('mysql_connect'))
|
||||
{
|
||||
$driver = '\\Akeeba\\Engine\\Driver\\Mysqli';
|
||||
}
|
||||
|
||||
self::$instances[$signature] = new $driver($options);
|
||||
}
|
||||
|
||||
return self::$instances[$signature];
|
||||
}
|
||||
|
||||
public static function unsetDatabase($options)
|
||||
{
|
||||
self::getDatabase($options, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a specific instance. This is supposed to be used only in Unit Tests.
|
||||
*
|
||||
* @param $key
|
||||
* @param $instance
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function forceInstance($key, $instance)
|
||||
{
|
||||
if (!interface_exists('PHPUnit_Exception', false))
|
||||
{
|
||||
$method = __METHOD__;
|
||||
throw new \Exception("You can only use $method in Unit Tests", 500);
|
||||
}
|
||||
|
||||
self::$instances[$key] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the instances. This is supposed to be used only in Unit Tests.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function nukeInstances()
|
||||
{
|
||||
if (!interface_exists('PHPUnit_Exception', false))
|
||||
{
|
||||
$method = __METHOD__;
|
||||
throw new \Exception("You can only use $method in Unit Tests", 500);
|
||||
}
|
||||
|
||||
self::$instances = array();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,372 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Dump\Base as DumpBase;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Multiple database backup engine.
|
||||
*/
|
||||
class Db extends Part
|
||||
{
|
||||
/** @var array A list of the databases to be packed */
|
||||
private $database_list = array();
|
||||
|
||||
/** @var array The current database configuration data */
|
||||
private $database_config = null;
|
||||
|
||||
/** @var DumpBase The current dumper engine used to backup tables */
|
||||
private $dump_engine = null;
|
||||
|
||||
/** @var string The contents of the databases.ini file */
|
||||
private $databases_ini = '';
|
||||
|
||||
/** @var array An array containing the database definitions of all dumped databases so far */
|
||||
private $dumpedDatabases = array();
|
||||
|
||||
/** @var int Total number of databases left to be processed */
|
||||
private $total_databases = 0;
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Db
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _prepare abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _prepare()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Preparing instance");
|
||||
|
||||
// Populating the list of databases
|
||||
$this->populate_database_list();
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->total_databases = count($this->database_list);
|
||||
|
||||
$this->setState('prepared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _run() abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _run()
|
||||
{
|
||||
if ($this->getState() == 'postrun')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setState('running');
|
||||
}
|
||||
|
||||
// Make sure we have a dumper instance loaded!
|
||||
if (is_null($this->dump_engine) && !empty($this->database_list))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Iterating next database");
|
||||
|
||||
// Create a new instance
|
||||
$this->dump_engine = Factory::getDumpEngine(true);
|
||||
|
||||
// Configure the dumper instance and pass on the volatile database root registry key
|
||||
$registry = Factory::getConfiguration();
|
||||
$rootkeys = array_keys($this->database_list);
|
||||
$root = array_shift($rootkeys);
|
||||
$registry->set('volatile.database.root', $root);
|
||||
|
||||
$this->database_config = array_shift($this->database_list);
|
||||
$this->database_config['root'] = $root;
|
||||
$this->database_config['process_empty_prefix'] = ($root == '[SITEDB]') ? true : false;
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Now backing up $root ({$this->database_config['database']})");
|
||||
|
||||
$this->dump_engine->setup($this->database_config);
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($this->dump_engine);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
elseif (is_null($this->dump_engine) && empty($this->database_list))
|
||||
{
|
||||
$this->setError('Current dump engine died while resuming the step');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to step the instance
|
||||
$retArray = $this->dump_engine->tick();
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($this->dump_engine);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setStep($retArray['Step']);
|
||||
$this->setSubstep($retArray['Substep']);
|
||||
|
||||
// Check if the instance has finished
|
||||
if (!$retArray['HasRun'])
|
||||
{
|
||||
// Set the number of parts
|
||||
$this->database_config['parts'] = $this->dump_engine->partNumber + 1;
|
||||
|
||||
// Push the definition
|
||||
array_push($this->dumpedDatabases, $this->database_config);
|
||||
|
||||
// Go to the next entry in the list and dispose the old AkeebaDumperDefault instance
|
||||
$this->dump_engine = null;
|
||||
|
||||
// Are we past the end of the list?
|
||||
if (empty($this->database_list))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: No more databases left to iterate");
|
||||
$this->setState('postrun');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _finalize() abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _finalize()
|
||||
{
|
||||
$this->setState('finished');
|
||||
|
||||
// If we are in db backup mode, don't create a databases.ini
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.databasesini', 1))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Skipping databases.ini");
|
||||
}
|
||||
// Create the databases.ini contents
|
||||
elseif ($this->installerSettings->databasesini)
|
||||
{
|
||||
$this->createDatabasesINI();
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Creating databases.ini");
|
||||
|
||||
// Create a new string
|
||||
$databasesINI = $this->databases_ini;
|
||||
|
||||
// BEGIN FIX 1.2 Stable -- databases.ini isn't written on disk
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Writing databases.ini contents");
|
||||
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$virtualLocation = (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'short') ? '' : $this->installerSettings->sqlroot;
|
||||
$archiver->addVirtualFile('databases.ini', $virtualLocation, $databasesINI);
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($archiver);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// On alldb mode, we have to finalize the archive as well
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.finalizearchive', 0))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "Finalizing database dump archive");
|
||||
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$archiver->finalize();
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($archiver);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In CLI mode we'll also close the database connection
|
||||
if (defined('AKEEBACLI'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "Closing the database connection to the main database");
|
||||
Factory::unsetDatabase();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates database_list with the list of databases in the settings
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function populate_database_list()
|
||||
{
|
||||
// Get database inclusion filters
|
||||
$filters = Factory::getFilters();
|
||||
$this->database_list = $filters->getInclusions('db');
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($filters);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.skipextradb', 0))
|
||||
{
|
||||
// On database only backups we prune extra databases
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Adding only main database");
|
||||
|
||||
if (count($this->database_list) > 1)
|
||||
{
|
||||
$this->database_list = array_slice($this->database_list, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDatabasesINI()
|
||||
{
|
||||
// caching databases.ini contents
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Creating databases.ini data");
|
||||
|
||||
// Create a new string
|
||||
$this->databases_ini = '';
|
||||
|
||||
$blankOutPass = Factory::getConfiguration()->get('engine.dump.common.blankoutpass', 0);
|
||||
$siteRoot = Factory::getConfiguration()->get('akeeba.platform.newroot', '');
|
||||
|
||||
// Loop through databases list
|
||||
foreach ($this->dumpedDatabases as $definition)
|
||||
{
|
||||
$section = basename($definition['dumpFile']);
|
||||
|
||||
$dboInstance = Factory::getDatabase($definition);
|
||||
$type = $dboInstance->name;
|
||||
$tech = $dboInstance->getDriverType();
|
||||
|
||||
// If the database is a sqlite one, we have to process the database name which contains the path
|
||||
// At the moment we only handle the case where the db file is UNDER site root
|
||||
if ($tech == 'sqlite')
|
||||
{
|
||||
$definition['database'] = str_replace($siteRoot, '#SITEROOT#', $definition['database']);
|
||||
}
|
||||
|
||||
if ($blankOutPass)
|
||||
{
|
||||
$this->databases_ini .= <<<ENDDEF
|
||||
[$section]
|
||||
dbtype = "$type"
|
||||
dbtech = "$tech"
|
||||
dbname = "{$definition['database']}"
|
||||
sqlfile = "{$definition['dumpFile']}"
|
||||
dbhost = "{$definition['host']}"
|
||||
dbuser = ""
|
||||
dbpass = ""
|
||||
prefix = "{$definition['prefix']}"
|
||||
parts = "{$definition['parts']}"
|
||||
|
||||
ENDDEF;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have to escape the password
|
||||
$escapedPassword = addcslashes($definition['password'], "\"\\\n\r");
|
||||
|
||||
$this->databases_ini .= <<<ENDDEF
|
||||
[$section]
|
||||
dbtype = "$type"
|
||||
dbtech = "$tech"
|
||||
dbname = "{$definition['database']}"
|
||||
sqlfile = "{$definition['dumpFile']}"
|
||||
dbhost = "{$definition['host']}"
|
||||
dbuser = "{$definition['username']}"
|
||||
dbpass = "$escapedPassword"
|
||||
prefix = "{$definition['prefix']}"
|
||||
parts = "{$definition['parts']}"
|
||||
|
||||
ENDDEF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the getProgress() percentage calculation based on how many
|
||||
* databases we have fully dumped and how much of the current database we
|
||||
* have dumped.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getProgress()
|
||||
{
|
||||
if ($this->total_databases)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the overall percentage (based on databases fully dumped so far)
|
||||
$remaining_steps = count($this->database_list);
|
||||
$remaining_steps++;
|
||||
$overall = 1 - ($remaining_steps / $this->total_databases);
|
||||
|
||||
// How much is this step worth?
|
||||
$this_max = 1 / $this->total_databases;
|
||||
|
||||
// Get the percentage done of the current database
|
||||
$local = $this->dump_engine->getProgress();
|
||||
|
||||
$percentage = $overall + $local * $this_max;
|
||||
|
||||
if ($percentage < 0)
|
||||
{
|
||||
$percentage = 0;
|
||||
}
|
||||
elseif ($percentage > 1)
|
||||
{
|
||||
$percentage = 1;
|
||||
}
|
||||
|
||||
return $percentage;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,463 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Backup initialization domain
|
||||
*/
|
||||
class Init extends Part
|
||||
{
|
||||
/** @var string The backup description */
|
||||
private $description = '';
|
||||
|
||||
/** @var string The backup comment */
|
||||
private $comment = '';
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Init
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _prepare abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _prepare()
|
||||
{
|
||||
// Load parameters (description and comment)
|
||||
$jpskey = '';
|
||||
$angiekey = '';
|
||||
|
||||
if (!empty($this->_parametersArray))
|
||||
{
|
||||
$params = $this->_parametersArray;
|
||||
|
||||
if (isset($params['description']))
|
||||
{
|
||||
$this->description = $params['description'];
|
||||
}
|
||||
|
||||
if (isset($params['comment']))
|
||||
{
|
||||
$this->comment = $params['comment'];
|
||||
}
|
||||
|
||||
if (isset($params['jpskey']))
|
||||
{
|
||||
$jpskey = $params['jpskey'];
|
||||
}
|
||||
|
||||
if (isset($params['angiekey']))
|
||||
{
|
||||
$angiekey = $params['angiekey'];
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration -- No. This is already done by the model. Doing it again removes all overrides.
|
||||
// Platform::getInstance()->load_configuration();
|
||||
|
||||
// Initialize counters
|
||||
$registry = Factory::getConfiguration();
|
||||
|
||||
if (!empty($jpskey))
|
||||
{
|
||||
$registry->set('engine.archiver.jps.key', $jpskey);
|
||||
}
|
||||
|
||||
if (!empty($angiekey))
|
||||
{
|
||||
$registry->set('engine.installer.angie.key', $angiekey);
|
||||
}
|
||||
|
||||
// Initialize temporary storage
|
||||
Factory::getFactoryStorage()->reset();
|
||||
|
||||
// Force load the tag -- do not delete!
|
||||
$kettenrad = Factory::getKettenrad();
|
||||
$tag = $kettenrad->getTag(); // Yes, this is an unused variable by we MUST run this method. DO NOT DELETE.
|
||||
|
||||
// Push the comment and description in temp vars for use in the installer phase
|
||||
$registry->set('volatile.core.description', $this->description);
|
||||
$registry->set('volatile.core.comment', $this->comment);
|
||||
|
||||
$this->setState('prepared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _run() abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _run()
|
||||
{
|
||||
if ($this->getState() == 'postrun')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setState('running');
|
||||
}
|
||||
|
||||
// Initialise the extra notes variable, used by platform classes to return warnings and errors
|
||||
$extraNotes = null;
|
||||
|
||||
// Load the version defines
|
||||
Platform::getInstance()->load_version_defines();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
|
||||
// Write log file's header
|
||||
$version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
|
||||
$date = defined('AKEEBABACKUP_DATE') ? AKEEBABACKUP_DATE : AKEEBA_DATE;
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "--------------------------------------------------------------------------------");
|
||||
Factory::getLog()->log(LogLevel::INFO, "Akeeba Backup " . $version . ' (' . $date . ')');
|
||||
Factory::getLog()->log(LogLevel::INFO, "Got backup?");
|
||||
Factory::getLog()->log(LogLevel::INFO, "--------------------------------------------------------------------------------");
|
||||
|
||||
// PHP configuration variables are tried to be logged only for debug and info log levels
|
||||
if ($registry->get('akeeba.basic.log_level') >= 2)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "--- System Information ---");
|
||||
Factory::getLog()->log(LogLevel::INFO, "PHP Version :" . PHP_VERSION);
|
||||
Factory::getLog()->log(LogLevel::INFO, "PHP OS :" . PHP_OS);
|
||||
Factory::getLog()->log(LogLevel::INFO, "PHP SAPI :" . PHP_SAPI);
|
||||
|
||||
if (function_exists('php_uname'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "OS Version :" . php_uname('s'));
|
||||
}
|
||||
|
||||
$db = Factory::getDatabase();
|
||||
Factory::getLog()->log(LogLevel::INFO, "DB Version :" . $db->getVersion());
|
||||
|
||||
if (isset($_SERVER['SERVER_SOFTWARE']))
|
||||
{
|
||||
$server = $_SERVER['SERVER_SOFTWARE'];
|
||||
}
|
||||
elseif (($sf = getenv('SERVER_SOFTWARE')))
|
||||
{
|
||||
$server = $sf;
|
||||
}
|
||||
else
|
||||
{
|
||||
$server = 'n/a';
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "Web Server :" . $server);
|
||||
|
||||
$platform = 'Unknown platform';
|
||||
$version = '(unknown version)';
|
||||
$platformData = Platform::getInstance()->getPlatformVersion();
|
||||
Factory::getLog()->log(LogLevel::INFO, $platformData['name'] . " version :" . $platformData['version']);
|
||||
|
||||
if (isset($_SERVER['HTTP_USER_AGENT']))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "User agent :" . $_SERVER['HTTP_USER_AGENT']);
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "Safe mode :" . ini_get("safe_mode"));
|
||||
Factory::getLog()->log(LogLevel::INFO, "Display errors :" . ini_get("display_errors"));
|
||||
Factory::getLog()->log(LogLevel::INFO, "Error reporting :" . self::error2string());
|
||||
Factory::getLog()->log(LogLevel::INFO, "Error display :" . self::errordisplay());
|
||||
Factory::getLog()->log(LogLevel::INFO, "Disabled functions :" . ini_get("disable_functions"));
|
||||
Factory::getLog()->log(LogLevel::INFO, "open_basedir restr.:" . ini_get('open_basedir'));
|
||||
Factory::getLog()->log(LogLevel::INFO, "Max. exec. time :" . ini_get("max_execution_time"));
|
||||
Factory::getLog()->log(LogLevel::INFO, "Memory limit :" . ini_get("memory_limit"));
|
||||
|
||||
if (function_exists("memory_get_usage"))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "Current mem. usage :" . memory_get_usage());
|
||||
}
|
||||
|
||||
if (function_exists("gzcompress"))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "GZIP Compression : available (good)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "GZIP Compression : n/a (no compression)");
|
||||
}
|
||||
|
||||
$extraNotes = Platform::getInstance()->log_platform_special_directories();
|
||||
|
||||
if (!empty($extraNotes) && is_array($extraNotes))
|
||||
{
|
||||
if (isset($extraNotes['warnings']) && is_array($extraNotes['warnings']))
|
||||
{
|
||||
foreach ($extraNotes['warnings'] as $warning)
|
||||
{
|
||||
$this->setWarning($warning);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($extraNotes['errors']) && is_array($extraNotes['errors']))
|
||||
{
|
||||
foreach ($extraNotes['errors'] as $error)
|
||||
{
|
||||
$this->setError($error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$min_time = $registry->get('akeeba.tuning.min_exec_time');
|
||||
$max_time = $registry->get('akeeba.tuning.max_exec_time');
|
||||
$bias = $registry->get('akeeba.tuning.run_time_bias');
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "Min/Max/Bias :" . $min_time.'/'.$max_time.'/'.$bias);
|
||||
Factory::getLog()->log(LogLevel::INFO, "Output directory :" . $registry->get('akeeba.basic.output_directory'));
|
||||
Factory::getLog()->log(LogLevel::INFO, "Part size (bytes) :" . $registry->get('engine.archiver.common.part_size', 0));
|
||||
Factory::getLog()->log(LogLevel::INFO, "--------------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
// Quirks reporting
|
||||
$quirks = Factory::getConfigurationChecks()->getDetailedStatus(true);
|
||||
|
||||
if (!empty($quirks))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, "Akeeba Backup has detected the following potential problems:");
|
||||
|
||||
foreach ($quirks as $q)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, '- ' . $q['code'] . ' ' . $q['description'] . ' (' . $q['severity'] . ')');
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "You probably do not have to worry about them, but you should be aware of them.");
|
||||
Factory::getLog()->log(LogLevel::INFO, "--------------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
|
||||
if (version_compare($phpVersion, '5.6.0', 'lt'))
|
||||
{
|
||||
$this->setWarning("You are using PHP $phpVersion which is officially End of Life. We recommend using PHP 7.0 or later for best results. Your version of PHP, $phpVersion, will stop being supported by this backup software in the future.");
|
||||
}
|
||||
|
||||
// Report profile ID
|
||||
$profile_id = Platform::getInstance()->get_active_profile();
|
||||
Factory::getLog()->log(LogLevel::INFO, "Loaded profile #$profile_id");
|
||||
|
||||
// Get archive name
|
||||
list($relativeArchiveName, $absoluteArchiveName) = $this->getArchiveName();
|
||||
|
||||
// ==== Stats initialisation ===
|
||||
$origin = Platform::getInstance()->get_backup_origin(); // Get backup origin
|
||||
$profile_id = Platform::getInstance()->get_active_profile(); // Get active profile
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$backupType = $registry->get('akeeba.basic.backup_type');
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Backup type is now set to '" . $backupType . "'");
|
||||
|
||||
// Substitute "variables" in the archive name
|
||||
$fsUtils = Factory::getFilesystemTools();
|
||||
$description = $fsUtils->replace_archive_name_variables($this->description);
|
||||
$comment = $fsUtils->replace_archive_name_variables($this->comment);
|
||||
|
||||
if ($registry->get('volatile.writer.store_on_server', true))
|
||||
{
|
||||
// Archive files are stored on our server
|
||||
$stat_relativeArchiveName = $relativeArchiveName;
|
||||
$stat_absoluteArchiveName = $absoluteArchiveName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Archive files are not stored on our server (FTP backup, cloud backup, sent by email, etc)
|
||||
$stat_relativeArchiveName = '';
|
||||
$stat_absoluteArchiveName = '';
|
||||
}
|
||||
|
||||
$kettenrad = Factory::getKettenrad();
|
||||
|
||||
$temp = array(
|
||||
'description' => $description,
|
||||
'comment' => $comment,
|
||||
'backupstart' => Platform::getInstance()->get_timestamp_database(),
|
||||
'status' => 'run',
|
||||
'origin' => $origin,
|
||||
'type' => $backupType,
|
||||
'profile_id' => $profile_id,
|
||||
'archivename' => $stat_relativeArchiveName,
|
||||
'absolute_path' => $stat_absoluteArchiveName,
|
||||
'multipart' => 0,
|
||||
'filesexist' => 1,
|
||||
'tag' => $kettenrad->getTag(),
|
||||
'backupid' => $kettenrad->getBackupId(),
|
||||
);
|
||||
|
||||
// Save the entry
|
||||
$statistics = Factory::getStatistics();
|
||||
$statistics->setStatistics($temp);
|
||||
|
||||
if ($statistics->getError())
|
||||
{
|
||||
$this->setError($statistics->getError());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$statistics->release_multipart_lock();
|
||||
|
||||
// Initialize the archive.
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('core.createarchive', true))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Expanded archive file name: " . $absoluteArchiveName);
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Initializing archiver engine");
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$archiver->initialize($absoluteArchiveName);
|
||||
$archiver->setComment($comment); // Add the comment to the archive itself.
|
||||
$archiver->propagateToObject($this);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setState('postrun');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the abstract _finalize method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _finalize()
|
||||
{
|
||||
$this->setState('finished');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PHP error to a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function error2string()
|
||||
{
|
||||
if (function_exists('error_reporting'))
|
||||
{
|
||||
$value = error_reporting();
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Not applicable; host too restrictive";
|
||||
}
|
||||
|
||||
$level_names = array(
|
||||
E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING',
|
||||
E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE',
|
||||
E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING',
|
||||
E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING',
|
||||
E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING',
|
||||
E_USER_NOTICE => 'E_USER_NOTICE'
|
||||
);
|
||||
|
||||
if (defined('E_STRICT'))
|
||||
{
|
||||
$level_names[E_STRICT] = 'E_STRICT';
|
||||
}
|
||||
|
||||
$levels = array();
|
||||
|
||||
if (($value & E_ALL) == E_ALL)
|
||||
{
|
||||
$levels[] = 'E_ALL';
|
||||
$value &= ~E_ALL;
|
||||
}
|
||||
|
||||
foreach ($level_names as $level => $name)
|
||||
{
|
||||
if (($value & $level) == $level)
|
||||
{
|
||||
$levels[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' | ', $levels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the error display (output to HTML) is enabled or not
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function errordisplay()
|
||||
{
|
||||
if (!function_exists('ini_get'))
|
||||
{
|
||||
return "Not applicable; host too restrictive";
|
||||
}
|
||||
|
||||
return ini_get('display_errors') ? 'on' : 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative and absolute path to the archive
|
||||
*/
|
||||
protected function getArchiveName()
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
|
||||
// Import volatile scripting keys to the registry
|
||||
Factory::getEngineParamsProvider()->importScriptingToRegistry();
|
||||
|
||||
// Determine the extension
|
||||
$force_extension = Factory::getEngineParamsProvider()->getScriptingParameter('core.forceextension', null);
|
||||
|
||||
if (is_null($force_extension))
|
||||
{
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$extension = $archiver->getExtension();
|
||||
}
|
||||
else
|
||||
{
|
||||
$extension = $force_extension;
|
||||
}
|
||||
|
||||
// Get the template name
|
||||
$templateName = $registry->get('akeeba.basic.archive_name');
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Archive template name: $templateName");
|
||||
|
||||
// Parse all tags
|
||||
$fsUtils = Factory::getFilesystemTools();
|
||||
$templateName = $fsUtils->replace_archive_name_variables($templateName);
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Expanded template name: $templateName");
|
||||
|
||||
$ds = DIRECTORY_SEPARATOR;
|
||||
$relative_path = $templateName . $extension;
|
||||
$absolute_path = $fsUtils->TranslateWinPath($registry->get('akeeba.basic.output_directory') . $ds . $relative_path);
|
||||
|
||||
return array($relative_path, $absolute_path);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Installer deployment
|
||||
*/
|
||||
class Installer extends Part
|
||||
{
|
||||
|
||||
/** @var int Installer image file offset last read */
|
||||
private $offset;
|
||||
|
||||
/** @var int How much installer data I have processed yet */
|
||||
private $runningSize = 0;
|
||||
|
||||
/** @var int Installer image file index last read */
|
||||
private $xformIndex = 0;
|
||||
|
||||
/** @var int Percentage of process done */
|
||||
private $progress = 0;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @return Installer
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _prepare abstract method
|
||||
*
|
||||
*/
|
||||
function _prepare()
|
||||
{
|
||||
$archive = Factory::getArchiverEngine();
|
||||
|
||||
// Add the backup description and comment in a README.html file in the
|
||||
// installation directory. This makes it the first file in the archive.
|
||||
if ($this->installerSettings->readme)
|
||||
{
|
||||
$data = $this->createReadme();
|
||||
$archive->addVirtualFile('README.html', $this->installerSettings->installerroot, $data);
|
||||
}
|
||||
|
||||
if ($this->installerSettings->extrainfo)
|
||||
{
|
||||
$data = $this->createExtrainfo();
|
||||
$archive->addVirtualFile('extrainfo.ini', $this->installerSettings->installerroot, $data);
|
||||
}
|
||||
|
||||
if ($this->installerSettings->password)
|
||||
{
|
||||
$data = $this->createPasswordFile();
|
||||
|
||||
if (!empty($data))
|
||||
{
|
||||
$archive->addVirtualFile('password.php', $this->installerSettings->installerroot, $data);
|
||||
}
|
||||
}
|
||||
|
||||
$this->progress = 0;
|
||||
|
||||
// Set our state to prepared
|
||||
$this->setState('prepared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _run() abstract method
|
||||
*/
|
||||
function _run()
|
||||
{
|
||||
if ($this->getState() == 'postrun')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setState('running');
|
||||
}
|
||||
|
||||
// Try to step the archiver
|
||||
$archive = Factory::getArchiverEngine();
|
||||
$ret = $archive->transformJPA($this->xformIndex, $this->offset);
|
||||
|
||||
// Error propagation
|
||||
$this->propagateFromObject($archive);
|
||||
|
||||
if (($ret !== false) && ($archive->getError() == ''))
|
||||
{
|
||||
$this->offset = $ret['offset'];
|
||||
$this->xformIndex = $ret['index'];
|
||||
$this->setStep($ret['filename']);
|
||||
}
|
||||
|
||||
// Check for completion
|
||||
if ($ret['done'])
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . ":: archive is initialized");
|
||||
$this->setState('finished');
|
||||
}
|
||||
|
||||
// Calculate percentage
|
||||
$this->runningSize += $ret['chunkProcessed'];
|
||||
|
||||
if ($ret['filesize'] > 0)
|
||||
{
|
||||
$this->progress = $this->runningSize / $ret['filesize'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _finalize() abstract method
|
||||
*
|
||||
*/
|
||||
function _finalize()
|
||||
{
|
||||
$this->setState('finished');
|
||||
$this->progress = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the contents of an HTML file with the description and comment of
|
||||
* the backup. This file will be saved as README.html in the installer's root
|
||||
* directory, as specified by the embedded installer's settings.
|
||||
*
|
||||
* @return string The contents of the HTML file.
|
||||
*/
|
||||
protected function createReadme()
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
|
||||
$version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
|
||||
$date = defined('AKEEBABACKUP_DATE') ? AKEEBABACKUP_DATE : AKEEBA_DATE;
|
||||
$pro = defined('AKEEBABACKUP_PRO') ? AKEEBABACKUP_PRO : AKEEBA_PRO;
|
||||
|
||||
$lbl_version = $version . ' (' . $date . ')';
|
||||
$lbl_coreorpro = ($pro == 1) ? 'Professional' : 'Core';
|
||||
|
||||
$description = $config->get('volatile.core.description', '');
|
||||
$comment = $config->get('volatile.core.comment', '');
|
||||
|
||||
$config->set('volatile.core.description', null);
|
||||
$config->set('volatile.core.comment', null);
|
||||
|
||||
return <<<ENDHTML
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>Akeeba Backup Archive Identity</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Backup Description</h1>
|
||||
<p id="description"><![CDATA[$description]]></p>
|
||||
<h1>Backup Comment</h1>
|
||||
<div id="comment">
|
||||
$comment
|
||||
</div>
|
||||
<hr/>
|
||||
<p>
|
||||
Akeeba Backup $lbl_coreorpro $lbl_version
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
ENDHTML;
|
||||
}
|
||||
|
||||
protected function createExtrainfo()
|
||||
{
|
||||
$abversion = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
|
||||
$host = Platform::getInstance()->get_host();
|
||||
$backupdate = gmdate('Y-m-d H:i:s');
|
||||
$phpversion = PHP_VERSION;
|
||||
$rootPath = Platform::getInstance()->get_site_root();
|
||||
$ret = <<<ENDINI
|
||||
; Akeeba Backup $abversion - Extra information used during restoration
|
||||
host="$host"
|
||||
backup_date="$backupdate"
|
||||
akeeba_version="$abversion"
|
||||
php_version="$phpversion"
|
||||
root="$rootPath"
|
||||
ENDINI;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
protected function createPasswordFile()
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
$ret = '';
|
||||
|
||||
$password = $config->get('engine.installer.angie.key', '');
|
||||
|
||||
if (empty($password))
|
||||
{
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$randVal = Factory::getRandval();
|
||||
|
||||
$salt = $randVal->generateString(32);
|
||||
$passhash = md5($password . $salt) . ':' . $salt;
|
||||
$ret = "<?php\n";
|
||||
$ret .= "define('AKEEBA_PASSHASH', '" . $passhash . "');\n";
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements the progress calculation based on how much of the installer image
|
||||
* archive we have processed so far.
|
||||
*/
|
||||
public function getProgress()
|
||||
{
|
||||
return $this->progress;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,436 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Filter\Base as FilterBase;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Akeeba filtering feature
|
||||
*/
|
||||
class Filters extends BaseObject
|
||||
{
|
||||
/** @var array An array holding data for all defined filters */
|
||||
private $filter_registry = array();
|
||||
|
||||
/** @var array Hash array with instances of all filters as $filter_name => filter_object */
|
||||
private $filters = array();
|
||||
|
||||
/** @var bool True after the filter clean up has run */
|
||||
private $cleanup_has_run = false;
|
||||
|
||||
/**
|
||||
* Public constructor, loads filter data and filter classes
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Load filter data from platform's database
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Fetching filter data from database');
|
||||
$this->filter_registry = Platform::getInstance()->load_filters();
|
||||
|
||||
// Load platform, plugin and core filters
|
||||
$this->filters = array();
|
||||
|
||||
$locations = array(
|
||||
Factory::getAkeebaRoot() . '/Filter'
|
||||
);
|
||||
|
||||
$platform_paths = Platform::getInstance()->getPlatformDirectories();
|
||||
|
||||
foreach ($platform_paths as $p)
|
||||
{
|
||||
$locations[] = $p . '/Filter';
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Loading filters');
|
||||
|
||||
foreach ($locations as $folder)
|
||||
{
|
||||
if (!@is_dir($folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!@is_readable($folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$di = new \DirectoryIterator($folder);
|
||||
|
||||
foreach ($di as $file)
|
||||
{
|
||||
if (!$file->isFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// PHP 5.3.5 and earlier do not support getExtension
|
||||
//if ($file->getExtension() != 'php')
|
||||
if (substr($file->getBasename(), -4) != '.php')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// Skip filter files starting with dot or dash
|
||||
if (in_array(substr($filename, 0, 1), array('.', '_')))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some hosts copy .ini and .php files, renaming them (ie foobar.1.php)
|
||||
// We need to exclude them, otherwise we'll get a fatal error for declaring the same class twice
|
||||
$bare_name = $file->getBasename('.php');
|
||||
|
||||
if (preg_match('/[^a-zA-Z0-9]/', $bare_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract filter base name
|
||||
$filter_name = ucfirst($bare_name);
|
||||
|
||||
// This is an abstract class; do not try to create instance
|
||||
if ($filter_name == 'Base')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip already loaded filters
|
||||
if (array_key_exists($filter_name, $this->filters))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, '-- Loading filter ' . $filter_name);
|
||||
|
||||
// Add the filter
|
||||
$this->filters[$filter_name] = Factory::getFilterObject($filter_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Load platform, plugin and core stacked filters
|
||||
$locations = array(
|
||||
Factory::getAkeebaRoot() . '/Filter/Stack'
|
||||
);
|
||||
|
||||
$platform_paths = Platform::getInstance()->getPlatformDirectories();
|
||||
$platform_stack_paths = array();
|
||||
|
||||
foreach ($platform_paths as $p)
|
||||
{
|
||||
$locations[] = $p . '/Filter';
|
||||
$locations[] = $p . '/Filter/Stack';
|
||||
$platform_stack_paths[] = $p . '/Filter/Stack';
|
||||
}
|
||||
|
||||
$config = Factory::getConfiguration();
|
||||
Factory::getLog()->log(LogLevel::DEBUG, 'Loading optional filters');
|
||||
|
||||
foreach ($locations as $folder)
|
||||
{
|
||||
if (!@is_dir($folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!@is_readable($folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$di = new \DirectoryIterator($folder);
|
||||
|
||||
/** @var \DirectoryIterator $file */
|
||||
foreach ($di as $file)
|
||||
{
|
||||
if (!$file->isFile())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// PHP 5.3.5 and earlier do not support getExtension
|
||||
// if ($file->getExtension() != 'php')
|
||||
if (substr($file->getBasename(), -4) != '.php')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some hosts copy .ini and .php files, renaming them (ie foobar.1.php)
|
||||
// We need to exclude them, otherwise we'll get a fatal error for declaring the same class twice
|
||||
$bare_name = strtolower($file->getBasename('.php'));
|
||||
|
||||
if (preg_match('/[^A-Za-z0-9]/', $bare_name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract filter base name
|
||||
if (substr($bare_name, 0, 5) == 'stack')
|
||||
{
|
||||
$bare_name = substr($bare_name, 5);
|
||||
}
|
||||
|
||||
$filter_name = 'Stack\\Stack' . ucfirst($bare_name);
|
||||
|
||||
// Skip already loaded filters
|
||||
if (array_key_exists($filter_name, $this->filters))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the INI file also exists
|
||||
if ( !file_exists($folder . '/' . $bare_name . '.ini'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = "core.filters.$bare_name.enabled";
|
||||
|
||||
if ($config->get($key, 0))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, '-- Loading optional filter ' . $filter_name);
|
||||
// Add the filter
|
||||
$this->filters[$filter_name] = Factory::getFilterObject($filter_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended filtering information of a given object. Applies only to exclusion filters.
|
||||
*
|
||||
* @param string $test The string to check for filter status (e.g. filename, dir name, table name, etc)
|
||||
* @param string $root The exclusion root test belongs to
|
||||
* @param string $object What type of object is it? dir|file|dbobject
|
||||
* @param string $subtype Filter subtype (all|content|children)
|
||||
* @param string $by_filter [out] The filter name which first matched $test, or an empty string
|
||||
*
|
||||
* @return bool True if it is a filtered element
|
||||
*/
|
||||
public function isFilteredExtended($test, $root, $object, $subtype, &$by_filter)
|
||||
{
|
||||
if ( !$this->cleanup_has_run)
|
||||
{
|
||||
// Loop the filters and clean up those with no data
|
||||
/**
|
||||
* @var string $filter_name
|
||||
* @var FilterBase $filter
|
||||
*/
|
||||
foreach ($this->filters as $filter_name => $filter)
|
||||
{
|
||||
if ( !$filter->hasFilters())
|
||||
{
|
||||
unset($this->filters[$filter_name]);
|
||||
} // Remove empty filters
|
||||
}
|
||||
$this->cleanup_has_run = true;
|
||||
}
|
||||
|
||||
$by_filter = '';
|
||||
if ( !empty($this->filters))
|
||||
{
|
||||
foreach ($this->filters as $filter_name => $filter)
|
||||
{
|
||||
if ($filter->isFiltered($test, $root, $object, $subtype))
|
||||
{
|
||||
$by_filter = strtolower($filter_name);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are still here, no filter matched
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filtering status of a given object
|
||||
*
|
||||
* @param string $test The string to check for filter status (e.g. filename, dir name, table name, etc)
|
||||
* @param string $root The exclusion root test belongs to
|
||||
* @param string $object What type of object is it? dir|file|dbobject
|
||||
* @param string $subtype Filter subtype (all|content|children)
|
||||
*
|
||||
* @return bool True if it is a filtered element
|
||||
*/
|
||||
public function isFiltered($test, $root, $object, $subtype)
|
||||
{
|
||||
$by_filter = '';
|
||||
|
||||
return $this->isFilteredExtended($test, $root, $object, $subtype, $by_filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inclusion filters for a specific object type
|
||||
*
|
||||
* @param string $object The inclusion object (dir|db)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function &getInclusions($object)
|
||||
{
|
||||
$inclusions = array();
|
||||
|
||||
if ( !empty($this->filters))
|
||||
{
|
||||
/**
|
||||
* @var string $filter_name
|
||||
* @var FilterBase $filter
|
||||
*/
|
||||
foreach ($this->filters as $filter_name => $filter)
|
||||
{
|
||||
if (!is_object($filter))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::ERROR, "Object for filter $filter_name not found. The engine will now crash.");
|
||||
}
|
||||
|
||||
$new_inclusions = $filter->getInclusions($object);
|
||||
|
||||
if ( !empty($new_inclusions))
|
||||
{
|
||||
$inclusions = array_merge($inclusions, $new_inclusions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $inclusions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter registry information for a specified filter class
|
||||
*
|
||||
* @param string $filter_name The name of the filter we want data for
|
||||
*
|
||||
* @return array The filter data for the requested filter
|
||||
*/
|
||||
public function &getFilterData($filter_name)
|
||||
{
|
||||
if (array_key_exists($filter_name, $this->filter_registry))
|
||||
{
|
||||
return $this->filter_registry[$filter_name];
|
||||
}
|
||||
else
|
||||
{
|
||||
$dummy = array();
|
||||
|
||||
return $dummy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the filter data of a specific filter with the new data
|
||||
*
|
||||
* @param string $filter_name The filter for which to modify the stored data
|
||||
* @param string $data The new data
|
||||
*/
|
||||
public function setFilterData($filter_name, &$data)
|
||||
{
|
||||
$this->filter_registry[$filter_name] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves all filters to the platform defined database
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
return Platform::getInstance()->save_filters($this->filter_registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SQL statements to append to the database backup file
|
||||
*
|
||||
* @param string $root
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function &getExtraSQL($root)
|
||||
{
|
||||
$ret = "";
|
||||
if (count($this->filters) >= 1)
|
||||
{
|
||||
/**
|
||||
* @var string $filter_name
|
||||
* @var FilterBase $filter
|
||||
*/
|
||||
foreach ($this->filters as $filter_name => $filter)
|
||||
{
|
||||
$extra_sql = $filter->getExtraSQL($root);
|
||||
if ( !empty($extra_sql))
|
||||
{
|
||||
if ( !empty($ret))
|
||||
{
|
||||
$ret .= "\n";
|
||||
}
|
||||
$ret .= $extra_sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is an active filter for the object/subtype requested.
|
||||
*
|
||||
* @param string $object The filtering object: dir|file|dbobject|db
|
||||
* @param string $subtype The filtering subtype: all|content|children|inclusion
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFilterType($object, $subtype = null)
|
||||
{
|
||||
foreach ($this->filters as $filter_name => $filter)
|
||||
{
|
||||
if ($filter->object == $object)
|
||||
{
|
||||
if (is_null($subtype))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
elseif ($filter->subtype == $subtype)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all filters, reverting them to a blank state
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->filter_registry = array();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,619 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* This is Akeeba Engine's heart. Kettenrad is reponsible for launching the
|
||||
* domain chain of a backup job.
|
||||
*/
|
||||
class Kettenrad extends Part
|
||||
{
|
||||
/** @var bool Set to true when deadOnTimeout is registered as a shutdown function */
|
||||
public static $registeredShutdownCallback = false;
|
||||
|
||||
/** @var bool Set to true when akeebaBackupErrorHandler is registered as an error handler */
|
||||
public static $registeredErrorHandler = false;
|
||||
|
||||
/** @var array Cached copy of the response array */
|
||||
private $array_cache = null;
|
||||
|
||||
/** @var array The list of remaining steps */
|
||||
private $domain_chain = array();
|
||||
|
||||
/** @var string The current domain's name */
|
||||
private $domain = '';
|
||||
|
||||
/**@ var string The active domain's class name */
|
||||
private $class = '';
|
||||
|
||||
/** @var string The current backup's tag (actually: the backup's origin) */
|
||||
private $tag = null;
|
||||
|
||||
/** @var int How many steps the domain_chain array contained when the backup began. Used for percentage calculations. */
|
||||
private $total_steps = 0;
|
||||
|
||||
/** @var string A unique backup ID which allows us to run multiple parallel backups using the same backup origin (tag) */
|
||||
private $backup_id = '';
|
||||
|
||||
/**
|
||||
* Set to true when there are warnings available when getStatusArray() is called. This is used at the end of the
|
||||
* backup to send a different push message depending on whether the backup completed with or without warnings.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $warnings_issued = false;
|
||||
|
||||
/**
|
||||
* Returns the unique Backup ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackupId()
|
||||
{
|
||||
return $this->backup_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unique backup ID.
|
||||
*
|
||||
* @param string $backup_id
|
||||
*/
|
||||
public function setBackupId($backup_id = null)
|
||||
{
|
||||
$this->backup_id = $backup_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current backup tag. If none is specified, it sets it to be the
|
||||
* same as the current backup origin and returns the new setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTag()
|
||||
{
|
||||
if (empty($this->tag))
|
||||
{
|
||||
// If no tag exists, we resort to the pre-set backup origin
|
||||
$tag = Platform::getInstance()->get_backup_origin();
|
||||
$this->tag = $tag;
|
||||
}
|
||||
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
protected function _prepare()
|
||||
{
|
||||
// Intialize the timer class
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Do we have a tag?
|
||||
if (!empty($this->_parametersArray['tag']))
|
||||
{
|
||||
$this->tag = $this->_parametersArray['tag'];
|
||||
}
|
||||
|
||||
// Make sure a tag exists (or create a new one)
|
||||
$this->tag = $this->getTag();
|
||||
|
||||
// Reset the log
|
||||
$logTag = $this->getLogTag();
|
||||
Factory::getLog()->open($logTag);
|
||||
Factory::getLog()->reset($logTag);
|
||||
|
||||
if (!static::$registeredErrorHandler)
|
||||
{
|
||||
static::$registeredErrorHandler = true;
|
||||
set_error_handler('\\Akeeba\\Engine\\Core\\akeebaBackupErrorHandler');
|
||||
}
|
||||
|
||||
// Reset the storage
|
||||
$factoryStorageTag = $this->tag . (empty($this->backup_id) ? '' : ('.' . $this->backup_id));
|
||||
Factory::getFactoryStorage()->reset($factoryStorageTag);
|
||||
|
||||
// Apply the configuration overrides
|
||||
$overrides = Platform::getInstance()->configOverrides;
|
||||
|
||||
if (is_array($overrides) && @count($overrides))
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$protected_keys = $registry->getProtectedKeys();
|
||||
$registry->resetProtectedKeys();
|
||||
|
||||
foreach ($overrides as $k => $v)
|
||||
{
|
||||
$registry->set($k, $v);
|
||||
}
|
||||
|
||||
$registry->setProtectedKeys($protected_keys);
|
||||
}
|
||||
|
||||
// Get the domain chain
|
||||
$this->domain_chain = Factory::getEngineParamsProvider()->getDomainChain();
|
||||
$this->total_steps = count($this->domain_chain) - 1; // Init shouldn't count in the progress bar
|
||||
|
||||
// Mark this engine for Nesting Logging
|
||||
$this->nest_logging = true;
|
||||
|
||||
// Preparation is over
|
||||
$this->array_cache = null;
|
||||
$this->setState('prepared');
|
||||
|
||||
// Send a push message to mark the start of backup
|
||||
$platform = Platform::getInstance();
|
||||
$timeStamp = date($platform->translate('DATE_FORMAT_LC2'));
|
||||
$pushSubject = sprintf($platform->translate('COM_AKEEBA_PUSH_STARTBACKUP_SUBJECT'), $platform->get_site_name(), $platform->get_host());
|
||||
$pushDetails = sprintf($platform->translate('COM_AKEEBA_PUSH_STARTBACKUP_BODY'), $platform->get_site_name(), $platform->get_host(), $timeStamp, $this->getLogTag());
|
||||
Factory::getPush()->message($pushSubject, $pushDetails);
|
||||
|
||||
//restore_error_handler();
|
||||
}
|
||||
|
||||
protected function _run()
|
||||
{
|
||||
$logTag = $this->getLogTag();
|
||||
$logger = Factory::getLog();
|
||||
$logger->open($logTag);
|
||||
|
||||
if (!static::$registeredErrorHandler)
|
||||
{
|
||||
static::$registeredErrorHandler = true;
|
||||
set_error_handler('\\Akeeba\\Engine\\Core\\akeebaBackupErrorHandler');
|
||||
}
|
||||
|
||||
// Maybe we're already done or in an error state?
|
||||
if (($this->getError()) || ($this->getState() == 'postrun'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set running state
|
||||
$this->setState('running');
|
||||
|
||||
// Initialize operation counter
|
||||
$registry = Factory::getConfiguration();
|
||||
$registry->set('volatile.operation_counter', 0);
|
||||
|
||||
// Advance step counter
|
||||
$stepCounter = $registry->get('volatile.step_counter', 0);
|
||||
$registry->set('volatile.step_counter', ++$stepCounter);
|
||||
|
||||
// Log step start number
|
||||
$logger->log(LogLevel::DEBUG, '====== Starting Step number ' . $stepCounter . ' ======');
|
||||
|
||||
if (defined('AKEEBADEBUG'))
|
||||
{
|
||||
$root = Platform::getInstance()->get_site_root();
|
||||
$logger->log(LogLevel::DEBUG, 'Site root: ' . $root);
|
||||
}
|
||||
|
||||
$timer = Factory::getTimer();
|
||||
$finished = false;
|
||||
$error = false;
|
||||
$breakFlag = false; // BREAKFLAG is optionally passed by domains to force-break current operation
|
||||
|
||||
// Apply an infinite time limit if required
|
||||
if ($registry->get('akeeba.tuning.settimelimit', 0))
|
||||
{
|
||||
if (function_exists('set_time_limit'))
|
||||
{
|
||||
set_time_limit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop until time's up, we're done or an error occurred, or BREAKFLAG is set
|
||||
$this->array_cache = null;
|
||||
while (($timer->getTimeLeft() > 0) && (!$finished) && (!$error) && (!$breakFlag))
|
||||
{
|
||||
// Reset the break flag
|
||||
$registry->set('volatile.breakflag', false);
|
||||
|
||||
// Do we have to switch domains? This only happens if there is no active
|
||||
// domain, or the current domain has finished
|
||||
$have_to_switch = false;
|
||||
$object = null;
|
||||
|
||||
if ($this->class == '')
|
||||
{
|
||||
$have_to_switch = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$object = Factory::getDomainObject($this->class);
|
||||
|
||||
if (!is_object($object))
|
||||
{
|
||||
$have_to_switch = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!in_array('getState', get_class_methods($object)))
|
||||
{
|
||||
$have_to_switch = true;
|
||||
}
|
||||
elseif ($object->getState() == 'finished')
|
||||
{
|
||||
$have_to_switch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Switch domain if necessary
|
||||
if ($have_to_switch)
|
||||
{
|
||||
$logger->debug('Kettenrad :: Switching domains');
|
||||
|
||||
if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.domains', 0))
|
||||
{
|
||||
$logger->log(LogLevel::DEBUG, "Kettenrad :: BREAKING STEP BEFORE SWITCHING DOMAIN");
|
||||
$registry->set('volatile.breakflag', true);
|
||||
}
|
||||
|
||||
// Free last domain
|
||||
$object = null;
|
||||
|
||||
if (empty($this->domain_chain))
|
||||
{
|
||||
// Aw, we're done! No more domains to run.
|
||||
$this->setState('postrun');
|
||||
$logger->log(LogLevel::DEBUG, "Kettenrad :: No more domains to process");
|
||||
$logger->log(LogLevel::DEBUG, '====== Finished Step number ' . $stepCounter . ' ======');
|
||||
$this->array_cache = null;
|
||||
|
||||
//restore_error_handler();
|
||||
return;
|
||||
}
|
||||
|
||||
// Shift the next definition off the stack
|
||||
$this->array_cache = null;
|
||||
$new_definition = array_shift($this->domain_chain);
|
||||
|
||||
if (array_key_exists('class', $new_definition))
|
||||
{
|
||||
$logger->debug("Switching to domain {$new_definition['domain']}, class {$new_definition['class']}");
|
||||
$this->domain = $new_definition['domain'];
|
||||
$this->class = $new_definition['class'];
|
||||
// Get a working object
|
||||
$object = Factory::getDomainObject($this->class);
|
||||
$object->setup($this->_parametersArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
$logger->log(LogLevel::WARNING, "Kettenrad :: No class defined trying to switch domains. The backup will crash.");
|
||||
$this->domain = null;
|
||||
$this->class = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_object($object))
|
||||
{
|
||||
$logger->debug("Kettenrad :: Getting domain object of class {$this->class}");
|
||||
$object = Factory::getDomainObject($this->class);
|
||||
}
|
||||
}
|
||||
|
||||
// Tick the object
|
||||
$logger->debug('Kettenrad :: Ticking the domain object');
|
||||
$result = $object->tick();
|
||||
|
||||
// Propagate errors
|
||||
$logger->debug('Kettenrad :: Domain object returned; propagating');
|
||||
$this->propagateFromObject($object);
|
||||
|
||||
// Advance operation counter
|
||||
$currentOperationNumber = $registry->get('volatile.operation_counter', 0);
|
||||
$currentOperationNumber++;
|
||||
$registry->set('volatile.operation_counter', $currentOperationNumber);
|
||||
|
||||
// Process return array
|
||||
$this->setDomain($this->domain);
|
||||
$this->setStep($result['Step']);
|
||||
$this->setSubstep($result['Substep']);
|
||||
|
||||
// Check for BREAKFLAG
|
||||
$breakFlag = $registry->get('volatile.breakflag', false);
|
||||
$logger->debug("Kettenrad :: Break flag status: " . ($breakFlag ? 'YES' : 'no'));
|
||||
|
||||
// Process errors
|
||||
$error = false;
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// Check if the backup procedure should finish now
|
||||
$finished = $error ? true : !($result['HasRun']);
|
||||
|
||||
// Log operation end
|
||||
$logger->log(LogLevel::DEBUG, '----- Finished operation ' . $currentOperationNumber . ' ------');
|
||||
}
|
||||
|
||||
// Log the result
|
||||
if (!$error)
|
||||
{
|
||||
$logger->log(LogLevel::DEBUG, "Successful Smart algorithm on " . get_class($object));
|
||||
}
|
||||
else
|
||||
{
|
||||
$logger->log(LogLevel::ERROR, "Failed Smart algorithm on " . get_class($object));
|
||||
}
|
||||
|
||||
// Log if we have to do more work or not
|
||||
if (!is_object($object))
|
||||
{
|
||||
$logger->log(LogLevel::WARNING, "Kettenrad :: Empty object found when processing domain '" . $this->domain . "'. This should never happen.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($object->getState() == 'running')
|
||||
{
|
||||
$logger->log(LogLevel::DEBUG, "Kettenrad :: More work required in domain '" . $this->domain . "'");
|
||||
// We need to set the break flag for the part processing to not batch successive steps
|
||||
$registry->set('volatile.breakflag', true);
|
||||
}
|
||||
elseif ($object->getState() == 'finished')
|
||||
{
|
||||
$logger->log(LogLevel::DEBUG, "Kettenrad :: Domain '" . $this->domain . "' has finished.");
|
||||
$registry->set('volatile.breakflag', false);
|
||||
}
|
||||
}
|
||||
|
||||
// Log step end
|
||||
$logger->log(LogLevel::DEBUG, '====== Finished Step number ' . $stepCounter . ' ======');
|
||||
|
||||
if (!$registry->get('akeeba.tuning.nobreak.domains', 0))
|
||||
{
|
||||
// Force break between steps
|
||||
$logger->debug('Kettenrad :: Setting the break flag between domains');
|
||||
$registry->set('volatile.breakflag', true);
|
||||
}
|
||||
//restore_error_handler();
|
||||
}
|
||||
|
||||
protected function _finalize()
|
||||
{
|
||||
// Open the log
|
||||
$logTag = $this->getLogTag();
|
||||
Factory::getLog()->open($logTag);
|
||||
|
||||
if (!static::$registeredErrorHandler)
|
||||
{
|
||||
static::$registeredErrorHandler = true;
|
||||
set_error_handler('\\Akeeba\\Engine\\Core\\akeebaBackupErrorHandler');
|
||||
}
|
||||
|
||||
// Kill the cached array
|
||||
$this->array_cache = null;
|
||||
|
||||
// Remove the memory file
|
||||
$tempVarsTag = $this->tag . (empty($this->backup_id) ? '' : ('.' . $this->backup_id));
|
||||
Factory::getFactoryStorage()->reset($tempVarsTag);
|
||||
|
||||
// All done.
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Kettenrad :: Just finished");
|
||||
$this->setState('finished');
|
||||
|
||||
// Send a push message to mark the end of backup
|
||||
$pushSubjectKey = $this->warnings_issued ? 'COM_AKEEBA_PUSH_ENDBACKUP_WARNINGS_SUBJECT' : 'COM_AKEEBA_PUSH_ENDBACKUP_SUCCESS_SUBJECT';
|
||||
$pushBodyKey = $this->warnings_issued ? 'COM_AKEEBA_PUSH_ENDBACKUP_WARNINGS_BODY' : 'COM_AKEEBA_PUSH_ENDBACKUP_SUCCESS_BODY';
|
||||
$platform = Platform::getInstance();
|
||||
$timeStamp = date($platform->translate('DATE_FORMAT_LC2'));
|
||||
$pushSubject = sprintf($platform->translate($pushSubjectKey), $platform->get_site_name(), $platform->get_host());
|
||||
$pushDetails = sprintf($platform->translate($pushBodyKey), $platform->get_site_name(), $platform->get_host(), $timeStamp);
|
||||
Factory::getPush()->message($pushSubject, $pushDetails);
|
||||
|
||||
//restore_error_handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the class's status array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStatusArray()
|
||||
{
|
||||
if (empty($this->array_cache))
|
||||
{
|
||||
// Get the default table
|
||||
$array = $this->_makeReturnTable();
|
||||
|
||||
// Did we have warnings?
|
||||
$warnings = $this->getWarnings();
|
||||
|
||||
if (count($warnings))
|
||||
{
|
||||
$this->warnings_issued = true;
|
||||
}
|
||||
|
||||
// Get the current step number
|
||||
$stepCounter = Factory::getConfiguration()->get('volatile.step_counter', 0);
|
||||
|
||||
// Add the archive name
|
||||
$statistics = Factory::getStatistics();
|
||||
$record = $statistics->getRecord();
|
||||
$array['Archive'] = isset($record['archivename']) ? $record['archivename'] : '';
|
||||
|
||||
// Translate HasRun to what the rest of the suite expects
|
||||
$array['HasRun'] = ($this->getState() == 'finished') ? 1 : 0;
|
||||
|
||||
// Translate no errors
|
||||
$array['Error'] = ($array['Error'] == false) ? '' : $array['Error'];
|
||||
|
||||
$array['tag'] = $this->tag;
|
||||
$array['Progress'] = $this->getProgress();
|
||||
$array['backupid'] = $this->getBackupId();
|
||||
$array['sleepTime'] = $this->waitTimeMsec;
|
||||
$array['stepNumber'] = $stepCounter;
|
||||
$array['stepState'] = $this->getState();
|
||||
|
||||
$this->array_cache = $array;
|
||||
}
|
||||
|
||||
return $this->array_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the percentage of the backup process done so far.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProgress()
|
||||
{
|
||||
// Get the overall percentage (based on domains complete so far)
|
||||
$remaining_steps = count($this->domain_chain);
|
||||
$remaining_steps++;
|
||||
$overall = 1 - ($remaining_steps / $this->total_steps);
|
||||
|
||||
// How much is this step worth?
|
||||
$this_max = 1 / $this->total_steps;
|
||||
|
||||
// Get the percentage done of the current object
|
||||
if (!empty($this->class))
|
||||
{
|
||||
$object = Factory::getDomainObject($this->class);
|
||||
}
|
||||
else
|
||||
{
|
||||
$object = null;
|
||||
}
|
||||
|
||||
if (!is_object($object))
|
||||
{
|
||||
$local = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
$local = $object->getProgress();
|
||||
}
|
||||
|
||||
$percentage = (int)(100 * ($overall + $local * $this_max));
|
||||
|
||||
if ($percentage < 0)
|
||||
{
|
||||
$percentage = 0;
|
||||
}
|
||||
elseif ($percentage > 100)
|
||||
{
|
||||
$percentage = 100;
|
||||
}
|
||||
|
||||
return $percentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag used to open the correct log file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getLogTag()
|
||||
{
|
||||
$tag = $this->getTag();
|
||||
|
||||
if (!empty($this->backup_id))
|
||||
{
|
||||
$tag .= '.' . $this->backup_id;
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout error handler
|
||||
*/
|
||||
function deadOnTimeOut()
|
||||
{
|
||||
if (connection_status() == 1)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::ERROR, 'The process was aborted on user\'s request');
|
||||
}
|
||||
elseif (connection_status() >= 2)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::ERROR, 'Akeeba Backup has timed out. Please read the documentation.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!Kettenrad::$registeredShutdownCallback)
|
||||
{
|
||||
Kettenrad::$registeredShutdownCallback = true;
|
||||
register_shutdown_function("\\Akeeba\\Engine\\Core\\deadOnTimeOut");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nifty trick to track and log PHP errors to Akeeba Backup's log
|
||||
*
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
function akeebaBackupErrorHandler($errno, $errstr, $errfile, $errline)
|
||||
{
|
||||
// Sanity check
|
||||
if (!function_exists('error_reporting'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not proceed if the error springs from an @function() construct, or if
|
||||
// the overall error reporting level is set to report no errors.
|
||||
$error_reporting = error_reporting();
|
||||
|
||||
if ($error_reporting == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($errno)
|
||||
{
|
||||
|
||||
case E_ERROR:
|
||||
case E_USER_ERROR:
|
||||
// Can I really catch fatal errors? It doesn't seem likely...
|
||||
Factory::getLog()->log(LogLevel::ERROR, "PHP FATAL ERROR on line $errline in file $errfile:");
|
||||
Factory::getLog()->log(LogLevel::ERROR, $errstr);
|
||||
Factory::getLog()->log(LogLevel::ERROR, "Execution aborted due to PHP fatal error");
|
||||
break;
|
||||
|
||||
case E_WARNING:
|
||||
case E_USER_WARNING:
|
||||
// Log as debug messages so that we don't spook the user with warnings
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "PHP WARNING (not an error; you can ignore) on line $errline in file $errfile:");
|
||||
Factory::getLog()->log(LogLevel::DEBUG, $errstr);
|
||||
break;
|
||||
|
||||
case E_NOTICE:
|
||||
case E_USER_NOTICE:
|
||||
// Log as debug messages so that we don't spook the user with notices
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "PHP NOTICE (not an error; you can ignore) on line $errline in file $errfile:");
|
||||
Factory::getLog()->log(LogLevel::DEBUG, $errstr);
|
||||
break;
|
||||
|
||||
default:
|
||||
// These are E_DEPRECATED, E_STRICT etc. Ignore that.
|
||||
break;
|
||||
}
|
||||
|
||||
// Uncomment to prevent the execution of PHP's internal error handler
|
||||
//return true;
|
||||
}
|
||||
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Timer class
|
||||
*/
|
||||
class Timer extends BaseObject
|
||||
{
|
||||
|
||||
/** @var int Maximum execution time allowance per step */
|
||||
private $max_exec_time = null;
|
||||
|
||||
/** @var int Timestamp of execution start */
|
||||
private $start_time = null;
|
||||
|
||||
/**
|
||||
* Public constructor, creates the timer object and calculates the execution time limits
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Initialize start time
|
||||
$this->start_time = $this->microtime_float();
|
||||
|
||||
// Get configured max time per step and bias
|
||||
$configuration = Factory::getConfiguration();
|
||||
$config_max_exec_time = $configuration->get('akeeba.tuning.max_exec_time', 14);
|
||||
$bias = $configuration->get('akeeba.tuning.run_time_bias', 75) / 100;
|
||||
|
||||
// Get PHP's maximum execution time (our upper limit)
|
||||
/**
|
||||
* if(@function_exists('ini_get'))
|
||||
* {
|
||||
* $php_max_exec_time = @ini_get("maximum_execution_time");
|
||||
* if ( (!is_numeric($php_max_exec_time)) || ($php_max_exec_time == 0) ) {
|
||||
* // If we have no time limit, set a hard limit of about 10 seconds
|
||||
* // (safe for Apache and IIS timeouts, verbose enough for users)
|
||||
* $php_max_exec_time = 14;
|
||||
* }
|
||||
* }
|
||||
* else
|
||||
* {
|
||||
* // If ini_get is not available, use a rough default
|
||||
* $php_max_exec_time = 14;
|
||||
* }
|
||||
*
|
||||
* // Apply an arbitrary correction to counter CMS load time
|
||||
* $php_max_exec_time--;
|
||||
*
|
||||
* // Apply bias
|
||||
* $php_max_exec_time = $php_max_exec_time * $bias;
|
||||
* $config_max_exec_time = $config_max_exec_time * $bias;
|
||||
*
|
||||
* // Use the most appropriate time limit value
|
||||
* if( $config_max_exec_time > $php_max_exec_time )
|
||||
* {
|
||||
* $this->max_exec_time = $php_max_exec_time;
|
||||
* }
|
||||
* else
|
||||
* {
|
||||
* $this->max_exec_time = $config_max_exec_time;
|
||||
* }
|
||||
* /**/
|
||||
|
||||
$this->max_exec_time = $config_max_exec_time * $bias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake-up function to reset internal timer when we get unserialized
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
// Re-initialize start time on wake-up
|
||||
$this->start_time = $this->microtime_float();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of seconds left, before we hit the "must break" threshold
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getTimeLeft()
|
||||
{
|
||||
return $this->max_exec_time - $this->getRunningTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time elapsed since object creation/unserialization, effectively how
|
||||
* long Akeeba Engine has been processing data
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getRunningTime()
|
||||
{
|
||||
return $this->microtime_float() - $this->start_time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timestamp in decimal seconds
|
||||
*/
|
||||
protected function microtime_float()
|
||||
{
|
||||
list($usec, $sec) = explode(" ", microtime());
|
||||
|
||||
return ((float)$usec + (float)$sec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce the minimum execution time
|
||||
*
|
||||
* @param bool $log Should I log what I'm doing? Default is true.
|
||||
* @param bool $serverSideSleep Should I sleep on the server side? If false we return the amount of time to wait in msec
|
||||
*
|
||||
* @return int Wait time to reach min_execution_time in msec
|
||||
*/
|
||||
public function enforce_min_exec_time($log = true, $serverSideSleep = true)
|
||||
{
|
||||
// Try to get a sane value for PHP's maximum_execution_time INI parameter
|
||||
if (@function_exists('ini_get'))
|
||||
{
|
||||
$php_max_exec = @ini_get("maximum_execution_time");
|
||||
}
|
||||
else
|
||||
{
|
||||
$php_max_exec = 10;
|
||||
}
|
||||
if (($php_max_exec == "") || ($php_max_exec == 0))
|
||||
{
|
||||
$php_max_exec = 10;
|
||||
}
|
||||
// Decrease $php_max_exec time by 500 msec we need (approx.) to tear down
|
||||
// the application, as well as another 500msec added for rounding
|
||||
// error purposes. Also make sure this is never gonna be less than 0.
|
||||
$php_max_exec = max($php_max_exec * 1000 - 1000, 0);
|
||||
|
||||
// Get the "minimum execution time per step" Akeeba Backup configuration variable
|
||||
$configuration = Factory::getConfiguration();
|
||||
$minexectime = $configuration->get('akeeba.tuning.min_exec_time', 0);
|
||||
if (!is_numeric($minexectime))
|
||||
{
|
||||
$minexectime = 0;
|
||||
}
|
||||
|
||||
// Make sure we are not over PHP's time limit!
|
||||
if ($minexectime > $php_max_exec)
|
||||
{
|
||||
$minexectime = $php_max_exec;
|
||||
}
|
||||
|
||||
// Get current running time
|
||||
$elapsed_time = $this->getRunningTime() * 1000;
|
||||
|
||||
$clientSideSleep = 0;
|
||||
|
||||
// Only run a sleep delay if we haven't reached the minexectime execution time
|
||||
if (($minexectime > $elapsed_time) && ($elapsed_time > 0))
|
||||
{
|
||||
$sleep_msec = $minexectime - $elapsed_time;
|
||||
|
||||
if (!$serverSideSleep)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Asking client to sleep for $sleep_msec msec");
|
||||
$clientSideSleep = $sleep_msec;
|
||||
}
|
||||
elseif (function_exists('usleep'))
|
||||
{
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Sleeping for $sleep_msec msec, using usleep()");
|
||||
}
|
||||
usleep(1000 * $sleep_msec);
|
||||
}
|
||||
elseif (function_exists('time_nanosleep'))
|
||||
{
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Sleeping for $sleep_msec msec, using time_nanosleep()");
|
||||
}
|
||||
$sleep_sec = floor($sleep_msec / 1000);
|
||||
$sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000));
|
||||
time_nanosleep($sleep_sec, $sleep_nsec);
|
||||
}
|
||||
elseif (function_exists('time_sleep_until'))
|
||||
{
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Sleeping for $sleep_msec msec, using time_sleep_until()");
|
||||
}
|
||||
$until_timestamp = time() + $sleep_msec / 1000;
|
||||
time_sleep_until($until_timestamp);
|
||||
}
|
||||
elseif (function_exists('sleep'))
|
||||
{
|
||||
$sleep_sec = ceil($sleep_msec / 1000);
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Sleeping for $sleep_sec seconds, using sleep()");
|
||||
}
|
||||
sleep($sleep_sec);
|
||||
}
|
||||
}
|
||||
elseif ($elapsed_time > 0)
|
||||
{
|
||||
// No sleep required, even if user configured us to be able to do so.
|
||||
if ($log)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "No need to sleep; execution time: $elapsed_time msec; min. exec. time: $minexectime msec");
|
||||
}
|
||||
}
|
||||
|
||||
return $clientSideSleep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timer. It should only be used in CLI mode!
|
||||
*/
|
||||
public function resetTime()
|
||||
{
|
||||
$this->start_time = $this->microtime_float();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
; Akeeba scripting for Akeeba Backup
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: scripting.ini 409 2011-01-24 09:30:22Z nikosdion $
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Key lists
|
||||
;
|
||||
; These volatile registry keys inform the engine about the available domains and
|
||||
; scripts (backup types).
|
||||
; ------------------------------------------------------------------------------
|
||||
volatile.akeebaengine.domains = "init|installer|packdb|packing|finale"
|
||||
volatile.akeebaengine.scripts = "full|dbonly|fileonly|alldb|incfile|incfull"
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Domain definitions
|
||||
;
|
||||
; Each domain defines the domain key to send to clients, the class to
|
||||
; instanciate and its textual representation in the backup GUI as volatile
|
||||
; registry keys
|
||||
; ------------------------------------------------------------------------------
|
||||
volatile.domain.init.domain = "init"
|
||||
volatile.domain.init.class = "Init"
|
||||
volatile.domain.init.text = "COM_AKEEBA_BACKUP_LABEL_DOMAIN_INIT"
|
||||
|
||||
volatile.domain.installer.domain = "installer"
|
||||
volatile.domain.installer.class = "Installer"
|
||||
volatile.domain.installer.text = "COM_AKEEBA_BACKUP_LABEL_DOMAIN_INSTALLER"
|
||||
|
||||
volatile.domain.packdb.domain = "PackDB"
|
||||
volatile.domain.packdb.class = "Db"
|
||||
volatile.domain.packdb.text = "COM_AKEEBA_BACKUP_LABEL_DOMAIN_PACKDB"
|
||||
|
||||
volatile.domain.packing.domain = "Packing"
|
||||
volatile.domain.packing.class = "Pack"
|
||||
volatile.domain.packing.text = "COM_AKEEBA_BACKUP_LABEL_DOMAIN_PACKING"
|
||||
|
||||
volatile.domain.finale.domain = "finale"
|
||||
volatile.domain.finale.class = "Finalization"
|
||||
volatile.domain.finale.text = "COM_AKEEBA_BACKUP_LABEL_DOMAIN_FINISHED"
|
||||
|
||||
; ------------------------------------------------------------------------------
|
||||
; Engine Scripting (backup types)
|
||||
;
|
||||
; Each script defines a domain chain, textual representation and hidden engine
|
||||
; tweaks as volatile registry settings
|
||||
; ------------------------------------------------------------------------------
|
||||
volatile.scripting.full.chain = "init|installer|packdb|packing|finale"
|
||||
volatile.scripting.full.text = "COM_AKEEBA_CONFIG_BACKUPTYPE_FULL"
|
||||
volatile.scripting.full.db.saveasname = "normal"
|
||||
volatile.scripting.full.db.databasesini = 1
|
||||
volatile.scripting.full.db.skipextradb = 0
|
||||
volatile.scripting.full.db.abstractnames = 1
|
||||
volatile.scripting.full.db.dropstatements = 0
|
||||
volatile.scripting.full.core.createarchive = 1
|
||||
|
||||
volatile.scripting.dbonly.chain = "init|packdb|finale"
|
||||
volatile.scripting.dbonly.text = COM_AKEEBA_CONFIG_BACKUPTYPE_DBONLY
|
||||
volatile.scripting.dbonly.db.saveasname = "output"
|
||||
volatile.scripting.dbonly.db.databasesini = 0
|
||||
volatile.scripting.dbonly.db.skipextradb = 1
|
||||
volatile.scripting.dbonly.db.abstractnames = 0
|
||||
volatile.scripting.dbonly.db.dropstatements = 1
|
||||
volatile.scripting.dbonly.core.forceextension = ".sql"
|
||||
volatile.scripting.dbonly.core.createarchive = 0
|
||||
|
||||
volatile.scripting.fileonly.chain = "init|packing|finale"
|
||||
volatile.scripting.fileonly.text = "COM_AKEEBA_CONFIG_BACKUPTYPE_FILEONLY"
|
||||
volatile.scripting.fileonly.core.createarchive = 1
|
||||
|
||||
volatile.scripting.alldb.chain = "init|installer|packdb|finale"
|
||||
volatile.scripting.alldb.text = "COM_AKEEBA_CONFIG_BACKUPTYPE_ALLDB"
|
||||
volatile.scripting.alldb.db.tempfile = "temporary"
|
||||
volatile.scripting.alldb.db.saveasname = "normal"
|
||||
volatile.scripting.alldb.db.databasesini = 1
|
||||
volatile.scripting.alldb.db.skipextradb = 0
|
||||
volatile.scripting.alldb.db.abstractnames = 1
|
||||
volatile.scripting.alldb.db.dropstatements = 0
|
||||
volatile.scripting.alldb.db.finalizearchive = 1
|
||||
volatile.scripting.alldb.core.createarchive = 1
|
||||
|
||||
volatile.scripting.incfile.chain = "init|packing|finale"
|
||||
volatile.scripting.incfile.text = "COM_AKEEBA_CONFIG_BACKUPTYPE_INCFILE"
|
||||
volatile.scripting.incfile.filter.incremental = 1
|
||||
|
||||
volatile.scripting.incfull.chain = "init|installer|packdb|packing|finale"
|
||||
volatile.scripting.incfull.text = "COM_AKEEBA_CONFIG_BACKUPTYPE_INCFULL"
|
||||
volatile.scripting.incfull.db.saveasname = "normal"
|
||||
volatile.scripting.incfull.db.databasesini = 1
|
||||
volatile.scripting.incfull.db.skipextradb = 0
|
||||
volatile.scripting.incfull.db.abstractnames = 1
|
||||
volatile.scripting.incfull.db.dropstatements = 0
|
||||
volatile.scripting.incfull.core.createarchive = 1
|
||||
volatile.scripting.incfull.filter.incremental = 1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,563 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Mysqli as QueryMysqli;
|
||||
|
||||
/**
|
||||
* MySQL Improved (mysqli) database driver for Akeeba Engine
|
||||
*
|
||||
* Based on Joomla! Platform 11.2
|
||||
*/
|
||||
class Mysqli extends Mysql
|
||||
{
|
||||
protected $port;
|
||||
protected $socket;
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
* @since 11.1
|
||||
*/
|
||||
public $name = 'mysqli';
|
||||
|
||||
/**
|
||||
* Database object constructor
|
||||
*
|
||||
* @param array $options List of options used to configure the connection
|
||||
*/
|
||||
public function __construct($options)
|
||||
{
|
||||
$this->driverType = 'mysql';
|
||||
|
||||
// Init
|
||||
$this->nameQuote = '`';
|
||||
|
||||
$host = array_key_exists('host', $options) ? $options['host'] : 'localhost';
|
||||
$port = array_key_exists('port', $options) ? $options['port'] : '';
|
||||
$user = array_key_exists('user', $options) ? $options['user'] : '';
|
||||
$password = array_key_exists('password', $options) ? $options['password'] : '';
|
||||
$database = array_key_exists('database', $options) ? $options['database'] : '';
|
||||
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
|
||||
$select = array_key_exists('select', $options) ? $options['select'] : true;
|
||||
$socket = null;
|
||||
|
||||
// Figure out if a port is included in the host name
|
||||
$this->fixHostnamePortSocket($host, $port, $socket);
|
||||
|
||||
// Set the information
|
||||
$this->host = $host;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
$this->port = $port;
|
||||
$this->socket = $socket;
|
||||
$this->_database = $database;
|
||||
$this->selectDatabase = $select;
|
||||
|
||||
// finalize initialization
|
||||
parent::__construct($options);
|
||||
|
||||
// Open the connection
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
$this->open();
|
||||
}
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
if ($this->connected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
// perform a number of fatality checks, then return gracefully
|
||||
if (!function_exists('mysqli_connect'))
|
||||
{
|
||||
$this->errorNum = 1;
|
||||
$this->errorMsg = 'The MySQL adapter "mysqli" is not available.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// connect to the server
|
||||
if (!($this->connection = @mysqli_connect($this->host, $this->user, $this->password, null, $this->port, $this->socket)))
|
||||
{
|
||||
$this->errorNum = 2;
|
||||
$this->errorMsg = 'Could not connect to MySQL';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Set sql_mode to non_strict mode
|
||||
mysqli_query($this->connection, "SET @@SESSION.sql_mode = '';");
|
||||
|
||||
if ($this->selectDatabase && !empty($this->_database))
|
||||
{
|
||||
if (!$this->select($this->_database))
|
||||
{
|
||||
$this->errorNum = 3;
|
||||
$this->errorMsg = "Cannot select database {$this->_database}";
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setUTF();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$return = false;
|
||||
if (is_resource($this->cursor))
|
||||
{
|
||||
mysqli_free_result($this->cursor);
|
||||
}
|
||||
if (is_object($this->connection))
|
||||
{
|
||||
$return = @mysqli_close($this->connection);
|
||||
}
|
||||
$this->connection = null;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape a string for usage in an SQL statement.
|
||||
*
|
||||
* @param string $text The string to be escaped.
|
||||
* @param boolean $extra Optional parameter to provide extra escaping.
|
||||
*
|
||||
* @return string The escaped string.
|
||||
*/
|
||||
public function escape($text, $extra = false)
|
||||
{
|
||||
$result = @mysqli_real_escape_string($this->getConnection(), $text);
|
||||
|
||||
if ($extra)
|
||||
{
|
||||
$result = addcslashes($result, '%_');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if the MySQL connector is available.
|
||||
*
|
||||
* @return boolean True on success, false otherwise.
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return (function_exists('mysqli_connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the connection to the server is active.
|
||||
*
|
||||
* @return boolean True if connected to the database engine.
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
if (is_object($this->connection))
|
||||
{
|
||||
return mysqli_ping($this->connection);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of affected rows for the previous executed SQL statement.
|
||||
*
|
||||
* @return integer The number of affected rows.
|
||||
*/
|
||||
public function getAffectedRows()
|
||||
{
|
||||
return mysqli_affected_rows($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of returned rows for the previous executed SQL statement.
|
||||
*
|
||||
* @param resource $cursor An optional database cursor resource to extract the row count from.
|
||||
*
|
||||
* @return integer The number of returned rows.
|
||||
*/
|
||||
public function getNumRows($cursor = null)
|
||||
{
|
||||
return mysqli_num_rows($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current or query, or new JDatabaseQuery object.
|
||||
*
|
||||
* @param boolean $new False to return the last query set, True to return a new JDatabaseQuery object.
|
||||
*
|
||||
* @return mixed The current value of the internal SQL variable or a new JDatabaseQuery object.
|
||||
*/
|
||||
public function getQuery($new = false)
|
||||
{
|
||||
if ($new)
|
||||
{
|
||||
return new QueryMysqli($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the database connector.
|
||||
*
|
||||
* @return string The database connector version.
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return mysqli_get_server_info($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the database engine supports UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True if supported.
|
||||
*/
|
||||
public function hasUTF()
|
||||
{
|
||||
$verParts = explode('.', $this->getVersion());
|
||||
|
||||
return ($verParts[0] == 5 || ($verParts[0] == 4 && $verParts[1] == 1 && (int)$verParts[2] >= 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the auto-incremented value from the last INSERT statement.
|
||||
*
|
||||
* @return integer The value of the auto-increment field from the last inserted row.
|
||||
*/
|
||||
public function insertid()
|
||||
{
|
||||
return mysqli_insert_id($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the SQL statement.
|
||||
*
|
||||
* @return mixed A database cursor resource on success, boolean false on failure.
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
static $isReconnecting = false;
|
||||
|
||||
$this->open();
|
||||
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Take a local copy so that we don't modify the original query and cause issues later
|
||||
$query = $this->replacePrefix((string)$this->sql);
|
||||
if ($this->limit > 0 || $this->offset > 0)
|
||||
{
|
||||
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
|
||||
}
|
||||
|
||||
// Increment the query counter.
|
||||
$this->count++;
|
||||
|
||||
// If debugging is enabled then let's log the query.
|
||||
if ($this->debug)
|
||||
{
|
||||
// Add the query to the object queue.
|
||||
$this->log[] = $query;
|
||||
}
|
||||
|
||||
// Reset the error values.
|
||||
$this->errorNum = 0;
|
||||
$this->errorMsg = '';
|
||||
|
||||
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
|
||||
$this->cursor = @mysqli_query($this->connection, $query);
|
||||
|
||||
// If an error occurred handle it.
|
||||
if (!$this->cursor)
|
||||
{
|
||||
$this->errorNum = (int)mysqli_errno($this->connection);
|
||||
$this->errorMsg = (string)mysqli_error($this->connection) . ' SQL=' . $query;
|
||||
|
||||
// Check if the server was disconnected.
|
||||
if (!$this->connected() && !$isReconnecting)
|
||||
{
|
||||
$isReconnecting = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to reconnect.
|
||||
$this->connection = null;
|
||||
$this->open();
|
||||
}
|
||||
// If connect fails, ignore that exception and throw the normal exception.
|
||||
catch (\RuntimeException $e)
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Since we were able to reconnect, run the query again.
|
||||
$result = $this->query();
|
||||
$isReconnecting = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
// The server was not disconnected.
|
||||
elseif ($this->errorNum != 0)
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a database for use.
|
||||
*
|
||||
* @param string $database The name of the database to select for use.
|
||||
*
|
||||
* @return boolean True if the database was successfully selected.
|
||||
*/
|
||||
public function select($database)
|
||||
{
|
||||
if (!$database)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mysqli_select_db($this->connection, $database))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection to use UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*/
|
||||
public function setUTF()
|
||||
{
|
||||
$result = false;
|
||||
|
||||
if ($this->supportsUtf8mb4())
|
||||
{
|
||||
$result = @mysqli_set_charset($this->connection, 'utf8mb4');
|
||||
}
|
||||
|
||||
if (!$result)
|
||||
{
|
||||
$result = @mysqli_set_charset($this->connection, 'utf8');
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchArray($cursor = null)
|
||||
{
|
||||
return mysqli_fetch_row($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an associative array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
public function fetchAssoc($cursor = null)
|
||||
{
|
||||
return mysqli_fetch_assoc($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an object.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
* @param string $class The class name to use for the returned row object.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchObject($cursor = null, $class = 'stdClass')
|
||||
{
|
||||
return mysqli_fetch_object($cursor ? $cursor : $this->cursor, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to free up the memory used for the result set.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult($cursor = null)
|
||||
{
|
||||
mysqli_free_result($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this database server support UTF-8 four byte (utf8mb4) collation?
|
||||
*
|
||||
* libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
|
||||
*
|
||||
* This method's code is based on WordPress' wpdb::has_cap() method
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supportsUtf8mb4()
|
||||
{
|
||||
$client_version = mysqli_get_client_info();
|
||||
|
||||
if (strpos($client_version, 'mysqlnd') !== false)
|
||||
{
|
||||
$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);
|
||||
|
||||
return version_compare($client_version, '5.0.9', '>=');
|
||||
}
|
||||
else
|
||||
{
|
||||
return version_compare($client_version, '5.5.3', '>=');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse all the weird hostname definitions and normalize them into something that the MySQLi connector
|
||||
* will understand. Please note that there are some differences to the old MySQL driver:
|
||||
*
|
||||
* * Port and socket MUST be provided separately from the hostname. Hostnames in the form of 127.0.0.1:8336 are no
|
||||
* longer acceptable.
|
||||
*
|
||||
* * The hostname "localhost" has special meaning. It means "use named pipes / sockets". Anything else uses TCP/IP.
|
||||
* This is the ONLY way to specify a. TCP/IP or b. named pipes / sockets connection.
|
||||
*
|
||||
* * You SHOULD NOT use a numeric TCP/IP port with hostname localhost. For some strange reason it's still allowed
|
||||
* but the manual is self-contradicting over what this really does...
|
||||
*
|
||||
* * Likewise you CANNOT use a socket / named pipe path with hostname other than localhost. Named pipes and sockets
|
||||
* can only be used with the local machine, therefore the hostname MUST be localhost.
|
||||
*
|
||||
* * You cannot give a TCP/IP port number in the socket parameter or a named pipe / socket path to the port
|
||||
* parameter. This leads to an error.
|
||||
*
|
||||
* * You cannot use an empty string, 0 or any other non-null value when you want to omit either of the port or
|
||||
* socket parameters.
|
||||
*
|
||||
* * Persistent connections must be prefixed with the string literal 'p:'. Therefore you cannot have a hostname
|
||||
* called 'p' (not to mention that'd be daft). You can also not specify something like 'p:1234' to make a
|
||||
* persistent connection to a port. This wasn't even supported by the old MySQL driver. As a result we don't even
|
||||
* try to catch that degenerate case.
|
||||
*
|
||||
* This method will try to apply all of the aforementioned rules with one additional disambiguation rule:
|
||||
*
|
||||
* A port / socket set in the hostname overrides a port specified separately. A port specified separately overrides
|
||||
* a socket specified separately.
|
||||
*
|
||||
* @param string $host The hostname. Can contain legacy hostname:port or hostname:sc=ocket definitions.
|
||||
* @param int $port The port. Alternatively it can contain the path to the socket.
|
||||
* @param string $socket The path to the socket. You could abuse it to enter the port number. DON'T!
|
||||
*
|
||||
* @return void All parameters are passed by reference.
|
||||
*/
|
||||
protected function fixHostnamePortSocket(&$host, &$port, &$socket)
|
||||
{
|
||||
// Is this a persistent connection? Persistent connections are indicated by the literal "p:" in front of the hostname
|
||||
$isPersistent = (substr($host, 0, 2) == 'p:');
|
||||
$host = $isPersistent ? substr($host, 2) : $host;
|
||||
|
||||
/*
|
||||
* Unlike mysql_connect(), mysqli_connect() takes the port and socket as separate arguments. Therefore, we
|
||||
* have to extract them from the host string.
|
||||
*/
|
||||
$port = !empty($port) ? $port : 3306;
|
||||
|
||||
$regex = '/^(?P<host>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(:(?P<port>.+))?$/';
|
||||
|
||||
// It's an IPv4 address with or without port
|
||||
if (preg_match($regex, $host, $matches))
|
||||
{
|
||||
$host = $matches['host'];
|
||||
|
||||
if (!empty($matches['port']))
|
||||
{
|
||||
$port = $matches['port'];
|
||||
}
|
||||
}
|
||||
// Square-bracketed IPv6 address with or without port, e.g. [fe80:102::2%eth1]:3306
|
||||
elseif (preg_match('/^(?P<host>\[.*\])(:(?P<port>.+))?$/', $host, $matches))
|
||||
{
|
||||
$host = $matches['host'];
|
||||
|
||||
if (!empty($matches['port']))
|
||||
{
|
||||
$port = $matches['port'];
|
||||
}
|
||||
}
|
||||
// Named host (e.g example.com or localhost) with or without port
|
||||
elseif (preg_match('/^(?P<host>(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P<port>[^:]+))?$/i', $host, $matches))
|
||||
{
|
||||
$host = $matches['host'];
|
||||
|
||||
if (!empty($matches['port']))
|
||||
{
|
||||
$port = $matches['port'];
|
||||
}
|
||||
}
|
||||
// Empty host, just port, e.g. ':3306'
|
||||
elseif (preg_match('/^:(?P<port>[^:]+)$/', $host, $matches))
|
||||
{
|
||||
$host = 'localhost';
|
||||
$port = $matches['port'];
|
||||
}
|
||||
// ... else we assume normal (naked) IPv6 address, so host and port stay as they are or default
|
||||
|
||||
// Get the port number or socket name
|
||||
if (is_numeric($port))
|
||||
{
|
||||
$port = (int) $port;
|
||||
$socket = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
$socket = $port;
|
||||
|
||||
// If we're going to use sockets, port MUST BE null, otherwise mysqli_connect will try to use it ignoring
|
||||
// the socket, causing a connection error
|
||||
$port = null;
|
||||
}
|
||||
|
||||
// Finally, if it's a persistent connection we have to prefix the hostname with 'p:'
|
||||
$host = $isPersistent ? "p:$host" : $host;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
* This class is adapted from the Joomla! Framework
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Base as QueryBase;
|
||||
|
||||
/**
|
||||
* Dummy driver class for flat-file CMS
|
||||
*/
|
||||
class None extends Base
|
||||
{
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0
|
||||
*/
|
||||
public $name = 'none';
|
||||
|
||||
public static $dbtech = 'none';
|
||||
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->driverType = 'none';
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if this db driver is available
|
||||
*
|
||||
* @return boolean True on success, false otherwise.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the database connection
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the connection to the server is active.
|
||||
*
|
||||
* @return boolean True if connected to the database engine.
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table from the database.
|
||||
*
|
||||
* @param string $table The name of the database table to drop.
|
||||
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
|
||||
*
|
||||
* @return Base Returns this object to support chaining.
|
||||
*/
|
||||
public function dropTable($table, $ifExists = true)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape a string for usage in an SQL statement.
|
||||
*
|
||||
* @param string $text The string to be escaped.
|
||||
* @param boolean $extra Optional parameter to provide extra escaping.
|
||||
*
|
||||
* @return string The escaped string.
|
||||
*/
|
||||
public function escape($text, $extra = false)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchArray($cursor = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an associative array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
public function fetchAssoc($cursor = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an object.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
* @param string $class The class name to use for the returned row object.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchObject($cursor = null, $class = 'stdClass')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to free up the memory used for the result set.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult($cursor = null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of affected rows for the previous executed SQL statement.
|
||||
*
|
||||
* @return integer The number of affected rows.
|
||||
*/
|
||||
public function getAffectedRows()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the database collation in use by sampling a text field of a table in the database.
|
||||
*
|
||||
* @return mixed The collation in use by the database or boolean false if not supported.
|
||||
*/
|
||||
public function getCollation()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of returned rows for the previous executed SQL statement.
|
||||
*
|
||||
* @param resource $cursor An optional database cursor resource to extract the row count from.
|
||||
*
|
||||
* @return integer The number of returned rows.
|
||||
*/
|
||||
public function getNumRows($cursor = null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current query object or a new QueryBase object.
|
||||
*
|
||||
* @param boolean $new False to return the current query object, True to return a new QueryBase object.
|
||||
*
|
||||
* @return QueryBase The current query object or a new object extending the QueryBase class.
|
||||
*/
|
||||
public function getQuery($new = false)
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field information about the given tables.
|
||||
*
|
||||
* @param string $table The name of the database table.
|
||||
* @param boolean $typeOnly True (default) to only return field types.
|
||||
*
|
||||
* @return array An array of fields by table.
|
||||
*/
|
||||
public function getTableColumns($table, $typeOnly = true)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the table CREATE statement that creates the given tables.
|
||||
*
|
||||
* @param mixed $tables A table name or a list of table names.
|
||||
*
|
||||
* @return array A list of the create SQL for the tables.
|
||||
*/
|
||||
public function getTableCreate($tables)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field information about the given tables.
|
||||
*
|
||||
* @param mixed $tables A table name or a list of table names.
|
||||
*
|
||||
* @return array An array of keys for the table(s).
|
||||
*/
|
||||
public function getTableKeys($tables)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get an array of all tables in the database.
|
||||
*
|
||||
* @return array An array of all the tables in the database.
|
||||
*/
|
||||
public function getTableList()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the names of tables, views, procedures, functions and triggers
|
||||
* in the database. The table names are the keys of the tables, whereas the value is
|
||||
* the type of each element: table, view, merge, temp, procedure, function or trigger.
|
||||
* Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
|
||||
* set up as temporary, black hole or federated tables. These two types should never,
|
||||
* ever, have their data dumped in the SQL dump file.
|
||||
*
|
||||
* @param bool $abstract Return or normal names? Defaults to true (names)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTables($abstract = true)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the database connector
|
||||
*
|
||||
* @return string The database connector version.
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the auto-incremented value from the last INSERT statement.
|
||||
*
|
||||
* @return integer The value of the auto-increment field from the last inserted row.
|
||||
*/
|
||||
public function insertid()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table in the database.
|
||||
*
|
||||
* @param string $tableName The name of the table to unlock.
|
||||
*
|
||||
* @return Base Returns this object to support chaining.
|
||||
*/
|
||||
public function lockTable($tableName)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the SQL statement.
|
||||
*
|
||||
* @return mixed A database cursor resource on success, boolean false on failure.
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a table in the database.
|
||||
*
|
||||
* @param string $oldTable The name of the table to be renamed
|
||||
* @param string $newTable The new name for the table.
|
||||
* @param string $backup Table prefix
|
||||
* @param string $prefix For the table - used to rename constraints in non-mysql databases
|
||||
*
|
||||
* @return Base Returns this object to support chaining.
|
||||
*/
|
||||
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a database for use.
|
||||
*
|
||||
* @param string $database The name of the database to select for use.
|
||||
*
|
||||
* @return boolean True if the database was successfully selected.
|
||||
*/
|
||||
public function select($database)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection to use UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*/
|
||||
public function setUTF()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to commit a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionCommit()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to roll back a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionRollback()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to initialize a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionStart()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks tables in the database.
|
||||
*
|
||||
* @return Base Returns this object to support chaining.
|
||||
*/
|
||||
public function unlockTables()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,726 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Pdomysql as QueryPdomysql;
|
||||
|
||||
/**
|
||||
* PDO MySQL database driver for Akeeba Engine
|
||||
*
|
||||
* Based on Joomla! Platform 12.1
|
||||
*/
|
||||
class Pdomysql extends Mysql
|
||||
{
|
||||
/** @var \PDO The db connection resource */
|
||||
protected $connection = null;
|
||||
|
||||
/** @var \PDOStatement The database connection cursor from the last query. */
|
||||
protected $cursor;
|
||||
|
||||
/** @var string Connection character set */
|
||||
protected $charset = 'utf8mb4';
|
||||
|
||||
/** @var array Driver options for PDO */
|
||||
protected $driverOptions = array();
|
||||
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'pdomysql';
|
||||
|
||||
/**
|
||||
* Database object constructor
|
||||
*
|
||||
* @param array $options List of options used to configure the connection
|
||||
*/
|
||||
public function __construct($options)
|
||||
{
|
||||
$this->driverType = 'mysql';
|
||||
|
||||
// Init
|
||||
$this->nameQuote = '`';
|
||||
|
||||
$host = array_key_exists('host', $options) ? $options['host'] : 'localhost';
|
||||
$port = array_key_exists('port', $options) ? $options['port'] : '';
|
||||
$user = array_key_exists('user', $options) ? $options['user'] : '';
|
||||
$password = array_key_exists('password', $options) ? $options['password'] : '';
|
||||
$database = array_key_exists('database', $options) ? $options['database'] : '';
|
||||
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
|
||||
$select = array_key_exists('select', $options) ? $options['select'] : true;
|
||||
$charset = array_key_exists('charset', $options) ? $options['charset'] : 'utf8mb4';
|
||||
$driverOptions = array_key_exists('driverOptions', $options) ? $options['driverOptions'] : array();
|
||||
$connection = array_key_exists('connection', $options) ? $options['connection'] : null;
|
||||
$socket = null;
|
||||
|
||||
// Figure out if a port is included in the host name
|
||||
if (empty($port))
|
||||
{
|
||||
// Unlike mysql_connect(), mysqli_connect() takes the port and socket
|
||||
// as separate arguments. Therefore, we have to extract them from the
|
||||
// host string.
|
||||
$port = null;
|
||||
$socket = null;
|
||||
$targetSlot = substr(strstr($host, ":"), 1);
|
||||
if (!empty($targetSlot))
|
||||
{
|
||||
// Get the port number or socket name
|
||||
if (is_numeric($targetSlot))
|
||||
{
|
||||
$port = $targetSlot;
|
||||
}
|
||||
else
|
||||
{
|
||||
$socket = $targetSlot;
|
||||
}
|
||||
|
||||
// Extract the host name only
|
||||
$host = substr($host, 0, strlen($host) - (strlen($targetSlot) + 1));
|
||||
// This will take care of the following notation: ":3306"
|
||||
if ($host == '')
|
||||
{
|
||||
$host = 'localhost';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open the connection
|
||||
$this->host = $host;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
$this->port = $port;
|
||||
$this->socket = $socket;
|
||||
$this->charset = $charset;
|
||||
$this->_database = $database;
|
||||
$this->selectDatabase = $select;
|
||||
$this->driverOptions = $driverOptions;
|
||||
$this->tablePrefix = $prefix;
|
||||
$this->connection = $connection;
|
||||
$this->errorNum = 0;
|
||||
$this->count = 0;
|
||||
$this->log = array();
|
||||
$this->options = $options;
|
||||
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
$this->open();
|
||||
}
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
if ($this->connected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
if (!isset($this->charset))
|
||||
{
|
||||
$this->charset = 'utf8mb4';
|
||||
}
|
||||
|
||||
$this->port = $this->port ? $this->port : 3306;
|
||||
|
||||
$format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#';
|
||||
|
||||
if ($this->socket)
|
||||
{
|
||||
$format = 'mysql:socket=#SOCKET#;dbname=#DBNAME#;charset=#CHARSET#';
|
||||
}
|
||||
|
||||
$replace = array('#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#', '#CHARSET#');
|
||||
$with = array($this->host, $this->port, $this->socket, $this->_database, $this->charset);
|
||||
|
||||
// Create the connection string:
|
||||
$connectionString = str_replace($replace, $with, $format);
|
||||
|
||||
// connect to the server
|
||||
try
|
||||
{
|
||||
$this->connection = new \PDO(
|
||||
$connectionString,
|
||||
$this->user,
|
||||
$this->password,
|
||||
$this->driverOptions
|
||||
);
|
||||
}
|
||||
catch (\PDOException $e)
|
||||
{
|
||||
// If we tried connecting through utf8mb4 and we failed let's retry with regular utf8
|
||||
if ($this->charset == 'utf8mb4')
|
||||
{
|
||||
$this->charset = 'UTF8';
|
||||
$this->open();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->errorNum = 2;
|
||||
$this->errorMsg = 'Could not connect to MySQL via PDO: ' . $e->getMessage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the SQL mode of the connection
|
||||
try
|
||||
{
|
||||
$this->connection->exec("SET @@SESSION.sql_mode = '';");
|
||||
}
|
||||
// Ignore any exceptions (incompatible MySQL versions)
|
||||
catch (\Exception $e)
|
||||
{
|
||||
}
|
||||
|
||||
$this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
$this->connection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
|
||||
|
||||
if ($this->selectDatabase && !empty($this->_database))
|
||||
{
|
||||
$this->select($this->_database);
|
||||
}
|
||||
|
||||
$this->freeResult();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$return = false;
|
||||
|
||||
if (is_object($this->cursor))
|
||||
{
|
||||
$this->cursor->closeCursor();
|
||||
}
|
||||
|
||||
$this->connection = null;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape a string for usage in an SQL statement.
|
||||
*
|
||||
* @param string $text The string to be escaped.
|
||||
* @param boolean $extra Optional parameter to provide extra escaping.
|
||||
*
|
||||
* @return string The escaped string.
|
||||
*/
|
||||
public function escape($text, $extra = false)
|
||||
{
|
||||
if (is_int($text) || is_float($text))
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
$result = substr($this->connection->quote($text), 1, -1);
|
||||
|
||||
if ($extra)
|
||||
{
|
||||
$result = addcslashes($result, '%_');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the SQL statement.
|
||||
*
|
||||
* @return mixed A database cursor resource on success, boolean false on failure.
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
static $isReconnecting = false;
|
||||
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
$this->open();
|
||||
}
|
||||
|
||||
$this->freeResult();
|
||||
|
||||
// Take a local copy so that we don't modify the original query and cause issues later
|
||||
$query = $this->replacePrefix((string)$this->sql);
|
||||
|
||||
if ($this->limit > 0 || $this->offset > 0)
|
||||
{
|
||||
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
|
||||
}
|
||||
|
||||
// Increment the query counter.
|
||||
$this->count++;
|
||||
|
||||
// If debugging is enabled then let's log the query.
|
||||
if ($this->debug)
|
||||
{
|
||||
// Add the query to the object queue.
|
||||
$this->log[] = $query;
|
||||
}
|
||||
|
||||
// Reset the error values.
|
||||
$this->errorNum = 0;
|
||||
$this->errorMsg = '';
|
||||
|
||||
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
|
||||
try
|
||||
{
|
||||
$this->cursor = $this->connection->query($query);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
}
|
||||
|
||||
// If an error occurred handle it.
|
||||
if (!$this->cursor)
|
||||
{
|
||||
$errorInfo = $this->connection->errorInfo();
|
||||
$this->errorNum = $errorInfo[1];
|
||||
$this->errorMsg = $errorInfo[2] . ' SQL=' . $query;
|
||||
|
||||
// Check if the server was disconnected.
|
||||
if (!$this->connected() && !$isReconnecting)
|
||||
{
|
||||
$isReconnecting = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to reconnect.
|
||||
$this->connection = null;
|
||||
$this->open();
|
||||
}
|
||||
// If connect fails, ignore that exception and throw the normal exception.
|
||||
catch (\RuntimeException $e)
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Since we were able to reconnect, run the query again.
|
||||
$result = $this->query();
|
||||
$isReconnecting = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
// The server was not disconnected.
|
||||
else
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if the MySQL connector is available.
|
||||
*
|
||||
* @return boolean True on success, false otherwise.
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
if (!defined('\PDO::ATTR_DRIVER_NAME'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array('mysql', \PDO::getAvailableDrivers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the connection to the server is active.
|
||||
*
|
||||
* @return boolean True if connected to the database engine.
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/** @var \PDOStatement $statement */
|
||||
$statement = $this->connection->prepare('SELECT 1');
|
||||
$executed = $statement->execute();
|
||||
$ret = 0;
|
||||
|
||||
if ($executed)
|
||||
{
|
||||
$row = array(0);
|
||||
|
||||
if (!empty($statement) && $statement instanceof \PDOStatement)
|
||||
{
|
||||
$row = $statement->fetch(\PDO::FETCH_NUM);
|
||||
}
|
||||
|
||||
$ret = $row[0];
|
||||
}
|
||||
|
||||
$status = $ret == 1;
|
||||
|
||||
$statement->closeCursor();
|
||||
$statement = null;
|
||||
}
|
||||
// If we catch an exception here, we must not be connected.
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$status = false;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of affected rows for the previous executed SQL statement.
|
||||
*
|
||||
* @return integer The number of affected rows.
|
||||
*/
|
||||
public function getAffectedRows()
|
||||
{
|
||||
if ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $this->cursor->rowCount();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of returned rows for the previous executed SQL statement.
|
||||
*
|
||||
* @param resource $cursor An optional database cursor resource to extract the row count from.
|
||||
*
|
||||
* @return integer The number of returned rows.
|
||||
*/
|
||||
public function getNumRows($cursor = null)
|
||||
{
|
||||
if ($cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $cursor->rowCount();
|
||||
}
|
||||
|
||||
if ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $this->cursor->rowCount();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current or query, or new JDatabaseQuery object.
|
||||
*
|
||||
* @param boolean $new False to return the last query set, True to return a new JDatabaseQuery object.
|
||||
*
|
||||
* @return mixed The current value of the internal SQL variable or a new JDatabaseQuery object.
|
||||
*/
|
||||
public function getQuery($new = false)
|
||||
{
|
||||
if ($new)
|
||||
{
|
||||
return new QueryPdomysql($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the database connector.
|
||||
*
|
||||
* @return string The database connector version.
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the database engine supports UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True if supported.
|
||||
*/
|
||||
public function hasUTF()
|
||||
{
|
||||
$verParts = explode('.', $this->getVersion());
|
||||
|
||||
return ($verParts[0] == 5 || ($verParts[0] == 4 && $verParts[1] == 1 && (int)$verParts[2] >= 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the auto-incremented value from the last INSERT statement.
|
||||
*
|
||||
* @return integer The value of the auto-increment field from the last inserted row.
|
||||
*/
|
||||
public function insertid()
|
||||
{
|
||||
// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
|
||||
return @$this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a database for use.
|
||||
*
|
||||
* @param string $database The name of the database to select for use.
|
||||
*
|
||||
* @return boolean True if the database was successfully selected.
|
||||
*/
|
||||
public function select($database)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->connection->exec('USE ' . $this->quoteName($database));
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$errorInfo = $this->connection->errorInfo();
|
||||
$this->errorNum = $errorInfo[1];
|
||||
$this->errorMsg = $errorInfo[2];
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection to use UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*/
|
||||
public function setUTF()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to commit a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionCommit()
|
||||
{
|
||||
$this->connection->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to roll back a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionRollback()
|
||||
{
|
||||
$this->connection->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to initialize a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionStart()
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchArray($cursor = null)
|
||||
{
|
||||
$ret = null;
|
||||
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $cursor->fetch(\PDO::FETCH_NUM);
|
||||
}
|
||||
elseif ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $this->cursor->fetch(\PDO::FETCH_NUM);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an associative array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
public function fetchAssoc($cursor = null)
|
||||
{
|
||||
$ret = null;
|
||||
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $cursor->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
elseif ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $this->cursor->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an object.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
* @param string $class The class name to use for the returned row object.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchObject($cursor = null, $class = 'stdClass')
|
||||
{
|
||||
$ret = null;
|
||||
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $cursor->fetchObject($class);
|
||||
}
|
||||
elseif ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
$ret = $this->cursor->fetchObject($class);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to free up the memory used for the result set.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult($cursor = null)
|
||||
{
|
||||
if ($cursor instanceof \PDOStatement)
|
||||
{
|
||||
$cursor->closeCursor();
|
||||
$cursor = null;
|
||||
}
|
||||
|
||||
if ($this->cursor instanceof \PDOStatement)
|
||||
{
|
||||
$this->cursor->closeCursor();
|
||||
$this->cursor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the next row in the result set from the database query as an object.
|
||||
*
|
||||
* @param string $class The class name to use for the returned row object.
|
||||
*
|
||||
* @return mixed The result of the query as an array, false if there are no more rows.
|
||||
*/
|
||||
public function loadNextObject($class = 'stdClass')
|
||||
{
|
||||
// Execute the query and get the result set cursor.
|
||||
if (!$this->cursor)
|
||||
{
|
||||
if (!($this->execute()))
|
||||
{
|
||||
return $this->errorNum ? null : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the next row from the result set as an object of type $class.
|
||||
if ($row = $this->fetchObject(null, $class))
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
// Free up system resources and return.
|
||||
$this->freeResult();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the next row in the result set from the database query as an array.
|
||||
*
|
||||
* @return mixed The result of the query as an array, false if there are no more rows.
|
||||
*/
|
||||
public function loadNextRow()
|
||||
{
|
||||
// Execute the query and get the result set cursor.
|
||||
if (!$this->cursor)
|
||||
{
|
||||
if (!($this->execute()))
|
||||
{
|
||||
return $this->errorNum ? null : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the next row from the result set as an object of type $class.
|
||||
if ($row = $this->fetchArray())
|
||||
{
|
||||
return $row;
|
||||
}
|
||||
|
||||
// Free up system resources and return.
|
||||
$this->freeResult();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* PDO does not support serialize
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
$serializedProperties = array();
|
||||
|
||||
$reflect = new \ReflectionClass($this);
|
||||
|
||||
// Get properties of the current class
|
||||
$properties = $reflect->getProperties();
|
||||
|
||||
foreach ($properties as $property)
|
||||
{
|
||||
// Do not serialize properties that are \PDO
|
||||
if ($property->isStatic() == false && !($this->{$property->name} instanceof \PDO))
|
||||
{
|
||||
array_push($serializedProperties, $property->name);
|
||||
}
|
||||
}
|
||||
|
||||
return $serializedProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake up after serialization
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
// Get connection back
|
||||
$this->__construct($this->options);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Query Element Class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Element
|
||||
{
|
||||
/**
|
||||
* @var string The name of the element.
|
||||
*/
|
||||
protected $name = null;
|
||||
|
||||
/**
|
||||
* @var array An array of elements.
|
||||
*/
|
||||
protected $elements = null;
|
||||
|
||||
/**
|
||||
* @var string Glue piece.
|
||||
*/
|
||||
protected $glue = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name The name of the element.
|
||||
* @param mixed $elements String or array.
|
||||
* @param string $glue The glue for elements.
|
||||
*/
|
||||
public function __construct($name, $elements, $glue = ',')
|
||||
{
|
||||
$this->elements = array();
|
||||
$this->name = $name;
|
||||
$this->glue = $glue;
|
||||
|
||||
$this->append($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function to convert the query element to a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if (substr($this->name, -2) == '()')
|
||||
{
|
||||
return PHP_EOL . substr($this->name, 0, -2) . '(' . implode($this->glue, $this->elements) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return PHP_EOL . $this->name . ' ' . implode($this->glue, $this->elements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends element parts to the internal list.
|
||||
*
|
||||
* @param mixed $elements String or array.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function append($elements)
|
||||
{
|
||||
if (is_array($elements))
|
||||
{
|
||||
$this->elements = array_merge($this->elements, $elements);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->elements = array_merge($this->elements, array($elements));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the elements of this element.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getElements()
|
||||
{
|
||||
return $this->elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to provide deep copy support to nested objects and arrays
|
||||
* when cloning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this as $k => $v)
|
||||
{
|
||||
if (is_object($v) || is_array($v))
|
||||
{
|
||||
$this->{$k} = unserialize(serialize($v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Base as BaseQuery;
|
||||
|
||||
/**
|
||||
* Query Limitable Interface.
|
||||
* Adds bind/unbind methods as well as a getBounded() method
|
||||
* to retrieve the stored bounded variables on demand prior to
|
||||
* query execution.
|
||||
*
|
||||
* Based on Joomla! Platform 11.2
|
||||
*/
|
||||
interface Limitable
|
||||
{
|
||||
/**
|
||||
* Method to modify a query already in string format with the needed
|
||||
* additions to make the query limited to a particular number of
|
||||
* results, or start at a particular offset. This method is used
|
||||
* automatically by the __toString() method if it detects that the
|
||||
* query implements the Limitable interface.
|
||||
*
|
||||
* @param string $query The query in string format
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function processLimit($query, $limit, $offset = 0);
|
||||
|
||||
/**
|
||||
* Sets the offset and limit for the result set, if the database driver supports it.
|
||||
*
|
||||
* Usage:
|
||||
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
|
||||
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
|
||||
*
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function setLimit($limit = 0, $offset = 0);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Query Building Class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Mysql extends Mysqli
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Base as BaseQuery;
|
||||
|
||||
/**
|
||||
* Query Building Class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Mysqli extends Base implements Limitable
|
||||
{
|
||||
/**
|
||||
* @var integer The offset for the result set.
|
||||
*/
|
||||
protected $offset;
|
||||
|
||||
/**
|
||||
* @var integer The limit for the result set.
|
||||
*/
|
||||
protected $limit;
|
||||
|
||||
/**
|
||||
* Method to modify a query already in string format with the needed
|
||||
* additions to make the query limited to a particular number of
|
||||
* results, or start at a particular offset.
|
||||
*
|
||||
* @param string $query The query in string format
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function processLimit($query, $limit, $offset = 0)
|
||||
{
|
||||
if ($limit > 0 || $offset > 0)
|
||||
{
|
||||
$query .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset and limit for the result set, if the database driver supports it.
|
||||
*
|
||||
* Usage:
|
||||
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
|
||||
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
|
||||
*
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*/
|
||||
public function setLimit($limit = 0, $offset = 0)
|
||||
{
|
||||
$this->limit = (int)$limit;
|
||||
$this->offset = (int)$offset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates an array of column names or values.
|
||||
*
|
||||
* @param array $values An array of values to concatenate.
|
||||
* @param string $separator As separator to place between each value.
|
||||
*
|
||||
* @return string The concatenated values.
|
||||
*/
|
||||
public function concatenate($values, $separator = null)
|
||||
{
|
||||
if ($separator)
|
||||
{
|
||||
$concat_string = 'CONCAT_WS(' . $this->quote($separator);
|
||||
|
||||
foreach ($values as $value)
|
||||
{
|
||||
$concat_string .= ', ' . $value;
|
||||
}
|
||||
|
||||
return $concat_string . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'CONCAT(' . implode(',', $values) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Query Building Class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Pdomysql extends Mysqli implements Limitable
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,632 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Base as BaseQuery;
|
||||
use Akeeba\Engine\Driver\Query\Element as QueryElement;
|
||||
|
||||
/**
|
||||
* Query Building Class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Postgresql extends Base implements Limitable
|
||||
{
|
||||
/**
|
||||
* @var object The FOR UPDATE element used in "FOR UPDATE" lock
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $forUpdate = null;
|
||||
|
||||
/**
|
||||
* @var object The FOR SHARE element used in "FOR SHARE" lock
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $forShare = null;
|
||||
|
||||
/**
|
||||
* @var object The NOWAIT element used in "FOR SHARE" and "FOR UPDATE" lock
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $noWait = null;
|
||||
|
||||
/**
|
||||
* @var object The LIMIT element
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $limit = null;
|
||||
|
||||
/**
|
||||
* @var object The OFFSET element
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $offset = null;
|
||||
|
||||
/**
|
||||
* @var object The RETURNING element of INSERT INTO
|
||||
* @since 11.3
|
||||
*/
|
||||
protected $returning = null;
|
||||
|
||||
/**
|
||||
* Magic function to convert the query to a string, only for postgresql specific query
|
||||
*
|
||||
* @return string The completed query.
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$query = '';
|
||||
|
||||
switch ($this->type)
|
||||
{
|
||||
case 'select':
|
||||
$query .= (string)$this->select;
|
||||
$query .= (string)$this->from;
|
||||
if ($this->join)
|
||||
{
|
||||
// Special case for joins
|
||||
foreach ($this->join as $join)
|
||||
{
|
||||
$query .= (string)$join;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->where)
|
||||
{
|
||||
$query .= (string)$this->where;
|
||||
}
|
||||
|
||||
if ($this->group)
|
||||
{
|
||||
$query .= (string)$this->group;
|
||||
}
|
||||
|
||||
if ($this->having)
|
||||
{
|
||||
$query .= (string)$this->having;
|
||||
}
|
||||
|
||||
if ($this->order)
|
||||
{
|
||||
$query .= (string)$this->order;
|
||||
}
|
||||
|
||||
if ($this->limit)
|
||||
{
|
||||
$query .= (string)$this->limit;
|
||||
}
|
||||
|
||||
if ($this->offset)
|
||||
{
|
||||
$query .= (string)$this->offset;
|
||||
}
|
||||
|
||||
if ($this->forUpdate)
|
||||
{
|
||||
$query .= (string)$this->forUpdate;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($this->forShare)
|
||||
{
|
||||
$query .= (string)$this->forShare;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->noWait)
|
||||
{
|
||||
$query .= (string)$this->noWait;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$query .= (string)$this->update;
|
||||
$query .= (string)$this->set;
|
||||
|
||||
if ($this->join)
|
||||
{
|
||||
$onWord = ' ON ';
|
||||
|
||||
// Workaround for special case of JOIN with UPDATE
|
||||
foreach ($this->join as $join)
|
||||
{
|
||||
$joinElem = $join->getElements();
|
||||
|
||||
$joinArray = explode($onWord, $joinElem[0]);
|
||||
|
||||
$this->from($joinArray[0]);
|
||||
$this->where($joinArray[1]);
|
||||
}
|
||||
|
||||
$query .= (string)$this->from;
|
||||
}
|
||||
|
||||
if ($this->where)
|
||||
{
|
||||
$query .= (string)$this->where;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'insert':
|
||||
$query .= (string)$this->insert;
|
||||
|
||||
if ($this->values)
|
||||
{
|
||||
if ($this->columns)
|
||||
{
|
||||
$query .= (string)$this->columns;
|
||||
}
|
||||
|
||||
$elements = $this->values->getElements();
|
||||
if (!($elements[0] instanceof $this))
|
||||
{
|
||||
$query .= ' VALUES ';
|
||||
}
|
||||
|
||||
$query .= (string)$this->values;
|
||||
|
||||
if ($this->returning)
|
||||
{
|
||||
$query .= (string)$this->returning;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$query = parent::__toString();
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear data from the query or a specific clause of the query.
|
||||
*
|
||||
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function clear($clause = null)
|
||||
{
|
||||
switch ($clause)
|
||||
{
|
||||
case 'limit':
|
||||
$this->limit = null;
|
||||
break;
|
||||
|
||||
case 'offset':
|
||||
$this->offset = null;
|
||||
break;
|
||||
|
||||
case 'forUpdate':
|
||||
$this->forUpdate = null;
|
||||
break;
|
||||
|
||||
case 'forShare':
|
||||
$this->forShare = null;
|
||||
break;
|
||||
|
||||
case 'noWait':
|
||||
$this->noWait = null;
|
||||
break;
|
||||
|
||||
case 'returning':
|
||||
$this->returning = null;
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
case 'update':
|
||||
case 'delete':
|
||||
case 'insert':
|
||||
case 'from':
|
||||
case 'join':
|
||||
case 'set':
|
||||
case 'where':
|
||||
case 'group':
|
||||
case 'having':
|
||||
case 'order':
|
||||
case 'columns':
|
||||
case 'values':
|
||||
parent::clear($clause);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->type = null;
|
||||
$this->limit = null;
|
||||
$this->offset = null;
|
||||
$this->forUpdate = null;
|
||||
$this->forShare = null;
|
||||
$this->noWait = null;
|
||||
$this->returning = null;
|
||||
parent::clear($clause);
|
||||
break;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a value to a char.
|
||||
*
|
||||
* Ensure that the value is properly quoted before passing to the method.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->castAsChar('a'));
|
||||
*
|
||||
* @param string $value The value to cast as a char.
|
||||
*
|
||||
* @return string Returns the cast value.
|
||||
*
|
||||
* @since 11.1
|
||||
*/
|
||||
public function castAsChar($value)
|
||||
{
|
||||
return $value . '::text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates an array of column names or values.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->concatenate(array('a', 'b')));
|
||||
*
|
||||
* @param array $values An array of values to concatenate.
|
||||
* @param string $separator As separator to place between each value.
|
||||
*
|
||||
* @return string The concatenated values.
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function concatenate($values, $separator = null)
|
||||
{
|
||||
if ($separator)
|
||||
{
|
||||
return implode(' || ' . $this->quote($separator) . ' || ', $values);
|
||||
}
|
||||
else
|
||||
{
|
||||
return implode(' || ', $values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current date and time.
|
||||
*
|
||||
* @return string Return string used in query to obtain
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function currentTimestamp()
|
||||
{
|
||||
return 'NOW()';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FOR UPDATE lock on select's output row
|
||||
*
|
||||
* @param string $table_name The table to lock
|
||||
* @param boolean $glue The glue by which to join the conditions. Defaults to ',' .
|
||||
*
|
||||
* @return BaseQuery FOR UPDATE query element
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function forUpdate($table_name, $glue = ',')
|
||||
{
|
||||
$this->type = 'forUpdate';
|
||||
|
||||
if (is_null($this->forUpdate))
|
||||
{
|
||||
$glue = strtoupper($glue);
|
||||
$this->forUpdate = new QueryElement('FOR UPDATE', 'OF ' . $table_name, "$glue ");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->forUpdate->append($table_name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FOR SHARE lock on select's output row
|
||||
*
|
||||
* @param string $table_name The table to lock
|
||||
* @param boolean $glue The glue by which to join the conditions. Defaults to ',' .
|
||||
*
|
||||
* @return BaseQuery FOR SHARE query element
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function forShare($table_name, $glue = ',')
|
||||
{
|
||||
$this->type = 'forShare';
|
||||
|
||||
if (is_null($this->forShare))
|
||||
{
|
||||
$glue = strtoupper($glue);
|
||||
$this->forShare = new QueryElement('FOR SHARE', 'OF ' . $table_name, "$glue ");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->forShare->append($table_name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract year from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->year($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing year to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract year from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function year($date)
|
||||
{
|
||||
return 'EXTRACT (YEAR FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract month from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->month($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing month to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract month from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function month($date)
|
||||
{
|
||||
return 'EXTRACT (MONTH FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract day from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->day($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing day to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract day from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function day($date)
|
||||
{
|
||||
return 'EXTRACT (DAY FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract hour from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->hour($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing hour to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract hour from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function hour($date)
|
||||
{
|
||||
return 'EXTRACT (HOUR FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract minute from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->minute($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing minute to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract minute from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function minute($date)
|
||||
{
|
||||
return 'EXTRACT (MINUTE FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get a string to extract seconds from date column.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->second($query->quoteName('dateColumn')));
|
||||
*
|
||||
* @param string $date Date column containing second to be extracted.
|
||||
*
|
||||
* @return string Returns string to extract second from a date.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function second($date)
|
||||
{
|
||||
return 'EXTRACT (SECOND FROM ' . $date . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the NOWAIT lock on select's output row
|
||||
*
|
||||
* @return BaseQuery NO WAIT query element
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function noWait()
|
||||
{
|
||||
$this->type = 'noWait';
|
||||
|
||||
if (is_null($this->noWait))
|
||||
{
|
||||
$this->noWait = new QueryElement('NOWAIT', null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the LIMIT clause to the query
|
||||
*
|
||||
* @param int $limit An int of how many row will be returned
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function limit($limit = 0)
|
||||
{
|
||||
if (is_null($this->limit))
|
||||
{
|
||||
$this->limit = new QueryElement('LIMIT', (int)$limit);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OFFSET clause to the query
|
||||
*
|
||||
* @param int $offset An int for skipping row
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function offset($offset = 0)
|
||||
{
|
||||
if (is_null($this->offset))
|
||||
{
|
||||
$this->offset = new QueryElement('OFFSET', (int)$offset);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the RETURNING element to INSERT INTO statement.
|
||||
*
|
||||
* @param mixed $pkCol The name of the primary key column.
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*
|
||||
* @since 11.3
|
||||
*/
|
||||
public function returning($pkCol)
|
||||
{
|
||||
if (is_null($this->returning))
|
||||
{
|
||||
$this->returning = new QueryElement('RETURNING', $pkCol);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset and limit for the result set, if the database driver supports it.
|
||||
*
|
||||
* Usage:
|
||||
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
|
||||
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
|
||||
*
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return BaseQuery Returns this object to allow chaining.
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function setLimit($limit = 0, $offset = 0)
|
||||
{
|
||||
$this->limit = (int)$limit;
|
||||
$this->offset = (int)$offset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to modify a query already in string format with the needed
|
||||
* additions to make the query limited to a particular number of
|
||||
* results, or start at a particular offset.
|
||||
*
|
||||
* @param string $query The query in string format
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 12.1
|
||||
*/
|
||||
public function processLimit($query, $limit, $offset = 0)
|
||||
{
|
||||
if ($limit > 0)
|
||||
{
|
||||
$query .= ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
if ($offset > 0)
|
||||
{
|
||||
$query .= ' OFFSET ' . $offset;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the current date and time in Postgresql.
|
||||
* Usage:
|
||||
* $query->select($query->dateAdd());
|
||||
* Prefixing the interval with a - (negative sign) will cause subtraction to be used.
|
||||
*
|
||||
* @param string $date The date to add to, in SQL format
|
||||
* @param string $interval The string representation of the appropriate number of units
|
||||
* @param string $datePart The part of the date to perform the addition on
|
||||
*
|
||||
* @return string The string with the appropriate sql for addition of dates
|
||||
*
|
||||
* @since 13.1
|
||||
* @note Not all drivers support all units. Check appropriate references
|
||||
* @link http://www.postgresql.org/docs/9.0/static/functions-datetime.html.
|
||||
*/
|
||||
public function dateAdd($date, $interval, $datePart)
|
||||
{
|
||||
if (substr($interval, 0, 1) != '-')
|
||||
{
|
||||
return "timestamp '" . $date . "' + interval '" . $interval . " " . $datePart . "'";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "timestamp '" . $date . "' - interval '" . ltrim($interval, '-') . " " . $datePart . "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
/**
|
||||
* Database Query Preparable Interface.
|
||||
*
|
||||
* Adds bind/unbind methods as well as a getBounded() method
|
||||
* to retrieve the stored bounded variables on demand prior to
|
||||
* query execution.
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
interface Preparable
|
||||
{
|
||||
/**
|
||||
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
|
||||
* removes a variable that has been bounded from the internal bounded array when the passed in value is null.
|
||||
*
|
||||
* @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of
|
||||
* the form ':key', but can also be an integer.
|
||||
* @param mixed &$value The value that will be bound. The value is passed by reference to support output
|
||||
* parameters such as those possible with stored procedures.
|
||||
* @param integer $dataType Constant corresponding to a SQL datatype.
|
||||
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
|
||||
* @param array $driverOptions Optional driver options to be used.
|
||||
*
|
||||
* @return Preparable
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function bind($key = null, &$value = null, $dataType = \PDO::PARAM_STR, $length = 0, $driverOptions = array());
|
||||
|
||||
/**
|
||||
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
|
||||
* returned.
|
||||
*
|
||||
* @param mixed $key The bounded variable key to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function &getBounded($key = null);
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Awf
|
||||
* @copyright 2006-2017 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or later
|
||||
*
|
||||
* This class is adapted from the Joomla! Framework
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
/**
|
||||
* SQLite Query Building Class.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
class Sqlite extends Base implements Preparable, Limitable
|
||||
{
|
||||
/**
|
||||
* The limit for the result set.
|
||||
*
|
||||
* @var integer
|
||||
* @since 1.0
|
||||
*/
|
||||
protected $limit;
|
||||
|
||||
/**
|
||||
* The offset for the result set.
|
||||
*
|
||||
* @var integer
|
||||
* @since 1.0
|
||||
*/
|
||||
protected $offset;
|
||||
|
||||
/**
|
||||
* Holds key / value pair of bound objects.
|
||||
*
|
||||
* @var mixed
|
||||
* @since 1.0
|
||||
*/
|
||||
protected $bounded = array();
|
||||
|
||||
/**
|
||||
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
|
||||
* removes a variable that has been bounded from the internal bounded array when the passed in value is null.
|
||||
*
|
||||
* @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of
|
||||
* the form ':key', but can also be an integer.
|
||||
* @param mixed &$value The value that will be bound. The value is passed by reference to support output
|
||||
* parameters such as those possible with stored procedures.
|
||||
* @param integer $dataType Constant corresponding to a SQL datatype.
|
||||
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
|
||||
* @param array $driverOptions Optional driver options to be used.
|
||||
*
|
||||
* @return Sqlite Returns this object to allow chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function bind($key = null, &$value = null, $dataType = \PDO::PARAM_STR, $length = 0, $driverOptions = array())
|
||||
{
|
||||
// Case 1: Empty Key (reset $bounded array)
|
||||
if (empty($key))
|
||||
{
|
||||
$this->bounded = array();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Case 2: Key Provided, null value (unset key from $bounded array)
|
||||
if (is_null($value))
|
||||
{
|
||||
if (isset($this->bounded[$key]))
|
||||
{
|
||||
unset($this->bounded[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$obj = new \stdClass;
|
||||
|
||||
$obj->value = &$value;
|
||||
$obj->dataType = $dataType;
|
||||
$obj->length = $length;
|
||||
$obj->driverOptions = $driverOptions;
|
||||
|
||||
// Case 3: Simply add the Key/Value into the bounded array
|
||||
$this->bounded[$key] = $obj;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
|
||||
* returned.
|
||||
*
|
||||
* @param mixed $key The bounded variable key to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function &getBounded($key = null)
|
||||
{
|
||||
if (empty($key))
|
||||
{
|
||||
return $this->bounded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isset($this->bounded[$key]))
|
||||
{
|
||||
return $this->bounded[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of characters in a string.
|
||||
*
|
||||
* Note, use 'length' to find the number of bytes in a string.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->charLength('a'));
|
||||
*
|
||||
* @param string $field A value.
|
||||
* @param string $operator Comparison operator between charLength integer value and $condition
|
||||
* @param string $condition Integer value to compare charLength with.
|
||||
*
|
||||
* @return string The required char length call.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function charLength($field, $operator = null, $condition = null)
|
||||
{
|
||||
return 'length(' . $field . ')' . (isset($operator) && isset($condition) ? ' ' . $operator . ' ' . $condition : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear data from the query or a specific clause of the query.
|
||||
*
|
||||
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
|
||||
*
|
||||
* @return Sqlite Returns this object to allow chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function clear($clause = null)
|
||||
{
|
||||
switch ($clause)
|
||||
{
|
||||
case null:
|
||||
$this->bounded = array();
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::clear($clause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates an array of column names or values.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->concatenate(array('a', 'b')));
|
||||
*
|
||||
* @param array $values An array of values to concatenate.
|
||||
* @param string $separator As separator to place between each value.
|
||||
*
|
||||
* @return string The concatenated values.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function concatenate($values, $separator = null)
|
||||
{
|
||||
if ($separator)
|
||||
{
|
||||
return implode(' || ' . $this->quote($separator) . ' || ', $values);
|
||||
}
|
||||
else
|
||||
{
|
||||
return implode(' || ', $values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to modify a query already in string format with the needed
|
||||
* additions to make the query limited to a particular number of
|
||||
* results, or start at a particular offset. This method is used
|
||||
* automatically by the __toString() method if it detects that the
|
||||
* query implements the LimitableInterface.
|
||||
*
|
||||
* @param string $query The query in string format
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function processLimit($query, $limit, $offset = 0)
|
||||
{
|
||||
if ($limit > 0 || $offset > 0)
|
||||
{
|
||||
$query .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset and limit for the result set, if the database driver supports it.
|
||||
*
|
||||
* Usage:
|
||||
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
|
||||
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
|
||||
*
|
||||
* @param integer $limit The limit for the result set
|
||||
* @param integer $offset The offset for the result set
|
||||
*
|
||||
* @return Sqlite Returns this object to allow chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function setLimit($limit = 0, $offset = 0)
|
||||
{
|
||||
$this->limit = (int) $limit;
|
||||
$this->offset = (int) $offset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver\Query;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Microsoft SQL Server query building class.
|
||||
*
|
||||
* Based on Joomla! Platform 11.3
|
||||
*/
|
||||
class Sqlsrv extends Base
|
||||
{
|
||||
/**
|
||||
* The character(s) used to quote SQL statement names such as table names or field names,
|
||||
* etc. The child classes should define this as necessary. If a single character string the
|
||||
* same character is used for both sides of the quoted name, else the first character will be
|
||||
* used for the opening quote and the second for the closing quote.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name_quotes = '`';
|
||||
|
||||
/**
|
||||
* The null or zero representation of a timestamp for the database driver. This should be
|
||||
* defined in child classes to hold the appropriate value for the engine.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $null_date = '1900-01-01 00:00:00';
|
||||
|
||||
/**
|
||||
* Magic function to convert the query to a string.
|
||||
*
|
||||
* @return string The completed query.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$query = '';
|
||||
|
||||
switch ($this->type)
|
||||
{
|
||||
case 'insert':
|
||||
$query .= (string)$this->insert;
|
||||
|
||||
// Set method
|
||||
if ($this->set)
|
||||
{
|
||||
$query .= (string)$this->set;
|
||||
}
|
||||
// Columns-Values method
|
||||
elseif ($this->values)
|
||||
{
|
||||
if ($this->columns)
|
||||
{
|
||||
$query .= (string)$this->columns;
|
||||
}
|
||||
|
||||
$elements = $this->insert->getElements();
|
||||
$tableName = array_shift($elements);
|
||||
|
||||
$query .= 'VALUES ';
|
||||
$query .= (string)$this->values;
|
||||
|
||||
if ($this->autoIncrementField)
|
||||
{
|
||||
$query = 'SET IDENTITY_INSERT ' . $tableName . ' ON;' . $query . 'SET IDENTITY_INSERT ' . $tableName . ' OFF;';
|
||||
}
|
||||
|
||||
if ($this->where)
|
||||
{
|
||||
$query .= (string)$this->where;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$query = parent::__toString();
|
||||
break;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts a value to a char.
|
||||
*
|
||||
* Ensure that the value is properly quoted before passing to the method.
|
||||
*
|
||||
* @param string $value The value to cast as a char.
|
||||
*
|
||||
* @return string Returns the cast value.
|
||||
*/
|
||||
public function castAsChar($value)
|
||||
{
|
||||
return 'CAST(' . $value . ' as NVARCHAR(10))';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of characters in a string.
|
||||
*
|
||||
* Note, use 'length' to find the number of bytes in a string.
|
||||
*
|
||||
* Usage:
|
||||
* $query->select($query->charLength('a'));
|
||||
*
|
||||
* @param string $field A value.
|
||||
* @param string $operator Comparison operator between charLength integer value and $condition
|
||||
* @param string $condition Integer value to compare charLength with.
|
||||
*
|
||||
* @return string The required char length call.
|
||||
*/
|
||||
public function charLength($field, $operator = null, $condition = null)
|
||||
{
|
||||
if (empty($operator) && empty($condition))
|
||||
{
|
||||
$operator = 'IS NOT';
|
||||
$condition = 'NULL';
|
||||
}
|
||||
|
||||
return 'DATALENGTH(' . $field . ')' . (isset($operator) && isset($condition) ? ' ' . $operator . ' ' . $condition : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates an array of column names or values.
|
||||
*
|
||||
* @param array $values An array of values to concatenate.
|
||||
* @param string $separator As separator to place between each value.
|
||||
*
|
||||
* @return string The concatenated values.
|
||||
*/
|
||||
public function concatenate($values, $separator = null)
|
||||
{
|
||||
if ($separator)
|
||||
{
|
||||
return '(' . implode('+' . $this->quote($separator) . '+', $values) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return '(' . implode('+', $values) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current date and time.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function currentTimestamp()
|
||||
{
|
||||
return 'GETDATE()';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of a string in bytes.
|
||||
*
|
||||
* @param string $value The string to measure.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function length($value)
|
||||
{
|
||||
return 'LEN(' . $value . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the current date and time.
|
||||
* Usage:
|
||||
* $query->select($query->dateAdd());
|
||||
* Prefixing the interval with a - (negative sign) will cause subtraction to be used.
|
||||
*
|
||||
* @param string $date The date (SQL formatted) to add to; type may be a string of a time or a datetime.
|
||||
* @param string $interval The string representation of the appropriate number of units
|
||||
* @param string $datePart The part of the date to perform the addition on
|
||||
*
|
||||
* @return string The string with the appropriate sql for addition of dates
|
||||
*
|
||||
* @note Not all drivers support all units.
|
||||
* @link http://msdn.microsoft.com/en-us/library/ms186819.aspx for more information
|
||||
*/
|
||||
public function dateAdd($date, $interval, $datePart)
|
||||
{
|
||||
return "DATEADD('" . $datePart . "', '" . $interval . "', '" . $date . "'" . ')';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
class QueryException extends \Exception {};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* SQL Azure database driver
|
||||
*
|
||||
* Based on Joomla! Platform 11.2
|
||||
*/
|
||||
class Sqlazure extends Sqlsrv
|
||||
{
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'sqlazure';
|
||||
|
||||
}
|
||||
@ -0,0 +1,901 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
* This class is adapted from the Joomla! Framework
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Base as QueryBase;
|
||||
use Akeeba\Engine\Driver\Query\Limitable;
|
||||
use Akeeba\Engine\Driver\Query\Preparable;
|
||||
use SQLite3;
|
||||
|
||||
/**
|
||||
* SQLite database driver supporting PDO based connections
|
||||
*
|
||||
* @see http://php.net/manual/en/ref.pdo-sqlite.php
|
||||
* @since 1.0
|
||||
*/
|
||||
class Sqlite extends Base
|
||||
{
|
||||
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0
|
||||
*/
|
||||
public $name = 'sqlite';
|
||||
|
||||
public static $dbtech = 'sqlite';
|
||||
|
||||
/**
|
||||
* The character(s) used to quote SQL statement names such as table names or field names,
|
||||
* etc. The child classes should define this as necessary. If a single character string the
|
||||
* same character is used for both sides of the quoted name, else the first character will be
|
||||
* used for the opening quote and the second for the closing quote.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0
|
||||
*/
|
||||
protected $nameQuote = '`';
|
||||
|
||||
/** @var \PDOStatement The database connection cursor from the last query. */
|
||||
protected $cursor;
|
||||
|
||||
/** @var resource The prepared statement. */
|
||||
protected $prepared;
|
||||
|
||||
/** @var array Contains the current query execution status */
|
||||
protected $executed = false;
|
||||
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->driverType = 'sqlite';
|
||||
|
||||
parent::__construct($options);
|
||||
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
$this->open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->freeResult();
|
||||
unset($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the database.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
$this->freeResult();
|
||||
unset($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table from the database.
|
||||
*
|
||||
* @param string $tableName The name of the database table to drop.
|
||||
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
|
||||
*
|
||||
* @return Sqlite Returns this object to support chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function dropTable($tableName, $ifExists = true)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
$this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($tableName));
|
||||
|
||||
$this->execute();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape a string for usage in an SQLite statement.
|
||||
*
|
||||
* Note: Using query objects with bound variables is preferable to the below.
|
||||
*
|
||||
* @param string $text The string to be escaped.
|
||||
* @param boolean $extra Unused optional parameter to provide extra escaping.
|
||||
*
|
||||
* @return string The escaped string.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function escape($text, $extra = false)
|
||||
{
|
||||
if (is_int($text) || is_float($text))
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
return SQLite3::escapeString($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the database collation in use by sampling a text field of a table in the database.
|
||||
*
|
||||
* @return mixed The collation in use by the database or boolean false if not supported.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function getCollation()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the table CREATE statement that creates the given tables.
|
||||
*
|
||||
* Note: Doesn't appear to have support in SQLite
|
||||
*
|
||||
* @param mixed $tables A table name or a list of table names.
|
||||
*
|
||||
* @return array A list of the create SQL for the tables.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTableCreate($tables)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
// Sanitize input to an array and iterate over the list.
|
||||
settype($tables, 'array');
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field information about a given table.
|
||||
*
|
||||
* @param string $table The name of the database table.
|
||||
* @param boolean $typeOnly True to only return field types.
|
||||
*
|
||||
* @return array An array of fields for the database table.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTableColumns($table, $typeOnly = true)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$columns = array();
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
$fieldCasing = $this->getOption(\PDO::ATTR_CASE);
|
||||
|
||||
$this->setOption(\PDO::ATTR_CASE, \PDO::CASE_UPPER);
|
||||
|
||||
$table = strtoupper($table);
|
||||
|
||||
$query->setQuery('pragma table_info(' . $table . ')');
|
||||
|
||||
$this->setQuery($query);
|
||||
$fields = $this->loadObjectList();
|
||||
|
||||
if ($typeOnly)
|
||||
{
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
$columns[$field->NAME] = $field->TYPE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
// Do some dirty translation to MySQL output.
|
||||
$columns[$field->NAME] = (object)array(
|
||||
'Field' => $field->NAME,
|
||||
'Type' => $field->TYPE,
|
||||
'Null' => ($field->NOTNULL == '1' ? 'NO' : 'YES'),
|
||||
'Default' => $field->DFLT_VALUE,
|
||||
'Key' => ($field->PK == '1' ? 'PRI' : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setOption(\PDO::ATTR_CASE, $fieldCasing);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details list of keys for a table.
|
||||
*
|
||||
* @param string $table The name of the table.
|
||||
*
|
||||
* @return array An array of the column specification for the table.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTableKeys($table)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$keys = array();
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
$fieldCasing = $this->getOption(\PDO::ATTR_CASE);
|
||||
|
||||
$this->setOption(\PDO::ATTR_CASE, \PDO::CASE_UPPER);
|
||||
|
||||
$table = strtoupper($table);
|
||||
$query->setQuery('pragma table_info( ' . $table . ')');
|
||||
|
||||
// $query->bind(':tableName', $table);
|
||||
|
||||
$this->setQuery($query);
|
||||
$rows = $this->loadObjectList();
|
||||
|
||||
foreach ($rows as $column)
|
||||
{
|
||||
if ($column->PK == 1)
|
||||
{
|
||||
$keys[$column->NAME] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setOption(\PDO::ATTR_CASE, $fieldCasing);
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get an array of all tables in the database (schema).
|
||||
*
|
||||
* @return array An array of all the tables in the database.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getTableList()
|
||||
{
|
||||
$this->open();
|
||||
|
||||
/* @type Query\Sqlite $query */
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
$type = 'table';
|
||||
|
||||
$query->select('name');
|
||||
$query->from('sqlite_master');
|
||||
$query->where('type = :type');
|
||||
$query->bind(':type', $type);
|
||||
$query->order('name');
|
||||
|
||||
$this->setQuery($query);
|
||||
|
||||
$tables = $this->loadColumn();
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* There's no point on return "a list of tables" inside a SQLite database: we are simple going to
|
||||
* copy the whole database file in the new location
|
||||
*
|
||||
* @param bool $abstract
|
||||
* @return array
|
||||
*/
|
||||
public function getTables($abstract = true)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the database connector.
|
||||
*
|
||||
* @return string The database connector version.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$this->setQuery("SELECT sqlite_version()");
|
||||
|
||||
return $this->loadResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a database for use.
|
||||
*
|
||||
* @param string $database The name of the database to select for use.
|
||||
*
|
||||
* @return boolean True if the database was successfully selected.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function select($database)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$this->_database = $database;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection to use UTF-8 character encoding.
|
||||
*
|
||||
* Returns false automatically for the Oracle driver since
|
||||
* you can only set the character set when the connection
|
||||
* is created.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function setUTF()
|
||||
{
|
||||
$this->open();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table in the database.
|
||||
*
|
||||
* @param string $table The name of the table to unlock.
|
||||
*
|
||||
* @return Sqlite Returns this object to support chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function lockTable($table)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a table in the database.
|
||||
*
|
||||
* @param string $oldTable The name of the table to be renamed
|
||||
* @param string $newTable The new name for the table.
|
||||
* @param string $backup Not used by Sqlite.
|
||||
* @param string $prefix Not used by Sqlite.
|
||||
*
|
||||
* @return Sqlite Returns this object to support chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
|
||||
{
|
||||
$this->setQuery('ALTER TABLE ' . $oldTable . ' RENAME TO ' . $newTable)->execute();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks tables in the database.
|
||||
*
|
||||
* @return Sqlite Returns this object to support chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function unlockTables()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if the PDO ODBC connector is available.
|
||||
*
|
||||
* @return boolean True on success, false otherwise.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return class_exists('\\PDO') && in_array('sqlite', \PDO::getAvailableDrivers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the connection to the server is active.
|
||||
*
|
||||
* @return boolean True if connected to the database engine.
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
return !empty($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to commit a transaction.
|
||||
*
|
||||
* @param boolean $toSavepoint If true, commit to the last savepoint.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function transactionCommit($toSavepoint = false)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if (!$toSavepoint || $this->transactionDepth <= 1)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if (!$toSavepoint || $this->transactionDepth == 1)
|
||||
{
|
||||
$this->connection->commit();
|
||||
}
|
||||
|
||||
$this->transactionDepth--;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->transactionDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to roll back a transaction.
|
||||
*
|
||||
* @param boolean $toSavepoint If true, rollback to the last savepoint.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function transactionRollback($toSavepoint = false)
|
||||
{
|
||||
$this->connected();
|
||||
|
||||
if (!$toSavepoint || $this->transactionDepth <= 1)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if (!$toSavepoint || $this->transactionDepth == 1)
|
||||
{
|
||||
$this->connection->rollBack();
|
||||
}
|
||||
|
||||
$this->transactionDepth--;
|
||||
}
|
||||
else
|
||||
{
|
||||
$savepoint = 'SP_' . ($this->transactionDepth - 1);
|
||||
$this->setQuery('ROLLBACK TO ' . $this->quoteName($savepoint));
|
||||
|
||||
if ($this->execute())
|
||||
{
|
||||
$this->transactionDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to initialize a transaction.
|
||||
*
|
||||
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function transactionStart($asSavepoint = false)
|
||||
{
|
||||
$this->connected();
|
||||
|
||||
if (!$asSavepoint || !$this->transactionDepth)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if (!$asSavepoint || !$this->transactionDepth)
|
||||
{
|
||||
$this->connection->beginTransaction();
|
||||
}
|
||||
|
||||
$this->transactionDepth++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$savepoint = 'SP_' . $this->transactionDepth;
|
||||
$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
|
||||
|
||||
if ($this->execute())
|
||||
{
|
||||
$this->transactionDepth++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current query object or a new Query object.
|
||||
* We have to override the parent method since it will always return a PDO query, while we have a
|
||||
* specialized class for SQLite
|
||||
*
|
||||
* @param boolean $new False to return the current query object, True to return a new Query object.
|
||||
*
|
||||
* @return QueryBase The current query object or a new object extending the Query class.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getQuery($new = false)
|
||||
{
|
||||
if ($new)
|
||||
{
|
||||
$class = '\\Akeeba\\Engine\\Driver\\Query\\Sqlite';
|
||||
|
||||
return new $class($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
if ($this->connected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
if (isset($this->options['version']) && $this->options['version'] == 2)
|
||||
{
|
||||
$format = 'sqlite2:#DBNAME#';
|
||||
}
|
||||
else
|
||||
{
|
||||
$format = 'sqlite:#DBNAME#';
|
||||
}
|
||||
|
||||
$replace = array('#DBNAME#');
|
||||
$with = array($this->options['database']);
|
||||
|
||||
// Create the connection string:
|
||||
$connectionString = str_replace($replace, $with, $format);
|
||||
|
||||
try
|
||||
{
|
||||
$this->connection = new \PDO(
|
||||
$connectionString,
|
||||
$this->options['user'],
|
||||
$this->options['password']
|
||||
);
|
||||
}
|
||||
catch (\PDOException $e)
|
||||
{
|
||||
throw new \RuntimeException('Could not connect to PDO' . ': ' . $e->getMessage(), 2, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$return = false;
|
||||
|
||||
if (is_object($this->cursor))
|
||||
{
|
||||
$this->cursor->closeCursor();
|
||||
}
|
||||
|
||||
$this->connection = null;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
protected function fetchArray($cursor = NULL)
|
||||
{
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $cursor->fetch(\PDO::FETCH_NUM);
|
||||
}
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
return $this->prepared->fetch(\PDO::FETCH_NUM);
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchAssoc($cursor = NULL)
|
||||
{
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $cursor->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
return $this->prepared->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchObject($cursor = NULL, $class = 'stdClass')
|
||||
{
|
||||
if (!empty($cursor) && $cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $cursor->fetchObject($class);
|
||||
}
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
return $this->prepared->fetchObject($class);
|
||||
}
|
||||
}
|
||||
|
||||
public function freeResult($cursor = NULL)
|
||||
{
|
||||
$this->executed = false;
|
||||
|
||||
if ($cursor instanceof \PDOStatement)
|
||||
{
|
||||
$cursor->closeCursor();
|
||||
$cursor = null;
|
||||
}
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
$this->prepared->closeCursor();
|
||||
$this->prepared = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getAffectedRows()
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
return $this->prepared->rowCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function getNumRows($cursor = NULL)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
if ($cursor instanceof \PDOStatement)
|
||||
{
|
||||
return $cursor->rowCount();
|
||||
}
|
||||
elseif ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
return $this->prepared->rowCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function insertid()
|
||||
{
|
||||
$this->open();
|
||||
|
||||
// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
|
||||
return @$this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
public function query()
|
||||
{
|
||||
static $isReconnecting = false;
|
||||
|
||||
$this->open();
|
||||
|
||||
if (!is_object($this->connection))
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Take a local copy so that we don't modify the original query and cause issues later
|
||||
$sql = $this->replacePrefix((string) $this->sql);
|
||||
|
||||
if ($this->limit > 0 || $this->offset > 0)
|
||||
{
|
||||
// @TODO
|
||||
$sql .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
|
||||
}
|
||||
|
||||
// Increment the query counter.
|
||||
$this->count++;
|
||||
|
||||
// If debugging is enabled then let's log the query.
|
||||
if ($this->debug)
|
||||
{
|
||||
// Add the query to the object queue.
|
||||
$this->log[] = $sql;
|
||||
}
|
||||
|
||||
// Reset the error values.
|
||||
$this->errorNum = 0;
|
||||
$this->errorMsg = '';
|
||||
|
||||
// Execute the query.
|
||||
$this->executed = false;
|
||||
|
||||
if ($this->prepared instanceof \PDOStatement)
|
||||
{
|
||||
// Bind the variables:
|
||||
if ($this->sql instanceof Preparable)
|
||||
{
|
||||
$bounded =& $this->sql->getBounded();
|
||||
|
||||
foreach ($bounded as $key => $obj)
|
||||
{
|
||||
$this->prepared->bindParam($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
|
||||
}
|
||||
}
|
||||
|
||||
$this->executed = $this->prepared->execute();
|
||||
}
|
||||
|
||||
// If an error occurred handle it.
|
||||
if (!$this->executed)
|
||||
{
|
||||
// Get the error number and message before we execute any more queries.
|
||||
$errorNum = (int) $this->connection->errorCode();
|
||||
$errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());
|
||||
|
||||
// Check if the server was disconnected.
|
||||
if (!$this->connected() && !$isReconnecting)
|
||||
{
|
||||
$isReconnecting = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to reconnect.
|
||||
$this->connection = null;
|
||||
$this->open();
|
||||
}
|
||||
catch (\RuntimeException $e)
|
||||
// If connect fails, ignore that exception and throw the normal exception.
|
||||
{
|
||||
// Get the error number and message.
|
||||
$this->errorNum = (int) $this->connection->errorCode();
|
||||
$this->errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());
|
||||
|
||||
// Throw the normal query exception.
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Since we were able to reconnect, run the query again.
|
||||
$result = $this->query();
|
||||
$isReconnecting = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
else
|
||||
// The server was not disconnected.
|
||||
{
|
||||
// Get the error number and message from before we tried to reconnect.
|
||||
$this->errorNum = $errorNum;
|
||||
$this->errorMsg = $errorMsg;
|
||||
|
||||
// Throw the normal query exception.
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->prepared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PDO database connection attribute
|
||||
* http://www.php.net/manual/en/pdo.getattribute.php
|
||||
*
|
||||
* Usage: $db->getOption(PDO::ATTR_CASE);
|
||||
*
|
||||
* @param mixed $key One of the PDO::ATTR_* Constants
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function getOption($key)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
return $this->connection->getAttribute($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an attribute on the PDO database handle.
|
||||
* http://www.php.net/manual/en/pdo.setattribute.php
|
||||
*
|
||||
* Usage: $db->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
|
||||
*
|
||||
* @param integer $key One of the PDO::ATTR_* Constants
|
||||
* @param mixed $value One of the associated PDO Constants
|
||||
* related to the particular attribute
|
||||
* key.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function setOption($key, $value)
|
||||
{
|
||||
$this->open();
|
||||
|
||||
return $this->connection->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SQL statement string for later execution.
|
||||
*
|
||||
* @param mixed $query The SQL statement to set either as a JDatabaseQuery object or a string.
|
||||
* @param integer $offset The affected row offset to set.
|
||||
* @param integer $limit The maximum affected rows to set.
|
||||
* @param array $driverOptions The optional PDO driver options
|
||||
*
|
||||
* @return Base This object to support method chaining.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function setQuery($query, $offset = null, $limit = null, $driverOptions = array())
|
||||
{
|
||||
$this->open();
|
||||
|
||||
$this->freeResult();
|
||||
|
||||
if (is_string($query))
|
||||
{
|
||||
// Allows taking advantage of bound variables in a direct query:
|
||||
$query = $this->getQuery(true)->setQuery($query);
|
||||
}
|
||||
|
||||
if ($query instanceof Limitable && !is_null($offset) && !is_null($limit))
|
||||
{
|
||||
$query->setLimit($limit, $offset);
|
||||
}
|
||||
|
||||
$sql = $this->replacePrefix((string) $query);
|
||||
|
||||
$this->prepared = $this->connection->prepare($sql, $driverOptions);
|
||||
|
||||
// Store reference to the DatabaseQuery instance:
|
||||
parent::setQuery($query, $offset, $limit);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,992 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Driver;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Query\Sqlsrv as QuerySqlsrv;
|
||||
|
||||
/**
|
||||
* SQL Server database driver
|
||||
*
|
||||
* Based on Joomla! Platform 11.2
|
||||
*/
|
||||
class Sqlsrv extends Base
|
||||
{
|
||||
/**
|
||||
* The name of the database driver.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'sqlsrv';
|
||||
|
||||
/**
|
||||
* The character(s) used to quote SQL statement names such as table names or field names,
|
||||
* etc. The child classes should define this as necessary. If a single character string the
|
||||
* same character is used for both sides of the quoted name, else the first character will be
|
||||
* used for the opening quote and the second for the closing quote.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $nameQuote = '[]';
|
||||
|
||||
/**
|
||||
* The null or zero representation of a timestamp for the database driver. This should be
|
||||
* defined in child classes to hold the appropriate value for the engine.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $nullDate = '1900-01-01 00:00:00';
|
||||
|
||||
/**
|
||||
* @var string The minimum supported database version.
|
||||
* @since 12.1
|
||||
*/
|
||||
protected static $dbMinimum = '10.50.1600.1';
|
||||
|
||||
/**
|
||||
* Returns an array with the names of tables, views, procedures, functions and triggers
|
||||
* in the database. The table names are the keys of the tables, whereas the value is
|
||||
* the type of each element: table, view, merge, temp, procedure, function or trigger.
|
||||
* Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
|
||||
* set up as temporary, black hole or federated tables. These two types should never,
|
||||
* ever, have their data dumped in the SQL dump file.
|
||||
*
|
||||
* @param bool $abstract Return abstract or normal names? Defaults to true (abstract names)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTables($abstract = true)
|
||||
{
|
||||
static $tables = array();
|
||||
|
||||
if (!empty($tables))
|
||||
{
|
||||
return $tables;
|
||||
}
|
||||
|
||||
$this->open();
|
||||
|
||||
// Add tables
|
||||
$this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Tables WHERE type = \'U\';');
|
||||
$all_tables = $this->loadColumn();
|
||||
|
||||
if (!empty($all_tables))
|
||||
{
|
||||
foreach ($all_tables as $table_name)
|
||||
{
|
||||
if ($abstract)
|
||||
{
|
||||
$table_name = $this->getAbstract($table_name);
|
||||
}
|
||||
|
||||
$tables[$table_name] = 'table';
|
||||
}
|
||||
}
|
||||
|
||||
// Add VIEWs
|
||||
$this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Views WHERE type_desc = \'VIEW\';');
|
||||
$all_tables = $this->loadColumn();
|
||||
|
||||
if (!empty($all_tables))
|
||||
{
|
||||
foreach ($all_tables as $table_name)
|
||||
{
|
||||
if ($abstract)
|
||||
{
|
||||
$table_name = $this->getAbstract($table_name);
|
||||
}
|
||||
|
||||
$tables[$table_name] = 'view';
|
||||
}
|
||||
}
|
||||
|
||||
ksort($tables);
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if the SQLSRV connector is available.
|
||||
*
|
||||
* @return boolean True on success, false otherwise.
|
||||
*
|
||||
* @since 11.1
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return (function_exists('sqlsrv_connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $options List of options used to configure the connection
|
||||
*
|
||||
* @since 11.1
|
||||
*/
|
||||
public function __construct($options)
|
||||
{
|
||||
$this->driverType = 'mssql';
|
||||
|
||||
// Get some basic values from the options.
|
||||
$host = array_key_exists('host', $options) ? $options['host'] : 'localhost';
|
||||
$port = array_key_exists('port', $options) ? $options['port'] : '';
|
||||
$user = array_key_exists('user', $options) ? $options['user'] : '';
|
||||
$password = array_key_exists('password', $options) ? $options['password'] : '';
|
||||
$database = array_key_exists('database', $options) ? $options['database'] : '';
|
||||
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
|
||||
$select = array_key_exists('select', $options) ? $options['select'] : true;
|
||||
|
||||
// Build the connection configuration array.
|
||||
$this->connectionConfig = array(
|
||||
'Database' => $database,
|
||||
'uid' => $user,
|
||||
'pwd' => $password,
|
||||
'CharacterSet' => 'UTF-8',
|
||||
'ReturnDatesAsStrings' => true
|
||||
);
|
||||
|
||||
parent::__construct($options);
|
||||
|
||||
$this->host = $host;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
$this->_database = $database;
|
||||
$this->selectDatabase = $select;
|
||||
|
||||
if (!is_resource($this->connection))
|
||||
{
|
||||
$this->open();
|
||||
}
|
||||
}
|
||||
|
||||
public function open()
|
||||
{
|
||||
if (is_resource($this->connection))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$config = $this->connectionConfig;
|
||||
|
||||
// Make sure the SQLSRV extension for PHP is installed and enabled.
|
||||
if (!function_exists('sqlsrv_connect'))
|
||||
{
|
||||
$this->errorNum = 1;
|
||||
$this->errorMsg = 'You do not have the sqlsrv extension installed on this server';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to connect to the server.
|
||||
if (!($this->connection = @ sqlsrv_connect($this->host, $config)))
|
||||
{
|
||||
|
||||
$this->errorNum = 2;
|
||||
$this->errorMsg = 'Can not connect to Microsoft SQL Server';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that DB warnings are not returned as errors.
|
||||
sqlsrv_configure('WarningsReturnAsErrors', 0);
|
||||
|
||||
// If auto-select is enabled select the given database.
|
||||
if ($this->selectDatabase && !empty($this->_database))
|
||||
{
|
||||
$this->select($this->_database);
|
||||
}
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$return = false;
|
||||
|
||||
if (is_resource($this->cursor))
|
||||
{
|
||||
sqlsrv_free_result($this->cursor);
|
||||
}
|
||||
|
||||
if (is_resource($this->connection))
|
||||
{
|
||||
$return = sqlsrv_close($this->connection);
|
||||
}
|
||||
|
||||
$this->connection = null;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table constraints
|
||||
*
|
||||
* @param string $tableName The name of the database table.
|
||||
*
|
||||
* @return array Any constraints available for the table.
|
||||
*/
|
||||
protected function getTableConstraints($tableName)
|
||||
{
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
$this->setQuery(
|
||||
'SELECT CONSTRAINT_NAME FROM' . ' INFORMATION_SCHEMA.TABLE_CONSTRAINTS' . ' WHERE TABLE_NAME = ' . $query->quote($tableName)
|
||||
);
|
||||
|
||||
return $this->loadColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename constraints.
|
||||
*
|
||||
* @param array $constraints Array(strings) of table constraints
|
||||
* @param string $prefix A string
|
||||
* @param string $backup A string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function renameConstraints($constraints = array(), $prefix = null, $backup = null)
|
||||
{
|
||||
foreach ($constraints as $constraint)
|
||||
{
|
||||
$this->setQuery('sp_rename ' . $constraint . ',' . str_replace($prefix, $backup, $constraint));
|
||||
$this->query();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to escape a string for usage in an SQL statement.
|
||||
*
|
||||
* The escaping for MSSQL isn't handled in the driver though that would be nice. Because of this we need
|
||||
* to handle the escaping ourselves.
|
||||
*
|
||||
* @param string $text The string to be escaped.
|
||||
* @param boolean $extra Optional parameter to provide extra escaping.
|
||||
*
|
||||
* @return string The escaped string.
|
||||
*/
|
||||
public function escape($text, $extra = false)
|
||||
{
|
||||
$result = addslashes($text);
|
||||
$result = str_replace("\'", "''", $result);
|
||||
$result = str_replace('\"', '"', $result);
|
||||
$result = str_replace('\/', '/', $result);
|
||||
|
||||
if ($extra)
|
||||
{
|
||||
// We need the below str_replace since the search in sql server doesn't recognize _ character.
|
||||
$result = str_replace('_', '[_]', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the connection to the server is active.
|
||||
*
|
||||
* @return boolean True if connected to the database engine.
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
// TODO: Run a blank query here
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table from the database.
|
||||
*
|
||||
* @param string $tableName The name of the database table to drop.
|
||||
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
|
||||
*
|
||||
* @return Sqlsrv Returns this object to support chaining.
|
||||
*/
|
||||
public function dropTable($tableName, $ifExists = true)
|
||||
{
|
||||
$query = $this->getQuery(true);
|
||||
|
||||
if ($ifExists)
|
||||
{
|
||||
$this->setQuery(
|
||||
'IF EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ' . $query->quote($tableName) . ') DROP TABLE ' . $tableName
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->setQuery('DROP TABLE ' . $tableName);
|
||||
}
|
||||
|
||||
$this->execute();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of affected rows for the previous executed SQL statement.
|
||||
*
|
||||
* @return integer The number of affected rows.
|
||||
*/
|
||||
public function getAffectedRows()
|
||||
{
|
||||
return sqlsrv_rows_affected($this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the database collation in use by sampling a text field of a table in the database.
|
||||
*
|
||||
* @return mixed The collation in use by the database or boolean false if not supported.
|
||||
*/
|
||||
public function getCollation()
|
||||
{
|
||||
// TODO: Not fake this
|
||||
return 'MSSQL UTF-8 (UCS2)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of returned rows for the previous executed SQL statement.
|
||||
*
|
||||
* @param resource $cursor An optional database cursor resource to extract the row count from.
|
||||
*
|
||||
* @return integer The number of returned rows.
|
||||
*/
|
||||
public function getNumRows($cursor = null)
|
||||
{
|
||||
return sqlsrv_num_rows($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current or query, or new JDatabaseQuery object.
|
||||
*
|
||||
* @param boolean $new False to return the last query set, True to return a new JDatabaseQuery object.
|
||||
*
|
||||
* @return mixed The current value of the internal SQL variable or a new QuerySqlsrv object.
|
||||
*/
|
||||
public function getQuery($new = false)
|
||||
{
|
||||
if ($new)
|
||||
{
|
||||
return new QuerySqlsrv($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field information about the given tables.
|
||||
*
|
||||
* @param mixed $table A table name
|
||||
* @param boolean $typeOnly True to only return field types.
|
||||
*
|
||||
* @return array An array of fields.
|
||||
*/
|
||||
public function getTableColumns($table, $typeOnly = true)
|
||||
{
|
||||
$result = array();
|
||||
|
||||
$table_temp = $this->replacePrefix((string)$table);
|
||||
|
||||
// Set the query to get the table fields statement.
|
||||
$this->setQuery(
|
||||
'SELECT column_name as Field, data_type as Type, is_nullable as \'Null\', column_default as \'Default\'' .
|
||||
' FROM information_schema.columns WHERE table_name = ' . $this->quote($table_temp)
|
||||
);
|
||||
$fields = $this->loadObjectList();
|
||||
|
||||
// If we only want the type as the value add just that to the list.
|
||||
if ($typeOnly)
|
||||
{
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
$result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type);
|
||||
}
|
||||
}
|
||||
// If we want the whole field data object add that to the list.
|
||||
else
|
||||
{
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
$result[$field->Field] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the table CREATE statement that creates the given tables.
|
||||
*
|
||||
* This is unsupported by MSSQL.
|
||||
*
|
||||
* @param mixed $tables A table name or a list of table names.
|
||||
*
|
||||
* @return array A list of the create SQL for the tables.
|
||||
*/
|
||||
public function getTableCreate($tables)
|
||||
{
|
||||
// @todo Implement my own approximate function
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details list of keys for a table.
|
||||
*
|
||||
* @param string $table The name of the table.
|
||||
*
|
||||
* @return array An array of the column specification for the table.
|
||||
*/
|
||||
public function getTableKeys($table)
|
||||
{
|
||||
// TODO To implement.
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get an array of all tables in the database.
|
||||
*
|
||||
* @return array An array of all the tables in the database.
|
||||
*/
|
||||
public function getTableList()
|
||||
{
|
||||
// Set the query to get the tables statement.
|
||||
$this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Tables WHERE type = \'U\';');
|
||||
$tables = $this->loadColumn();
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the database connector.
|
||||
*
|
||||
* @return string The database connector version.
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
$version = sqlsrv_server_info($this->connection);
|
||||
|
||||
return $version['SQLServerVersion'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the database engine supports UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True if supported.
|
||||
*/
|
||||
public function hasUTF()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a row into a table based on an object's properties.
|
||||
*
|
||||
* @param string $table The name of the database table to insert into.
|
||||
* @param object &$object A reference to an object whose public properties match the table fields.
|
||||
* @param string $key The name of the primary key. If provided the object property is updated.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*/
|
||||
public function insertObject($table, &$object, $key = null)
|
||||
{
|
||||
$fields = array();
|
||||
$values = array();
|
||||
$statement = 'INSERT INTO ' . $this->quoteName($table) . ' (%s) VALUES (%s)';
|
||||
foreach (get_object_vars($object) as $k => $v)
|
||||
{
|
||||
if (is_array($v) or is_object($v) or $v === null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!$this->checkFieldExists($table, $k))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ($k[0] == '_')
|
||||
{
|
||||
// Internal field
|
||||
continue;
|
||||
}
|
||||
if ($k == $key && $key == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$fields[] = $this->quoteName($k);
|
||||
$values[] = $this->quote($v);
|
||||
}
|
||||
// Set the query and execute the insert.
|
||||
$this->setQuery(sprintf($statement, implode(',', $fields), implode(',', $values)));
|
||||
if (!$this->execute())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
$id = $this->insertid();
|
||||
if ($key && $id)
|
||||
{
|
||||
$object->$key = $id;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the auto-incremented value from the last INSERT statement.
|
||||
*
|
||||
* @return integer The value of the auto-increment field from the last inserted row.
|
||||
*/
|
||||
public function insertid()
|
||||
{
|
||||
$this->setQuery('SELECT @@IDENTITY');
|
||||
|
||||
return (int)$this->loadResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the first field of the first row of the result set from the database query.
|
||||
*
|
||||
* @return mixed The return value or null if the query failed.
|
||||
*/
|
||||
public function loadResult()
|
||||
{
|
||||
$ret = null;
|
||||
|
||||
// Execute the query and get the result set cursor.
|
||||
if (!($cursor = $this->execute()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the first row from the result set as an array.
|
||||
if ($row = sqlsrv_fetch_array($cursor, SQLSRV_FETCH_NUMERIC))
|
||||
{
|
||||
$ret = $row[0];
|
||||
}
|
||||
// Free up system resources and return.
|
||||
$this->freeResult($cursor);
|
||||
|
||||
// For SQLServer - we need to strip slashes
|
||||
$ret = stripslashes($ret);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the SQL statement.
|
||||
*
|
||||
* @return mixed A database cursor resource on success, boolean false on failure.
|
||||
*
|
||||
* @since 11.1
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
static $isReconnecting = false;
|
||||
|
||||
$this->open();
|
||||
|
||||
if (!is_resource($this->connection))
|
||||
{
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Take a local copy so that we don't modify the original query and cause issues later
|
||||
$query = $this->replacePrefix((string)$this->sql);
|
||||
if ($this->limit > 0 || $this->offset > 0)
|
||||
{
|
||||
$query = $this->limit($query, $this->limit, $this->offset);
|
||||
}
|
||||
|
||||
// Increment the query counter.
|
||||
$this->count++;
|
||||
|
||||
// If debugging is enabled then let's log the query.
|
||||
if ($this->debug)
|
||||
{
|
||||
// Add the query to the object queue.
|
||||
$this->log[] = $query;
|
||||
}
|
||||
|
||||
// Reset the error values.
|
||||
$this->errorNum = 0;
|
||||
$this->errorMsg = '';
|
||||
|
||||
// SQLSrv_num_rows requires a static or keyset cursor.
|
||||
if (strncmp(ltrim(strtoupper($query)), 'SELECT', strlen('SELECT')) == 0)
|
||||
{
|
||||
$array = array('Scrollable' => SQLSRV_CURSOR_KEYSET);
|
||||
}
|
||||
else
|
||||
{
|
||||
$array = array();
|
||||
}
|
||||
|
||||
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
|
||||
$this->cursor = @sqlsrv_query($this->connection, $query, array(), $array);
|
||||
|
||||
// If an error occurred handle it.
|
||||
if (!$this->cursor)
|
||||
{
|
||||
// Check if the server was disconnected.
|
||||
if (!$this->connected() && !$isReconnecting)
|
||||
{
|
||||
$isReconnecting = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to reconnect.
|
||||
$this->connection = null;
|
||||
$this->open();
|
||||
}
|
||||
// If connect fails, ignore that exception and throw the normal exception.
|
||||
catch (\RuntimeException $e)
|
||||
{
|
||||
// Get the error number and message.
|
||||
$errors = sqlsrv_errors();
|
||||
$this->errorNum = $errors[0]['SQLSTATE'];
|
||||
$this->errorMsg = $errors[0]['message'] . 'SQL=' . $query;
|
||||
|
||||
// Throw the normal query exception.
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
|
||||
// Since we were able to reconnect, run the query again.
|
||||
$result = $this->query();
|
||||
$isReconnecting = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
// The server was not disconnected.
|
||||
else
|
||||
{
|
||||
// Get the error number and message.
|
||||
$errors = sqlsrv_errors();
|
||||
$this->errorNum = $errors[0]['SQLSTATE'];
|
||||
$this->errorMsg = $errors[0]['message'] . 'SQL=' . $query;
|
||||
|
||||
// Throw the normal query exception.
|
||||
throw new \RuntimeException($this->errorMsg, $this->errorNum);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function replaces a string identifier <var>$prefix</var> with the string held is the
|
||||
* <var>tablePrefix</var> class variable.
|
||||
*
|
||||
* @param string $sql The SQL statement to prepare.
|
||||
* @param string $prefix The common table prefix.
|
||||
*
|
||||
* @return string The processed SQL statement.
|
||||
*
|
||||
* @since 11.1
|
||||
*/
|
||||
public function replacePrefix($query, $prefix = '#__')
|
||||
{
|
||||
$escaped = false;
|
||||
$startPos = 0;
|
||||
$quoteChar = '';
|
||||
$literal = '';
|
||||
|
||||
$query = trim($query);
|
||||
$n = strlen($query);
|
||||
|
||||
while ($startPos < $n)
|
||||
{
|
||||
$ip = strpos($query, $prefix, $startPos);
|
||||
if ($ip === false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$j = strpos($query, "N'", $startPos);
|
||||
$k = strpos($query, '"', $startPos);
|
||||
if (($k !== false) && (($k < $j) || ($j === false)))
|
||||
{
|
||||
$quoteChar = '"';
|
||||
$j = $k;
|
||||
}
|
||||
else
|
||||
{
|
||||
$quoteChar = "'";
|
||||
}
|
||||
|
||||
if ($j === false)
|
||||
{
|
||||
$j = $n;
|
||||
}
|
||||
|
||||
$literal .= str_replace($prefix, $this->tablePrefix, substr($query, $startPos, $j - $startPos));
|
||||
$startPos = $j;
|
||||
|
||||
$j = $startPos + 1;
|
||||
|
||||
if ($j >= $n)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Quote comes first, find end of quote
|
||||
while (true)
|
||||
{
|
||||
$k = strpos($query, $quoteChar, $j);
|
||||
$escaped = false;
|
||||
if ($k === false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
$l = $k - 1;
|
||||
while ($l >= 0 && $query{$l} == '\\')
|
||||
{
|
||||
$l--;
|
||||
$escaped = !$escaped;
|
||||
}
|
||||
if ($escaped)
|
||||
{
|
||||
$j = $k + 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($k === false)
|
||||
{
|
||||
// Error in the query - no end quote; ignore it
|
||||
break;
|
||||
}
|
||||
$literal .= substr($query, $startPos, $k - $startPos + 1);
|
||||
$startPos = $k + 1;
|
||||
}
|
||||
if ($startPos < $n)
|
||||
{
|
||||
$literal .= substr($query, $startPos, $n - $startPos);
|
||||
}
|
||||
|
||||
return $literal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a database for use.
|
||||
*
|
||||
* @param string $database The name of the database to select for use.
|
||||
*
|
||||
* @return boolean True if the database was successfully selected.
|
||||
*
|
||||
* @since 11.1
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function select($database)
|
||||
{
|
||||
if (!$database)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sqlsrv_query($this->connection, 'USE ' . $database, null, array('scrollable' => SQLSRV_CURSOR_STATIC)))
|
||||
{
|
||||
throw new \RuntimeException('Could not connect to database');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connection to use UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*/
|
||||
public function setUTF()
|
||||
{
|
||||
// TODO: Remove this?
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to commit a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionCommit()
|
||||
{
|
||||
$this->setQuery('COMMIT TRANSACTION');
|
||||
$this->query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to roll back a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionRollback()
|
||||
{
|
||||
$this->setQuery('ROLLBACK TRANSACTION');
|
||||
$this->query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to initialize a transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function transactionStart()
|
||||
{
|
||||
$this->setQuery('BEGIN TRANSACTION');
|
||||
$this->query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchArray($cursor = null)
|
||||
{
|
||||
return sqlsrv_fetch_array($cursor ? $cursor : $this->cursor, SQLSRV_FETCH_NUMERIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an associative array.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
public function fetchAssoc($cursor = null)
|
||||
{
|
||||
return sqlsrv_fetch_array($cursor ? $cursor : $this->cursor, SQLSRV_FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to fetch a row from the result set cursor as an object.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
* @param string $class The class name to use for the returned row object.
|
||||
*
|
||||
* @return mixed Either the next row from the result set or false if there are no more rows.
|
||||
*/
|
||||
protected function fetchObject($cursor = null, $class = 'stdClass')
|
||||
{
|
||||
return sqlsrv_fetch_object($cursor ? $cursor : $this->cursor, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to free up the memory used for the result set.
|
||||
*
|
||||
* @param mixed $cursor The optional result set cursor from which to fetch the row.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult($cursor = null)
|
||||
{
|
||||
sqlsrv_free_stmt($cursor ? $cursor : $this->cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check and see if a field exists in a table.
|
||||
*
|
||||
* @param string $table The table in which to verify the field.
|
||||
* @param string $field The field to verify.
|
||||
*
|
||||
* @return boolean True if the field exists in the table.
|
||||
*/
|
||||
protected function checkFieldExists($table, $field)
|
||||
{
|
||||
$table = $this->replacePrefix((string)$table);
|
||||
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS" . " WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" .
|
||||
" ORDER BY ORDINAL_POSITION";
|
||||
$this->setQuery($query);
|
||||
|
||||
if ($this->loadResult())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to wrap an SQL statement to provide a LIMIT and OFFSET behavior for scrolling through a result set.
|
||||
*
|
||||
* @param string $query The SQL statement to process.
|
||||
* @param integer $limit The maximum affected rows to set.
|
||||
* @param integer $offset The affected row offset to set.
|
||||
*
|
||||
* @return string The processed SQL statement.
|
||||
*/
|
||||
protected function limit($query, $limit, $offset)
|
||||
{
|
||||
$orderBy = stristr($query, 'ORDER BY');
|
||||
if (is_null($orderBy) || empty($orderBy))
|
||||
{
|
||||
$orderBy = 'ORDER BY (select 0)';
|
||||
}
|
||||
$query = str_ireplace($orderBy, '', $query);
|
||||
|
||||
$rowNumberText = ',ROW_NUMBER() OVER (' . $orderBy . ') AS RowNumber FROM ';
|
||||
|
||||
$query = preg_replace('/\\s+FROM/', '\\1 ' . $rowNumberText . ' ', $query, 1);
|
||||
$query = 'SELECT TOP ' . $this->limit . ' * FROM (' . $query . ') _myResults WHERE RowNumber > ' . $this->offset;
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a table in the database.
|
||||
*
|
||||
* @param string $oldTable The name of the table to be renamed
|
||||
* @param string $newTable The new name for the table.
|
||||
* @param string $backup Table prefix
|
||||
* @param string $prefix For the table - used to rename constraints in non-mysql databases
|
||||
*
|
||||
* @return Sqlsrv Returns this object to support chaining.
|
||||
*/
|
||||
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
|
||||
{
|
||||
$constraints = array();
|
||||
|
||||
if (!is_null($prefix) && !is_null($backup))
|
||||
{
|
||||
$constraints = $this->getTableConstraints($oldTable);
|
||||
}
|
||||
if (!empty($constraints))
|
||||
{
|
||||
$this->renameConstraints($constraints, $prefix, $backup);
|
||||
}
|
||||
|
||||
$this->setQuery("sp_rename '" . $oldTable . "', '" . $newTable . "'");
|
||||
|
||||
return $this->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a table in the database.
|
||||
*
|
||||
* @param string $tableName The name of the table to lock.
|
||||
*
|
||||
* @return Sqlsrv Returns this object to support chaining.
|
||||
*/
|
||||
public function lockTable($tableName)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks tables in the database.
|
||||
*
|
||||
* @return Sqlsrv Returns this object to support chaining.
|
||||
*/
|
||||
public function unlockTables()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,877 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Core\Domain\Pack;
|
||||
use Akeeba\Engine\Driver\Base as DriverBase;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
abstract class Base extends Part
|
||||
{
|
||||
// **********************************************************************
|
||||
// Configuration parameters
|
||||
// **********************************************************************
|
||||
|
||||
/** @var string Prefix to this database */
|
||||
protected $prefix = '';
|
||||
|
||||
/** @var string MySQL database server host name or IP address */
|
||||
protected $host = '';
|
||||
|
||||
/** @var string MySQL database server port (optional) */
|
||||
protected $port = '';
|
||||
|
||||
/** @var string MySQL user name, for authentication */
|
||||
protected $username = '';
|
||||
|
||||
/** @var string MySQL password, for authentication */
|
||||
protected $password = '';
|
||||
|
||||
/** @var string MySQL database */
|
||||
protected $database = '';
|
||||
|
||||
/** @var string The database driver to use */
|
||||
protected $driver = '';
|
||||
|
||||
/** @var boolean Should I post process quoted values */
|
||||
protected $postProcessValues = false;
|
||||
|
||||
// **********************************************************************
|
||||
// File handling fields
|
||||
// **********************************************************************
|
||||
|
||||
/** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */
|
||||
protected $dumpFile = '';
|
||||
|
||||
/** @var string Data cache, used to cache data before being written to disk */
|
||||
protected $data_cache = '';
|
||||
|
||||
/** @var int */
|
||||
protected $largest_query = 0;
|
||||
|
||||
/** @var int Size of the data cache, default 128Kb */
|
||||
protected $cache_size = 131072;
|
||||
|
||||
/** @var bool Should I process empty prefixes when creating abstracted names? */
|
||||
protected $processEmptyPrefix = true;
|
||||
|
||||
/** @var int Current dump file part number */
|
||||
public $partNumber = 0;
|
||||
|
||||
/** @var resource Filepointer to the current dump part */
|
||||
private $fp = null;
|
||||
|
||||
/** @var string Absolute path to the temp file */
|
||||
protected $tempFile = '';
|
||||
|
||||
/** @var string Relative path of how the file should be saved in the archive */
|
||||
protected $saveAsName = '';
|
||||
|
||||
// **********************************************************************
|
||||
// Protected fields (data handling)
|
||||
// **********************************************************************
|
||||
|
||||
/** @var array Contains the sorted (by dependencies) list of tables/views to backup */
|
||||
protected $tables = array();
|
||||
|
||||
/** @var array Contains the configuration data of the tables */
|
||||
protected $tables_data = array();
|
||||
|
||||
/** @var array Maps database table names to their abstracted format */
|
||||
protected $table_name_map = array();
|
||||
|
||||
/** @var array Contains the dependencies of tables and views (temporary) */
|
||||
protected $dependencies = array();
|
||||
|
||||
/** @var string The next table to backup */
|
||||
protected $nextTable;
|
||||
|
||||
/** @var integer The next row of the table to start backing up from */
|
||||
protected $nextRange;
|
||||
|
||||
/** @var integer Current table's row count */
|
||||
protected $maxRange;
|
||||
|
||||
/** @var bool Use extended INSERTs */
|
||||
protected $extendedInserts = false;
|
||||
|
||||
/** @var integer Maximum packet size for extended INSERTs, in bytes */
|
||||
protected $packetSize = 0;
|
||||
|
||||
/** @var string Extended INSERT query, while it's being constructed */
|
||||
protected $query = '';
|
||||
|
||||
/** @var int Dump part's maximum size */
|
||||
protected $partSize = 0;
|
||||
|
||||
/**
|
||||
* Find where to store the backup files
|
||||
*
|
||||
* @param $partNumber int The SQL part number, default is 0 (.sql)
|
||||
*/
|
||||
protected function getBackupFilePaths($partNumber = 0)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Getting temporary file");
|
||||
$this->tempFile = Factory::getTempFiles()->registerTempFile(dechex(crc32(microtime())) . '.sql');
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Temporary file is {$this->tempFile}");
|
||||
// Get the base name of the dump file
|
||||
$partNumber = intval($partNumber);
|
||||
$baseName = $this->dumpFile;
|
||||
if ($partNumber > 0)
|
||||
{
|
||||
// The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc
|
||||
if (strtolower(substr($baseName, -4)) == '.sql')
|
||||
{
|
||||
$baseName = substr($baseName, 0, -4) . '.s' . sprintf('%02u', $partNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
$baseName = $baseName . '.s' . sprintf('%02u', $partNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->installerSettings))
|
||||
{
|
||||
// Fetch the installer settings
|
||||
$this->installerSettings = (object)array(
|
||||
'installerroot' => 'installation',
|
||||
'sqlroot' => 'installation/sql',
|
||||
'databasesini' => 1,
|
||||
'readme' => 1,
|
||||
'extrainfo' => 1
|
||||
);
|
||||
$config = Factory::getConfiguration();
|
||||
$installerKey = $config->get('akeeba.advanced.embedded_installer');
|
||||
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
|
||||
if (array_key_exists($installerKey, $installerDescriptors))
|
||||
{
|
||||
// The selected installer exists, use it
|
||||
$this->installerSettings = (object)$installerDescriptors[$installerKey];
|
||||
}
|
||||
elseif (array_key_exists('angie', $installerDescriptors))
|
||||
{
|
||||
// The selected installer doesn't exist, but ANGIE exists; use that instead
|
||||
$this->installerSettings = (object)$installerDescriptors['angie'];
|
||||
}
|
||||
}
|
||||
|
||||
switch (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal'))
|
||||
{
|
||||
case 'output':
|
||||
// The SQL file will be stored uncompressed in the output directory
|
||||
$statistics = Factory::getStatistics();
|
||||
$statRecord = $statistics->getRecord();
|
||||
$this->saveAsName = $statRecord['absolute_path'];
|
||||
break;
|
||||
|
||||
case 'normal':
|
||||
// The SQL file will be stored in the SQL root of the archive, as
|
||||
// specified by the particular embedded installer's settings
|
||||
$this->saveAsName = $this->installerSettings->sqlroot . '/' . $baseName;
|
||||
break;
|
||||
|
||||
case 'short':
|
||||
// The SQL file will be stored on archive's root
|
||||
$this->saveAsName = $baseName;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($partNumber > 0)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
|
||||
}
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any leftover files from previous backup attempts
|
||||
*
|
||||
*/
|
||||
protected function removeOldFiles()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "AkeebaDomainDBBackup :: Deleting leftover files, if any");
|
||||
if (file_exists($this->tempFile))
|
||||
{
|
||||
@unlink($this->tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the table arrays with the information for the db entities to backup
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected abstract function getTablesToBackup();
|
||||
|
||||
/**
|
||||
* Runs a step of the database dump
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected abstract function stepDatabaseDump();
|
||||
|
||||
/**
|
||||
* Implements the _prepare abstract method
|
||||
*
|
||||
*/
|
||||
protected function _prepare()
|
||||
{
|
||||
$this->setStep('Initialization');
|
||||
$this->setSubstep('');
|
||||
|
||||
// Process parameters, passed to us using the setup() public method
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processing parameters");
|
||||
if (is_array($this->_parametersArray))
|
||||
{
|
||||
$this->driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : $this->driver;
|
||||
$this->host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : $this->host;
|
||||
$this->port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : $this->port;
|
||||
$this->username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : $this->username;
|
||||
$this->username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $this->username;
|
||||
$this->password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : $this->password;
|
||||
$this->database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : $this->database;
|
||||
$this->prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : $this->prefix;
|
||||
$this->dumpFile = array_key_exists('dumpFile', $this->_parametersArray) ? $this->_parametersArray['dumpFile'] : $this->dumpFile;
|
||||
$this->processEmptyPrefix = array_key_exists('process_empty_prefix', $this->_parametersArray) ? $this->_parametersArray['process_empty_prefix'] : $this->processEmptyPrefix;
|
||||
}
|
||||
|
||||
// Make sure we have self-assigned the first part
|
||||
$this->partNumber = 0;
|
||||
|
||||
// Get DB backup only mode
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Find tables to be included and put them in the $_tables variable
|
||||
$this->getTablesToBackup();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Find where to store the database backup files
|
||||
$this->getBackupFilePaths($this->partNumber);
|
||||
|
||||
// Remove any leftovers
|
||||
$this->removeOldFiles();
|
||||
|
||||
// Initialize the extended INSERTs feature
|
||||
$this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0);
|
||||
$this->packetSize = $configuration->get('engine.dump.common.packet_size', 0);
|
||||
if ($this->packetSize == 0)
|
||||
{
|
||||
$this->extendedInserts = false;
|
||||
}
|
||||
|
||||
// Initialize the split dump feature
|
||||
$this->partSize = $configuration->get('engine.dump.common.splitsize', 1048576);
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
|
||||
{
|
||||
$this->partSize = 0;
|
||||
}
|
||||
if (($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize))
|
||||
{
|
||||
$this->packetSize = $this->partSize / 2;
|
||||
}
|
||||
|
||||
// Initialize the algorithm
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Initializing algorithm for first run");
|
||||
$this->nextTable = array_shift($this->tables);
|
||||
|
||||
// If there is no table to back up we are done with the database backup
|
||||
if (empty($this->nextTable))
|
||||
{
|
||||
$this->setState('postrun');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->nextRange = 0;
|
||||
$this->query = '';
|
||||
|
||||
// FIX 2.2: First table of extra databases was not being written to disk.
|
||||
// This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in
|
||||
// _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's
|
||||
// definition and first chunk of its data) were not written to disk. This call causes $fp to be
|
||||
// nullified, causing it to be recreated, pointing to the correct file.
|
||||
$null = null;
|
||||
$this->writeline($null);
|
||||
|
||||
// Finally, mark ourselves "prepared".
|
||||
$this->setState('prepared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _run() abstract method
|
||||
*/
|
||||
protected function _run()
|
||||
{
|
||||
// Check if we are already done
|
||||
if ($this->getState() == 'postrun')
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished");
|
||||
$this->setStep("");
|
||||
$this->setSubstep("");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark ourselves as still running (we will test if we actually do towards the end ;) )
|
||||
$this->setState('running');
|
||||
|
||||
/**
|
||||
* Resume packing / post-processing part files if necessary.
|
||||
*
|
||||
* @see \Akeeba\Engine\Archiver\BaseArchiver::putDataFromFileIntoArchive
|
||||
*
|
||||
* Sometimes the SQL part file may be bigger than the big file threshold (engine.archiver.common.
|
||||
* big_file_threshold). In this case when we try to add it to the backup archive the archiver engine figures
|
||||
* out it has to be added uncompressed, one chunk (engine.archiver.common.chunk_size) bytes at a time. This
|
||||
* happens in a loop. We read a chunk, push it to the archive, rinse and repeat.
|
||||
*
|
||||
* There are two cases when we might break the loop:
|
||||
*
|
||||
* 1. Not enough free space in the backup archive part and engine.postproc.common.after_part (immediate post-
|
||||
* processing) is enabled. We break the step to let the backup part be post-processed.
|
||||
*
|
||||
* 2. We ran out of time copying data.
|
||||
*
|
||||
* The following if-blocks deal with these two cases.
|
||||
*/
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
|
||||
{
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Case 1. Immediate post-processing was triggered in the previous step
|
||||
if ($configuration->get('engine.postproc.common.after_part', 0) && !empty($archiver->finishedPart))
|
||||
{
|
||||
if (Pack::postProcessDonePartFile($this, $archiver, $configuration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We had already started archiving the db file, but it needs more time
|
||||
if ($configuration->get('volatile.engine.archiver.processingfile', false))
|
||||
{
|
||||
/**
|
||||
* We MUST NOT try to continue adding the file to the backup archive manually. Instead, we have to go
|
||||
* through getNextDumpPart. This method will continue adding the part to the backup archive and when
|
||||
* this is done it will remove the file and create a new one.
|
||||
*
|
||||
* If that method returns false it means that we either hit an error or the archiver didn't have enough
|
||||
* time to add the part to the backup archive. In either case we have to return and let the Engine step.
|
||||
*/
|
||||
if ($this->getNextDumpPart() === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->stepDatabaseDump();
|
||||
|
||||
$null = null;
|
||||
$this->writeline($null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the _finalize() abstract method
|
||||
*
|
||||
*/
|
||||
protected function _finalize()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Adding any extra SQL statements imposed by the filters");
|
||||
$filters = Factory::getFilters();
|
||||
$this->writeline($filters->getExtraSQL($this->databaseRoot));
|
||||
|
||||
// Close the file pointer (otherwise the SQL file is left behind)
|
||||
$this->closeFile();
|
||||
|
||||
// If we are not just doing a main db only backup, add the SQL file to the archive
|
||||
$finished = true;
|
||||
$configuration = Factory::getConfiguration();
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
|
||||
{
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('volatile.engine.archiver.processingfile', false))
|
||||
{
|
||||
// We had already started archiving the db file, but it needs more time
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Continuing adding the SQL dump to the archive");
|
||||
$archiver->addFile(null, null, null);
|
||||
|
||||
$this->propagateFromObject($archiver);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have to add the dump file to the archive
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Adding the final SQL dump to the archive");
|
||||
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
|
||||
|
||||
$this->propagateFromObject($archiver);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We just have to move the dump file to its final destination
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Moving the SQL dump to its final location");
|
||||
$result = Platform::getInstance()->move($this->tempFile, $this->saveAsName);
|
||||
|
||||
if (!$result)
|
||||
{
|
||||
$this->setError('Could not move the SQL dump to its final location');
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that if the archiver needs more time to process the file we can supply it
|
||||
if ($finished)
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Removing temporary file of final SQL dump");
|
||||
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setState('finished');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dump part
|
||||
*/
|
||||
protected function getNextDumpPart()
|
||||
{
|
||||
// On database dump only mode we mustn't create part files!
|
||||
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the archiver still processing?
|
||||
$configuration = Factory::getConfiguration();
|
||||
$archiver = Factory::getArchiverEngine();
|
||||
$stillProcessing = $configuration->get('volatile.engine.archiver.processingfile', false);
|
||||
|
||||
if ($stillProcessing)
|
||||
{
|
||||
/**
|
||||
* The archiver is still adding the previous dump part. This means that we are called from the top few lines
|
||||
* of the _run method. We must continue adding the previous dump part.
|
||||
*/
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Continuing adding the SQL dump part to the archive");
|
||||
$result = $archiver->addFile('', '', '');
|
||||
}
|
||||
else
|
||||
{
|
||||
/**
|
||||
* There is no other dump part being processed. Therefore the current SQL dump part is still open. We must
|
||||
* close it and ask the archiver to add it to the backup archive.
|
||||
*/
|
||||
$this->closeFile();
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Adding the SQL dump part to the archive");
|
||||
$result = $archiver->addFileRenamed($this->tempFile, $this->saveAsName);
|
||||
}
|
||||
|
||||
// Propagate errors and warnings from the archiver
|
||||
$this->propagateFromObject($archiver);
|
||||
|
||||
// Did the archiver return an error?
|
||||
if ($this->getError())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return false if the file didn't finish getting added to the archive
|
||||
if ($configuration->get('volatile.engine.archiver.processingfile', false))
|
||||
{
|
||||
Factory::getLog()->debug("The SQL dump file has not been processed thoroughly by the archiver. Resuming in the next step.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you are here the SQL dump part file is completely added to the backup archive. All we have to do now is
|
||||
* remove it and create a new dump part file.
|
||||
*/
|
||||
|
||||
// Remove the old file
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Removing dump part's temporary file");
|
||||
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
|
||||
|
||||
// Create the new dump part
|
||||
$this->partNumber++;
|
||||
$this->getBackupFilePaths($this->partNumber);
|
||||
$null = null;
|
||||
$this->writeline($null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dump part, but only if required to do so
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function createNewPartIfRequired()
|
||||
{
|
||||
if ($this->partSize == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$filesize = 0;
|
||||
|
||||
if (@file_exists($this->tempFile))
|
||||
{
|
||||
$filesize = @filesize($this->tempFile);
|
||||
}
|
||||
|
||||
$projectedSize = $filesize + strlen($this->query);
|
||||
|
||||
if ($this->extendedInserts)
|
||||
{
|
||||
$projectedSize = $filesize + $this->packetSize;
|
||||
}
|
||||
|
||||
if ($projectedSize > $this->partSize)
|
||||
{
|
||||
return $this->getNextDumpPart();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a table's abstract name (replacing the prefix with the magic #__ string)
|
||||
*
|
||||
* @param string $tableName The canonical name, e.g. 'jos_content'
|
||||
*
|
||||
* @return string The abstract name, e.g. '#__content'
|
||||
*/
|
||||
protected function getAbstract($tableName)
|
||||
{
|
||||
// Don't return abstract names for non-CMS tables
|
||||
if (is_null($this->prefix))
|
||||
{
|
||||
return $tableName;
|
||||
}
|
||||
|
||||
switch ($this->prefix)
|
||||
{
|
||||
case '':
|
||||
if ($this->processEmptyPrefix)
|
||||
{
|
||||
// This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty.
|
||||
return '#__' . $tableName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't
|
||||
// assume anything.
|
||||
return $tableName;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Normal behaviour for 99% of sites
|
||||
// Fix 2.4 : Abstracting the prefix only if it's found in the beginning of the table name
|
||||
$tableAbstract = $tableName;
|
||||
if (!empty($this->prefix))
|
||||
{
|
||||
if (substr($tableName, 0, strlen($this->prefix)) == $this->prefix)
|
||||
{
|
||||
$tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
|
||||
}
|
||||
else
|
||||
{
|
||||
// FIX 2.4: If there is no prefix, it's a non-core table.
|
||||
$tableAbstract = $tableName;
|
||||
}
|
||||
}
|
||||
|
||||
return $tableAbstract;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the SQL dump into the output files. If it fails, it sets the error
|
||||
*
|
||||
* @param string $data Data to write to the dump file. Pass NULL to force flushing to file.
|
||||
*
|
||||
* @return boolean TRUE on successful write, FALSE otherwise
|
||||
*/
|
||||
protected function writeDump(&$data)
|
||||
{
|
||||
if (!empty($data))
|
||||
{
|
||||
$this->data_cache .= $data;
|
||||
|
||||
if (strlen($data) > $this->largest_query)
|
||||
{
|
||||
$this->largest_query = strlen($data);
|
||||
Factory::getConfiguration()->set('volatile.database.largest_query', $this->largest_query);
|
||||
}
|
||||
|
||||
}
|
||||
if ((strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache))))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Writing " . strlen($this->data_cache) . " bytes to the dump file");
|
||||
$result = $this->writeline($this->data_cache);
|
||||
if (!$result)
|
||||
{
|
||||
$errorMessage = 'Couldn\'t write to the SQL dump file ' . $this->tempFile . '; check the temporary directory permissions and make sure you have enough disk space available.';
|
||||
$this->setError($errorMessage);
|
||||
|
||||
return false;
|
||||
}
|
||||
$this->data_cache = '';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving
|
||||
* failed, return value is FALSE.
|
||||
*
|
||||
* @param string $fileData Data to write. Set to null to close the file handle.
|
||||
*
|
||||
* @return boolean TRUE is saving to the file succeeded
|
||||
*/
|
||||
protected function writeline(&$fileData)
|
||||
{
|
||||
if (!is_resource($this->fp))
|
||||
{
|
||||
$this->fp = @fopen($this->tempFile, 'a');
|
||||
if ($this->fp === false)
|
||||
{
|
||||
$this->setError('Could not open ' . $this->tempFile . ' for append, in DB dump.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($fileData))
|
||||
{
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
$this->fp = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($this->fp)
|
||||
{
|
||||
$ret = fwrite($this->fp, $fileData);
|
||||
@clearstatcache();
|
||||
|
||||
// Make sure that all data was written to disk
|
||||
return ($ret == strlen($fileData));
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _onSerialize()
|
||||
{
|
||||
$this->closeFile();
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
{
|
||||
$this->closeFile();
|
||||
}
|
||||
|
||||
public function closeFile()
|
||||
{
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Closing SQL dump file.");
|
||||
|
||||
@fclose($this->fp);
|
||||
$this->fp = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of DriverBase
|
||||
*
|
||||
* @return DriverBase
|
||||
*/
|
||||
protected function &getDB()
|
||||
{
|
||||
$host = $this->host . ($this->port != '' ? ':' . $this->port : '');
|
||||
$user = $this->username;
|
||||
$password = $this->password;
|
||||
$driver = $this->driver;
|
||||
$database = $this->database;
|
||||
$prefix = is_null($this->prefix) ? '' : $this->prefix;
|
||||
$options = array('driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'prefix' => $prefix);
|
||||
|
||||
$db = Factory::getDatabase($options);
|
||||
|
||||
if ($error = $db->getError())
|
||||
{
|
||||
$this->setError(__CLASS__ . ' :: Database Error: ' . $error);
|
||||
$false = false;
|
||||
|
||||
return $false;
|
||||
}
|
||||
|
||||
if ($db->getErrorNum() > 0)
|
||||
{
|
||||
$error = $db->getErrorMsg();
|
||||
$this->setError(__CLASS__ . ' :: Database Error: ' . $error);
|
||||
$false = false;
|
||||
|
||||
return $false;
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getDatabaseNameFromConnection();
|
||||
|
||||
/**
|
||||
* Returns the database name. If the name was not declared when the object was created we will go through the
|
||||
* getDatabaseNameFromConnection method to populate it.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDatabaseName()
|
||||
{
|
||||
if (empty($this->database))
|
||||
{
|
||||
$this->database = $this->getDatabaseNameFromConnection();
|
||||
}
|
||||
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function callStage($stage)
|
||||
{
|
||||
switch ($stage)
|
||||
{
|
||||
case '_prepare':
|
||||
return $this->_prepare();
|
||||
break;
|
||||
|
||||
case '_run':
|
||||
return $this->_run();
|
||||
break;
|
||||
|
||||
case '_finalize':
|
||||
return $this->_finalize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process a quoted value before it's written to the database dump.
|
||||
* So far it's only required for SQL Server which has a problem escaping
|
||||
* newline characters...
|
||||
*
|
||||
* @param string $value The quoted value to post-process
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function postProcessQuotedValue($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preamble for the data dump portion of the SQL backup. This is
|
||||
* used to output commands before the first INSERT INTO statement for a
|
||||
* table when outputting a plain SQL file.
|
||||
*
|
||||
* Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server
|
||||
*
|
||||
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
|
||||
* @param string $tableName Real name of the table, e.g. abc_foobar
|
||||
* @param integer $maxRange Row count on this table
|
||||
*
|
||||
* @return string The SQL commands you want to be written in the dump file
|
||||
*/
|
||||
protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an epilogue for the data dump portion of the SQL backup. This is
|
||||
* used to output commands after the last INSERT INTO statement for a
|
||||
* table when outputting a plain SQL file.
|
||||
*
|
||||
* Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server
|
||||
*
|
||||
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
|
||||
* @param string $tableName Real name of the table, e.g. abc_foobar
|
||||
* @param integer $maxRange Row count on this table
|
||||
*
|
||||
* @return string The SQL commands you want to be written in the dump file
|
||||
*/
|
||||
protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of field names for the INSERT INTO statements. This is only
|
||||
* required for Microsoft SQL Server because without it the SET IDENTITY_INSERT
|
||||
* has no effect.
|
||||
*
|
||||
* @param array $fieldNames A list of field names in array format
|
||||
* @param integer $numOfFields The number of fields we should be dumping
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFieldListSQL($fieldNames, $numOfFields)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Dump\Base as DumpBase;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class Native extends Part
|
||||
{
|
||||
/** @var DumpBase */
|
||||
private $_engine = null;
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Native
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
protected function _prepare()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processing parameters");
|
||||
|
||||
$options = null;
|
||||
|
||||
// Get the DB connection parameters
|
||||
if (is_array($this->_parametersArray))
|
||||
{
|
||||
$driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : 'mysql';
|
||||
$host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : '';
|
||||
$port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : '';
|
||||
$username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : '';
|
||||
$username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $username;
|
||||
$password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : '';
|
||||
$database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : '';
|
||||
$prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : '';
|
||||
|
||||
if (($driver == 'mysql') && !function_exists('mysql_connect'))
|
||||
{
|
||||
$driver = 'mysqli';
|
||||
}
|
||||
|
||||
$options = array(
|
||||
'driver' => $driver,
|
||||
'host' => $host . ($port != '' ? ':' . $port : ''),
|
||||
'user' => $username,
|
||||
'password' => $password,
|
||||
'database' => $database,
|
||||
'prefix' => is_null($prefix) ? '' : $prefix
|
||||
);
|
||||
}
|
||||
|
||||
$db = Factory::getDatabase($options);
|
||||
|
||||
$driverType = $db->getDriverType();
|
||||
$className = '\\Akeeba\\Engine\\Dump\\Native\\' . ucfirst($driverType);
|
||||
|
||||
// Check if we have a native dump driver
|
||||
if (!class_exists($className, true))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Native database dump engine $className not found; trying Reverse Engineering instead");
|
||||
// Native driver nor found, I will try falling back to reverse engineering
|
||||
$className = '\\Akeeba\\Engine\\Dump\\Reverse\\' . ucfirst($driverType);
|
||||
}
|
||||
|
||||
if (!class_exists($className, true))
|
||||
{
|
||||
$this->setState('error', 'Akeeba Engine does not have a native dump engine for ' . $driverType . ' databases');
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Instanciating new native database dump engine $className");
|
||||
$this->_engine = new $className;
|
||||
$this->_engine->setup($this->_parametersArray);
|
||||
$this->_engine->callStage('_prepare');
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->propagateFromObject($this->_engine);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _finalize()
|
||||
{
|
||||
$this->_engine->callStage('_finalize');
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->propagateFromObject($this->_engine);
|
||||
}
|
||||
|
||||
protected function _run()
|
||||
{
|
||||
$this->_engine->callStage('_run');
|
||||
$this->propagateFromObject($this->_engine);
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->setStep($this->_engine->getStep());
|
||||
$this->setSubstep($this->_engine->getSubstep());
|
||||
$this->partNumber = $this->_engine->partNumber;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Native;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Reverse\None as ReverseDumpEngine;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
|
||||
/**
|
||||
* Dump class for the "None" database driver (ie no database used by the application)
|
||||
*
|
||||
*/
|
||||
class None extends ReverseDumpEngine
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "There is no native engine for backing up databases with the None driver. Using the Reverse Engineering class instead.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Native;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Reverse\Postgresql as ReverseDumpEngine;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
|
||||
/**
|
||||
* Dump class for the "None" database driver (ie no database used by the application)
|
||||
*
|
||||
*/
|
||||
class Postgresql extends ReverseDumpEngine
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "There is no native engine for backing up PostgreSQL databases. Using the Reverse Engineering class instead.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Native;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Reverse\Sqlite as ReverseDumpEngine;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
|
||||
/**
|
||||
* Dump class for the "None" database driver (ie no database used by the application)
|
||||
*
|
||||
*/
|
||||
class Sqlite extends ReverseDumpEngine
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "There is no native engine for backing up SQLite databases. Using the Reverse Engineering class instead.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Native;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Reverse\Sqlsrv as ReverseDumpEngine;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
|
||||
/**
|
||||
* Dump class for the "None" database driver (ie no database used by the application)
|
||||
*
|
||||
*/
|
||||
class Sqlsrv extends ReverseDumpEngine
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
Factory::getLog()->log(LogLevel::INFO, "There is no native engine for backing up Microsoft SQL Server databases. Using the Reverse Engineering class instead.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\Part;
|
||||
use Akeeba\Engine\Dump\Base as DumpBase;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class Reverse extends Part
|
||||
{
|
||||
/** @var DumpBase */
|
||||
private $_engine = null;
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Reverse
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
protected function _prepare()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processing parameters");
|
||||
|
||||
// Get the DB connection parameters
|
||||
if (is_array($this->_parametersArray))
|
||||
{
|
||||
$driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : 'mysql';
|
||||
$host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : '';
|
||||
$port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : '';
|
||||
$username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : '';
|
||||
$username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $username;
|
||||
$password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : '';
|
||||
$database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : '';
|
||||
$prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : '';
|
||||
}
|
||||
|
||||
if (($driver == 'mysql') && !function_exists('mysql_connect'))
|
||||
{
|
||||
$driver = 'mysqli';
|
||||
}
|
||||
|
||||
$options = array(
|
||||
'driver' => $driver,
|
||||
'host' => $host . ($port != '' ? ':' . $port : ''),
|
||||
'user' => $username,
|
||||
'password' => $password,
|
||||
'database' => $database,
|
||||
'prefix' => is_null($prefix) ? '' : $prefix
|
||||
);
|
||||
$db = Factory::getDatabase($options);
|
||||
|
||||
$driverType = $db->getDriverType();
|
||||
if ($driverType == 'mssql')
|
||||
{
|
||||
$driverType = 'sqlsrv';
|
||||
}
|
||||
$className = '\\Akeeba\\Engine\\Dump\\Reverse\\' . ucfirst($driverType);
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Instanciating new reverse engineering database dump engine $className");
|
||||
if (!class_exists($className, true))
|
||||
{
|
||||
$this->setState('error', 'Akeeba Engine does not have a reverse engineering dump engine for ' . $driverType . ' databases');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->_engine = new $className;
|
||||
$this->_engine->setup($this->_parametersArray);
|
||||
$this->_engine->callStage('_prepare');
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->propagateFromObject($this->_engine);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _finalize()
|
||||
{
|
||||
$this->_engine->callStage('_finalize');
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->propagateFromObject($this->_engine);
|
||||
}
|
||||
|
||||
protected function _run()
|
||||
{
|
||||
$this->_engine->callStage('_run');
|
||||
$this->setState($this->_engine->getState(), $this->_engine->getError());
|
||||
$this->propagateFromObject($this->_engine);
|
||||
$this->setStep($this->_engine->getStep());
|
||||
$this->setSubstep($this->_engine->getSubstep());
|
||||
$this->partNumber = $this->_engine->partNumber;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,597 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Reverse;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Base as DriverBase;
|
||||
use Akeeba\Engine\Dump\Native\Mysql as NativeMysql;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* A MySQL database dump class, using reverse engineering of the
|
||||
* INFORMATION_SCHEMA views to deduce the DDL of the database entities.
|
||||
*
|
||||
* Configuration parameters:
|
||||
* host <string> MySQL database server host name or IP address
|
||||
* port <string> MySQL database server port (optional)
|
||||
* username <string> MySQL user name, for authentication
|
||||
* password <string> MySQL password, for authentication
|
||||
* database <string> MySQL database
|
||||
* dumpFile <string> Absolute path to dump file; must be writable (optional; if left blank it is
|
||||
* automatically calculated)
|
||||
*/
|
||||
class Mysql extends NativeMysql
|
||||
{
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Mysql
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
protected function getTablesToBackup()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
// First, get a map of table names <--> abstract names
|
||||
$this->reverse_engineer_db();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$notracking)
|
||||
{
|
||||
// Process dependencies and rearrange tables respecting them
|
||||
$this->process_dependencies();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove dependencies array
|
||||
$this->dependencies = array();
|
||||
}
|
||||
}
|
||||
|
||||
protected function reverse_engineer_db()
|
||||
{
|
||||
// Get a database connection
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering database");
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset internal tables
|
||||
$this->table_name_map = array();
|
||||
$this->tables_data = array();
|
||||
$this->dependencies = array();
|
||||
|
||||
// Clone the db object and point it to information_schema
|
||||
$dbi = clone $this->getDB();
|
||||
if (!$dbi->select('information_schema'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::ERROR, __CLASS__ . " :: Could not connect to the INFORMATION_SCHEMA database");
|
||||
}
|
||||
|
||||
// Get the list of all database tables and views
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering Tables");
|
||||
$this->reverse_engineer_tables($dbi);
|
||||
$this->reverse_engineer_views($dbi);
|
||||
|
||||
// If we have MySQL > 5.0 add the list of stored procedures, stored functions
|
||||
// and triggers, but only if user has allows that and the target compatibility is
|
||||
// not MySQL 4! Also, if dependency tracking is disabled, we won't dump triggers,
|
||||
// functions and procedures.
|
||||
$registry = Factory::getConfiguration();
|
||||
$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
|
||||
$notracking = $registry->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
if ($enable_entities && ($notracking == 0))
|
||||
{
|
||||
// @todo Triggers
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Triggers");
|
||||
|
||||
// @todo Functions
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Functions");
|
||||
|
||||
// @todo Procedures
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Procedures");
|
||||
}
|
||||
|
||||
$dbi->select($this->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the Table definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_tables(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM `tables` WHERE `table_schema` = ' . $dbi->quote($schema_name);
|
||||
$dbi->setQuery($sql);
|
||||
$all_tables = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
if (!empty($all_tables))
|
||||
{
|
||||
foreach ($all_tables as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->TABLE_NAME;
|
||||
|
||||
// Skip system objects
|
||||
if ($table_object->TABLE_TYPE != 'BASE TABLE')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Still here? The table is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'table',
|
||||
'dump_records' => true
|
||||
);
|
||||
|
||||
switch ($table_object->ENGINE)
|
||||
{
|
||||
// Merge tables
|
||||
case 'MRG_MYISAM':
|
||||
$new_entry['type'] = 'merge';
|
||||
$new_entry['dump_records'] = false;
|
||||
break;
|
||||
|
||||
// Tables whose data we do not back up (memory, federated and can-have-no-data tables)
|
||||
case 'MEMORY':
|
||||
case 'EXAMPLE':
|
||||
case 'BLACKHOLE':
|
||||
case 'FEDERATED':
|
||||
$new_entry['dump_records'] = false;
|
||||
break;
|
||||
|
||||
// Normal tables
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Table Data Filter - skip dumping table contents of filtered out tables
|
||||
if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
|
||||
{
|
||||
$new_entry['dump_records'] = false;
|
||||
}
|
||||
|
||||
$new_entry['create'] = $this->get_create_table($dbi, $table_name, $table_abstract, $table_object, $dependencies);
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CREATE TABLE command of a given table
|
||||
*
|
||||
* @param DriverBase $dbi The db connection to the INFORMATION_SCHEMA db
|
||||
* @param string $table_name The name of the table
|
||||
* @param string $table_abstract The abstract name of the table
|
||||
* @param \stdClass $table_object The TABLES object for this table
|
||||
* @param array $dependencies Dependency tracking information
|
||||
*
|
||||
* @return string The CREATE TABLE definition
|
||||
*/
|
||||
protected function get_create_table(&$dbi, $table_name, $table_abstract, $table_object, &$dependencies)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
$useabstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1);
|
||||
|
||||
$columns_sql = array();
|
||||
$keys_sql = array();
|
||||
$constraints_sql = array();
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR COLUMNS
|
||||
// =====================================================================
|
||||
|
||||
// Get columns
|
||||
$query = 'SELECT * FROM `columns` WHERE `table_schema` = ' . $dbi->quote($this->database)
|
||||
. ' AND `table_name` = ' . $dbi->quote($table_name) . ' ORDER BY `ordinal_position` ASC';
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
$line = $dbi->quoteName($oColumn->COLUMN_NAME) . ' ' . $oColumn->COLUMN_TYPE . ' ';
|
||||
$line .= ($oColumn->IS_NULLABLE == 'YES') ? 'NULL ' : 'NOT NULL ';
|
||||
if (!in_array($oColumn->EXTRA, array('CURRENT_TIMESTAMP')))
|
||||
{
|
||||
$line .= ($oColumn->EXTRA == '') ? '' : ($oColumn->EXTRA . ' ');
|
||||
}
|
||||
if (in_array($oColumn->COLUMN_DEFAULT, array('CURRENT_TIMESTAMP')))
|
||||
{
|
||||
$line .= ' DEFAULT ' . $oColumn->COLUMN_DEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
$line .= ($oColumn->COLUMN_DEFAULT == '') ? '' : (' DEFAULT ' . $dbi->quote($oColumn->COLUMN_DEFAULT) . ' ');
|
||||
}
|
||||
$line .= ($oColumn->COLUMN_COMMENT == '') ? '' : (' COMMENT ' . $dbi->quote($oColumn->COLUMN_COMMENT));
|
||||
$columns_sql[] = $line;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR KEYS AND INDICES
|
||||
// =====================================================================
|
||||
|
||||
// Get the primary and unique key names
|
||||
$query = 'SELECT `constraint_name`, `constraint_type` FROM `table_constraints` WHERE `table_schema` = ' . $dbi->quote($this->database)
|
||||
. ' AND `table_name` = ' . $dbi->quote($table_name) . ' AND `constraint_type` IN(' .
|
||||
$dbi->quote('PRIMARY KEY') . ', ' . $dbi->quote('UNIQUE') . ')';
|
||||
$dbi->setQuery($query);
|
||||
$specialKeys = $dbi->loadObjectList('constraint_name');
|
||||
|
||||
// Get the columns per key and key information
|
||||
$query = 'SELECT * FROM `statistics` WHERE `table_schema` = ' . $dbi->quote($this->database)
|
||||
. ' AND `table_name` = ' . $dbi->quote($table_name) . ' ORDER BY `INDEX_NAME` ASC, SEQ_IN_INDEX ASC';
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
$rawKeys = array();
|
||||
if (!empty($allColumns))
|
||||
{
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
if (!array_key_exists($oColumn->INDEX_NAME, $rawKeys))
|
||||
{
|
||||
$entry = array(
|
||||
'name' => $oColumn->INDEX_NAME,
|
||||
'def' => 'KEY',
|
||||
'columns' => array(),
|
||||
'type' => 'BTREE',
|
||||
);
|
||||
if (array_key_exists($oColumn->INDEX_NAME, $specialKeys))
|
||||
{
|
||||
$entry['def'] = $specialKeys[$oColumn->INDEX_NAME]->constraint_type;
|
||||
if ($entry['def'] == 'UNIQUE')
|
||||
{
|
||||
$entry['def'] = 'UNIQUE KEY';
|
||||
}
|
||||
elseif ($entry['def'] == 'PRIMARY')
|
||||
{
|
||||
$entry['def'] = 'PRIMARY KEY';
|
||||
}
|
||||
}
|
||||
$rawKeys[$oColumn->INDEX_NAME] = $entry;
|
||||
}
|
||||
|
||||
// This is the optional key length for each column
|
||||
$subpart = '';
|
||||
if ($oColumn->SUB_PART)
|
||||
{
|
||||
$subpart = '(' . $oColumn->SUB_PART . ')';
|
||||
}
|
||||
// Add the column to the index
|
||||
$rawKeys[$oColumn->INDEX_NAME]['columns'][] = '`' . $oColumn->COLUMN_NAME . '`' . $subpart;
|
||||
// Setup the index type
|
||||
$rawKeys[$oColumn->INDEX_NAME]['type'] = $oColumn->INDEX_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
// Piece together the keys' SQL statements
|
||||
if (!empty($rawKeys))
|
||||
{
|
||||
foreach ($rawKeys as $keydef)
|
||||
{
|
||||
$line = ' ' . $keydef['def'] . ' ';
|
||||
if ($keydef['type'] == 'FULLTEXT')
|
||||
{
|
||||
$line = ' ' . $keydef['type'] . $line;
|
||||
$keydef['type'] = '';
|
||||
}
|
||||
if ($keydef['def'] != 'PRIMARY KEY')
|
||||
{
|
||||
$line .= "`{$keydef['name']}` ";
|
||||
}
|
||||
$line .= '(';
|
||||
$line .= implode(',', $keydef['columns']);
|
||||
$line .= ')';
|
||||
|
||||
if (!empty($keydef['type']) && ($keydef['def'] != 'PRIMARY KEY'))
|
||||
{
|
||||
$line .= ' USING ' . $keydef['type'];
|
||||
}
|
||||
|
||||
$keys_sql[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR CONSTRAINTS
|
||||
// =====================================================================
|
||||
// Get the foreign key names
|
||||
$query = 'SELECT * FROM `referential_constraints` WHERE `constraint_schema` = ' . $dbi->quote($this->database)
|
||||
. ' AND `table_name` = ' . $dbi->quote($table_name);
|
||||
$dbi->setQuery($query);
|
||||
$foreignKeyInfo = $dbi->loadObjectList('CONSTRAINT_NAME');
|
||||
|
||||
// Get the columns per key and key information
|
||||
$query = 'SELECT * FROM `key_column_usage` WHERE `constraint_schema` = ' . $dbi->quote($this->database)
|
||||
. ' AND `table_name` = ' . $dbi->quote($table_name) . ' AND `referenced_table_name` IS NOT NULL';
|
||||
$dbi->setQuery($query);
|
||||
$allFKColumns = $dbi->loadObjectList();
|
||||
$rawConstraints = array();
|
||||
|
||||
if (!empty($allFKColumns))
|
||||
{
|
||||
foreach ($allFKColumns as $oColumn)
|
||||
{
|
||||
if (!array_key_exists($oColumn->CONSTRAINT_NAME, $rawConstraints))
|
||||
{
|
||||
$entry = array(
|
||||
'name' => $oColumn->CONSTRAINT_NAME,
|
||||
'cols' => array(),
|
||||
'refcols' => array(),
|
||||
'reftable' => '',
|
||||
'update' => '',
|
||||
'delete' => '',
|
||||
);
|
||||
if ($useabstract)
|
||||
{
|
||||
$entry['name'] = $this->getAbstract($entry['name']);
|
||||
}
|
||||
if (array_key_exists($oColumn->CONSTRAINT_NAME, $foreignKeyInfo))
|
||||
{
|
||||
$entry['update'] = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->UPDATE_RULE;
|
||||
$entry['delete'] = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->DELETE_RULE;
|
||||
|
||||
$reftable = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->REFERENCED_TABLE_NAME;
|
||||
// Add a reference hit
|
||||
$this->dependencies[$reftable][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $reftable;
|
||||
|
||||
if ($useabstract)
|
||||
{
|
||||
$reftable = $this->getAbstract($reftable);
|
||||
}
|
||||
$entry['reftable'] = $reftable;
|
||||
}
|
||||
$rawConstraints[$oColumn->CONSTRAINT_NAME] = $entry;
|
||||
}
|
||||
|
||||
$rawConstraints[$oColumn->CONSTRAINT_NAME]['cols'][] = '`' . $oColumn->COLUMN_NAME . '`';
|
||||
$rawConstraints[$oColumn->CONSTRAINT_NAME]['refcols'][] = '`' . $oColumn->REFERENCED_COLUMN_NAME . '`';
|
||||
}
|
||||
}
|
||||
|
||||
// Piece together the constraints' SQL statements
|
||||
if (!empty($rawConstraints))
|
||||
{
|
||||
foreach ($rawConstraints as $keydef)
|
||||
{
|
||||
$line = ' CONSTRAINT ';
|
||||
if ($keydef['name'])
|
||||
{
|
||||
$line .= "`{$keydef['name']}` ";
|
||||
}
|
||||
$line .= 'FOREIGN KEY (';
|
||||
$line .= implode(',', $keydef['cols']);
|
||||
$line .= ') REFERENCES `' . $keydef['reftable'] . '` (';
|
||||
$line .= implode(',', $keydef['refcols']);
|
||||
$line .= ')';
|
||||
if ($keydef['delete'])
|
||||
{
|
||||
$line .= ' ON DELETE ' . $keydef['delete'];
|
||||
}
|
||||
if ($keydef['update'])
|
||||
{
|
||||
$line .= ' ON UPDATE ' . $keydef['update'];
|
||||
}
|
||||
|
||||
$constraints_sql[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =====================================================================
|
||||
// ========== CONSTRUCT THE TABLE CREATE STATEMENT
|
||||
// =====================================================================
|
||||
|
||||
// Create the SQL output
|
||||
if ($useabstract)
|
||||
{
|
||||
$table_sql = "CREATE TABLE `$table_abstract` (";
|
||||
}
|
||||
else
|
||||
{
|
||||
$table_sql = "CREATE TABLE `$table_name` (";
|
||||
}
|
||||
$table_sql .= implode(',', $columns_sql);
|
||||
if (count($keys_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $keys_sql);
|
||||
}
|
||||
if (count($constraints_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $constraints_sql);
|
||||
}
|
||||
$table_sql .= ")";
|
||||
|
||||
// Engine and stuff must also be exported here
|
||||
if ($table_object->ENGINE)
|
||||
{
|
||||
$table_sql .= ' ENGINE=' . $table_object->ENGINE;
|
||||
}
|
||||
if ($table_object->TABLE_COLLATION)
|
||||
{
|
||||
$table_sql .= ' DEFAULT COLLATE ' . $table_object->TABLE_COLLATION;
|
||||
}
|
||||
|
||||
$table_sql .= ";\n";
|
||||
|
||||
return $table_sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the View definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_views(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM `views` WHERE `table_schema` = ' . $dbi->quote($schema_name);
|
||||
$dbi->setQuery($sql);
|
||||
$all_views = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
// First pass: populate the table_name_map
|
||||
if (!empty($all_views))
|
||||
{
|
||||
foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->TABLE_NAME;
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: get the create commands
|
||||
if (!empty($all_views))
|
||||
{
|
||||
foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->TABLE_NAME;
|
||||
|
||||
if (!in_array($table_name, $this->table_name_map))
|
||||
{
|
||||
// Skip any views which have been filtered out
|
||||
continue;
|
||||
}
|
||||
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
|
||||
// Still here? The view is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'view',
|
||||
'dump_records' => false
|
||||
);
|
||||
|
||||
$dependencies = array();
|
||||
$table_sql = 'CREATE OR REPLACE VIEW `' . $table_name . '` AS ' . $table_object->VIEW_DEFINITION;
|
||||
$old_table_sql = $table_sql;
|
||||
foreach ($this->table_name_map as $ref_normal => $ref_abstract)
|
||||
{
|
||||
if ($pos = strpos($table_sql, "`$ref_normal`"))
|
||||
{
|
||||
// Add a reference hit
|
||||
$this->dependencies[$ref_normal][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $ref_normal;
|
||||
// Do the replacement
|
||||
$table_sql = str_replace("`$ref_normal`", "`$ref_abstract`", $table_sql);
|
||||
}
|
||||
}
|
||||
|
||||
// On DB only backup we don't want any replacing to take place, do we?
|
||||
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
|
||||
{
|
||||
$table_sql = $old_table_sql;
|
||||
}
|
||||
|
||||
// Replace newlines with spaces
|
||||
$table_sql = str_replace("\n", " ", $table_sql) . ";\n";
|
||||
$table_sql = str_replace("\r", " ", $table_sql);
|
||||
$table_sql = str_replace("\t", " ", $table_sql);
|
||||
|
||||
$new_entry['create'] = $table_sql;
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Reverse;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Base;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Dump class for the "None" database driver (ie no database used by the application)
|
||||
*
|
||||
*/
|
||||
class None extends Base
|
||||
{
|
||||
/**
|
||||
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDatabaseNameFromConnection()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return None
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs one more step of dumping database data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function stepDatabaseDump()
|
||||
{
|
||||
// We do not create any dump file
|
||||
Factory::getLog()->log(LogLevel::INFO, "No database is used, no SQL dump files will be created.");
|
||||
$this->setState('postrun');
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the database for tables to be backed up and sorts them according to
|
||||
* their dependencies on one another. Updates $this->dependencies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function getTablesToBackup()
|
||||
{
|
||||
// No tables will be included in the backup
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,747 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Reverse;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Base as DriverBase;
|
||||
use Akeeba\Engine\Dump\Native\Mysql as NativeMysql;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* A PostgreSQL database dump class, using reverse engineering of the
|
||||
* INFORMATION_SCHEMA views to deduce the DDL of the database entities.
|
||||
*
|
||||
* Configuration parameters:
|
||||
* host <string> PostgreSQL database server host name or IP address
|
||||
* port <string> PostgreSQL database server port (optional)
|
||||
* username <string> PostgreSQL user name, for authentication
|
||||
* password <string> PostgreSQL password, for authentication
|
||||
* database <string> PostgreSQL database
|
||||
* dumpFile <string> Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated)
|
||||
*/
|
||||
class Postgresql extends NativeMysql
|
||||
{
|
||||
/**
|
||||
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDatabaseNameFromConnection()
|
||||
{
|
||||
$db = $this->getDB();
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $db->setQuery('SELECT current_database()')->loadResult();
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return empty($ret) ? '' : $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Postgresql
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
|
||||
$this->postProcessValues = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the SQL compatibility setting
|
||||
*/
|
||||
protected function enforceSQLCompatibility()
|
||||
{
|
||||
// Do nothing for PostgreSQL
|
||||
}
|
||||
|
||||
protected function getTablesToBackup()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
// First, get a map of table names <--> abstract names
|
||||
$this->reverse_engineer_db();
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$notracking)
|
||||
{
|
||||
// Process dependencies and rearrange tables respecting them
|
||||
$this->process_dependencies();
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove dependencies array
|
||||
$this->dependencies = array();
|
||||
}
|
||||
}
|
||||
|
||||
protected function reverse_engineer_db()
|
||||
{
|
||||
// Get a database connection
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering database");
|
||||
$db = $this->getDB();
|
||||
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset internal tables
|
||||
$this->table_name_map = array();
|
||||
$this->tables_data = array();
|
||||
$this->dependencies = array();
|
||||
|
||||
// Clone the db object and point it to information_schema
|
||||
$dbi = clone $this->getDB();
|
||||
|
||||
if (!$dbi->select('information_schema'))
|
||||
{
|
||||
$this->setError(__CLASS__ . " :: Could not connect to the INFORMATION_SCHEMA database");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the list of all database tables and views
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering Tables");
|
||||
$this->reverse_engineer_tables($dbi);
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering Views");
|
||||
$this->reverse_engineer_views($dbi);
|
||||
|
||||
// If we have MySQL > 5.0 add the list of stored procedures, stored functions
|
||||
// and triggers, but only if user has allows that and the target compatibility is
|
||||
// not MySQL 4! Also, if dependency tracking is disabled, we won't dump triggers,
|
||||
// functions and procedures.
|
||||
$registry = Factory::getConfiguration();
|
||||
$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
|
||||
$notracking = $registry->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
if ($enable_entities && ($notracking == 0))
|
||||
{
|
||||
// @todo Triggers
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Triggers");
|
||||
// @todo Functions
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Functions");
|
||||
// @todo Procedures
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Procedures");
|
||||
}
|
||||
|
||||
$dbi->select($this->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the Table definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_tables(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM information_schema.tables WHERE "table_schema" = \'public\' AND "table_catalog" = ' . $dbi->quote($schema_name) .
|
||||
' AND "table_type" = \'BASE TABLE\'';
|
||||
$dbi->setQuery($sql);
|
||||
$all_tables = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
if (!empty($all_tables))
|
||||
{
|
||||
foreach ($all_tables as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->table_name;
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Still here? The table is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'table',
|
||||
'dump_records' => true
|
||||
);
|
||||
|
||||
switch ($table_object->is_insertable_into)
|
||||
{
|
||||
// Tables which can be inserted to
|
||||
case 'YES':
|
||||
case '1':
|
||||
$new_entry['dump_records'] = true;
|
||||
break;
|
||||
|
||||
// Tables which cannot be inserted to
|
||||
default:
|
||||
$new_entry['dump_records'] = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Table Data Filter - skip dumping table contents of filtered out tables
|
||||
if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
|
||||
{
|
||||
$new_entry['dump_records'] = false;
|
||||
}
|
||||
|
||||
$new_entry['create'] = $this->get_create_table($dbi, $table_name, $table_abstract, $table_object, $dependencies);
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CREATE TABLE command of a given table
|
||||
*
|
||||
* @param DriverBase $dbi The db connection to the INFORMATION_SCHEMA db
|
||||
* @param string $table_name The name of the table
|
||||
* @param string $table_abstract The abstract name of the table
|
||||
* @param \stdClass $table_object The TABLES object for this table
|
||||
* @param array $dependencies Dependency tracking information
|
||||
*
|
||||
* @return string The CREATE TABLE definition
|
||||
*/
|
||||
protected function get_create_table(&$dbi, $table_name, $table_abstract, $table_object, &$dependencies)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
$useabstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1);
|
||||
|
||||
$columns_sql = array();
|
||||
$keys_sql = array();
|
||||
$indexes_sql = array();
|
||||
$constraints_sql = array();
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR COLUMNS
|
||||
// =====================================================================
|
||||
// Get columns
|
||||
$query = 'SELECT * FROM information_schema.columns WHERE "table_catalog" = ' . $dbi->quote($this->database)
|
||||
. ' AND "table_schema" = \'public\''
|
||||
. ' AND "table_name" = ' . $dbi->quote($table_name) . ' ORDER BY "ordinal_position" ASC';
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
$default = $oColumn->column_default;
|
||||
|
||||
if (!empty($default) && (strstr($default, 'nextval(') !== false) && (strstr($default, '::regclass') !== false) && (strstr($oColumn->data_type, 'int') !== false))
|
||||
{
|
||||
$line = $dbi->quoteName($oColumn->column_name) . ' serial NOT NULL';
|
||||
}
|
||||
else
|
||||
{
|
||||
$line = $dbi->quoteName($oColumn->column_name) . ' ' . $oColumn->data_type . ' ';
|
||||
|
||||
if (!empty($oColumn->character_maximum_length) && !in_array($oColumn->data_type, array('text')))
|
||||
{
|
||||
$line .= '(' . $oColumn->character_maximum_length . ') ';
|
||||
}
|
||||
|
||||
$line .= ($oColumn->is_nullable == 'YES') ? 'NULL ' : 'NOT NULL ';
|
||||
|
||||
if (!empty($default))
|
||||
{
|
||||
$line .= ' DEFAULT ' . $default;
|
||||
}
|
||||
}
|
||||
|
||||
$columns_sql[] = $line;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR KEYS AND INDICES
|
||||
// =====================================================================
|
||||
// Get the primary and unique key names
|
||||
$query = 'SELECT "constraint_name", "constraint_type" FROM information_schema.table_constraints WHERE "table_catalog" = ' . $dbi->quote($this->database)
|
||||
. ' AND "table_schema" = \'public\''
|
||||
. ' AND "table_name" = ' . $dbi->quote($table_name) . ' AND "constraint_type" IN(' .
|
||||
$dbi->quote('PRIMARY KEY') . ', ' . $dbi->quote('UNIQUE') . ')';
|
||||
$dbi->setQuery($query);
|
||||
$specialKeys = $dbi->loadObjectList('constraint_name');
|
||||
|
||||
// Get the columns per key and key information
|
||||
$query = 'SELECT * FROM information_schema.constraint_column_usage WHERE "table_catalog" = ' . $dbi->quote($this->database)
|
||||
. ' AND "table_schema" = \'public\''
|
||||
. ' AND "table_name" = ' . $dbi->quote($table_name);
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
$rawKeys = array();
|
||||
|
||||
if (!empty($allColumns))
|
||||
{
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
if (!array_key_exists($oColumn->constraint_name, $rawKeys))
|
||||
{
|
||||
$entry = array(
|
||||
'name' => $oColumn->constraint_name,
|
||||
'def' => 'INDEX',
|
||||
'columns' => array(),
|
||||
);
|
||||
|
||||
if ($useabstract)
|
||||
{
|
||||
$entry['name'] = $this->getAbstract($entry['name']);
|
||||
}
|
||||
|
||||
if (array_key_exists($oColumn->constraint_name, $specialKeys))
|
||||
{
|
||||
$entry['def'] = $specialKeys[$oColumn->constraint_name]->constraint_type;
|
||||
if ($entry['def'] == 'PRIMARY')
|
||||
{
|
||||
$entry['def'] = 'PRIMARY KEY';
|
||||
}
|
||||
}
|
||||
$rawKeys[$oColumn->constraint_name] = $entry;
|
||||
}
|
||||
|
||||
// Add the column to the index
|
||||
$rawKeys[$oColumn->constraint_name]['columns'][] = '"' . $oColumn->column_name . '"';
|
||||
}
|
||||
}
|
||||
|
||||
// Piece together the keys' SQL statements
|
||||
if (!empty($rawKeys))
|
||||
{
|
||||
foreach ($rawKeys as $keydef)
|
||||
{
|
||||
$line = ' ' . $keydef['def'] . ' ';
|
||||
if (!in_array($keydef['def'], array('PRIMARY KEY', 'UNIQUE')))
|
||||
{
|
||||
$line = 'CREATE ' . $line;
|
||||
$thistable = $useabstract ? $table_abstract : $table_name;
|
||||
$line .= "\"{$keydef['name']}\" ON \"$thistable\"";
|
||||
}
|
||||
$line .= '(';
|
||||
$line .= implode(',', $keydef['columns']);
|
||||
$line .= ')';
|
||||
|
||||
if (!in_array($keydef['def'], array('PRIMARY KEY', 'UNIQUE')))
|
||||
{
|
||||
$indexes_sql[] = $line;
|
||||
}
|
||||
else
|
||||
{
|
||||
$keys_sql[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR CONSTRAINTS
|
||||
// =====================================================================
|
||||
// Get the foreign key names
|
||||
$query =
|
||||
'SELECT ccu.table_name as ftable, ccu.column_name as fcolumn, ' .
|
||||
'kku.table_name as ltable, kku.column_name as lcolumn,' .
|
||||
'ccu.constraint_name AS constraint_name, ' .
|
||||
'rc.match_option, rc.update_rule, rc.delete_rule ' .
|
||||
'FROM ' .
|
||||
'information_schema.constraint_column_usage as ccu ' .
|
||||
'INNER JOIN information_schema.key_column_usage AS kku ON ' .
|
||||
'(kku.constraint_name = ccu.constraint_name) ' .
|
||||
'INNER JOIN information_schema.referential_constraints AS rc ON (rc.constraint_name = ccu.constraint_name) ' .
|
||||
'WHERE ccu.table_name <> kku.table_name ' .
|
||||
'AND kku.table_name = ' . $dbi->quote($table_name) .
|
||||
' AND kku.constraint_catalog = ' . $dbi->quote($this->database) .
|
||||
' AND kku.constraint_schema = \'public\'';
|
||||
$dbi->setQuery($query);
|
||||
$allFKColumns = $dbi->loadObjectList();
|
||||
$rawConstraints = array();
|
||||
|
||||
if (!empty($allFKColumns))
|
||||
{
|
||||
foreach ($allFKColumns as $oColumn)
|
||||
{
|
||||
if (!array_key_exists($oColumn->constraint_name, $rawConstraints))
|
||||
{
|
||||
$entry = array(
|
||||
'name' => $oColumn->constraint_name,
|
||||
'cols' => array(),
|
||||
'refcols' => array(),
|
||||
'reftable' => '',
|
||||
'update' => '',
|
||||
'delete' => '',
|
||||
);
|
||||
|
||||
if ($useabstract)
|
||||
{
|
||||
$entry['name'] = $this->getAbstract($entry['name']);
|
||||
}
|
||||
|
||||
$entry['update'] = $oColumn->update_rule;
|
||||
$entry['delete'] = $oColumn->delete_rule;
|
||||
|
||||
$reftable = $oColumn->ftable;
|
||||
|
||||
// Add a reference hit
|
||||
$this->dependencies[$reftable][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $reftable;
|
||||
|
||||
if ($useabstract)
|
||||
{
|
||||
$reftable = $this->getAbstract($reftable);
|
||||
}
|
||||
$entry['reftable'] = $reftable;
|
||||
|
||||
$rawConstraints[$oColumn->constraint_name] = $entry;
|
||||
}
|
||||
|
||||
$rawConstraints[$oColumn->constraint_name]['cols'][] = '"' . $oColumn->lcolumn . '"';
|
||||
$rawConstraints[$oColumn->constraint_name]['refcols'][] = '"' . $oColumn->fcolumn . '"';
|
||||
}
|
||||
}
|
||||
|
||||
// Piece together the constraints' SQL statements
|
||||
if (!empty($rawConstraints))
|
||||
{
|
||||
foreach ($rawConstraints as $keydef)
|
||||
{
|
||||
$line = ' CONSTRAINT ';
|
||||
if ($keydef['name'])
|
||||
{
|
||||
$line .= "\"{$keydef['name']}\" ";
|
||||
}
|
||||
$line .= 'FOREIGN KEY (';
|
||||
$line .= implode(',', $keydef['cols']);
|
||||
$line .= ') REFERENCES "' . $keydef['reftable'] . '" (';
|
||||
$line .= implode(',', $keydef['refcols']);
|
||||
$line .= ')';
|
||||
if ($keydef['delete'])
|
||||
{
|
||||
$line .= ' ON DELETE ' . $keydef['delete'];
|
||||
}
|
||||
if ($keydef['update'])
|
||||
{
|
||||
$line .= ' ON UPDATE ' . $keydef['update'];
|
||||
}
|
||||
|
||||
$constraints_sql[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =====================================================================
|
||||
// ========== CONSTRUCT THE TABLE CREATE STATEMENT
|
||||
// =====================================================================
|
||||
// Create the SQL output
|
||||
if ($useabstract)
|
||||
{
|
||||
$table_sql = "CREATE TABLE \"$table_abstract\" (";
|
||||
}
|
||||
else
|
||||
{
|
||||
$table_sql = "CREATE TABLE \"$table_name\" (";
|
||||
}
|
||||
$table_sql .= implode(',', $columns_sql);
|
||||
if (count($keys_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $keys_sql);
|
||||
}
|
||||
if (count($constraints_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $constraints_sql);
|
||||
}
|
||||
$table_sql .= ")";
|
||||
|
||||
$table_sql .= ";\n";
|
||||
|
||||
if (!empty($indexes_sql))
|
||||
{
|
||||
foreach ($indexes_sql as $index)
|
||||
{
|
||||
$table_sql .= $index . ";\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $table_sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the View definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_views(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM information_schema.views WHERE "table_catalog" = ' . $dbi->quote($schema_name) .
|
||||
' AND "table_schema" = \'public\'';
|
||||
$dbi->setQuery($sql);
|
||||
$all_views = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
// First pass: populate the table_name_map
|
||||
if (!empty($all_views))
|
||||
{
|
||||
foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->table_name;
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: View $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding view $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping view $table_name (internal name $table_abstract)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup view $table_name automatically skipped.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: get the create commands
|
||||
if (!empty($all_views))
|
||||
{
|
||||
foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->table_name;
|
||||
|
||||
if (!in_array($table_name, $this->table_name_map))
|
||||
{
|
||||
// Skip any views which have been filtered out
|
||||
continue;
|
||||
}
|
||||
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
|
||||
// Still here? The view is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'view',
|
||||
'dump_records' => false
|
||||
);
|
||||
|
||||
$dependencies = array();
|
||||
$table_sql = 'CREATE OR REPLACE VIEW "' . $table_name . '" AS ' . $table_object->view_definition;
|
||||
$old_table_sql = $table_sql;
|
||||
foreach ($this->table_name_map as $ref_normal => $ref_abstract)
|
||||
{
|
||||
if ($pos = strpos($table_sql, "$ref_normal"))
|
||||
{
|
||||
// Add a reference hit
|
||||
$this->dependencies[$ref_normal][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $ref_normal;
|
||||
// Do the replacement
|
||||
$table_sql = str_replace("$ref_normal", "$ref_abstract", $table_sql);
|
||||
}
|
||||
}
|
||||
|
||||
// On DB only backup we don't want any replacing to take place, do we?
|
||||
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
|
||||
{
|
||||
$table_sql = $old_table_sql;
|
||||
}
|
||||
|
||||
// Replace newlines with spaces
|
||||
$table_sql = str_replace("\n", " ", $table_sql) . ";\n";
|
||||
$table_sql = str_replace("\r", " ", $table_sql);
|
||||
$table_sql = str_replace("\t", " ", $table_sql);
|
||||
|
||||
$new_entry['create'] = $table_sql;
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drop query from a CREATE query
|
||||
*
|
||||
* @param $query string The CREATE query to process
|
||||
*
|
||||
* @return string The DROP statement
|
||||
*/
|
||||
protected function createDrop($query)
|
||||
{
|
||||
$db = $this->getDB();
|
||||
|
||||
// Initialize
|
||||
$dropQuery = '';
|
||||
|
||||
// Parse CREATE TABLE commands
|
||||
if (substr($query, 0, 12) == 'CREATE TABLE')
|
||||
{
|
||||
// Try to get the table name
|
||||
$restOfQuery = trim(substr($query, 12, strlen($query) - 12)); // Rest of query, after CREATE TABLE
|
||||
// Is there a double quote?
|
||||
if (substr($restOfQuery, 0, 1) == '"')
|
||||
{
|
||||
// There is... Good, we'll just find the matching double quote
|
||||
$pos = strpos($restOfQuery, '"', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nope, let's assume the table name ends in the next blank character
|
||||
$pos = strpos($restOfQuery, ' ', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
|
||||
unset($restOfQuery);
|
||||
|
||||
/**
|
||||
* Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones.
|
||||
* Possibly not affecting PostgreSQL but we'd better be safe than sorry.
|
||||
*/
|
||||
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
|
||||
|
||||
// Try to drop the table anyway
|
||||
$dropQuery = 'DROP TABLE IF EXISTS ' . $db->qn($tableName) . ';';
|
||||
}
|
||||
// Parse CREATE VIEW commands
|
||||
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, ' VIEW ') !== false))
|
||||
{
|
||||
// Try to get the view name
|
||||
$view_pos = strpos($query, ' VIEW ');
|
||||
$restOfQuery = trim(substr($query, $view_pos + 6)); // Rest of query, after VIEW string
|
||||
// Is there a double quote?
|
||||
if (substr($restOfQuery, 0, 1) == '"')
|
||||
{
|
||||
// There is... Good, we'll just find the matching double quote
|
||||
$pos = strpos($restOfQuery, '"', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nope, let's assume the table name ends in the next blank character
|
||||
$pos = strpos($restOfQuery, ' ', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
|
||||
unset($restOfQuery);
|
||||
|
||||
/**
|
||||
* Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones.
|
||||
* Possibly not affecting PostgreSQL but we'd better be safe than sorry.
|
||||
*/
|
||||
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
|
||||
|
||||
$dropQuery = 'DROP TABLE IF EXISTS ' . $db->qn($tableName) . ';';
|
||||
}
|
||||
|
||||
return $dropQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process a quoted value before it's written to the database dump.
|
||||
* So far it's only required for SQL Server which has a problem escaping
|
||||
* newline characters...
|
||||
*
|
||||
* @param string $value The quoted value to post-process
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function postProcessQuotedValue($value)
|
||||
{
|
||||
if ((strstr($value, "\n") === false) && (strstr($value, "\r") === false))
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mark this string as "C-style Escaped".
|
||||
// See http://www.postgresql.org/docs/9.2/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
|
||||
$temp = 'E' . $value;
|
||||
// Escape special characters. Note that the escaped character looks like \\n (two slashes).
|
||||
// Single slashes must also be escaped.
|
||||
$temp = str_replace("\\", '\\\\', $temp);
|
||||
$temp = str_replace("\n", '\\n', $temp);
|
||||
$temp = str_replace("\r", '\\r', $temp);
|
||||
$temp = str_replace("\f", '\\f', $temp);
|
||||
$temp = str_replace("\t", '\\t', $temp);
|
||||
|
||||
return $temp;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setAutoIncrementInfo()
|
||||
{
|
||||
// Does nothing, there is no way to know if the table has an autoincrement field in PostgreSQL
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Reverse;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Dump\Base;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* A generic SQLite database dump class.
|
||||
*
|
||||
* At this moment the dump engine is not actually "dumping" any data: since the whole database is inside
|
||||
* a file, we simply get it and include inside the backup.
|
||||
*/
|
||||
class Sqlite extends Base
|
||||
{
|
||||
/**
|
||||
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDatabaseNameFromConnection()
|
||||
{
|
||||
// Doesn't make sense in SQLite.
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Sqlite
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs one more step of dumping database data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function stepDatabaseDump()
|
||||
{
|
||||
// We do not create any dump file, we will simply include the whole database file inside the backup
|
||||
Factory::getLog()->log(LogLevel::INFO, "SQLite database detected, no SQL dump files will be created.");
|
||||
$this->setState('postrun');
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the database for tables to be backed up and sorts them according to
|
||||
* their dependencies on one another. Updates $this->dependencies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function getTablesToBackup()
|
||||
{
|
||||
// No tables will be included in the backup
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,922 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Dump\Reverse;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Driver\Base as DriverBase;
|
||||
use Akeeba\Engine\Dump\Native\Mysql as NativeMysql;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
|
||||
/**
|
||||
* A SQL Server database dump class, using reverse engineering of the
|
||||
* INFORMATION_SCHEMA views to deduce the DDL of the database entities.
|
||||
*
|
||||
* Configuration parameters:
|
||||
* host <string> SQL database server host name or IP address
|
||||
* port <string> SQL database server port (optional)
|
||||
* username <string> SQL user name, for authentication
|
||||
* password <string> SQL password, for authentication
|
||||
* database <string> SQL database
|
||||
* dumpFile <string> Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated)
|
||||
*/
|
||||
class Sqlsrv extends NativeMysql
|
||||
{
|
||||
/**
|
||||
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDatabaseNameFromConnection()
|
||||
{
|
||||
$db = $this->getDB();
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $db->setQuery('SELECT DB_NAME()')->loadResult();
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return empty($ret) ? '' : $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the constructor of the class
|
||||
*
|
||||
* @return Sqlsrv
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
|
||||
|
||||
$this->postProcessValues = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the SQL compatibility setting
|
||||
*/
|
||||
protected function enforceSQLCompatibility()
|
||||
{
|
||||
// Do nothing for SQL server
|
||||
}
|
||||
|
||||
protected function getTablesToBackup()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
// First, get a map of table names <--> abstract names
|
||||
$this->reverse_engineer_db();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$notracking)
|
||||
{
|
||||
// Process dependencies and rearrange tables respecting them
|
||||
$this->process_dependencies();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove dependencies array
|
||||
$this->dependencies = array();
|
||||
}
|
||||
}
|
||||
|
||||
protected function reverse_engineer_db()
|
||||
{
|
||||
// Get a database connection
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering database");
|
||||
$db = $this->getDB();
|
||||
if ($this->getError())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset internal tables
|
||||
$this->table_name_map = array();
|
||||
$this->tables_data = array();
|
||||
$this->dependencies = array();
|
||||
|
||||
// Get the list of all database tables and views
|
||||
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering Tables");
|
||||
$this->reverse_engineer_tables($db);
|
||||
$this->reverse_engineer_views($db);
|
||||
|
||||
// Optional backup of triggers, functions and procedures
|
||||
$registry = Factory::getConfiguration();
|
||||
$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
|
||||
$notracking = $registry->get('engine.dump.native.nodependencies', 0);
|
||||
|
||||
if ($enable_entities && ($notracking == 0))
|
||||
{
|
||||
// @todo Triggers
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Triggers");
|
||||
|
||||
// @todo Functions
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Functions");
|
||||
|
||||
// @todo Procedures
|
||||
// Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Procedures");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the Table definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_tables(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM sys.objects WHERE type = ' . $dbi->quote('U') . ' ORDER BY [name] ASC';
|
||||
$dbi->setQuery($sql);
|
||||
$all_tables = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
if (!empty($all_tables))
|
||||
{
|
||||
foreach ($all_tables as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->name;
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Still here? The table is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'table',
|
||||
'dump_records' => true
|
||||
);
|
||||
|
||||
// Table Data Filter - skip dumping table contents of filtered out tables
|
||||
if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
|
||||
{
|
||||
$new_entry['dump_records'] = false;
|
||||
}
|
||||
|
||||
$new_entry['create'] = $this->get_create_table($dbi, $table_name, $table_abstract, $table_object, $dependencies);
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CREATE TABLE command of a given table
|
||||
*
|
||||
* @param DriverBase $dbi The db connection to the INFORMATION_SCHEMA db
|
||||
* @param string $table_name The name of the table
|
||||
* @param string $table_abstract The abstract name of the table
|
||||
* @param \stdClass $table_object The SYS.OBJECTS record for this table
|
||||
* @param array $dependencies Dependency tracking information
|
||||
*
|
||||
* @return string The CREATE TABLE definition
|
||||
*/
|
||||
protected function get_create_table(&$dbi, $table_name, $table_abstract, $table_object, &$dependencies)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
|
||||
$useabstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1);
|
||||
|
||||
$columns_sql = array();
|
||||
$keys_sql = array();
|
||||
$constraints_sql = array();
|
||||
$indexes_sql = array();
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR COLUMNS
|
||||
// =====================================================================
|
||||
// Get identity columns for this table
|
||||
$sysObjectID = $table_object->object_id;
|
||||
$query = 'SELECT COUNT(*) FROM sys.identity_columns WHERE object_id = ' . $dbi->quote($sysObjectID);
|
||||
$dbi->setQuery($query);
|
||||
$countIdentityColumns = $dbi->loadResult();
|
||||
if ($countIdentityColumns)
|
||||
{
|
||||
/**
|
||||
* $query = 'SELECT column_id, name, seed_value, increment_value FROM sys.identity_columns WHERE object_id = ' . $dbi->quote($sysObjectID)
|
||||
* . ' ORDER BY column_id ASC';
|
||||
**/
|
||||
$query = 'select * from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ' . $dbi->quote($table_name) .
|
||||
'and COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, \'IsIdentity\') = 1';
|
||||
$dbi->setQuery($query);
|
||||
$identityColumns = $dbi->loadAssocList('COLUMN_NAME');
|
||||
}
|
||||
else
|
||||
{
|
||||
$identityColumns = array();
|
||||
}
|
||||
|
||||
// Get columns
|
||||
$query = 'SELECT * FROM information_schema.columns WHERE table_catalog = ' . $dbi->quote($this->database)
|
||||
. ' AND table_name = ' . $dbi->quote($table_name) . ' ORDER BY ordinal_position ASC';
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
$line = '[' . $oColumn->COLUMN_NAME . '] [' . $oColumn->DATA_TYPE . ']';
|
||||
switch ($oColumn->DATA_TYPE)
|
||||
{
|
||||
case 'bigint':
|
||||
case 'int':
|
||||
case 'smallint':
|
||||
case 'tinyint':
|
||||
/* NOT SUPPORTED *
|
||||
$precision = $oColumn->NUMERIC_PRECISION;
|
||||
$scale = $oColumn->NUMERIC_SCALE;
|
||||
if ($precision)
|
||||
{
|
||||
$line .= '(';
|
||||
if ($precision && $scale)
|
||||
{
|
||||
$line .= $precision . ', ' . $scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
$line .= $precision;
|
||||
}
|
||||
$line .= ')';
|
||||
}
|
||||
*/
|
||||
break;
|
||||
|
||||
case 'nvarchar':
|
||||
case 'nchar':
|
||||
$len = $oColumn->CHARACTER_MAXIMUM_LENGTH;
|
||||
if ($len < 0)
|
||||
{
|
||||
$len = 'max';
|
||||
}
|
||||
$line .= '(' . $len . ')';
|
||||
break;
|
||||
|
||||
case 'datetime':
|
||||
case 'datetime2':
|
||||
/* NOT SUPPORTED *
|
||||
$precision = $oColumn->DATETIME_PRECISION;
|
||||
if ($precision)
|
||||
{
|
||||
$line .= '(' . $precision . ')';
|
||||
}
|
||||
*/
|
||||
break;
|
||||
|
||||
case 'float':
|
||||
case 'real':
|
||||
break;
|
||||
}
|
||||
$line .= ($oColumn->IS_NULLABLE == 'YES') ? 'NULL ' : 'NOT NULL ';
|
||||
|
||||
if (array_key_exists($oColumn->COLUMN_NAME, $identityColumns))
|
||||
{
|
||||
/**
|
||||
* $seed = $identityColumns[$oColumn->COLUMN_NAME]['seed_value'];
|
||||
* $increment = $identityColumns[$oColumn->COLUMN_NAME]['increment_value'];
|
||||
**/
|
||||
// fake the seed and increment because Microsoft sucks and their API is broken!
|
||||
|
||||
$qLala = 'SELECT MAX(' . $oColumn->COLUMN_NAME . ') FROM ' . $dbi->quoteName($table_name);
|
||||
$dbi->setQuery($qLala);
|
||||
$seed = (int)($dbi->loadResult());
|
||||
|
||||
$increment = 1;
|
||||
|
||||
$line .= ' IDENTITY (' . $seed . ', ' . $increment . ')';
|
||||
}
|
||||
|
||||
$line .= ($oColumn->COLUMN_DEFAULT == '') ? '' : (' DEFAULT ' . $oColumn->COLUMN_DEFAULT . ' ');
|
||||
|
||||
$columns_sql[] = $line;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR KEYS AND INDICES
|
||||
// =====================================================================
|
||||
// Get the primary and unique key names
|
||||
$query = 'SELECT * from sys.indexes where object_id = OBJECT_ID(' . $dbi->q($table_name) . ')';
|
||||
$dbi->setQuery($query);
|
||||
$allKeys = $dbi->loadObjectList('name');
|
||||
|
||||
// Get the columns per key and key information
|
||||
$query = 'select c.name, ic.* from sys.index_columns as ic inner join sys.columns as c on(c.object_id = ic.object_id and c.column_id = ic.column_id) '
|
||||
. 'where ic.object_id = OBJECT_ID(' . $dbi->q($table_name) . ') order by index_id ASC, index_column_id ASC';
|
||||
$dbi->setQuery($query);
|
||||
$allColumns = $dbi->loadObjectList();
|
||||
|
||||
$rawKeys = array();
|
||||
if (!empty($allKeys))
|
||||
{
|
||||
foreach ($allKeys as $currentKey)
|
||||
{
|
||||
if (empty($currentKey->name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$isUnique = $currentKey->is_unique == 1;
|
||||
$isPrimary = $currentKey->is_primary_key == 1;
|
||||
|
||||
$keyName = $currentKey->name;
|
||||
if ($useabstract && strlen($this->prefix) && substr($this->prefix, -1) == '_')
|
||||
{
|
||||
$keyName = str_replace($this->prefix, '#__', $keyName);
|
||||
}
|
||||
|
||||
if ($isPrimary)
|
||||
{
|
||||
$line = 'CONSTRAINT [' . $keyName . '] ';
|
||||
$line .= 'PRIMARY KEY ' . $currentKey->type_desc;
|
||||
}
|
||||
elseif ($isUnique)
|
||||
{
|
||||
$line = 'CONSTRAINT [' . $keyName . '] ';
|
||||
$line .= 'UNIQUE ' . $currentKey->type_desc;
|
||||
}
|
||||
else
|
||||
{
|
||||
//$line = 'CREATE ' . $currentKey->type_desc . ' INDEX [' . $this->getAbstract($currentKey->name) . '] ON [' . $table_abstract . ']';
|
||||
if ($useabstract)
|
||||
{
|
||||
$line = 'CREATE INDEX [' . $keyName . '] ON [' . $table_abstract . ']';
|
||||
}
|
||||
else
|
||||
{
|
||||
$line = 'CREATE INDEX [' . $keyName . '] ON [' . $table_name . ']';
|
||||
}
|
||||
}
|
||||
$line .= '(';
|
||||
|
||||
// Add columns
|
||||
$cols = array();
|
||||
foreach ($allColumns as $oColumn)
|
||||
{
|
||||
if ($oColumn->index_id != $currentKey->index_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$cols[] = '[' . $oColumn->name . '] ' . ($oColumn->is_descending_key ? 'DESC' : 'ASC');
|
||||
}
|
||||
$line .= implode(', ', $cols);
|
||||
|
||||
$line .= ')';
|
||||
|
||||
// append WITH (...blah...) ON [PRIMARY]
|
||||
$line .= ' WITH (';
|
||||
if ($isPrimary || $isUnique)
|
||||
{
|
||||
$line .= 'PAD_INDEX = ' . ($currentKey->is_padded ? 'ON' : 'OFF');
|
||||
$line .= ', STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]';
|
||||
}
|
||||
else
|
||||
{
|
||||
$line .= 'STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF)';
|
||||
}
|
||||
|
||||
// Add primary/unique keys to $keys_sql, add indices to $indexes_sql and paste them as new lines below the create command
|
||||
if ($isPrimary || $isUnique)
|
||||
{
|
||||
$keys_sql[] = $line;
|
||||
}
|
||||
else
|
||||
{
|
||||
$indexes_sql[] = $line . ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ========== GENERATE SQL FOR FOREIGN KEYS
|
||||
// =====================================================================
|
||||
// Get the foreign key names
|
||||
$query = 'SELECT * FROM sys.foreign_keys WHERE parent_object_id = ' . $dbi->quote($sysObjectID)
|
||||
. ' AND [type] = ' . $dbi->quote('F');
|
||||
$dbi->setQuery($query);
|
||||
$foreignKeyInfo = $dbi->loadObjectList('name');
|
||||
|
||||
|
||||
$rawConstraints = array();
|
||||
|
||||
if (!empty($foreignKeyInfo)) foreach ($foreignKeyInfo as $oKey)
|
||||
{
|
||||
// Get the columns per key and key information
|
||||
$query = 'SELECT * FROM sys.foreign_key_columns WHERE parent_object_id = ' . $dbi->quote($sysObjectID) .
|
||||
' AND constraint_object_id = ' . $dbi->q($oKey->object_id);
|
||||
$dbi->setQuery($query);
|
||||
$allFKColumns = $dbi->loadObjectList();
|
||||
|
||||
// Get referenced table's name
|
||||
$refID = $oKey->referenced_object_id;
|
||||
$query = 'SELECT name FROM sys.objects WHERE object_id = ' . $dbi->q($refID);
|
||||
$dbi->setQuery($query);
|
||||
$refObjectName = $dbi->loadResult();
|
||||
|
||||
// Initialise column maps
|
||||
$FKcolumnMap = array();
|
||||
|
||||
// Loop through each column and map parent_object_id / parent_column_id, referenced_object_id / referenced_column_id to column names
|
||||
foreach ($allFKColumns as $oColumn)
|
||||
{
|
||||
$objectIDs = array($oColumn->parent_object_id, $oColumn->referenced_object_id);
|
||||
foreach ($objectIDs as $oid)
|
||||
{
|
||||
if (!array_key_exists($oid, $FKcolumnMap))
|
||||
{
|
||||
$query = $dbi->getQuery(true)
|
||||
->select(array('name', 'column_id'))
|
||||
->from('sys.columns')
|
||||
->where('object_id = ' . $dbi->q($oid));
|
||||
$dbi->setQuery($query);
|
||||
$FKcolumnMap[$oid] = $dbi->loadObjectList('column_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$keyName = $oKey->name;
|
||||
if (strlen($this->prefix) && substr($this->prefix, -1) == '_')
|
||||
{
|
||||
$keyName = str_replace($this->prefix, '#__', $keyName);
|
||||
}
|
||||
$line = 'CONSTRAINT [' . $keyName . '] FOREIGN KEY (';
|
||||
|
||||
$tempCols = array();
|
||||
foreach ($allFKColumns as $oColumn)
|
||||
{
|
||||
$oid = $oColumn->parent_object_id;
|
||||
$cid = $oColumn->parent_column_id;
|
||||
$tempCols[] = '[' . $FKcolumnMap[$oid][$cid]->name . ']';
|
||||
}
|
||||
$line .= implode(', ', $tempCols);
|
||||
|
||||
// Add a reference hit
|
||||
$this->dependencies[$refObjectName][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $refObjectName;
|
||||
|
||||
if ($useabstract)
|
||||
{
|
||||
$refObjectName = $this->getAbstract($refObjectName);
|
||||
}
|
||||
|
||||
$line .= ') REFERENCES [' . $refObjectName . '] (';
|
||||
|
||||
$tempCols = array();
|
||||
foreach ($allFKColumns as $oColumn)
|
||||
{
|
||||
$oid = $oColumn->referenced_object_id;
|
||||
$cid = $oColumn->referenced_column_id;
|
||||
$tempCols[] = '[' . $FKcolumnMap[$oid][$cid]->name . ']';
|
||||
}
|
||||
$line .= implode(', ', $tempCols);
|
||||
|
||||
$line .= ') ';
|
||||
|
||||
// Tuck the delete and update actions
|
||||
$line .= ' ON DELETE ';
|
||||
switch ($oKey->delete_referential_action_desc)
|
||||
{
|
||||
case 'NO_ACTION';
|
||||
$line .= 'NO ACTION';
|
||||
break;
|
||||
|
||||
case 'CASCADE';
|
||||
$line .= 'CASCADE';
|
||||
break;
|
||||
|
||||
case 'SET_NULL';
|
||||
$line .= 'SET NULL';
|
||||
break;
|
||||
|
||||
case 'SET_DEFAULT';
|
||||
$line .= 'SET DEFAULT';
|
||||
break;
|
||||
}
|
||||
|
||||
$line .= ' ON UPDATE ';
|
||||
switch ($oKey->update_referential_action_desc)
|
||||
{
|
||||
case 'NO_ACTION';
|
||||
$line .= 'NO ACTION';
|
||||
break;
|
||||
|
||||
case 'CASCADE';
|
||||
$line .= 'CASCADE';
|
||||
break;
|
||||
|
||||
case 'SET_NULL';
|
||||
$line .= 'SET NULL';
|
||||
break;
|
||||
|
||||
case 'SET_DEFAULT';
|
||||
$line .= 'SET DEFAULT';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($oKey->is_not_for_replication)
|
||||
{
|
||||
$line .= ' NOT FOR REPLICATION';
|
||||
}
|
||||
|
||||
// add to the $constraints_sql array
|
||||
$constraints_sql[] = $line;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ==========CONSTRUCT THE TABLE CREATE STATEMENT
|
||||
// =====================================================================
|
||||
// Create the SQL output
|
||||
if ($useabstract)
|
||||
{
|
||||
$table_sql = "CREATE TABLE [$table_abstract] (";
|
||||
}
|
||||
else
|
||||
{
|
||||
$table_sql = "CREATE TABLE [$table_name] (";
|
||||
}
|
||||
|
||||
$table_sql .= implode(',', $columns_sql);
|
||||
if (count($keys_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $keys_sql);
|
||||
}
|
||||
if (count($constraints_sql))
|
||||
{
|
||||
$table_sql .= ',' . implode(',', $constraints_sql);
|
||||
}
|
||||
$table_sql .= ")";
|
||||
|
||||
$table_sql .= ";\n";
|
||||
|
||||
$table_sql .= implode(";\n", $indexes_sql);
|
||||
if (count($indexes_sql) >= 1)
|
||||
{
|
||||
$table_sql .= "\n";
|
||||
}
|
||||
|
||||
return $table_sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse engineers the View definitions of this database
|
||||
*
|
||||
* @param DriverBase $dbi Database connection to INFORMATION_SCHEMA
|
||||
*/
|
||||
protected function reverse_engineer_views(&$dbi)
|
||||
{
|
||||
$schema_name = $this->database;
|
||||
$sql = 'SELECT * FROM [INFORMATION_SCHEMA].[VIEWS] WHERE [table_catalog] = ' . $dbi->quote($schema_name);
|
||||
$dbi->setQuery($sql);
|
||||
$all_views = $dbi->loadObjectList();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$root = $registry->get('volatile.database.root', '[SITEDB]');
|
||||
|
||||
// If we have filters, make sure the tables pass the filtering
|
||||
$filters = Factory::getFilters();
|
||||
// First pass: populate the table_name_map
|
||||
if (!empty($all_views)) foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->TABLE_NAME;
|
||||
|
||||
// Filter and convert
|
||||
if (substr($table_name, 0, 3) == '#__')
|
||||
{
|
||||
$this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
|
||||
{
|
||||
// Apply exclusion filters
|
||||
if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
|
||||
$this->table_name_map[$table_name] = $table_abstract;
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup view $table_name automatically skipped.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: get the create commands
|
||||
if (!empty($all_views)) foreach ($all_views as $table_object)
|
||||
{
|
||||
// Extract the table name
|
||||
$table_name = $table_object->TABLE_NAME;
|
||||
|
||||
if (!in_array($table_name, $this->table_name_map))
|
||||
{
|
||||
// Skip any views which have been filtered out
|
||||
continue;
|
||||
}
|
||||
|
||||
$table_abstract = $this->getAbstract($table_name);
|
||||
|
||||
// Still here? The view is added. We now have to store its
|
||||
// create command, dependency info and so on
|
||||
$new_entry = array(
|
||||
'type' => 'view',
|
||||
'dump_records' => false
|
||||
);
|
||||
|
||||
$dependencies = array();
|
||||
$table_sql = $table_object->VIEW_DEFINITION;
|
||||
$old_table_sql = $table_sql;
|
||||
foreach ($this->table_name_map as $ref_normal => $ref_abstract)
|
||||
{
|
||||
if ($pos = strpos($table_sql, ".$ref_normal"))
|
||||
{
|
||||
// Add a reference hit
|
||||
$this->dependencies[$ref_normal][] = $table_name;
|
||||
// Add the dependency to this table's metadata
|
||||
$dependencies[] = $ref_normal;
|
||||
// Do the replacement
|
||||
$table_sql = str_replace(".$ref_normal", ".$ref_abstract", $table_sql);
|
||||
}
|
||||
}
|
||||
|
||||
// On DB only backup we don't want any replacing to take place, do we?
|
||||
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
|
||||
{
|
||||
$table_sql = $old_table_sql;
|
||||
}
|
||||
|
||||
// Replace newlines with spaces
|
||||
$table_sql = str_replace("\n", " ", $table_sql) . ";\n";
|
||||
$table_sql = str_replace("\r", " ", $table_sql);
|
||||
$table_sql = str_replace("\t", " ", $table_sql);
|
||||
|
||||
$new_entry['create'] = $table_sql;
|
||||
$new_entry['dependencies'] = $dependencies;
|
||||
|
||||
$this->tables_data[$table_name] = $new_entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drop query from a CREATE query
|
||||
*
|
||||
* @param $query string The CREATE query to process
|
||||
*
|
||||
* @return string The DROP statement
|
||||
*/
|
||||
protected function createDrop($query)
|
||||
{
|
||||
$db = $this->getDB();
|
||||
|
||||
// Initialize
|
||||
$dropQuery = '';
|
||||
|
||||
// Parse CREATE TABLE commands
|
||||
if (substr($query, 0, 12) == 'CREATE TABLE')
|
||||
{
|
||||
// Try to get the table name
|
||||
$restOfQuery = trim(substr($query, 12, strlen($query) - 12)); // Rest of query, after CREATE TABLE
|
||||
// Is there a backtick?
|
||||
if (substr($restOfQuery, 0, 1) == '[')
|
||||
{
|
||||
// There is... Good, we'll just find the matching backtick
|
||||
$pos = strpos($restOfQuery, ']', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nope, let's assume the table name ends in the next blank character
|
||||
$pos = strpos($restOfQuery, ' ', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
|
||||
unset($restOfQuery);
|
||||
|
||||
/**
|
||||
* Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones.
|
||||
* Possibly not affecting Microsoft SQL Server but we'd better be safe than sorry.
|
||||
*/
|
||||
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
|
||||
|
||||
// Try to drop the table anyway
|
||||
$dropQuery = 'IF OBJECT_ID(' . $db->q($tableName) . ') IS NOT NULL DROP TABLE ' . $db->qn($tableName) . ';';
|
||||
}
|
||||
// Parse CREATE VIEW commands
|
||||
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, ' VIEW ') !== false))
|
||||
{
|
||||
// Try to get the view name
|
||||
$view_pos = strpos($query, ' VIEW ');
|
||||
$restOfQuery = trim(substr($query, $view_pos + 6)); // Rest of query, after VIEW string
|
||||
// Is there a backtick?
|
||||
if (substr($restOfQuery, 0, 1) == '[')
|
||||
{
|
||||
// There is... Good, we'll just find the matching backtick
|
||||
$pos = strpos($restOfQuery, ']', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nope, let's assume the table name ends in the next blank character
|
||||
$pos = strpos($restOfQuery, ' ', 1);
|
||||
$tableName = substr($restOfQuery, 1, $pos - 1);
|
||||
}
|
||||
|
||||
unset($restOfQuery);
|
||||
|
||||
/**
|
||||
* Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones.
|
||||
* Possibly not affecting Microsoft SQL Server but we'd better be safe than sorry.
|
||||
*/
|
||||
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
|
||||
|
||||
$dropQuery = 'IF OBJECT_ID(' . $db->q($tableName) . ') IS NOT NULL DROP VIEW ' . $db->qn($tableName) . ';';
|
||||
}
|
||||
|
||||
return $dropQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process a quoted value before it's written to the database dump.
|
||||
* So far it's only required for SQL Server which has a problem escaping
|
||||
* newline characters...
|
||||
*
|
||||
* @param string $value The quoted value to post-process
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function postProcessQuotedValue($value)
|
||||
{
|
||||
$value = str_replace("\r\n", "' + CHAR(10) + CHAR(13) + N'", $value);
|
||||
$value = str_replace("\r", "' + CHAR(13) + N'", $value);
|
||||
$value = str_replace("\n", "' + CHAR(10) + N'", $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preamble for the data dump portion of the SQL backup. This is
|
||||
* used to output commands before the first INSERT INTO statement for a
|
||||
* table when outputting a plain SQL file.
|
||||
*
|
||||
* Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server
|
||||
*
|
||||
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
|
||||
* @param string $tableName Real name of the table, e.g. abc_foobar
|
||||
* @param integer $maxRange Row count on this table
|
||||
*
|
||||
* @return string The SQL commands you want to be written in the dump file
|
||||
*/
|
||||
protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange)
|
||||
{
|
||||
if ($maxRange > 0)
|
||||
{
|
||||
// Do we have an identity column?
|
||||
$db = $this->getDB();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('sys.identity_columns')
|
||||
->where("object_id = OBJECT_ID(" . $db->q($tableName) . ")");
|
||||
$db->setQuery($query);
|
||||
$idColumns = $db->loadResult();
|
||||
|
||||
if ($idColumns < 1)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return "SET IDENTITY_INSERT [$tableName] ON;\n";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an epilogue for the data dump portion of the SQL backup. This is
|
||||
* used to output commands after the last INSERT INTO statement for a
|
||||
* table when outputting a plain SQL file.
|
||||
*
|
||||
* Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server
|
||||
*
|
||||
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
|
||||
* @param string $tableName Real name of the table, e.g. abc_foobar
|
||||
* @param integer $maxRange Row count on this table
|
||||
*
|
||||
* @return string The SQL commands you want to be written in the dump file
|
||||
*/
|
||||
protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange)
|
||||
{
|
||||
if ($maxRange > 0)
|
||||
{
|
||||
// Do we have an identity column?
|
||||
$db = $this->getDB();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('sys.identity_columns')
|
||||
->where("object_id = OBJECT_ID(" . $db->q($tableName) . ")");
|
||||
$db->setQuery($query);
|
||||
$idColumns = $db->loadResult();
|
||||
|
||||
if ($idColumns < 1)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return "SET IDENTITY_INSERT [$tableName] OFF;\n";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of field names for the INSERT INTO statements. This is only
|
||||
* required for Microsoft SQL Server because without it the SET IDENTITY_INSERT
|
||||
* has no effect.
|
||||
*
|
||||
* @param array $fieldNames A list of field names in array format
|
||||
* @param integer $numOfFields The number of fields we should be dumping
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFieldListSQL($fieldNames, $numOfFields)
|
||||
{
|
||||
if (count($fieldNames) < $numOfFields)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
elseif (count($fieldNames) > $numOfFields)
|
||||
{
|
||||
$fieldNames = array_slice($fieldNames, 0, $numOfFields);
|
||||
}
|
||||
|
||||
$db = $this->getDB();
|
||||
|
||||
$temp = array();
|
||||
foreach ($fieldNames as $f)
|
||||
{
|
||||
$temp[] = $db->quoteName($f);
|
||||
}
|
||||
|
||||
return '(' . implode(', ', $temp) . ')';
|
||||
}
|
||||
|
||||
protected function setAutoIncrementInfo()
|
||||
{
|
||||
// Does nothing, there is no way to know if the table has an autoincrement field in SQL server
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
; Akeeba native (MySQL) database dump engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: native.ini 554 2011-04-13 12:16:48Z nikosdion $
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title = COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_DESCRIPTION
|
||||
|
||||
; COMMON SETTINGS
|
||||
[engine.dump.divider.common]
|
||||
default = 0
|
||||
type = separator
|
||||
title = COM_AKEEBA_CONFIG_DUMP_DIVIDER_COMMON
|
||||
bold = 1
|
||||
|
||||
[engine.dump.common.blankoutpass]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_BLANKOUTPASS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BLANKOUTPASS_DESCRIPTION
|
||||
|
||||
; Generate extended inserts? Common between archive engines.
|
||||
[engine.dump.common.extended_inserts]
|
||||
default = 1
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_EXTENDEDINSERTS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_EXTENDEDINSERTS_DESCRIPTION
|
||||
|
||||
; Extended INSERT packet size
|
||||
[engine.dump.common.packet_size]
|
||||
default = 131072
|
||||
type = integer
|
||||
min = 1
|
||||
max = 1048576
|
||||
shortcuts = "16384|32768|65536|131072|262144|524288|1048576"
|
||||
scale = 1024
|
||||
uom = KB
|
||||
title = COM_AKEEBA_CONFIG_MAXPACKET_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MAXPACKET_DESCRIPTION
|
||||
|
||||
; Split database dumps
|
||||
[engine.dump.common.splitsize]
|
||||
default = 524288
|
||||
type = integer
|
||||
min = 0
|
||||
max = 10485760
|
||||
shortcuts = "524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_SPLITDBDUMP_TITLE
|
||||
description = COM_AKEEBA_CONFIG_SPLITDBDUMP_DESCRIPTION
|
||||
|
||||
; SQL queries per batch
|
||||
[engine.dump.common.batchsize]
|
||||
default = 1000
|
||||
type = integer
|
||||
min = 0
|
||||
max = 100000
|
||||
shortcuts = "10|20|50|100|200|500|1000"
|
||||
scale = 1
|
||||
uom = queries
|
||||
title = COM_AKEEBA_CONFIG_BACTHSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BACTHSIZE_DESCRIPTION
|
||||
|
||||
; SETTINGS FOR MYSQL DUMP
|
||||
[engine.dump.divider.mysql]
|
||||
default = 0
|
||||
type = separator
|
||||
title = COM_AKEEBA_CONFIG_DUMP_DIVIDER_MYSQL
|
||||
bold = 1
|
||||
|
||||
; Enable dumping advanced entities
|
||||
[engine.dump.native.advanced_entitites]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_DESCRIPTION
|
||||
|
||||
|
||||
; Disable dependency tracking. Should only be used in special cases.
|
||||
[engine.dump.native.nodependencies]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_NODEPENDENCIES_TITLE
|
||||
description = COM_AKEEBA_CONFIG_NODEPENDENCIES_DESCRIPTION
|
||||
|
||||
[engine.dump.native.nobtree]
|
||||
default = 1
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_MYSQLNOBTREE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MYSQLNOBTREE_TIP
|
||||
@ -0,0 +1,85 @@
|
||||
; Akeeba reverse engineering database dump engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title = COM_AKEEBA_CONFIG_ENGINE_DUMP_REVERSE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ENGINE_DUMP_REVERSE_DESCRIPTION
|
||||
|
||||
; COMMON SETTINGS
|
||||
[engine.dump.divider.common]
|
||||
default = 0
|
||||
type = separator
|
||||
title = COM_AKEEBA_CONFIG_DUMP_DIVIDER_COMMON
|
||||
bold = 1
|
||||
|
||||
[engine.dump.common.blankoutpass]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_BLANKOUTPASS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BLANKOUTPASS_DESCRIPTION
|
||||
|
||||
; Generate extended inserts? Common between archive engines.
|
||||
[engine.dump.common.extended_inserts]
|
||||
default = 1
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_EXTENDEDINSERTS_TITLE
|
||||
description = COM_AKEEBA_CONFIG_EXTENDEDINSERTS_DESCRIPTION
|
||||
|
||||
; Extended INSERT packet size
|
||||
[engine.dump.common.packet_size]
|
||||
default = 131072
|
||||
type = integer
|
||||
min = 1
|
||||
max = 1048576
|
||||
shortcuts = "16384|32768|65536|131072|262144|524288|1048576"
|
||||
scale = 1024
|
||||
uom = KB
|
||||
title = COM_AKEEBA_CONFIG_MAXPACKET_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MAXPACKET_DESCRIPTION
|
||||
|
||||
; Split database dumps
|
||||
[engine.dump.common.splitsize]
|
||||
default = 524288
|
||||
type = integer
|
||||
min = 0
|
||||
max = 10485760
|
||||
shortcuts = "524288|1048576|2097152|5242880|10485760"
|
||||
scale = 1048576
|
||||
uom = MB
|
||||
title = COM_AKEEBA_CONFIG_SPLITDBDUMP_TITLE
|
||||
description = COM_AKEEBA_CONFIG_SPLITDBDUMP_DESCRIPTION
|
||||
|
||||
; SQL queries per batch
|
||||
[engine.dump.common.batchsize]
|
||||
default = 1000
|
||||
type = integer
|
||||
min = 0
|
||||
max = 100000
|
||||
shortcuts = "10|20|50|100|200|500|1000"
|
||||
scale = 1
|
||||
uom = queries
|
||||
title = COM_AKEEBA_CONFIG_BACTHSIZE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_BACTHSIZE_DESCRIPTION
|
||||
|
||||
; SETTINGS FOR REVERSE ENGINEERING DUMP
|
||||
[engine.dump.divider.reverse]
|
||||
default = 0
|
||||
type = separator
|
||||
title = COM_AKEEBA_CONFIG_DUMP_DIVIDER_REVERSE
|
||||
bold = 1
|
||||
|
||||
; Enable dumping advanced entities
|
||||
[engine.dump.native.advanced_entitites]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_DESCRIPTION
|
||||
|
||||
|
||||
; Disable dependency tracking. Should only be used in special cases.
|
||||
[engine.dump.native.nodependencies]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_NODEPENDENCIES_TITLE
|
||||
description = COM_AKEEBA_CONFIG_NODEPENDENCIES_DESCRIPTION
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,694 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject;
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
abstract class Base extends BaseObject
|
||||
{
|
||||
/** @var string Filter's internal name; defaults to filename without .php extension */
|
||||
public $filter_name = '';
|
||||
|
||||
/** @var string The filtering object: dir|file|dbobject|db */
|
||||
public $object = 'dir';
|
||||
|
||||
/** @var string The filtering subtype (all|content|children|inclusion) */
|
||||
public $subtype = null;
|
||||
|
||||
/** @var string The filtering method (direct|regex|api) */
|
||||
public $method = 'direct';
|
||||
|
||||
/** @var bool Is the filter active? */
|
||||
public $enabled = true;
|
||||
|
||||
/** @var array An array holding filter or regex strings per root, i.e. $filter_data[$root] = array() */
|
||||
protected $filter_data = null;
|
||||
|
||||
/** @var \Akeeba\Engine\Util\FileSystem Used by treatDirectory */
|
||||
protected $fsTools = null;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Set the filter name if it's missing (filename in lowercase, minus the .php extension)
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must be overriden by API-type exclusion filters.
|
||||
*
|
||||
* @param string $test The object to test for exclusion
|
||||
* @param string $root The object's root
|
||||
*
|
||||
* @return bool Return true if it matches your filters
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function is_excluded_by_api($test, $root)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must be overriden by API-type inclusion filters.
|
||||
*
|
||||
* @return array The inclusion filters
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function &get_inclusions_by_api()
|
||||
{
|
||||
$dummy = array();
|
||||
|
||||
return $dummy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra SQL statements to append to the SQL dump file. Useful for extension
|
||||
* filters which have to filter out specific database records. This method
|
||||
* must be overriden in children classes.
|
||||
*
|
||||
* @param string $root The database for which to get the extra SQL statements
|
||||
*
|
||||
* @return string Extra SQL statements
|
||||
*/
|
||||
public function &getExtraSQL($root)
|
||||
{
|
||||
$dummy = "";
|
||||
|
||||
return $dummy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns filtering (exclusion) status of the $test object
|
||||
*
|
||||
* @param string $test The string to check for filter status (e.g. filename, dir name, table name, etc)
|
||||
* @param string $root The exclusion root test belongs to
|
||||
* @param string $object What type of object is it? dir|file|dbobject
|
||||
* @param string $subtype Filter subtype (all|content|children)
|
||||
*
|
||||
* @return bool True if it excluded, false otherwise
|
||||
*/
|
||||
public function isFiltered($test, $root, $object, $subtype)
|
||||
{
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Factory::getLog()->log(LogLevel::DEBUG,"Filtering [$object:$subtype] $root // $test");
|
||||
|
||||
// Inclusion filters do not qualify for exclusion
|
||||
if ($this->subtype == 'inclusion')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The object and subtype must match
|
||||
if (($this->object != $object) || ($this->subtype != $subtype))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($this->method, array('direct', 'regex')))
|
||||
{
|
||||
// -- Direct or regex based filters --
|
||||
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
// Check if the root exists and if there's a filter for the $test
|
||||
if (!array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
// Root not found
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Root found, search in the array
|
||||
if ($this->method == 'direct')
|
||||
{
|
||||
// Direct filtering
|
||||
return in_array($test, $this->filter_data[$root]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regex matching
|
||||
foreach ($this->filter_data[$root] as $regex)
|
||||
{
|
||||
if (substr($regex, 0, 1) == '!')
|
||||
{
|
||||
// Custom Akeeba Backup extension to PCRE notation. If you put a ! before the PCRE, it negates the result of the PCRE.
|
||||
if (!preg_match(substr($regex, 1), $test))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal PCRE
|
||||
if (preg_match($regex, $test))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're here, no match exists
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// -- API-based filters --
|
||||
return $this->is_excluded_by_api($test, $root);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inclusion filters defined by this class for the requested $object
|
||||
*
|
||||
* @param string $object The object to get inclusions for (dir|db)
|
||||
*
|
||||
* @return array The inclusion filters
|
||||
*/
|
||||
public function &getInclusions($object)
|
||||
{
|
||||
$dummy = array();
|
||||
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return $dummy;
|
||||
}
|
||||
|
||||
if (($this->subtype != 'inclusion') || ($this->object != $object))
|
||||
{
|
||||
return $dummy;
|
||||
}
|
||||
|
||||
switch ($this->method)
|
||||
{
|
||||
case 'api':
|
||||
return $this->get_inclusions_by_api();
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
return $this->filter_data;
|
||||
break;
|
||||
|
||||
default:
|
||||
// regex inclusion is not supported at the moment
|
||||
$dummy = array();
|
||||
|
||||
return $dummy;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an exclusion filter, or add/replace an inclusion filter
|
||||
*
|
||||
* @param string $root Filter's root
|
||||
* @param mixed $test Exclusion: the filter string. Inclusion: the root definition data
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function set($root, $test)
|
||||
{
|
||||
if (in_array($this->subtype, array('all', 'content', 'children')))
|
||||
{
|
||||
return $this->setExclusion($root, $test);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->setInclusion($root, $test);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a filter, for direct and regex exclusion filter types
|
||||
*
|
||||
* @param string $root The filter root object
|
||||
* @param string $test The filter string to set
|
||||
*
|
||||
* @return bool True on success
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function setExclusion($root, $test)
|
||||
{
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
// we can't set new filter elements for API-type filters
|
||||
return false;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
case 'regex':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
// Direct filters
|
||||
if (array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
if (!in_array($test, $this->filter_data[$root]))
|
||||
{
|
||||
$this->filter_data[$root][] = $test;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->filter_data[$root] = array($test);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$filters = Factory::getFilters();
|
||||
$filters->setFilterData($this->filter_name, $this->filter_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a filter, for direct inclusion filter types
|
||||
*
|
||||
* @param string $root The inclusion filter key (root)
|
||||
* @param string $test The inclusion filter raw data
|
||||
*
|
||||
* @return bool True on success
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function setInclusion($root, $test)
|
||||
{
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
case 'regex':
|
||||
// we can't set new filter elements for API or regex type filters
|
||||
return false;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
$this->filter_data[$root] = $test;
|
||||
break;
|
||||
}
|
||||
|
||||
$filters = Factory::getFilters();
|
||||
$filters->setFilterData($this->filter_name, $this->filter_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets a given filter
|
||||
*
|
||||
* @param string $root Filter's root
|
||||
* @param string $test The filter to remove
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function remove($root, $test = null)
|
||||
{
|
||||
if ($this->subtype == 'inclusion')
|
||||
{
|
||||
return $this->removeInclusion($root);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->removeExclusion($root, $test);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely removes all filters off a specific root
|
||||
*
|
||||
* @param string $root
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function reset($root)
|
||||
{
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
return false;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
case 'regex':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
// Direct filters
|
||||
if (array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
unset($this->filter_data[$root]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Root not found
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$filters = Factory::getFilters();
|
||||
$filters->setFilterData($this->filter_name, $this->filter_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from direct and regex filters
|
||||
*
|
||||
* @param string $root The filter root object
|
||||
* @param string $test The filter string to set
|
||||
*
|
||||
* @return bool True on success
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function removeExclusion($root, $test)
|
||||
{
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
// we can't remove filter elements from API-type filters
|
||||
return false;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
case 'regex':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
// Direct filters
|
||||
if (array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
if (in_array($test, $this->filter_data[$root]))
|
||||
{
|
||||
if (count($this->filter_data[$root]) == 1)
|
||||
{
|
||||
// If it's the only element, remove the entire root key
|
||||
unset($this->filter_data[$root]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there are more elements, remove just the $test value
|
||||
$key = array_search($test, $this->filter_data[$root]);
|
||||
unset($this->filter_data[$root][$key]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Filter object not found
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Root not found
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$filters = Factory::getFilters();
|
||||
$filters->setFilterData($this->filter_name, $this->filter_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an inclusion filter
|
||||
*
|
||||
* @param string $root The root of the filter to remove
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function removeInclusion($root)
|
||||
{
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
case 'regex':
|
||||
// we can't remove filter elements from API or regex type filters
|
||||
return false;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
if (array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
unset($this->filter_data[$root]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Root not found
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$filters = Factory::getFilters();
|
||||
$filters->setFilterData($this->filter_name, $this->filter_data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a filter
|
||||
*
|
||||
* @param string $root The filter root object
|
||||
* @param string $test The filter string to toggle
|
||||
* @param bool $new_status The new filter status after the operation (true: enabled, false: disabled)
|
||||
*
|
||||
* @return bool True on successful change, false if we failed to change it
|
||||
*/
|
||||
public function toggle($root, $test, &$new_status)
|
||||
{
|
||||
// Can't toggle inclusion filters!
|
||||
if ($this->subtype == 'inclusion')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_set = $this->isFiltered($test, $root, $this->object, $this->subtype);
|
||||
$new_status = !$is_set;
|
||||
if ($is_set)
|
||||
{
|
||||
$status = $this->remove($root, $test);
|
||||
}
|
||||
else
|
||||
{
|
||||
$status = $this->set($root, $test);
|
||||
}
|
||||
if (!$status)
|
||||
{
|
||||
$new_status = $is_set;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this class has any filters? If it doesn't, its methods are never called by
|
||||
* Akeeba's engine to speed things up.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFilters()
|
||||
{
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
// API filters always have data!
|
||||
return true;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
case 'regex':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
return !empty($this->filter_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of filter strings for the given root. Used by MySQLDump engine.
|
||||
*
|
||||
* @param string $root
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFilters($root)
|
||||
{
|
||||
$dummy = array();
|
||||
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return $dummy;
|
||||
}
|
||||
|
||||
switch ($this->method)
|
||||
{
|
||||
default:
|
||||
case 'api':
|
||||
// API filters never have a list
|
||||
return $dummy;
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
case 'regex':
|
||||
// Get a local reference of the filter data, if necessary
|
||||
if (is_null($this->filter_data))
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$this->filter_data = $filters->getFilterData($this->filter_name);
|
||||
}
|
||||
|
||||
if (is_null($root))
|
||||
{
|
||||
// When NULL is passed as the root, we return all roots
|
||||
return $this->filter_data;
|
||||
}
|
||||
elseif (array_key_exists($root, $this->filter_data))
|
||||
{
|
||||
// The root exists, return its data
|
||||
return $this->filter_data[$root];
|
||||
}
|
||||
else
|
||||
{
|
||||
// The root doesn't exist, return an empty array
|
||||
return $dummy;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the root prefix from an absolute path
|
||||
*
|
||||
* @param string $directory The absolute path
|
||||
*
|
||||
* @return string The translated path, relative to the root directory of the backup job
|
||||
*/
|
||||
protected function treatDirectory($directory)
|
||||
{
|
||||
if (!is_object($this->fsTools))
|
||||
{
|
||||
$this->fsTools = Factory::getFilesystemTools();
|
||||
}
|
||||
|
||||
// Get the site's root
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($configuration->get('akeeba.platform.override_root', 0))
|
||||
{
|
||||
$root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]');
|
||||
}
|
||||
else
|
||||
{
|
||||
$root = '[SITEROOT]';
|
||||
}
|
||||
|
||||
if(stristr($root, '['))
|
||||
{
|
||||
$root = $this->fsTools->translateStockDirs($root);
|
||||
}
|
||||
|
||||
$site_root = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($root));
|
||||
|
||||
$directory = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($directory));
|
||||
|
||||
// Trim site root from beginning of directory
|
||||
if( substr($directory, 0, strlen($site_root)) == $site_root )
|
||||
{
|
||||
$directory = substr($directory, strlen($site_root));
|
||||
|
||||
if( substr($directory,0,1) == '/' )
|
||||
{
|
||||
$directory = substr($directory,1);
|
||||
}
|
||||
}
|
||||
|
||||
return $directory;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Directory exclusion filter
|
||||
*/
|
||||
class Directories extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Extra Directories inclusion filter
|
||||
*/
|
||||
class Extradirs extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'inclusion';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Files exclusion filter
|
||||
*/
|
||||
class Files extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Incremental file filter
|
||||
*
|
||||
* It will only backup files which are newer than the last backup taken with this profile
|
||||
*/
|
||||
class Incremental extends Base
|
||||
{
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'api';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function is_excluded_by_api($test, $root)
|
||||
{
|
||||
static $filter_switch = null;
|
||||
static $last_backup = null;
|
||||
|
||||
if (is_null($filter_switch))
|
||||
{
|
||||
$config = Factory::getConfiguration();
|
||||
$filter_switch = Factory::getEngineParamsProvider()->getScriptingParameter('filter.incremental', 0);
|
||||
$filter_switch = ($filter_switch == 1);
|
||||
|
||||
$last_backup = $config->get('volatile.filter.last_backup', null);
|
||||
|
||||
if (is_null($last_backup) && $filter_switch)
|
||||
{
|
||||
// Get a list of backups on this profile
|
||||
$backups = Platform::getInstance()->get_statistics_list(array(
|
||||
'filters' => array(
|
||||
array(
|
||||
'field' => 'profile_id',
|
||||
'value' => Platform::getInstance()->get_active_profile())
|
||||
)
|
||||
));
|
||||
|
||||
// Find this backup's ID
|
||||
$model = Factory::getStatistics();
|
||||
$id = $model->getId();
|
||||
|
||||
if (is_null($id))
|
||||
{
|
||||
$id = -1;
|
||||
}
|
||||
|
||||
// Initialise
|
||||
$last_backup = time();
|
||||
$now = $last_backup;
|
||||
|
||||
// Find the last time a successful backup with this profile was made
|
||||
if (count($backups))
|
||||
{
|
||||
foreach ($backups as $backup)
|
||||
{
|
||||
// Skip the current backup
|
||||
if ($backup['id'] == $id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip non-complete backups
|
||||
if ($backup['status'] != 'complete')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$tzUTC = new \DateTimeZone('UTC');
|
||||
$dateTime = new \DateTime($backup['backupstart'], $tzUTC);
|
||||
$backuptime = $dateTime->getTimestamp();
|
||||
|
||||
$last_backup = $backuptime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($last_backup == $now)
|
||||
{
|
||||
// No suitable backup found; disable this filter
|
||||
$config->set('volatile.scripting.incfile.filter.incremental', 0);
|
||||
$filter_switch = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cache the last backup timestamp
|
||||
$config->set('volatile.filter.last_backup', $last_backup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$filter_switch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the filesystem path for $root
|
||||
$config = Factory::getConfiguration();
|
||||
$fsroot = $config->get('volatile.filesystem.current_root', '');
|
||||
$ds = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
|
||||
$filename = $fsroot . $ds . $test;
|
||||
|
||||
// Get the timestamp of the file
|
||||
$timestamp = @filemtime($filename);
|
||||
|
||||
// If we could not get this information, include the file in the archive
|
||||
if ($timestamp === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare it with the last backup timestamp and exclude if it's older than the last backup
|
||||
if ($timestamp <= $last_backup)
|
||||
{
|
||||
//Factory::getLog()->log(LogLevel::DEBUG, "Excluding $filename due to incremental backup restrictions");
|
||||
return true;
|
||||
}
|
||||
|
||||
// No match? Just include the file!
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Multiple Database inclusion filter
|
||||
*/
|
||||
class Multidb extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'db';
|
||||
$this->subtype = 'inclusion';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Directory exclusion filter based on regular expressions
|
||||
*/
|
||||
class Regexdirectories extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Files exclusion filter based on regular expressions
|
||||
*/
|
||||
class Regexfiles extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Subdirectories exclusion filter based on regular expressions
|
||||
*/
|
||||
class Regexskipdirs extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'children';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Directory contents (files) exclusion filter based on regular expressions
|
||||
*/
|
||||
class Regexskipfiles extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'content';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Database table records exclusion filter
|
||||
*
|
||||
* This is simple stuff. If a table's on the list, it will backup just its structure, not
|
||||
* its contents. Fair and square...
|
||||
*/
|
||||
class Regextabledata extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dbobject';
|
||||
$this->subtype = 'content';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Database table exclusion filter
|
||||
*/
|
||||
class Regextables extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dbobject';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Subdirectories exclusion filter
|
||||
*/
|
||||
class Skipdirs extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'children';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Directory contents (files) exclusion filter
|
||||
*/
|
||||
class Skipfiles extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'content';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<title>AkeebaBackup :: Filter Stack</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>What is this directory?</h1>
|
||||
|
||||
<p>In this directory, Akeeba Backup and Akeeba Solo store the optional filters (Optional Filters in the Configuration
|
||||
page). Unlike regular filters which are always loaded, optional filters are only loaded when the user chooses to
|
||||
enable them. Each filter consists of two files: <var>filtername</var>.ini and <var>filtername</var>.php. The former
|
||||
contains the filter-specific configuration options and the later contains the actual filter code.</p>
|
||||
|
||||
<p>
|
||||
If you want to create new optional filters, put them in here. Do note that the INI file must always contain a
|
||||
boolean key named core.filters.<var>filtername</var>.enabled which controls the loading of this particular filter.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Optional filters are always named Akeeba\Engine\Filter\Stack\Stack<var>Filtername</var> so that the autoloader can
|
||||
find them. For the same reason their filename must be Stack<var>Filtername</var>.php Please watch out for the
|
||||
letter case in the names, it's important.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter\Stack;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Filter\Base;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Date conditional filter
|
||||
*
|
||||
* It will only backup files modified after a specific date and time
|
||||
*/
|
||||
class StackDateconditional extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'api';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function is_excluded_by_api($test, $root)
|
||||
{
|
||||
static $from_datetime;
|
||||
|
||||
$config = Factory::getConfiguration();
|
||||
|
||||
if (is_null($from_datetime))
|
||||
{
|
||||
$user_setting = $config->get('core.filters.dateconditional.start');
|
||||
$from_datetime = strtotime($user_setting);
|
||||
}
|
||||
|
||||
// Get the filesystem path for $root
|
||||
$fsroot = $config->get('volatile.filesystem.current_root', '');
|
||||
$ds = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
|
||||
$filename = $fsroot . $ds . $test;
|
||||
|
||||
// Get the timestamp of the file
|
||||
$timestamp = @filemtime($filename);
|
||||
|
||||
// If we could not get this information, include the file in the archive
|
||||
if ($timestamp === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare it with the user-defined minimum timestamp and exclude if it's older than that
|
||||
if ($timestamp <= $from_datetime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// No match? Just include the file!
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter\Stack;
|
||||
|
||||
use Akeeba\Engine\Filter\Base;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Files exclusion filter based on regular expressions
|
||||
*/
|
||||
class StackErrorlogs extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'api';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function is_excluded_by_api($test, $root)
|
||||
{
|
||||
// Is it an error log? Exclude the file.
|
||||
if (in_array(basename($test), array(
|
||||
'php_error',
|
||||
'php_errorlog',
|
||||
'error_log',
|
||||
'error.log'
|
||||
))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No match? Just include the file!
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter\Stack;
|
||||
|
||||
use Akeeba\Engine\Filter\Base;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
|
||||
/**
|
||||
* Exclude folders and files belonging to the host web stat (ie Webalizer)
|
||||
*/
|
||||
class StackHoststats extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'api';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function is_excluded_by_api($test, $root)
|
||||
{
|
||||
if($test == 'stats')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// No match? Just include the file!
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
; "Date conditional" optional filter
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: dateconditional.ini 409 2011-01-24 09:30:22Z nikosdion $
|
||||
|
||||
; ======================================================================
|
||||
; Advanced configuration
|
||||
; ======================================================================
|
||||
|
||||
[core.filters.dateconditional.enabled]
|
||||
default = 0
|
||||
type = bool
|
||||
title = COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_TITLE
|
||||
description = COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_DESCRIPTION
|
||||
bold = 1
|
||||
|
||||
[core.filters.dateconditional.start]
|
||||
default = 1981-02-20 12:15 GMT+2
|
||||
type = string
|
||||
title = COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_TITLE
|
||||
description = COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_DESCRIPTION
|
||||
@ -0,0 +1,14 @@
|
||||
; "Error Logs" optional filter
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: dateconditional.ini 409 2011-01-24 09:30:22Z nikosdion $
|
||||
|
||||
; ======================================================================
|
||||
; Advanced configuration
|
||||
; ======================================================================
|
||||
|
||||
[core.filters.errorlogs.enabled]
|
||||
default=1
|
||||
type=bool
|
||||
title=COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_TITLE
|
||||
description=COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_DESCRIPTION
|
||||
bold=1
|
||||
@ -0,0 +1,14 @@
|
||||
; "Error Logs" optional filter
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: dateconditional.ini 409 2011-01-24 09:30:22Z nikosdion $
|
||||
|
||||
; ======================================================================
|
||||
; Advanced configuration
|
||||
; ======================================================================
|
||||
|
||||
[core.filters.hoststats.enabled]
|
||||
default=1
|
||||
type=bool
|
||||
title=COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_TITLE
|
||||
description=COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_DESCRIPTION
|
||||
bold=1
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Database table records exclusion filter
|
||||
*
|
||||
* This is simple stuff. If a table's on the list, it will backup just its structure, not
|
||||
* its contents. Fair and square...
|
||||
*/
|
||||
class Tabledata extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dbobject';
|
||||
$this->subtype = 'content';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Filter;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Database table exclusion filter
|
||||
*/
|
||||
class Tables extends Base
|
||||
{
|
||||
function __construct()
|
||||
{
|
||||
$this->object = 'dbobject';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
if (Factory::getKettenrad()->getTag() == 'restorepoint')
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,352 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
* @since 3.4
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Platform\Base;
|
||||
use Akeeba\Engine\Platform\PlatformInterface;
|
||||
|
||||
/**
|
||||
* Platform abstraction. Manages the loading of platform connector objects and delegates calls to itself the them.
|
||||
*
|
||||
* @property string $tableNameProfiles The name of the table where backup profiles are stored
|
||||
* @property string $tableNameStats The name of the table where backup records are stored
|
||||
*/
|
||||
class Platform
|
||||
{
|
||||
/** @var \Akeeba\Engine\Platform\Base|null The currently loaded platform connector object instance */
|
||||
protected static $platformConnectorInstance = null;
|
||||
|
||||
/** @var array A list of additional directories where platform classes can be found */
|
||||
protected static $knownPlatformsDirectories = array();
|
||||
|
||||
/** @var Platform The currently loaded object instance of this class. WARNING: This is NOT the platform connector! */
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Implements the Singleton pattern for this class
|
||||
*
|
||||
* @staticvar Platform $instance The static object instance
|
||||
*
|
||||
* @param string $platform Optional; platform name. Autodetect if blank.
|
||||
*
|
||||
* @return PlatformInterface
|
||||
*/
|
||||
public static function &getInstance($platform = null)
|
||||
{
|
||||
if (!is_object(static::$instance))
|
||||
{
|
||||
static::$instance = new Platform($platform);
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all directories where platform classes can be found
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getPlatformDirectories()
|
||||
{
|
||||
$defaultPath = array();
|
||||
|
||||
if (is_object(static::$platformConnectorInstance))
|
||||
{
|
||||
$defaultPath[] = __DIR__ . '/Platform/' . static::$platformConnectorInstance->platformName;
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
static::$knownPlatformsDirectories,
|
||||
$defaultPath
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public class constructor
|
||||
*
|
||||
* @param string $platform Optional; platform name. Leave blank to auto-detect.
|
||||
*
|
||||
* @throws \Exception When the platform cannot be loaded
|
||||
*/
|
||||
public function __construct($platform = null)
|
||||
{
|
||||
if (empty($platform) || is_null($platform))
|
||||
{
|
||||
$platform = static::detectPlatform();
|
||||
}
|
||||
|
||||
if (empty($platform))
|
||||
{
|
||||
throw new \Exception('Can not find a suitable Akeeba Engine platform for your site');
|
||||
}
|
||||
|
||||
static::$platformConnectorInstance = static::loadPlatform($platform);
|
||||
|
||||
if (!is_object(static::$platformConnectorInstance))
|
||||
{
|
||||
throw new \Exception("Can not load Akeeba Engine platform $platform");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect the suitable platform for this site
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \Exception When no platform is detected
|
||||
*/
|
||||
protected static function detectPlatform()
|
||||
{
|
||||
$platforms = static::listPlatforms();
|
||||
|
||||
if (empty($platforms))
|
||||
{
|
||||
throw new \Exception('No Akeeba Engine platform class found');
|
||||
}
|
||||
|
||||
$bestPlatform = (object)array(
|
||||
'name' => null,
|
||||
'priority' => 0,
|
||||
);
|
||||
|
||||
foreach ($platforms as $platform => $path)
|
||||
{
|
||||
$o = static::loadPlatform($platform, $path);
|
||||
|
||||
if (is_null($o))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($o->isThisPlatform())
|
||||
{
|
||||
if ($o->priority > $bestPlatform->priority)
|
||||
{
|
||||
$bestPlatform->priority = $o->priority;
|
||||
$bestPlatform->name = $platform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bestPlatform->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a given platform and return the platform object
|
||||
*
|
||||
* @param string $platform Platform name
|
||||
* @param string $path The path to laod the platform from (optional)
|
||||
*
|
||||
* @return \Akeeba\Engine\Platform\Base
|
||||
*/
|
||||
protected static function &loadPlatform($platform, $path = null)
|
||||
{
|
||||
if (empty($path))
|
||||
{
|
||||
if (isset(static::$knownPlatformsDirectories[$platform]))
|
||||
{
|
||||
$path = static::$knownPlatformsDirectories[$platform];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($path))
|
||||
{
|
||||
$path = dirname(__FILE__) . '/' . $platform;
|
||||
}
|
||||
|
||||
$classFile = $path . '/Platform.php';
|
||||
$className = '\\Akeeba\\Engine\\Platform\\' . ucfirst($platform);
|
||||
|
||||
$null = null;
|
||||
|
||||
if (!file_exists($classFile))
|
||||
{
|
||||
return $null;
|
||||
}
|
||||
|
||||
require_once($classFile);
|
||||
|
||||
if (!class_exists($className, false))
|
||||
{
|
||||
return $null;
|
||||
}
|
||||
|
||||
$o = new $className;
|
||||
|
||||
return $o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available platforms
|
||||
*
|
||||
* @staticvar array $platforms Static cache of the available platforms
|
||||
*
|
||||
* @return array The list of available platforms
|
||||
*/
|
||||
static public function listPlatforms()
|
||||
{
|
||||
if (empty(static::$knownPlatformsDirectories))
|
||||
{
|
||||
$di = new \DirectoryIterator(__DIR__ . '/Platform');
|
||||
|
||||
/** @var \DirectoryIterator $file */
|
||||
foreach ($di as $file)
|
||||
{
|
||||
if (!$file->isDir())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($file->isDot())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$shortName = $file->getFilename();
|
||||
|
||||
static::$knownPlatformsDirectories[$shortName] = $file->getRealPath();
|
||||
}
|
||||
}
|
||||
|
||||
return static::$knownPlatformsDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a platform to the list of known platforms
|
||||
*
|
||||
* @param string $slug Short name of the platform
|
||||
* @param string $platformDirectory The path where you can find it
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function addPlatform($slug, $platformDirectory)
|
||||
{
|
||||
if (empty(static::$knownPlatformsDirectories))
|
||||
{
|
||||
static::listPlatforms();
|
||||
|
||||
static::$knownPlatformsDirectories[$slug] = $platformDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to proxy all calls to the loaded platform object
|
||||
*
|
||||
* @param string $name The name of the method to call
|
||||
* @param array $arguments The arguments to pass
|
||||
*
|
||||
* @return mixed The result of the method being called
|
||||
*
|
||||
* @throws \Exception When the platform isn't loaded or an non-existent method is called
|
||||
*/
|
||||
public function __call($name, array $arguments)
|
||||
{
|
||||
if (is_null(static::$platformConnectorInstance))
|
||||
{
|
||||
throw new \Exception('Akeeba Engine platform is not loaded');
|
||||
}
|
||||
|
||||
if (method_exists(static::$platformConnectorInstance, $name))
|
||||
{
|
||||
// Call_user_func_array is ~3 times slower than direct method calls.
|
||||
// See the on-line PHP documentation page of call_user_func_array for more information.
|
||||
switch (count($arguments))
|
||||
{
|
||||
case 0 :
|
||||
$result = static::$platformConnectorInstance->$name();
|
||||
break;
|
||||
case 1 :
|
||||
$result = static::$platformConnectorInstance->$name($arguments[0]);
|
||||
break;
|
||||
case 2:
|
||||
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1]);
|
||||
break;
|
||||
case 3:
|
||||
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2]);
|
||||
break;
|
||||
case 4:
|
||||
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
|
||||
break;
|
||||
case 5:
|
||||
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
|
||||
break;
|
||||
default:
|
||||
// Resort to using call_user_func_array for many segments
|
||||
$result = call_user_func_array(array(static::$platformConnectorInstance, $name), $arguments);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \Exception('Method ' . $name . ' not found in Akeeba Platform');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter for the properties of the loaded platform
|
||||
*
|
||||
* @param string $name The name of the property to get
|
||||
*
|
||||
* @return mixed The value of the property
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
if (!isset(static::$platformConnectorInstance->$name) || !property_exists(static::$platformConnectorInstance, $name))
|
||||
{
|
||||
static::$platformConnectorInstance->$name = null;
|
||||
user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
|
||||
}
|
||||
|
||||
return static::$platformConnectorInstance->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic setter for the properties of the loaded platform
|
||||
*
|
||||
* @param string $name The name of the property to set
|
||||
* @param mixed $value The value of the property to set
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
if (isset(static::$platformConnectorInstance->$name) || property_exists(static::$platformConnectorInstance, $name))
|
||||
{
|
||||
static::$platformConnectorInstance->$name = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$platformConnectorInstance->$name = null;
|
||||
user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a platform connector object instance. This is used only in Unit Tests.
|
||||
*
|
||||
* @param \Akeeba\Engine\Platform\Base $platform The platform connector object to force
|
||||
*
|
||||
* @throws \Exception when used outside of Unit Tests
|
||||
*/
|
||||
public static function forcePlatformInstance(Base $platform)
|
||||
{
|
||||
if (!interface_exists('PHPUnit_Exception', false))
|
||||
{
|
||||
$method = __METHOD__;
|
||||
throw new \Exception("You can only use $method in Unit Tests", 500);
|
||||
}
|
||||
|
||||
static::$platformConnectorInstance = $platform;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Platform\Exception;
|
||||
|
||||
// Protection against direct access
|
||||
use Akeeba\Engine\Platform;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
/**
|
||||
* Thrown when the settings cannot be decrypted, e.g. when the server no longer has encyrption enabled or the key has
|
||||
* changed.
|
||||
*/
|
||||
class DecryptionException extends RuntimeException
|
||||
{
|
||||
public function __construct($message = null, $code = 500, Exception $previous = null)
|
||||
{
|
||||
if (empty($message))
|
||||
{
|
||||
$message = Platform::getInstance()->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION');
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,392 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Platform;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject as BaseObject;
|
||||
|
||||
/**
|
||||
* Interface PlatformInterface
|
||||
*
|
||||
* @property string $tableNameProfiles The name of the table where backup profiles are stored
|
||||
* @property string $tableNameStats The name of the table where backup records are stored
|
||||
* @property array $configOverrides Configuration overrides
|
||||
*/
|
||||
interface PlatformInterface
|
||||
{
|
||||
/**
|
||||
* Returns an array with the directory/-ies in which the magic autoloader
|
||||
* should look for platform overrides.
|
||||
*/
|
||||
public function getPlatformDirectories();
|
||||
|
||||
/**
|
||||
* Performs heuristics to determine if this platform object is the ideal
|
||||
* candidate for the environment Akeeba Engine is running in.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isThisPlatform();
|
||||
|
||||
/**
|
||||
* Saves the current configuration to the database table
|
||||
*
|
||||
* @param integer $profile_id The profile where to save the configuration
|
||||
* to, defaults to current profile
|
||||
*
|
||||
* @return boolean True if everything was saved properly
|
||||
*/
|
||||
public function save_configuration($profile_id = null);
|
||||
|
||||
/**
|
||||
* Loads the current configuration off the database table
|
||||
*
|
||||
* @param integer $profile_id The profile where to read the configuration from, defaults to current profile
|
||||
*
|
||||
* @return bool True if everything was read properly
|
||||
*/
|
||||
public function load_configuration($profile_id = null);
|
||||
|
||||
/**
|
||||
* Returns an associative array of stock platform directories
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stock_directories();
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the site's root
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_site_root();
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the installer images directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_installer_images_path();
|
||||
|
||||
/**
|
||||
* Returns the active profile number
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function get_active_profile();
|
||||
|
||||
/**
|
||||
* Returns the selected profile's name. If no ID is specified, the current
|
||||
* profile's name is returned.
|
||||
*
|
||||
* @param integer|null $id The ID of the profile, skip for current profile
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_profile_name($id = null);
|
||||
|
||||
/**
|
||||
* Returns the backup origin
|
||||
*
|
||||
* @return string Backup origin: backend|frontend
|
||||
*/
|
||||
public function get_backup_origin();
|
||||
|
||||
/**
|
||||
* Returns a timestamp formatted for the current site's database driver
|
||||
*
|
||||
* @param string $date [optional] The timestamp to use. Omit to use current timestamp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_timestamp_database($date = 'now');
|
||||
|
||||
/**
|
||||
* Returns the current timestamp, taking into account any TZ information,
|
||||
* in the format specified by $format.
|
||||
*
|
||||
* @param string $format Timestamp format string (standard PHP format string)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_local_timestamp($format);
|
||||
|
||||
/**
|
||||
* Returns the current host name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_host();
|
||||
|
||||
/**
|
||||
* Returns the current site's name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_site_name();
|
||||
|
||||
/**
|
||||
* Creates or updates the statistics record of the current backup attempt
|
||||
*
|
||||
* @param integer $id Backup record ID, use null for new record
|
||||
* @param array $data The data to store
|
||||
* @param BaseObject $caller The calling object
|
||||
*
|
||||
* @return mixed The new record id, or null if this doesn't apply, or false if it failed
|
||||
*/
|
||||
public function set_or_update_statistics($id = null, $data = array(), &$caller);
|
||||
|
||||
/**
|
||||
* Loads and returns a backup statistics record as a hash array
|
||||
*
|
||||
* @param integer $id Backup record ID
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_statistics($id);
|
||||
|
||||
/**
|
||||
* Completely removes a backup statistics record
|
||||
*
|
||||
* @param integer $id Backup record ID
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete_statistics($id);
|
||||
|
||||
/**
|
||||
* Returns a list of backup statistics records, respecting the pagination
|
||||
*
|
||||
* The $config array allows the following options to be set:
|
||||
* limitstart int Offset in the recordset to start from
|
||||
* limit int How many records to return at once
|
||||
* filters array An array of filters to apply to the results. Alternatively you can just pass a profile
|
||||
* ID to filter by that profile. order array Record ordering information (by and ordering)
|
||||
*
|
||||
* @param array $config See above
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function &get_statistics_list($config = array());
|
||||
|
||||
/**
|
||||
* Return the total number of statistics records
|
||||
*
|
||||
* @param array $filters An array of filters to apply to the results. Alternatively you can just pass a profile
|
||||
* ID to filter by that profile.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
function get_statistics_count($filters = null);
|
||||
|
||||
/**
|
||||
* Returns an array with the specifics of running backups
|
||||
*
|
||||
* @param string $tag The backup type (e.g. backend)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_running_backups($tag = null);
|
||||
|
||||
/**
|
||||
* Multiple backup attempts can share the same backup file name. Only
|
||||
* the last backup attempt's file is considered valid. Previous attempts
|
||||
* have to be deemed "obsolete". This method returns a list of backup
|
||||
* statistics ID's with "valid"-looking names. IT DOES NOT CHECK FOR THE
|
||||
* EXISTENCE OF THE BACKUP FILE!
|
||||
*
|
||||
* @param boolean $useprofile If true, it will only return backup records of the current profile
|
||||
* @param array $tagFilters Which tags to include; leave blank for all. If the first item is "NOT", then all
|
||||
* tags EXCEPT those listed will be included.
|
||||
* @param string $ordering Ordering of the records, default DESC (descending), use DESC or ASC
|
||||
*
|
||||
* @return array A list of ID's for records w/ "valid"-looking backup files
|
||||
*/
|
||||
public function &get_valid_backup_records($useprofile = false, $tagFilters = array(), $ordering = 'DESC');
|
||||
|
||||
/**
|
||||
* Invalidates older records sharing the same $archivename
|
||||
*
|
||||
* @param string $archivename The archive name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function remove_duplicate_backup_records($archivename);
|
||||
|
||||
/**
|
||||
* Marks the specified backup records as having no files
|
||||
*
|
||||
* @param array $ids Array of backup record IDs to ivalidate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function invalidate_backup_records($ids);
|
||||
|
||||
/**
|
||||
* Gets a list of records with remotely stored files in the selected remote storage
|
||||
* provider and profile.
|
||||
*
|
||||
* @param integer $profile (optional) The profile to use. Skip or use null for active profile.
|
||||
* @param string $engine (optional) The remote engine to looks for. Skip or use null for the active profile's
|
||||
* engine.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_valid_remote_records($profile = null, $engine = null);
|
||||
|
||||
/**
|
||||
* Returns the filter data for the entire filter group collection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function &load_filters();
|
||||
|
||||
/**
|
||||
* Saves the nested filter data array $filter_data to the database
|
||||
*
|
||||
* @param array $filter_data The filter data to save
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function save_filters(&$filter_data);
|
||||
|
||||
/**
|
||||
* Gets the best matching database driver class, according to CMS settings
|
||||
*
|
||||
* @param boolean $use_platform If set to false, it will forcibly try to assign one of the primitive type
|
||||
* (Mysql/Mysqli) and NEVER tell you to use an platform driver
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_database_driver($use_platform = true);
|
||||
|
||||
/**
|
||||
* Returns a set of options to connect to the default database of the current CMS
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_platform_database_options();
|
||||
|
||||
/**
|
||||
* Provides a platform-specific translation function
|
||||
*
|
||||
* @param string $key The translation key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function translate($key);
|
||||
|
||||
/**
|
||||
* Populates global constants holding the Akeeba version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load_version_defines();
|
||||
|
||||
/**
|
||||
* Returns the platform name and version
|
||||
*
|
||||
* @return array Contains the platform name and version
|
||||
*/
|
||||
public function getPlatformVersion();
|
||||
|
||||
/**
|
||||
* Logs platform-specific directories with LogLevel::INFO log level
|
||||
*
|
||||
* @return string If there are any extra notes / warnings on this platform
|
||||
*/
|
||||
public function log_platform_special_directories();
|
||||
|
||||
/**
|
||||
* Loads a platform-specific software configuration option. These are
|
||||
* configuration values for the backup engine, but unlike the rest of the
|
||||
* configuration options they are not stored in the backup profile.
|
||||
* Instead, these are stored globally using a platform-specific method.
|
||||
* So these are not configuration values for the platform itself.
|
||||
*
|
||||
* @param string $key The configuration option to retrieve
|
||||
* @param mixed $default The default value to return if it's not defined
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_platform_configuration_option($key, $default);
|
||||
|
||||
/**
|
||||
* Returns a list of emails to the administrators
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_administrator_emails();
|
||||
|
||||
/**
|
||||
* Sends a very simple email using the platform's emailer facility
|
||||
*
|
||||
* @param string $to Recipient address
|
||||
* @param string $subject Email subject line
|
||||
* @param string $body Email body (plain text)
|
||||
* @param string $attachFile (optional) Full path to the file being attached
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function send_email($to, $subject, $body, $attachFile = null);
|
||||
|
||||
/**
|
||||
* Deletes a file from the local server using direct file access or FTP
|
||||
*
|
||||
* @param string $file The file to unlink
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function unlink($file);
|
||||
|
||||
/**
|
||||
* Moves a file around within the local server using direct file access or FTP
|
||||
*
|
||||
* @param string $from Full path of the file to move
|
||||
* @param string $to Full path of where the file will be moved to
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to);
|
||||
|
||||
/**
|
||||
* Stores a flash (temporary) variable in the session.
|
||||
*
|
||||
* @param string $name The name of the variable to store
|
||||
* @param string $value The value of the variable to store
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_flash_variable($name, $value);
|
||||
|
||||
/**
|
||||
* Return the value of a flash (temporary) variable from the session and
|
||||
* immediately removes it.
|
||||
*
|
||||
* @param string $name The name of the flash variable
|
||||
* @param mixed $default Default value, if the variable is not defined
|
||||
*
|
||||
* @return mixed The value of the variable or $default if it's not set
|
||||
*/
|
||||
public function get_flash_variable($name, $default = null);
|
||||
|
||||
/**
|
||||
* Perform an immediate redirection to the defined URL
|
||||
*
|
||||
* @param string $url The URL to redirect to
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function redirect($url);
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Base\BaseObject;
|
||||
|
||||
/**
|
||||
* Akeeba Engine post processing abstract class
|
||||
*
|
||||
* @author Nicholas
|
||||
*
|
||||
*/
|
||||
abstract class Base extends BaseObject
|
||||
{
|
||||
/** @var bool Should we break the step before post-processing? */
|
||||
public $break_before = true;
|
||||
|
||||
/** @var bool Should we break the step after post-processing? */
|
||||
public $break_after = true;
|
||||
|
||||
/** @var bool Does this engine processes the files in a way that makes deleting the originals safe? */
|
||||
public $allow_deletes = true;
|
||||
|
||||
/** @var bool Does this engine support remote file deletes? */
|
||||
public $can_delete = false;
|
||||
|
||||
/** @var bool Does this engine support downloads to files? */
|
||||
public $can_download_to_file = false;
|
||||
|
||||
/** @var bool Does this engine support downloads to browser? */
|
||||
public $can_download_to_browser = false;
|
||||
|
||||
/** @var bool Set to true if raw data will be dumped to the browser when downloading the file to the browser. Set to false if a URL is returned instead. */
|
||||
public $downloads_to_browser_inline = false;
|
||||
|
||||
/**
|
||||
* The remote absolute path to the file which was just processed. Leave null if the file is meant to
|
||||
* be non-retrievable, i.e. sent to email or any other one way service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $remote_path = null;
|
||||
|
||||
/**
|
||||
* This function takes care of post-processing a backup archive's part, or the
|
||||
* whole backup archive if it's not a split archive type. If the process fails
|
||||
* it should return false. If it succeeds and the entirety of the file has been
|
||||
* processed, it should return true. If only a part of the file has been uploaded,
|
||||
* it must return 1.
|
||||
*
|
||||
* @param string $absolute_filename Absolute path to the part we'll have to process
|
||||
* @param string $upload_as Base name of the uploaded file, skip to use $absolute_filename's
|
||||
*
|
||||
* @return boolean|integer False on failure, true on success, 1 if more work is required
|
||||
*/
|
||||
public abstract function processPart($absolute_filename, $upload_as = null);
|
||||
|
||||
/**
|
||||
* Deletes a remote file
|
||||
*
|
||||
* @param $path string Absolute path to the file we're deleting
|
||||
*
|
||||
* @return bool|int False on failure, true on success, 1 if more work is required
|
||||
*/
|
||||
public function delete($path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a remote file to a local file, optionally doing a range download. If the
|
||||
* download fails we return false. If the download succeeds we return true. If range
|
||||
* downloads are not supported, -1 is returned and nothing is written to disk.
|
||||
*
|
||||
* @param $remotePath string The path to the remote file
|
||||
* @param $localFile string The absolute path to the local file we're writing to
|
||||
* @param $fromOffset int|null The offset (in bytes) to start downloading from
|
||||
* @param $length int|null The amount of data (in bytes) to download
|
||||
*
|
||||
* @return bool|int True on success, false on failure, -1 if ranges are not supported
|
||||
*/
|
||||
public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a public download URL or starts a browser-side download of a remote file.
|
||||
* In the case of a public download URL, a string is returned. If a browser-side
|
||||
* download is initiated, it returns true. In any other case (e.g. unsupported, not
|
||||
* found, etc) it returns false.
|
||||
*
|
||||
* @param $remotePath string The file to download
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function downloadToBrowser($remotePath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to call arbitrary methods in this engine through an AJAX call
|
||||
*
|
||||
* @param string $method The method to call.
|
||||
* @param array $params Any parameters to send to the method, in array format
|
||||
*
|
||||
* @return mixed Whatever the method has to return. It will be JSON encoded by the AJAX handler.
|
||||
*/
|
||||
public function customAPICall($method, $params = array())
|
||||
{
|
||||
if ( !method_exists($this, $method))
|
||||
{
|
||||
header('HTTP/1.0 501 Not Implemented');
|
||||
exit();
|
||||
}
|
||||
else
|
||||
{
|
||||
return call_user_func_array(array($this, $method), array($params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an OAuth window (perform redirection), or return false if this is not supported
|
||||
*
|
||||
* @param array $params Any parameters required to launch OAuth
|
||||
*
|
||||
* @return boolean|void False if not supported, or nothing (redirection performed) otherwise.
|
||||
*/
|
||||
public function oauthOpen($params = array())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
*
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class Email extends Base
|
||||
{
|
||||
public function processPart($absolute_filename, $upload_as = null)
|
||||
{
|
||||
// Retrieve engine configuration data
|
||||
$config = Factory::getConfiguration();
|
||||
|
||||
$address = trim($config->get('engine.postproc.email.address', ''));
|
||||
$subject = $config->get('engine.postproc.email.subject', '0');
|
||||
|
||||
// Sanity checks
|
||||
if (empty($address))
|
||||
{
|
||||
$this->setError('You have not set up a recipient\'s email address for the backup files');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the file
|
||||
$basename = empty($upload_as) ? basename($absolute_filename) : $upload_as;
|
||||
Factory::getLog()->log(LogLevel::INFO, "Preparing to email $basename to $address");
|
||||
if (empty($subject))
|
||||
{
|
||||
if (class_exists('JText'))
|
||||
{
|
||||
$subject = \JText::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
|
||||
}
|
||||
elseif (class_exists('\Awf\Text\Text'))
|
||||
{
|
||||
$subject = \Awf\Text\Text::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
|
||||
}
|
||||
else
|
||||
{
|
||||
$subject = "You have a new backup part";
|
||||
}
|
||||
}
|
||||
$body = "Emailing $basename";
|
||||
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Subject: $subject");
|
||||
Factory::getLog()->log(LogLevel::DEBUG, "Body: $body");
|
||||
|
||||
$result = Platform::getInstance()->send_email($address, $subject, $body, $absolute_filename);
|
||||
|
||||
// Return the result
|
||||
if ($result !== true)
|
||||
{
|
||||
// An error occurred
|
||||
$this->setError($result);
|
||||
|
||||
// Notify that we failed
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return success
|
||||
Factory::getLog()->log(LogLevel::INFO, "Email sent successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
* The modular PHP5 site backup engine
|
||||
* @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU GPL version 3 or, at your option, any later version
|
||||
* @package akeebaengine
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Postproc;
|
||||
|
||||
// Protection against direct access
|
||||
defined('AKEEBAENGINE') or die();
|
||||
|
||||
class None extends Base
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No point in breaking the step; we simply do nothing :)
|
||||
$this->break_after = false;
|
||||
$this->break_before = false;
|
||||
$this->allow_deletes = false;
|
||||
}
|
||||
|
||||
public function processPart($absolute_filename, $upload_as = null)
|
||||
{
|
||||
// Really nothing to do!!
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
; Akeeba Send by Email post processing engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: email.ini 738 2011-06-15 13:11:38Z nikosdion $
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title=COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_TITLE
|
||||
description=COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_DESCRIPTION
|
||||
|
||||
; Post-process after generating each part?
|
||||
[engine.postproc.common.after_part]
|
||||
default=0
|
||||
type=bool
|
||||
title=COM_AKEEBA_CONFIG_POSTPROCPARTS_TITLE
|
||||
description=COM_AKEEBA_CONFIG_POSTPROCPARTS_DESCRIPTION
|
||||
|
||||
; Delete from server after processing?
|
||||
[engine.postproc.common.delete_after]
|
||||
default=1
|
||||
type=bool
|
||||
title=COM_AKEEBA_CONFIG_DELETEAFTER_TITLE
|
||||
description=COM_AKEEBA_CONFIG_DELETEAFTER_DESCRIPTION
|
||||
|
||||
; Email address
|
||||
[engine.postproc.email.address]
|
||||
default=""
|
||||
type=string
|
||||
title=COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_TITLE
|
||||
description=COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_DESCRIPTION
|
||||
|
||||
; Subject
|
||||
[engine.postproc.email.subject]
|
||||
default=""
|
||||
type=string
|
||||
title=COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_TITLE
|
||||
description=COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_DESCRIPTION
|
||||
@ -0,0 +1,8 @@
|
||||
; Akeeba no post=processing engine
|
||||
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
; Version $Id: none.ini 409 2011-01-24 09:30:22Z nikosdion $
|
||||
|
||||
; Engine information
|
||||
[_information]
|
||||
title = COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_TITLE
|
||||
description = COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_DESCRIPTION
|
||||
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* This is a simple Logger implementation that other Loggers can inherit from.
|
||||
*
|
||||
* It simply delegates all log-level-specific methods to the `log` method to
|
||||
* reduce boilerplate code that a simple Logger that does the same thing with
|
||||
* messages regardless of the error level has to implement.
|
||||
*/
|
||||
abstract class AbstractLogger implements LoggerInterface
|
||||
{
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function emergency($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function alert($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::ALERT, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function critical($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function error($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function warning($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function notice($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::NOTICE, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function info($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function debug($message, array $context = array())
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, $message, $context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Describes log levels
|
||||
*/
|
||||
class LogLevel
|
||||
{
|
||||
const EMERGENCY = 'emergency';
|
||||
const ALERT = 'alert';
|
||||
const CRITICAL = 'critical';
|
||||
const ERROR = 'error';
|
||||
const WARNING = 'warning';
|
||||
const NOTICE = 'notice';
|
||||
const INFO = 'info';
|
||||
const DEBUG = 'debug';
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Describes a logger-aware instance
|
||||
*/
|
||||
interface LoggerAwareInterface
|
||||
{
|
||||
/**
|
||||
* Sets a logger instance on the object
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return null
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Psr\Log;
|
||||
|
||||
/**
|
||||
* Basic Implementation of LoggerAwareInterface.
|
||||
*/
|
||||
trait LoggerAwareTrait
|
||||
{
|
||||
/** @var LoggerInterface */
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Sets a logger.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user