www.archline.hu — clean post-compromise baseline #2

Merged
imre.agent merged 88 commits from baseline/47 into main 2026-07-16 14:40:45 +02:00
884 changed files with 256222 additions and 0 deletions
Showing only changes of commit 9c9a0366e8 - Show all commits

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}
}
}

View 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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;
}
}

View File

@ -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;
}
}
?>

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}

View File

@ -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
{
}

View File

@ -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) . ')';
}
}
}

View File

@ -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
{
}

View File

@ -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 . "'";
}
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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 . "'" . ')';
}
}

View File

@ -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 {};

View File

@ -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';
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 '';
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}

View File

@ -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.");
}
}

View File

@ -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.");
}
}

View File

@ -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.");
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Psr\Log;
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@ -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';
}

View File

@ -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);
}

View File

@ -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