diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Base.php new file mode 100644 index 00000000..1871776f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Base.php @@ -0,0 +1,830 @@ +__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("
", "\n", $comment); // Replace HTML4
with single newlines + $comment = str_replace("
", "\n", $comment); // Replace HTML4
with single newlines + $comment = str_replace("
", "\n", $comment); // Replace HTML
with single newlines + $comment = str_replace("

", "\n\n", $comment); // Replace paragraph endings with double newlines + $comment = str_replace("", "*", $comment); // Replace bold with star notation + $comment = str_replace("", "*", $comment); // Replace bold with star notation + $comment = str_replace("", "_", $comment); // Replace italics with underline notation + $comment = str_replace("", "_", $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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseArchiver.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseArchiver.php new file mode 100644 index 00000000..d93f9ff3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseArchiver.php @@ -0,0 +1,708 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseFileManagement.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseFileManagement.php new file mode 100644 index 00000000..71c88247 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/BaseFileManagement.php @@ -0,0 +1,214 @@ +_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]); + } + } + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Jpa.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Jpa.php new file mode 100644 index 00000000..4c9854bc --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Jpa.php @@ -0,0 +1,569 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Zip.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Zip.php new file mode 100644 index 00000000..4842b516 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/Zip.php @@ -0,0 +1,908 @@ +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); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/jpa.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/jpa.ini new file mode 100644 index 00000000..9e1f5110 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/jpa.ini @@ -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. diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/zip.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/zip.ini new file mode 100644 index 00000000..c4df10b7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Archiver/zip.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Autoloader.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Autoloader.php new file mode 100644 index 00000000..ca73842f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Autoloader.php @@ -0,0 +1,184 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Base/Exceptions/ErrorException.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Base/Exceptions/ErrorException.php new file mode 100644 index 00000000..3646dc5e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Base/Exceptions/ErrorException.php @@ -0,0 +1,25 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Configuration.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Configuration.php new file mode 100644 index 00000000..34ea3cce --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Configuration.php @@ -0,0 +1,671 @@ +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; + } +} + +?> diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/01.basic.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/01.basic.ini new file mode 100644 index 00000000..2896b9fb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/01.basic.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/02.advanced.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/02.advanced.ini new file mode 100644 index 00000000..345fc8ad --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/02.advanced.ini @@ -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" diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/04.quota.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/04.quota.ini new file mode 100644 index 00000000..e08799e5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/04.quota.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/05.tuning.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/05.tuning.ini new file mode 100644 index 00000000..59644d17 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/05.tuning.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Database.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Database.php new file mode 100644 index 00000000..2bd086d9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Database.php @@ -0,0 +1,137 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Db.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Db.php new file mode 100644 index 00000000..e0b796fa --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Db.php @@ -0,0 +1,372 @@ +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 .= <<databases_ini .= <<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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Finalization.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Finalization.php new file mode 100644 index 00000000..896e4650 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Finalization.php @@ -0,0 +1,1484 @@ +get('volatile.breakflag', false); + + // Populate actions queue + $this->action_queue = array( + 'remove_temp_files', + 'update_statistics', + 'update_filesizes', + 'run_post_processing', + 'kickstart_post_processing', + 'apply_quotas', + 'apply_remote_quotas', + 'mail_administrators', + 'update_statistics', // Run it a second time to update the backup end time after post processing, emails, etc ;) + ); + + // Remove the Kickstart post-processing if we do not have the option set + $uploadKickstart = $configuration->get('akeeba.advanced.uploadkickstart', 0); + + if (!$uploadKickstart) + { + unset ($this->action_queue['kickstart_post_processing']); + } + + // Allow adding finalization action objects using the volatile.core.finalization.action_handlers array + $customHandlers = $configuration->get('volatile.core.finalization.action_handlers', null); + + if (is_array($customHandlers) && !empty($customHandlers)) + { + foreach ($customHandlers as $handler) + { + $this->action_handlers[] = $handler; + } + } + + // Do we have a custom action queue set in volatile.core.finalization.action_queue? + $customQueue = $configuration->get('volatile.core.finalization.action_queue', null); + $customQueueBefore = $configuration->get('volatile.core.finalization.action_queue_before', null); + + if (is_array($customQueue) && !empty($customQueue)) + { + Factory::getLog()->log(LogLevel::DEBUG, 'Overriding finalization action queue'); + $this->action_queue = array(); + + foreach ($customQueue as $action) + { + if (method_exists($this, $action)) + { + $this->action_queue[] = $action; + } + else + { + foreach ($this->action_handlers as $handler) + { + if (method_exists($handler, $action)) + { + $this->action_queue[] = $action; + break; + } + } + } + } + } + + if (is_array($customQueueBefore) && !empty($customQueueBefore)) + { + // Get all actions before run_post_processing + $temp = array(); + $temp[] = array_shift($this->action_queue); + $temp[] = array_shift($this->action_queue); + $temp[] = array_shift($this->action_queue); + + // Add the custom handlers from volatile.core.finalization.action_handlers_before + while (!empty($customQueueBefore)) + { + $action = array_pop($customQueueBefore); + + if (method_exists($this, $action)) + { + array_unshift($this->action_queue, $action); + } + else + { + foreach ($this->action_handlers as $handler) + { + if (method_exists($handler, $action)) + { + array_unshift($this->action_queue, $action); + + break; + } + } + } + } + + // Add back the handlers we shifted before + foreach ($temp as $action) + { + array_unshift($this->action_queue, $action); + } + } + + Factory::getLog()->log(LogLevel::DEBUG, 'Finalization action queue: ' . implode(', ', $this->action_queue)); + + $this->steps_total = count($this->action_queue); + $this->steps_done = 0; + $this->substeps_total = 0; + $this->substeps_done = 0; + + // Seed the method + $this->current_method = array_shift($this->action_queue); + + // Set ourselves to running state + $this->setState('running'); + } + + /** + * Implements the abstract method + * + * @return void + */ + protected function _run() + { + $configuration = Factory::getConfiguration(); + + if ($this->getState() == 'postrun') + { + return; + } + + $finished = (empty($this->action_queue)) && ($this->current_method == ''); + + if ($finished) + { + $this->setState('postrun'); + + return; + } + + $this->setState('running'); + + $timer = Factory::getTimer(); + + // Continue processing while we have still enough time and stuff to do + while (($timer->getTimeLeft() > 0) && (!$finished) && (!$configuration->get('volatile.breakflag', false))) + { + $method = $this->current_method; + + if (method_exists($this, $method)) + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . "::_run() Running built-in method $method"); + $status = $this->$method(); + } + else + { + $status = true; + + if (!empty($this->action_handlers)) + { + foreach ($this->action_handlers as $handler) + { + if (method_exists($handler, $method)) + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . "::_run() Running add-on method $method"); + $status = $handler->$method($this); + break; + } + } + } + } + + if ($status === true) + { + $this->current_method = ''; + $this->steps_done++; + $finished = (empty($this->action_queue)); + + if (!$finished) + { + $this->current_method = array_shift($this->action_queue); + $this->substeps_total = 0; + $this->substeps_done = 0; + } + } + } + + if ($finished) + { + $this->setState('postrun'); + $this->setStep(''); + $this->setSubstep(''); + } + } + + /** + * Implements the abstract method + * + * @return void + */ + protected function _finalize() + { + $this->setState('finished'); + } + + /** + * Sends an email to the administrators + * + * @return bool True on success + */ + protected function mail_administrators() + { + $this->setStep('Processing emails to administrators'); + $this->setSubstep(''); + + // Skip email for back-end backups + if (Platform::getInstance()->get_backup_origin() == 'backend') + { + return true; + } + + $must_email = Platform::getInstance()->get_platform_configuration_option('frontend_email_on_finish', 0) != 0; + + if (!$must_email) + { + return true; + } + + Factory::getLog()->log(LogLevel::DEBUG, "Preparing to send e-mail to administrators"); + + $email = Platform::getInstance()->get_platform_configuration_option('frontend_email_address', ''); + $email = trim($email); + + if (!empty($email)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Using pre-defined list of emails"); + $emails = explode(',', $email); + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "Fetching list of Super Administrator emails"); + $emails = Platform::getInstance()->get_administrator_emails(); + } + + if (!empty($emails)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Creating email subject and body"); + // Fetch user's preferences + $subject = trim(Platform::getInstance()->get_platform_configuration_option('frontend_email_subject', '')); + $body = trim(Platform::getInstance()->get_platform_configuration_option('frontend_email_body', '')); + + // Get the statistics + $statistics = Factory::getStatistics(); + $stat = $statistics->getRecord(); + $parts = Factory::getStatistics()->get_all_filenames($stat, false); + + $profile_number = Platform::getInstance()->get_active_profile(); + $profile_name = Platform::getInstance()->get_profile_name($profile_number); + $parts = Factory::getStatistics()->get_all_filenames($stat, false); + $stat = (object)$stat; + $num_parts = $stat->multipart; + + // Non-split archives have a part count of 0 + if ($num_parts == 0) + { + $num_parts = 1; + } + + $parts_list = ''; + + if (!empty($parts)) + { + foreach ($parts as $file) + { + $parts_list .= "\t" . basename($file) . "\n"; + } + } + + // Get the remote storage status + $remote_status = ''; + $post_proc_engine = Factory::getConfiguration()->get('akeeba.advanced.postproc_engine'); + + if (!empty($post_proc_engine) && ($post_proc_engine != 'none')) + { + if (empty($stat->remote_filename)) + { + $remote_status = Platform::getInstance()->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_FAILED'); + } + else + { + $remote_status = Platform::getInstance()->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_SUCCESS'); + } + } + + // Do we need a default subject? + if (empty($subject)) + { + // Get the default subject + $subject = Platform::getInstance()->translate('COM_AKEEBA_COMMON_EMAIL_SUBJECT_OK'); + } + else + { + // Post-process the subject + $subject = Factory::getFilesystemTools()->replace_archive_name_variables($subject); + } + + // Do we need a default body? + if (empty($body)) + { + $body = Platform::getInstance()->translate('COM_AKEEBA_COMMON_EMAIL_BODY_OK'); + $info_source = Platform::getInstance()->translate('COM_AKEEBA_COMMON_EMAIL_BODY_INFO'); + $body .= "\n\n" . sprintf($info_source, $profile_number, $num_parts) . "\n\n"; + $body .= $parts_list; + } + else + { + // Post-process the body + $body = Factory::getFilesystemTools()->replace_archive_name_variables($body); + $body = str_replace('[PROFILENUMBER]', $profile_number, $body); + $body = str_replace('[PROFILENAME]', $profile_name, $body); + $body = str_replace('[PARTCOUNT]', $num_parts, $body); + $body = str_replace('[FILELIST]', $parts_list, $body); + $body = str_replace('[REMOTESTATUS]', $remote_status, $body); + } + // Sometimes $body contains literal \n instead of newlines + $body = str_replace('\\n', "\n", $body); + + foreach ($emails as $email) + { + Factory::getLog()->log(LogLevel::DEBUG, "Sending email to $email"); + Platform::getInstance()->send_email($email, $subject, $body); + } + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "No email recipients found! Skipping email."); + } + + return true; + } + + /** + * Removes temporary files + * + * @return bool True on success + */ + protected function remove_temp_files() + { + $this->setStep('Removing temporary files'); + $this->setSubstep(''); + Factory::getLog()->log(LogLevel::DEBUG, "Removing temporary files"); + Factory::getTempFiles()->deleteTempFiles(); + + return true; + } + + /** + * Runs the writer's post-processing steps + * + * @return bool True on success + */ + protected function run_post_processing() + { + $this->setStep('Post-processing'); + + // Do not run if the archive engine doesn't produce archives + $configuration = Factory::getConfiguration(); + $this->setSubstep(''); + + $engine_name = $configuration->get('akeeba.advanced.postproc_engine'); + + Factory::getLog()->log(LogLevel::DEBUG, "Loading post-processing engine object ($engine_name)"); + + $post_proc = Factory::getPostprocEngine($engine_name); + + // Initialize the archive part list if required + if (empty($this->backup_parts)) + { + Factory::getLog()->log(LogLevel::INFO, 'Initializing post-processing engine'); + + // Initialize the flag for multistep post-processing of parts + $configuration->set('volatile.postproc.filename', null); + $configuration->set('volatile.postproc.directory', null); + + // Populate array w/ absolute names of backup parts + $statistics = Factory::getStatistics(); + $stat = $statistics->getRecord(); + $this->backup_parts = Factory::getStatistics()->get_all_filenames($stat, false); + + if (is_null($this->backup_parts)) + { + // No archive produced, or they are all already post-processed + Factory::getLog()->log(LogLevel::INFO, 'No archive files found to post-process'); + + return true; + } + + Factory::getLog()->log(LogLevel::DEBUG, count($this->backup_parts) . ' files to process found'); + + $this->substeps_total = count($this->backup_parts); + $this->substeps_done = 0; + + $this->backup_parts_index = 0; + + // If we have an empty array, do not run + if (empty($this->backup_parts)) + { + return true; + } + + // Break step before processing? + if ($post_proc->break_before && !Factory::getConfiguration() + ->get('akeeba.tuning.nobreak.finalization', 0) + ) + { + Factory::getLog()->log(LogLevel::DEBUG, 'Breaking step before post-processing run'); + $configuration->set('volatile.breakflag', true); + + return false; + } + } + + // Make sure we don't accidentally break the step when not required to do so + $configuration->set('volatile.breakflag', false); + + // Do we have a filename from the previous run of the post-proc engine? + $filename = $configuration->get('volatile.postproc.filename', null); + + if (empty($filename)) + { + $filename = $this->backup_parts[$this->backup_parts_index]; + Factory::getLog()->log(LogLevel::INFO, 'Beginning post processing file ' . $filename); + } + else + { + Factory::getLog()->log(LogLevel::INFO, 'Continuing post processing file ' . $filename); + } + + $this->setStep('Post-processing'); + $this->setSubstep(basename($filename)); + $timer = Factory::getTimer(); + $startTime = $timer->getRunningTime(); + $result = $post_proc->processPart($filename); + $this->propagateFromObject($post_proc); + + if ($result === false) + { + $this->setWarning('Failed to process file ' . $filename); + Factory::getLog()->log(LogLevel::WARNING, 'Error received from the post-processing engine:'); + Factory::getLog()->log(LogLevel::WARNING, implode("\n", array_merge($this->getWarnings(), $this->getErrors()))); + } + elseif ($result === true) + { + // The post-processing of this file ended successfully + Factory::getLog()->log(LogLevel::INFO, 'Finished post-processing file ' . $filename); + $configuration->set('volatile.postproc.filename', null); + } + else + { + // More work required + Factory::getLog()->log(LogLevel::INFO, 'More post-processing steps required for file ' . $filename); + $configuration->set('volatile.postproc.filename', $filename); + + // Do we need to break the step? + $endTime = $timer->getRunningTime(); + $stepTime = $endTime - $startTime; + $timeLeft = $timer->getTimeLeft(); + + if ($timeLeft < $stepTime) + { + // We predict that running yet another step would cause a timeout + $configuration->set('volatile.breakflag', true); + } + else + { + // We have enough time to run yet another step + $configuration->set('volatile.breakflag', false); + } + } + + // Should we delete the file afterwards? + if ( + $configuration->get('engine.postproc.common.delete_after', false) + && $post_proc->allow_deletes + && ($result === true) + ) + { + Factory::getLog()->log(LogLevel::DEBUG, 'Deleting already processed file ' . $filename); + Platform::getInstance()->unlink($filename); + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, 'Not removing processed file ' . $filename); + } + + if ($result === true) + { + // Move the index forward if the part finished processing + $this->backup_parts_index++; + + // Mark substep done + $this->substeps_done++; + + // Break step after processing? + if ($post_proc->break_after && !Factory::getConfiguration()->get('akeeba.tuning.nobreak.finalization', 0)) + { + $configuration->set('volatile.breakflag', true); + } + + // If we just finished processing the first archive part, save its remote path in the statistics. + if (($this->substeps_done == 1) || ($this->substeps_total == 0)) + { + if (!empty($post_proc->remote_path)) + { + $statistics = Factory::getStatistics(); + $remote_filename = $engine_name . '://'; + $remote_filename .= $post_proc->remote_path; + $data = array( + 'remote_filename' => $remote_filename + ); + $remove_after = $configuration->get('engine.postproc.common.delete_after', false); + + if ($remove_after) + { + $data['filesexist'] = 0; + } + + $statistics->setStatistics($data); + $this->propagateFromObject($statistics); + } + } + + // Are we past the end of the array (i.e. we're finished)? + if ($this->backup_parts_index >= count($this->backup_parts)) + { + Factory::getLog()->log(LogLevel::INFO, 'Post-processing has finished for all files'); + + return true; + } + } + elseif ($result === false) + { + // If the post-processing failed, make sure we don't process anything else + $this->backup_parts_index = count($this->backup_parts); + $this->setWarning('Post-processing interrupted -- no more files will be transferred'); + + return true; + } + + // Indicate we're not done yet + return false; + } + + /** + * Runs the Kickstart post-processing step + * + * @return bool True on success + */ + protected function kickstart_post_processing() + { + $this->setStep('Post-processing Kickstart'); + + $configuration = Factory::getConfiguration(); + $this->setSubstep(''); + + // Do not run if we are not told to upload Kickstart + $uploadKickstart = $configuration->get('akeeba.advanced.uploadkickstart', 0); + + if (!$uploadKickstart) + { + Factory::getLog()->log(LogLevel::INFO, "Getting ready to upload Kickstart"); + + return true; + } + + $engine_name = $configuration->get('akeeba.advanced.postproc_engine'); + Factory::getLog()->log(LogLevel::DEBUG, "Loading post-processing engine object ($engine_name)"); + $post_proc = Factory::getPostprocEngine($engine_name); + + // Set $filename to kickstart's source file + $filename = Platform::getInstance()->get_installer_images_path() . '/kickstart.txt'; + + // Post-process the file + $this->setSubstep('kickstart.php'); + + if (!@file_exists($filename) || !is_file($filename)) + { + $this->setWarning('Failed to upload kickstart.php. Missing file ' . $filename); + + // Indicate we're done. + return true; + } + + $result = $post_proc->processPart($filename, 'kickstart.php'); + $this->propagateFromObject($post_proc); + + if ($result === false) + { + $this->setWarning('Failed to upload kickstart.php'); + + Factory::getLog()->log(LogLevel::WARNING, 'Error received from the post-processing engine:'); + Factory::getLog()->log(LogLevel::WARNING, implode("\n", $this->getWarnings())); + } + elseif ($result === true) + { + // The post-processing of this file ended successfully + Factory::getLog()->log(LogLevel::INFO, 'Finished uploading kickstart.php'); + $configuration->set('volatile.postproc.filename', null); + } + + // Indicate we're done + return true; + } + + /** + * Updates the backup statistics record + * + * @return bool True on success + */ + protected function update_statistics() + { + $this->setStep('Updating backup record information'); + $this->setSubstep(''); + + Factory::getLog()->log(LogLevel::DEBUG, "Updating statistics"); + // We finished normally. Fetch the stats record + $statistics = Factory::getStatistics(); + $registry = Factory::getConfiguration(); + $data = array( + 'backupend' => Platform::getInstance()->get_timestamp_database(), + 'status' => 'complete', + 'multipart' => $registry->get('volatile.statistics.multipart', 0) + ); + $result = $statistics->setStatistics($data); + + if ($result === false) + { + // Most likely a "MySQL has gone away" issue... + $configuration = Factory::getConfiguration(); + $configuration->set('volatile.breakflag', true); + + return false; + } + + $this->propagateFromObject($statistics); + + $stat = (object)$statistics->getRecord(); + Platform::getInstance()->remove_duplicate_backup_records($stat->archivename); + + return true; + } + + protected function update_filesizes() + { + $this->setStep('Updating file sizes'); + $this->setSubstep(''); + Factory::getLog()->log(LogLevel::DEBUG, "Updating statistics with file sizes"); + + // Fetch the stats record + $statistics = Factory::getStatistics(); + $record = $statistics->getRecord(); + $filenames = $statistics->get_all_filenames($record); + $filesize = 0.0; + + // Calculate file sizes of files remaining on the server + if (!empty($filenames)) + { + foreach ($filenames as $file) + { + $size = @filesize($file); + + if ($size !== false) + { + $filesize += $size * 1.0; + } + } + } + + // Get the part size in volatile storage, set from the immediate part uploading effected by the + // "Process each part immediately" option, and add it to the total file size + $config = Factory::getConfiguration(); + $postProcImmediately = $config->get('engine.postproc.common.after_part', 0, false); + $deleteAfter = $config->get('engine.postproc.common.delete_after', 0, false); + $postProcEngine = $config->get('akeeba.advanced.postproc_engine', 'none'); + + if ($postProcImmediately && $deleteAfter && ($postProcEngine != 'none')) + { + $volatileTotalSize = Factory::getConfiguration()->get('volatile.engine.archiver.totalsize', 0); + + if ($volatileTotalSize) + { + $filesize += $volatileTotalSize; + } + } + + $data = array( + 'total_size' => $filesize + ); + + Factory::getLog()->log(LogLevel::DEBUG, "Total size of backup archive (in bytes): $filesize"); + + $statistics->setStatistics($data); + $this->propagateFromObject($statistics); + + return true; + } + + /** + * Applies the size and count quotas + * + * @return bool True on success + */ + protected function apply_quotas() + { + $this->setStep('Applying quotas'); + $this->setSubstep(''); + + // If no quota settings are enabled, quit + $registry = Factory::getConfiguration(); + $useDayQuotas = $registry->get('akeeba.quota.maxage.enable'); + $useCountQuotas = $registry->get('akeeba.quota.enable_count_quota'); + $useSizeQuotas = $registry->get('akeeba.quota.enable_size_quota'); + + if (!($useDayQuotas || $useCountQuotas || $useSizeQuotas)) + { + $this->apply_obsolete_quotas(); + + Factory::getLog()->log(LogLevel::DEBUG, "No quotas were defined; old backup files will be kept intact"); + + return true; // No quota limits were requested + } + + // Try to find the files to be deleted due to quota settings + $statistics = Factory::getStatistics(); + $latestBackupId = $statistics->getId(); + + // Get quota values + $countQuota = $registry->get('akeeba.quota.count_quota'); + $sizeQuota = $registry->get('akeeba.quota.size_quota'); + $daysQuota = $registry->get('akeeba.quota.maxage.maxdays'); + $preserveDay = $registry->get('akeeba.quota.maxage.keepday'); + + // Get valid-looking backup ID's + $validIDs = Platform::getInstance()->get_valid_backup_records(true, array('NOT', 'restorepoint')); + + // Create a list of valid files + $allFiles = array(); + + if (count($validIDs)) + { + foreach ($validIDs as $id) + { + $stat = Platform::getInstance()->get_statistics($id); + + try + { + $backupstart = new \DateTime($stat['backupstart']); + $backupTS = $backupstart->format('U'); + $backupDay = $backupstart->format('d'); + } + catch (\Exception $e) + { + $backupTS = 0; + $backupDay = 0; + } + + // Get the log file name + $tag = $stat['tag']; + $backupId = isset($stat['backupid']) ? $stat['backupid'] : ''; + $logName = ''; + + if (!empty($backupId)) + { + $logName = 'akeeba.' . $tag . '.' . $backupId . '.log'; + } + + // Multipart processing + $filenames = Factory::getStatistics()->get_all_filenames($stat, true); + + if (!is_null($filenames)) + { + // Only process existing files + $filesize = 0; + + foreach ($filenames as $filename) + { + $filesize += @filesize($filename); + } + + $allFiles[] = array( + 'id' => $id, + 'filenames' => $filenames, + 'size' => $filesize, + 'backupstart' => $backupTS, + 'day' => $backupDay, + 'logname' => $logName, + ); + } + } + } + + unset($validIDs); + + // If there are no files, exit early + if (count($allFiles) == 0) + { + Factory::getLog()->log(LogLevel::DEBUG, "There were no old backup files to apply quotas on"); + + return true; + } + + // Init arrays + $killids = array(); + $killLogs = array(); + $ret = array(); + $leftover = array(); + + // Do we need to apply maximum backup age quotas? + if ($useDayQuotas) + { + $killDatetime = new \DateTime(); + $killDatetime->modify('-' . $daysQuota . ($daysQuota == 1 ? ' day' : ' days')); + $killTS = $killDatetime->format('U'); + + foreach ($allFiles as $file) + { + if ($file['id'] == $latestBackupId) + { + continue; + } + + // Is this on a preserve day? + if ($preserveDay > 0) + { + if ($preserveDay == $file['day']) + { + $leftover[] = $file; + continue; + } + } + + // Otherwise, check the timestamp + if ($file['backupstart'] < $killTS) + { + $ret[] = $file['filenames']; + $killids[] = $file['id']; + + if (!empty($file['logname'])) + { + $filePath = reset($file['filenames']); + + if (!empty($filePath)) + { + $killLogs[] = dirname($filePath) . '/' . $file['logname']; + } + } + } + else + { + $leftover[] = $file; + } + } + } + + // Do we need to apply count quotas? + if ($useCountQuotas && is_numeric($countQuota) && !($countQuota <= 0) && !$useDayQuotas) + { + // Are there more files than the quota limit? + if (!(count($allFiles) > $countQuota)) + { + // No, effectively skip the quota checking + $leftover = $allFiles; + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "Processing count quotas"); + // Yes, aply the quota setting. Add to $ret all entries minus the last + // $countQuota ones. + $totalRecords = count($allFiles); + $checkLimit = $totalRecords - $countQuota; + + // Only process if at least one file (current backup!) is to be left + for ($count = 0; $count < $totalRecords; $count++) + { + $def = array_pop($allFiles); + + if ($def['id'] == $latestBackupId) + { + array_push($allFiles, $def); + continue; + } + if (count($ret) < $checkLimit) + { + if ($latestBackupId != $def['id']) + { + $ret[] = $def['filenames']; + $killids[] = $def['id']; + + if (!empty($def['logname'])) + { + $filePath = reset($def['filenames']); + + if (!empty($filePath)) + { + $killLogs[] = dirname($filePath) . '/' . $def['logname']; + } + } + } + } + else + { + $leftover[] = $def; + } + } + unset($allFiles); + } + } + else + { + // No count quotas are applied + $leftover = $allFiles; + } + + // Do we need to apply size quotas? + if ($useSizeQuotas && is_numeric($sizeQuota) && !($sizeQuota <= 0) && (count($leftover) > 0) && !$useDayQuotas) + { + Factory::getLog()->log(LogLevel::DEBUG, "Processing size quotas"); + // OK, let's start counting bytes! + $runningSize = 0; + + while (count($leftover) > 0) + { + // Each time, remove the last element of the backup array and calculate + // running size. If it's over the limit, add the archive to the return array. + $def = array_pop($leftover); + $runningSize += $def['size']; + + if ($runningSize >= $sizeQuota) + { + if ($latestBackupId == $def['id']) + { + $runningSize -= $def['size']; + } + else + { + $ret[] = $def['filenames']; + $killids[] = $def['id']; + + if (!empty($def['logname'])) + { + $filePath = reset($def['filenames']); + + if (!empty($filePath)) + { + $killLogs[] = dirname($filePath) . '/' . $def['logname']; + } + } + } + } + } + } + + // Convert the $ret 2-dimensional array to single dimensional + $quotaFiles = array(); + + foreach ($ret as $temp) + { + foreach ($temp as $filename) + { + $quotaFiles[] = $filename; + } + } + + // Update the statistics record with the removed remote files + if (!empty($killids)) + { + foreach ($killids as $id) + { + $data = array('filesexist' => '0'); + Platform::getInstance()->set_or_update_statistics($id, $data, $this); + } + } + + // Apply quotas to backup archives + if (count($quotaFiles) > 0) + { + Factory::getLog()->log(LogLevel::DEBUG, "Applying quotas"); + + foreach ($quotaFiles as $file) + { + if (!@Platform::getInstance()->unlink($file)) + { + $this->setWarning("Failed to remove old backup file " . $file); + } + } + } + + // Apply quotas to log files + if (!empty($killLogs)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Removing obsolete log files"); + + foreach ($killLogs as $logPath) + { + @Platform::getInstance()->unlink($logPath); + } + } + + $this->apply_obsolete_quotas(); + + return true; + } + + /** + * Apply quotas for remotely stored files + * + * @return bool True on success + */ + protected function apply_remote_quotas() + { + $this->setStep('Applying remote storage quotas'); + $this->setSubstep(''); + // Make sure we are enabled + $config = Factory::getConfiguration(); + $enableRemote = $config->get('akeeba.quota.remote', 0); + + if (!$enableRemote) + { + return true; + } + + // Get the list of files to kill + if (empty($this->remote_files_killlist)) + { + Factory::getLog()->log(LogLevel::DEBUG, 'Applying remote file quotas'); + $this->remote_files_killlist = $this->get_remote_quotas(); + + if (empty($this->remote_files_killlist)) + { + Factory::getLog()->log(LogLevel::DEBUG, 'No remote files to apply quotas to were found'); + + return true; + } + } + + // Remove the files + $timer = Factory::getTimer(); + + while ($timer->getRunningTime() && count($this->remote_files_killlist)) + { + $filename = array_shift($this->remote_files_killlist); + + list($engineName, $path) = explode('://', $filename); + + $engine = Factory::getPostprocEngine($engineName); + + if (!$engine->can_delete) + { + continue; + } + + Factory::getLog()->log(LogLevel::DEBUG, "Removing $filename"); + $result = $engine->delete($path); + + if (!$result) + { + Factory::getLog()->log(LogLevel::DEBUG, "Removal failed: " . $engine->getWarning()); + } + } + + // Return false if we have more work to do or true if we're done + if (count($this->remote_files_killlist)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Remote file removal will continue in the next step"); + + return false; + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "Remote file quotas applied successfully"); + + return true; + } + } + + /** + * Applies the size and count quotas + * + * @return bool True on success + */ + protected function get_remote_quotas() + { + // Get all records with a remote filename + $allRecords = Platform::getInstance()->get_valid_remote_records(); + + // Bail out if no records found + if (empty($allRecords)) + { + return array(); + } + + // Try to find the files to be deleted due to quota settings + $statistics = Factory::getStatistics(); + $latestBackupId = $statistics->getId(); + + // Filter out the current record + $temp = array(); + + foreach ($allRecords as $item) + { + if ($item['id'] == $latestBackupId) + { + continue; + } + + $item['files'] = $this->get_remote_files($item['remote_filename'], $item['multipart']); + $temp[] = $item; + } + + $allRecords = $temp; + + // Bail out if only the current backup was included in the list + if (count($allRecords) == 0) + { + return array(); + } + + // Get quota values + $registry = Factory::getConfiguration(); + $countQuota = $registry->get('akeeba.quota.count_quota'); + $sizeQuota = $registry->get('akeeba.quota.size_quota'); + $useCountQuotas = $registry->get('akeeba.quota.enable_count_quota'); + $useSizeQuotas = $registry->get('akeeba.quota.enable_size_quota'); + $useDayQuotas = $registry->get('akeeba.quota.maxage.enable'); + $daysQuota = $registry->get('akeeba.quota.maxage.maxdays'); + $preserveDay = $registry->get('akeeba.quota.maxage.keepday'); + + $leftover = array(); + $ret = array(); + $killids = array(); + + if ($useDayQuotas) + { + $killDatetime = new \DateTime(); + $killDatetime->modify('-' . $daysQuota . ($daysQuota == 1 ? ' day' : ' days')); + $killTS = $killDatetime->format('U'); + + foreach ($allRecords as $def) + { + $backupstart = new \DateTime($def['backupstart']); + $backupTS = $backupstart->format('U'); + $backupDay = $backupstart->format('d'); + + // Is this on a preserve day? + if ($preserveDay > 0) + { + if ($preserveDay == $backupDay) + { + $leftover[] = $def; + continue; + } + } + + // Otherwise, check the timestamp + if ($backupTS < $killTS) + { + $ret[] = $def['files']; + $killids[] = $def['id']; + } + else + { + $leftover[] = $def; + } + } + } + + // Do we need to apply count quotas? + if ($useCountQuotas && ($countQuota >= 1) && !$useDayQuotas) + { + $countQuota--; + // Are there more files than the quota limit? + if (!(count($allRecords) > $countQuota)) + { + // No, effectively skip the quota checking + $leftover = $allRecords; + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "Processing remote count quotas"); + // Yes, apply the quota setting. + $totalRecords = count($allRecords); + + for ($count = 0; $count <= $totalRecords; $count++) + { + $def = array_pop($allRecords); + + if (count($leftover) >= $countQuota) + { + $ret[] = $def['files']; + $killids[] = $def['id']; + } + else + { + $leftover[] = $def; + } + } + + unset($allRecords); + } + } + else + { + // No count quotas are applied + $leftover = $allRecords; + } + + // Do we need to apply size quotas? + if ($useSizeQuotas && ($sizeQuota > 0) && (count($leftover) > 0) && !$useDayQuotas) + { + Factory::getLog()->log(LogLevel::DEBUG, "Processing remote size quotas"); + // OK, let's start counting bytes! + $runningSize = 0; + + while (count($leftover) > 0) + { + // Each time, remove the last element of the backup array and calculate + // running size. If it's over the limit, add the archive to the $ret array. + $def = array_pop($leftover); + $runningSize += $def['total_size']; + + if ($runningSize >= $sizeQuota) + { + $ret[] = $def['files']; + $killids[] = $def['id']; + } + } + } + + // Convert the $ret 2-dimensional array to single dimensional + $quotaFiles = array(); + + foreach ($ret as $temp) + { + if (!is_array($temp) || empty($temp)) + { + continue; + } + + foreach ($temp as $filename) + { + $quotaFiles[] = $filename; + } + } + + // Update the statistics record with the removed remote files + if (!empty($killids)) + { + foreach ($killids as $id) + { + if (empty($id)) + { + continue; + } + + $data = array('remote_filename' => ''); + Platform::getInstance()->set_or_update_statistics($id, $data, $this); + } + } + + return $quotaFiles; + } + + /** + * Get the full paths to all remote backup parts + * + * @param string $filename The full filename of the last part stored in the database + * @param int $multipart How many parts does this archive consist of? + * + * @return array A list of the full paths of all remotely stored backup archive parts + */ + protected function get_remote_files($filename, $multipart) + { + $result = array(); + + $extension = substr($filename, -3); + $base = substr($filename, 0, -4); + + $result[] = $filename; + + if ($multipart > 1) + { + for ($i = 1; $i < $multipart; $i++) + { + $newExt = substr($extension, 0, 1) . sprintf('%02u', $i); + $result[] = $base . '.' . $newExt; + } + } + + return $result; + } + + /** + * Keeps a maximum number of "obsolete" records + * + * @return void + */ + protected function apply_obsolete_quotas() + { + $this->setStep('Applying quota limit on obsolete backup records'); + $this->setSubstep(''); + $registry = Factory::getConfiguration(); + $limit = $registry->get('akeeba.quota.obsolete_quota', 0); + $limit = (int)$limit; + + if ($limit <= 0) + { + return; + } + + $statsTable = Platform::getInstance()->tableNameStats; + $db = Factory::getDatabase(Platform::getInstance()->get_platform_database_options()); + $query = $db->getQuery(true) + ->select(array( + $db->qn('id'), + $db->qn('tag'), + $db->qn('backupid'), + $db->qn('absolute_path'), + )) + ->from($db->qn($statsTable)) + ->where($db->qn('profile_id') . ' = ' . $db->q(Platform::getInstance()->get_active_profile())) + ->where($db->qn('status') . ' = ' . $db->q('complete')) + ->where($db->qn('filesexist') . '=' . $db->q('0')) + ->order($db->qn('id') . ' DESC'); + + $db->setQuery($query, $limit, 100000); + $records = $db->loadAssocList(); + + if (empty($records)) + { + return; + } + + $array = array(); + + // Delete backup-specific log files if they exist and add the IDs of the records to delete in the $array + foreach ($records as $stat) + { + $array[] = $stat['id']; + + // We can't delete logs if there is no backup ID in the record + if (!isset($stat['backupid']) || empty($stat['backupid'])) + { + continue; + } + + $logFileName = 'akeeba.' . $stat['tag'] . '.' . $stat['backupid'] . '.log'; + $logPath = dirname($stat['absolute_path']) . '/' . $logFileName; + + if (file_exists($logPath)) + { + @unlink($logPath); + } + } + + $ids = array(); + + foreach ($array as $id) + { + $ids[] = $db->q($id); + } + + $ids = implode(',', $ids); + + $query = $db->getQuery(true) + ->delete($db->qn($statsTable)) + ->where($db->qn('id') . " IN ($ids)"); + $db->setQuery($query); + $db->query(); + } + + /** + * Get the percentage of finalization steps done + * + * @return float + */ + public function getProgress() + { + if ($this->steps_total <= 0) + { + return 0; + } + + $overall = $this->steps_done / $this->steps_total; + $local = 0; + + if ($this->substeps_total > 0) + { + $local = $this->substeps_done / $this->substeps_total; + } + + return $overall + ($local / $this->steps_total); + } + + /** + * Used by additional handler classes to relay their step to us + * + * @param string $step The current step + */ + public function relayStep($step) + { + $this->setStep($step); + } + + /** + * Used by additional handler classes to relay their substep to us + * + * @param string $substep The current sub-step + */ + public function relaySubstep($substep) + { + $this->setSubstep($substep); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Init.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Init.php new file mode 100644 index 00000000..82b5f187 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Init.php @@ -0,0 +1,463 @@ +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); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Installer.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Installer.php new file mode 100644 index 00000000..6a00f0d6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Installer.php @@ -0,0 +1,243 @@ +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 << + + + + Akeeba Backup Archive Identity + + +

Backup Description

+

+

Backup Comment

+
+ $comment +
+
+

+ Akeeba Backup $lbl_coreorpro $lbl_version +

+ + +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 = <<get('engine.installer.angie.key', ''); + + if (empty($password)) + { + return $ret; + } + + $randVal = Factory::getRandval(); + + $salt = $randVal->generateString(32); + $passhash = md5($password . $salt) . ':' . $salt; + $ret = "progress; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Pack.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Pack.php new file mode 100644 index 00000000..34288eeb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Domain/Pack.php @@ -0,0 +1,1195 @@ +log(LogLevel::DEBUG, __CLASS__ . " :: new instance"); + } + + /** + * Implements the _prepare() abstract method + * + * @return void + */ + protected function _prepare() + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Starting _prepare()"); + + // Get a list of directories to include + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Getting directory inclusion filters"); + $filters = Factory::getFilters(); + $this->root_definitions = $filters->getInclusions('dir'); + + $this->total_roots = count($this->root_definitions); + + // Add the mapping text file if there are external directories defined! + if (count($this->root_definitions) > 1) + { + // The site's root is the last directory to be backed up. Um, no, + // this is not what we need + $temp = array_pop($this->root_definitions); + array_unshift($this->root_definitions, $temp); + + // We add a README.txt file in our virtual directory... + Factory::getLog()->log(LogLevel::DEBUG, "Creating README.txt in the EFF virtual folder"); + $virtualContents = <<get('akeeba.advanced.virtual_folder'), '/') . '/'; + foreach ($this->root_definitions as $dir) + { + $counter++; + // Skip over the first filter, because it's the site's root + if ($counter == 1) + { + continue; + } + $test = trim($dir[1]); + if ($test == '/') + { + $counter--; + continue; + } + $virtualContents .= $dir[1] . "\tis the backup of\t" . $dir[0] . "\n"; + + $effini .= '"' . $dir[0] . '"="' . $vdir . $dir[1] . '"'."\n"; + } + // Add the file to our archive + + $archiver = Factory::getArchiverEngine(); + if ($counter > 1) + { + $archiver->addVirtualFile('README.txt', $registry->get('akeeba.advanced.virtual_folder'), $virtualContents); + $archiver->addVirtualFile('eff.ini', $this->installerSettings->installerroot, $effini); + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "README.txt was not created; all EFF directories are being backed up to the archive's root"); + } + } + + // Find the site's root element and shift it into the directory list + $dir_definition = array_shift($this->root_definitions); + $count = 0; + $max_dir_count = count($this->root_definitions); + while (!is_null($dir_definition[1]) && ($count < $max_dir_count)) + { + $count++; + array_push($this->root_definitions, $dir_definition); + $dir_definition = array_shift($this->root_definitions); + } + + // Settling with whatever we have, let's put it to use, shall we? + $this->remove_path_prefix = $dir_definition[0]; // Remove absolute path to directory when storing the file + if (is_null($dir_definition[1])) + { + $this->path_prefix = ''; // No added path for main site + if (empty($dir_definition[0])) + { + $this->root = '[SITEROOT]'; + } + else + { + $this->root = $dir_definition[0]; + } + } + else + { + $dir_definition[1] = trim($dir_definition[1]); + if (empty($dir_definition[1]) || $dir_definition[1] == '/') + { + $this->path_prefix = ''; + } + else + { + $this->path_prefix = $registry->get('akeeba.advanced.virtual_folder') . '/' . $dir_definition[1]; + } + $this->root = $dir_definition[0]; + } + // Translate the root into an absolute path + $stock_dirs = Platform::getInstance()->get_stock_directories(); + $absolute_dir = substr($this->root, 0); + if (!empty($stock_dirs)) + { + foreach ($stock_dirs as $key => $replacement) + { + $absolute_dir = str_replace($key, $replacement, $absolute_dir); + } + } + $this->directory_list[] = $absolute_dir; + $this->remove_path_prefix = $absolute_dir; + $registry = Factory::getConfiguration(); + $registry->set('volatile.filesystem.current_root', $absolute_dir); + + $this->done_subdir_scanning = true; + $this->done_file_scanning = true; + $this->total_files = 0; + $this->done_files = 0; + $this->total_folders = 0; + $this->done_folders = 0; + + $this->setState('prepared'); + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: prepared"); + } + + protected function _run() + { + if ($this->getState() == 'postrun') + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Already finished"); + $this->setStep("-"); + $this->setSubstep(""); + + return true; + } + + // If I'm done scanning files and subdirectories and there are no more files to pack get the next + // directory. This block is triggered in the first step in a new root. + if (empty($this->file_list) && $this->done_subdir_scanning && $this->done_file_scanning) + { + $this->progressMarkFolderDone(); + + if (!$this->getNextDirectory()) + { + if ($this->getNextRoot()) + { + if (!$this->getNextDirectory()) + { + return true; + } + } + else + { + return true; + } + } + } + + /** + * Automated tests override + * + * If the file .akeeba_engine_automated_tests_error file is present in the site's root I will throw an error. + */ + list($root, $translated_root, $dir) = $this->getCleanDirectoryComponents(); + + if (@file_exists($translated_root . '/.akeeba_engine_automated_tests_error')) + { + $this->setError("Akeeba Engine automated tests: I am throwing an error because the file .akeeba_engine_automated_tests_error is present in the site's root folder."); + + return false; + } + + // If I'm not done scanning for files and the file list is empty then scan for more files + if (!$this->done_file_scanning && empty($this->file_list)) + { + $result = $this->scanFiles(); + } + // If I have files left, pack them + elseif (!empty($this->file_list)) + { + $result = $this->pack_files(); + } + // If I'm not done scanning subdirectories, go ahead and scan some more of them + elseif (!$this->done_subdir_scanning) + { + $result = $this->scanSubdirs(); + } + /** + * If we have excluded contained files or subdirectories BUT NOT the entire folder itself AND there are + * no files in this directory THEN add an empty directory to the archive. + **/ + elseif ( + ($this->excluded_files || $this->excluded_subdirectories) + && + !$this->excluded_folder + && + empty($this->file_list) + ) + { + Factory::getLog()->log(LogLevel::INFO, "Empty directory " . $this->current_directory . ' (files and directories are filtered)'); + + $archiver = Factory::getArchiverEngine(); + + if ($this->current_directory != $this->remove_path_prefix) + { + $archiver->addFile($this->current_directory, $this->remove_path_prefix, $this->path_prefix); + } + + // Error propagation + $this->propagateFromObject($archiver); + } + + // Do I have an error? + if ($this->getError()) + { + return false; + } + + return true; + } + + /** + * Implements the _finalize() abstract method + * + */ + protected function _finalize() + { + Factory::getLog()->log(LogLevel::INFO, "Finalizing archive"); + $archive = Factory::getArchiverEngine(); + $archive->finalize(); + // Error propagation + $this->propagateFromObject($archive); + if ($this->getError()) + { + return false; + } + + Factory::getLog()->log(LogLevel::DEBUG, "Archive is finalized"); + + $this->setState('finished'); + } + + // ============================================================================================ + // PRIVATE METHODS + // ============================================================================================ + + /** + * Gets the next directory to scan from the stack. It also applies folder + * filters (directory exclusion, subdirectory exclusion, file exclusion), + * updating the operation toggle properties of the class. + * + * @return boolean True if we found a directory, false if the directory + * stack is empty. It also returns true if the folder is + * filtered (we are told to skip it) + */ + protected function getNextDirectory() + { + // Reset the file / folder scanning positions + $this->getFiles_position = null; + $this->getFolders_position = null; + $this->done_file_scanning = false; + $this->done_subdir_scanning = false; + $this->excluded_folder = false; + $this->excluded_subdirectories = false; + $this->excluded_files = false; + + if (count($this->directory_list) == 0) + { + // No directories left to scan + return false; + } + else + { + // Get and remove the last entry from the $directory_list array + $this->current_directory = array_pop($this->directory_list); + $this->setStep($this->current_directory); + $this->processed_files_counter = 0; + } + + list($root, $translated_root, $dir) = $this->getCleanDirectoryComponents(); + + // Get a filters instance + $filters = Factory::getFilters(); + + // Apply DEF (directory exclusion filters) + // Note: the !empty($dir) prevents the site's root from being filtered out + if ($filters->isFiltered($dir, $root, 'dir', 'all') && !empty($dir)) + { + Factory::getLog()->log(LogLevel::INFO, "Skipping directory " . $this->current_directory); + $this->done_subdir_scanning = true; + $this->done_file_scanning = true; + $this->excluded_folder = true; + + return true; + } + + // Apply Skip Contained Directories Filters + if ($filters->isFiltered($dir, $root, 'dir', 'children')) + { + $this->excluded_subdirectories = true; + + Factory::getLog()->log(LogLevel::INFO, "Skipping subdirectories of directory " . $this->current_directory); + + $this->done_subdir_scanning = true; + } + + // Apply Skipfiles + if ($filters->isFiltered($dir, $root, 'dir', 'content')) + { + $this->excluded_files = true; + + Factory::getLog()->log(LogLevel::INFO, "Skipping files of directory " . $this->current_directory); + + $this->done_file_scanning = true; + + // When the files of a folder are skipped we will have to add some + // files anyway if they are present. These are files used to + // prevent direct access to the folder. + + // Try to find and include .htaccess and index.htm(l) files + // # Fix 2.4: Do not add DIRECTORY_SEPARATOR if we are on the site's root and it's an empty string + $ds = ($this->current_directory == '') || ($this->current_directory == '/') ? '' : DIRECTORY_SEPARATOR; + $checkForTheseFiles = array( + $this->current_directory . $ds . '.htaccess', + $this->current_directory . $ds . 'web.config', + $this->current_directory . $ds . 'index.html', + $this->current_directory . $ds . 'index.htm', + $this->current_directory . $ds . 'robots.txt' + ); + $this->processed_files_counter = 0; + + foreach ($checkForTheseFiles as $fileName) + { + if (@file_exists($fileName)) + { + // Fix 3.3 - We have to also put them through other filters, ahem! + if (!$filters->isFiltered($fileName, $root, 'file', 'all')) + { + $this->file_list[] = $fileName; + $this->processed_files_counter++; + } + } + } + } + + return true; + } + + /** + * Try to add some files from the $file_list into the archive + * + * @return boolean True if there were files packed, false otherwise + * (empty filelist or fatal error) + */ + protected function pack_files() + { + // Get a reference to the archiver and the timer classes + $archiver = Factory::getArchiverEngine(); + $timer = Factory::getTimer(); + $configuration = Factory::getConfiguration(); + + // If post-processing after part creation is enabled, make sure we do post-process each part before moving on + if ($configuration->get('engine.postproc.common.after_part', 0) && !empty($archiver->finishedPart)) + { + if (self::postProcessDonePartFile($this, $archiver, $configuration)) + { + return true; + } + } + + // If the archiver has work to do, make sure it finished up before continuing + if ($configuration->get('volatile.engine.archiver.processingfile', false)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Continuing file packing from previous step"); + $result = $archiver->addFile('', '', ''); + $this->propagateFromObject($archiver); + + if ($this->getError()) + { + return false; + } + + // If that was the last step for packing this file, mark a file done + if (!$configuration->get('volatile.engine.archiver.processingfile', false)) + { + $this->progressMarkFileDone(); + } + } + + // Did it finish, or does it have more work to do? + if ($configuration->get('volatile.engine.archiver.processingfile', false)) + { + // More work to do. Let's just tell our parent that we finished up successfully. + return true; + } + + // Normal file backup loop; we keep on processing the file list, packing files as we go. + if (count($this->file_list) == 0) + { + // No files left to pack. Return true and let the engine loop + $this->progressMarkFolderDone(); + + return true; + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, "Packing files"); + $packedSize = 0; + $numberOfFiles = 0; + + list($usec, $sec) = explode(" ", microtime()); + $opStartTime = ((float)$usec + (float)$sec); + + $largeFileThreshold = Factory::getConfiguration()->get('engine.scan.common.largefile', 10485760); + + while ((count($this->file_list) > 0)) + { + $file = @array_shift($this->file_list); + $size = 0; + if (file_exists($file)) + { + $size = @filesize($file); + } + // Anticipatory file size algorithm + if (($numberOfFiles > 0) && ($size > $largeFileThreshold)) + { + if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.beforelargefile', 0)) + { + // If the file is bigger than the big file threshold, break the step + // to avoid potential timeouts + $this->setBreakFlag(); + Factory::getLog()->log(LogLevel::INFO, "Breaking step _before_ large file: " . $file . " - size: " . $size); + // Push the file back to the list. + array_unshift($this->file_list, $file); + + // Return true and let the engine loop + return true; + } + } + + // Proactive potential timeout detection + // Rough estimation of packing speed in bytes per second + list($usec, $sec) = explode(" ", microtime()); + + $opEndTime = ((float)$usec + (float)$sec); + + if (($opEndTime - $opStartTime) == 0) + { + $_packSpeed = 0; + } + else + { + $_packSpeed = $packedSize / ($opEndTime - $opStartTime); + } + + // Estimate required time to pack next file. If it's the first file of this operation, + // do not impose any limitations. + $_reqTime = ($_packSpeed - 0.01) <= 0 ? 0 : $size / $_packSpeed; + + // Do we have enough time? + if ($timer->getTimeLeft() < $_reqTime) + { + if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.proactive', 0)) + { + array_unshift($this->file_list, $file); + Factory::getLog()->log(LogLevel::INFO, "Proactive step break - file: " . $file . " - size: " . $size . " - req. time " . sprintf('%2.2f', $_reqTime)); + $this->setBreakFlag(); + + return true; + } + } + + $packedSize += $size; + $numberOfFiles++; + $ret = $archiver->addFile($file, $this->remove_path_prefix, $this->path_prefix); + + // If no more processing steps are required, mark a done file + if (!$configuration->get('volatile.engine.archiver.processingfile', false)) + { + $this->progressMarkFileDone(); + } + + // Error propagation + $this->propagateFromObject($archiver); + + if ($this->getError()) + { + return false; + } + + // If this was the first file packed and we've already gone past + // the large file size threshold break the step. Continuing with + // more operations after packing such a big file is increasing + // the risk to hit a timeout. + if (($packedSize > $largeFileThreshold) && ($numberOfFiles == 1)) + { + if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.afterlargefile', 0)) + { + Factory::getLog()->log(LogLevel::INFO, "Breaking step *after* large file: " . $file . " - size: " . $size); + $this->setBreakFlag(); + + return true; + } + } + + // If we have to continue processing the file, break the file packing loop forcibly + if ($configuration->get('volatile.engine.archiver.processingfile', false)) + { + return true; + } + } + + // True if we have more files, false if we're done packing + return (count($this->file_list) > 0); + } + } + + /** + * Implements the getProgress() percentage calculation based on how many + * roots we have fully backed up and how much of the current root we + * have backed up. + */ + public function getProgress() + { + if (empty($this->total_roots)) + { + return 0; + } + + // Get the overall percentage (based on databases fully dumped so far) + $remaining_steps = count($this->root_definitions); + $remaining_steps++; + $overall = 1 - ($remaining_steps / $this->total_roots); + + // How much is this step worth? + $this_max = 1 / $this->total_roots; + + // Get the percentage done of the current root. Hey, the calculation *is* dodgy, I know it! + $local = 0; + if ($this->total_files > 0) + { + $local += 0.05 * $this->done_files / $this->total_files; + } + if ($this->total_folders > 0) + { + $local += 0.95 * $this->done_folders / $this->total_folders; + } + + $percentage = $overall + $local * $this_max; + if ($percentage < 0) + { + $percentage = 0; + } + if ($percentage > 1) + { + $percentage = 1; + } + + return $percentage; + } + + protected function progressAddFile() + { + $this->total_files++; + } + + protected function progressMarkFileDone() + { + $this->done_files++; + } + + protected function progressAddFolder() + { + $this->total_folders++; + } + + protected function progressMarkFolderDone() + { + $this->done_folders++; + } + + /** + * Returns the site root, the translated site root and the translated current directory + * + * @return array + */ + protected function getCleanDirectoryComponents() + { + $fsUtils = Factory::getFilesystemTools(); + + // Break directory components + if (Factory::getConfiguration()->get('akeeba.platform.override_root', 0)) + { + $siteroot = Factory::getConfiguration()->get('akeeba.platform.newroot', '[SITEROOT]'); + } + else + { + $siteroot = '[SITEROOT]'; + } + + $root = $this->root; + + if ($this->root == $siteroot) + { + $translated_root = $fsUtils->translateStockDirs($siteroot, true); + } + else + { + $translated_root = $this->remove_path_prefix; + } + + $dir = $fsUtils->TrimTrailingSlash($this->current_directory); + + if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') + { + $translated_root = $fsUtils->TranslateWinPath($translated_root); + $dir = $fsUtils->TranslateWinPath($dir); + } + + if (substr($dir, 0, strlen($translated_root)) == $translated_root) + { + $dir = substr($dir, strlen($translated_root)); + } + elseif (in_array(substr($translated_root, -1), array('/', '\\'))) + { + $new_translated_root = rtrim($translated_root, '/\\'); + if (substr($dir, 0, strlen($new_translated_root)) == $new_translated_root) + { + $dir = substr($dir, strlen($new_translated_root)); + } + } + + if (substr($dir, 0, 1) == '/') + { + $dir = substr($dir, 1); + } + + return array($root, $translated_root, $dir); + } + + /** + * Steps the subdirectory scanning of the current directory + * + * @return boolean True on success, false on fatal error + */ + protected function scanSubdirs() + { + $engine = Factory::getScanEngine(); + + list($root, $translated_root, $dir) = $this->getCleanDirectoryComponents(); + + // Get a filters instance + $filters = Factory::getFilters(); + + if (is_null($this->getFolders_position)) + { + Factory::getLog()->log(LogLevel::INFO, "Scanning directories of " . $this->current_directory); + } + else + { + Factory::getLog()->log(LogLevel::INFO, "Resuming scanning directories of " . $this->current_directory); + } + + // Get subdirectories + $subdirectories = $engine->getFolders($this->current_directory, $this->getFolders_position); + + // Error propagation + $this->propagateFromObject($engine); + + // If the list contains "too many" items, please break this step! + if (Factory::getConfiguration()->get('volatile.breakflag', false)) + { + // Log the step break decision, for debugging reasons + Factory::getLog()->log(LogLevel::INFO, "Large directory " . $this->current_directory . " while scanning for subdirectories; I will resume scanning in next step."); + + // Return immediately, marking that we are not done yet! + return true; + } + + // Error control + if ($this->getError()) + { + return false; + } + + // Start adding the subdirectories + if (!empty($subdirectories) && is_array($subdirectories)) + { + $dereferenceSymlinks = Factory::getConfiguration()->get('engine.archiver.common.dereference_symlinks'); + + // If we have to treat symlinks as real directories just add everything + if ($dereferenceSymlinks) + { + // Treat symlinks to directories as actual directories + foreach ($subdirectories as $subdirectory) + { + $this->directory_list[] = $subdirectory; + $this->progressAddFolder(); + } + } + // If we are told not to dereference symlinks we'll need to check each subdirectory thoroughly + else + { + // Treat symlinks to directories as simple symlink files (ONLY WORKS WITH CERTAIN ARCHIVERS!) + foreach ($subdirectories as $subdirectory) + { + if (is_link($subdirectory)) + { + // Symlink detected; apply directory filters to it + if (empty($dir)) + { + $dirSlash = $dir; + } + else + { + $dirSlash = $dir . '/'; + } + + $check = $dirSlash . basename($subdirectory); + Factory::getLog()->log(LogLevel::DEBUG, "Directory symlink detected: $check"); + + if (_AKEEBA_IS_WINDOWS) + { + $check = Factory::getFilesystemTools()->TranslateWinPath($check); + } + + // Do I need this? $dir contains a path relative to the root anyway... + $check = ltrim(str_replace($translated_root, '', $check), '/'); + + // Check for excluded symlinks (note that they are excluded as DIRECTORIES in the GUI) + if ($filters->isFiltered($check, $root, 'dir', 'all')) + { + Factory::getLog()->log(LogLevel::INFO, "Skipping directory symlink " . $check); + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, 'Adding folder symlink: ' . $check); + $this->file_list[] = $subdirectory; + $this->progressAddFile(); + } + } + else + { + $this->directory_list[] = $subdirectory; + $this->progressAddFolder(); + } + } + } + } + + // If the scanner nullified the next position to scan, we're done + // scanning for subdirectories + if (is_null($this->getFolders_position)) + { + $this->done_subdir_scanning = true; + } + + return true; + } + + /** + * Steps the files scanning of the current directory + * + * @return boolean True on success, false on fatal error + */ + protected function scanFiles() + { + $engine = Factory::getScanEngine(); + + list($root, $translated_root, $dir) = $this->getCleanDirectoryComponents(); + + // Get a filters instance + $filters = Factory::getFilters(); + + if (is_null($this->getFiles_position)) + { + Factory::getLog()->log(LogLevel::INFO, "Scanning files of " . $this->current_directory); + $this->processed_files_counter = 0; + } + else + { + Factory::getLog()->log(LogLevel::INFO, "Resuming scanning files of " . $this->current_directory); + } + + // Get file listing + $fileList = $engine->getFiles($this->current_directory, $this->getFiles_position); + + // Error propagation + $this->propagateFromObject($engine); + + // If the list contains "too many" items, please break this step! + if (Factory::getConfiguration()->get('volatile.breakflag', false)) + { + // Log the step break decision, for debugging reasons + Factory::getLog()->log(LogLevel::INFO, "Large directory " . $this->current_directory . " while scanning for files; I will resume scanning in next step."); + + // Return immediately, marking that we are not done yet! + return true; + } + + // Error control + if ($this->getError()) + { + return false; + } + + // Do I have an unreadable directory? + if (($fileList === false)) + { + $this->setWarning('Unreadable directory ' . $this->current_directory); + + $this->done_file_scanning = true; + } + // Directory was readable, process the file list + else + { + if (is_array($fileList) && !empty($fileList)) + { + // Add required trailing slash to $dir + if (!empty($dir)) + { + $dir .= '/'; + } + + // Scan all directory entries + foreach ($fileList as $fileName) + { + $check = $dir . basename($fileName); + + if (_AKEEBA_IS_WINDOWS) + { + $check = Factory::getFilesystemTools()->TranslateWinPath($check); + } + + // Do I need this? $dir contains a path relative to the root anyway... + $check = ltrim(str_replace($translated_root, '', $check), '/'); + $byFilter = ''; + $skipThisFile = $filters->isFilteredExtended($check, $root, 'file', 'all', $byFilter); + + if ($skipThisFile) + { + Factory::getLog()->log(LogLevel::INFO, "Skipping file $fileName (filter: $byFilter)"); + } + else + { + $this->file_list[] = $fileName; + $this->processed_files_counter++; + $this->progressAddFile(); + } + } + } + } + + // If the scanner engine nullified the next position we are done + // scanning for files + if (is_null($this->getFiles_position)) + { + $this->done_file_scanning = true; + } + + // If the directory was genuinely empty we will have to add an empty + // directory entry in the archive, otherwise this directory will never + // be restored. + if ($this->done_file_scanning && ($this->processed_files_counter == 0)) + { + Factory::getLog()->log(LogLevel::INFO, "Empty directory " . $this->current_directory); + + $archiver = Factory::getArchiverEngine(); + + if ($this->current_directory != $this->remove_path_prefix) + { + $archiver->addFile($this->current_directory, $this->remove_path_prefix, $this->path_prefix); + } + + // Error propagation + $this->propagateFromObject($archiver); + + // Check for errors + if ($this->getError()) + { + return false; + } + + unset($archiver); + } + + return true; + } + + /** + * Try to determine the next root folder to scan + * + * @return boolean True if there was a new root to scan + */ + protected function getNextRoot() + { + // We have finished with our directory list. Hmm... Do we have extra directories? + if (count($this->root_definitions) > 0) + { + Factory::getLog()->log(LogLevel::DEBUG, "More off-site directories detected"); + $registry = Factory::getConfiguration(); + $dir_definition = array_shift($this->root_definitions); + + $this->remove_path_prefix = $dir_definition[0]; // Remove absolute path to directory when storing the file + + if (is_null($dir_definition[1])) + { + $this->path_prefix = ''; // No added path for main site + } + else + { + $dir_definition[1] = trim($dir_definition[1]); + + if (empty($dir_definition[1]) || $dir_definition[1] == '/') + { + $this->path_prefix = ''; + } + else + { + $this->path_prefix = $registry->get('akeeba.advanced.virtual_folder') . '/' . $dir_definition[1]; + } + } + + $this->done_scanning = false; // Make sure we process this file list! + $this->root = $dir_definition[0]; + + // Translate the root into an absolute path + $stock_dirs = Platform::getInstance()->get_stock_directories(); + $absolute_dir = substr($this->root, 0); + + if (!empty($stock_dirs)) + { + foreach ($stock_dirs as $key => $replacement) + { + $absolute_dir = str_replace($key, $replacement, $absolute_dir); + } + } + + $this->directory_list[] = $absolute_dir; + $this->remove_path_prefix = $absolute_dir; + + $registry->set('volatile.filesystem.current_root', $absolute_dir); + + $this->total_files = 0; + $this->done_files = 0; + $this->total_folders = 0; + $this->done_folders = 0; + + Factory::getLog()->log(LogLevel::INFO, "Including new off-site directory to " . $dir_definition[1]); + + return true; + } + else + // Nope, we are completely done! + { + $this->setState('postrun'); + + return false; + } + } + + /** + * Immediate post-processing of a part file that's just been completed + * + * @param Part $domain The backup domain object calling us + * @param BaseArchiver $archiver The archiver engine + * @param Configuration $configuration Reference to the Factory configuration object + * + * @return bool + */ + public static function postProcessDonePartFile(Part $domain, BaseArchiver $archiver, Configuration $configuration) + { + $filename = array_shift($archiver->finishedPart); + Factory::getLog()->log(LogLevel::INFO, 'Preparing to post process ' . basename($filename)); + + $timer = Factory::getTimer(); + $startTime = $timer->getRunningTime(); + $post_proc = Factory::getPostprocEngine(); + $result = $post_proc->processPart($filename); + $domain->propagateFromObject($post_proc); + + if ($result === false) + { + $domain->setWarning('Failed to process file ' . basename($filename)); + + Factory::getLog()->log(LogLevel::WARNING, 'Error received from the post-processing engine:'); + Factory::getLog()->log(LogLevel::WARNING, implode("\n", array_merge($domain->getWarnings(), $domain->getErrors()))); + } + elseif($result === true) + { + // Add this part's size to the volatile storage + $volatileTotalSize = $configuration->get('volatile.engine.archiver.totalsize', 0); + $volatileTotalSize += (int)@filesize($filename); + $configuration->set('volatile.engine.archiver.totalsize', $volatileTotalSize); + + Factory::getLog()->log(LogLevel::INFO, 'Successfully processed file ' . basename($filename)); + } + else + { + // More work required + Factory::getLog()->log(LogLevel::INFO, 'More post-processing steps required for file ' . $filename); + $configuration->set('volatile.postproc.filename', $filename); + + // Let's push back the file into the archiver stack + array_unshift($archiver->finishedPart, $filename); + + // Do we need to break the step? + $endTime = $timer->getRunningTime(); + $stepTime = $endTime - $startTime; + $timeLeft = $timer->getTimeLeft(); + + if ($timeLeft < $stepTime) + { + // We predict that running yet another step would cause a timeout + $configuration->set('volatile.breakflag', true); + } + else + { + // We have enough time to run yet another step + $configuration->set('volatile.breakflag', false); + } + } + + // Should we delete the file afterwards? + if ( + $configuration->get('engine.postproc.common.delete_after', false) + && $post_proc->allow_deletes + && ($result === true) + ) + { + Factory::getLog()->log(LogLevel::DEBUG, 'Deleting already processed file ' . basename($filename)); + Platform::getInstance()->unlink($filename); + } + else + { + Factory::getLog()->log(LogLevel::DEBUG, 'Not removing processed file ' . $filename); + } + + if ($post_proc->break_after && ($result === true)) + { + $configuration->set('volatile.breakflag', true); + + return true; + } + + // This is required to let the backup continue even after a post-proc failure + $domain->resetErrors(); + $domain->setState('running'); + + return false; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Filters.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Filters.php new file mode 100644 index 00000000..82fbcd2b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Filters.php @@ -0,0 +1,436 @@ + 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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Kettenrad.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Kettenrad.php new file mode 100644 index 00000000..f84596fd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Kettenrad.php @@ -0,0 +1,619 @@ +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; +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Timer.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Timer.php new file mode 100644 index 00000000..bdee0d87 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/Timer.php @@ -0,0 +1,239 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/scripting.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/scripting.ini new file mode 100644 index 00000000..f7c11293 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Core/scripting.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Base.php new file mode 100644 index 00000000..21f6eff1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Base.php @@ -0,0 +1,1594 @@ +quote($args[0], isset($args[1]) ? $args[1] : true); + break; + case 'nq': + case 'qn': + return $this->quoteName($args[0]); + break; + } + + return null; + } + + /** + * Database object constructor + * + * @param array $options List of options used to configure the connection + */ + public function __construct($options) + { + $prefix = array_key_exists('prefix', $options) ? $options['prefix'] : ''; + $database = array_key_exists('database', $options) ? $options['database'] : ''; + $connection = array_key_exists('connection', $options) ? $options['connection'] : null; + + $this->tablePrefix = $prefix; + $this->_database = $database; + $this->connection = $connection; + $this->errorNum = 0; + $this->count = 0; + $this->log = array(); + $this->options = $options; + } + + /** + * Database object destructor + * + * @return bool + */ + public function __destruct() + { + return $this->close(); + } + + /** + * By default, when the object is shutting down, the connection is closed + */ + public function _onSerialize() + { + $this->close(); + } + + public function __wakeup() + { + $this->open(); + } + + /** + * Alter database's character set, obtaining query string from protected member. + * + * @param string $dbName The database name that will be altered + * + * @return string The query that alter the database query string + * + * @throws \RuntimeException + */ + public function alterDbCharacterSet($dbName) + { + if (is_null($dbName)) + { + throw new \RuntimeException('Database name must not be null.'); + } + + $this->setQuery($this->getAlterDbCharacterSet($dbName)); + + return $this->execute(); + } + + /** + * Opens a database connection. It MUST be overriden by children classes + * + * @return Base + */ + public function open() + { + // Don't try to reconnect if we're already connected + if (is_resource($this->connection) && !is_null($this->connection)) + { + return $this; + } + + // Determine utf-8 support + $this->utf = $this->hasUTF(); + + // Set charactersets (needed for MySQL 4.1.2+) + if ($this->utf) + { + $this->setUTF(); + } + + // Select the current database + $this->select($this->_database); + + return $this; + } + + /** + * Closes the database connection + */ + abstract public function close(); + + /** + * Determines if the connection to the server is active. + * + * @return boolean True if connected to the database engine. + */ + abstract public function connected(); + + /** + * Create a new database using information from $options object, obtaining query string + * from protected member. + * + * @param \stdClass $options Object used to pass user and database name to database driver. + * This object must have "db_name" and "db_user" set. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return string The query that creates database + * + * @throws \RuntimeException + */ + public function createDatabase($options, $utf = true) + { + if (is_null($options)) + { + throw new \RuntimeException('$options object must not be null.'); + } + elseif (empty($options->db_name)) + { + throw new \RuntimeException('$options object must have db_name set.'); + } + elseif (empty($options->db_user)) + { + throw new \RuntimeException('$options object must have db_user set.'); + } + + $this->setQuery($this->getCreateDatabaseQuery($options, $utf)); + + return $this->execute(); + } + + /** + * 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 abstract function dropTable($table, $ifExists = true); + + /** + * 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. + */ + abstract public function escape($text, $extra = false); + + /** + * 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. + */ + abstract protected function fetchArray($cursor = null); + + /** + * 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. + */ + abstract public function fetchAssoc($cursor = null); + + /** + * 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. + */ + abstract protected function fetchObject($cursor = null, $class = 'stdClass'); + + /** + * 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 + */ + abstract public function freeResult($cursor = null); + + /** + * Get the number of affected rows for the previous executed SQL statement. + * + * @return integer The number of affected rows. + */ + abstract public function getAffectedRows(); + + /** + * Return the query string to alter the database character set. + * + * @param string $dbName The database name + * + * @return string The query that alter the database query string + */ + protected function getAlterDbCharacterSet($dbName) + { + $query = 'ALTER DATABASE ' . $this->quoteName($dbName) . ' CHARACTER SET `utf8`'; + + return $query; + } + + /** + * Return the query string to create new Database. + * Each database driver, other than MySQL, need to override this member to return correct string. + * + * @param \stdClass $options Object used to pass user and database name to database driver. + * This object must have "db_name" and "db_user" set. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return string The query that creates database + */ + protected function getCreateDatabaseQuery($options, $utf) + { + if ($utf) + { + $query = 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' CHARACTER SET `utf8`'; + } + else + { + $query = 'CREATE DATABASE ' . $this->quoteName($options->db_name); + } + + return $query; + } + + /** + * 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. + */ + abstract public function getCollation(); + + /** + * Method that provides access to the underlying database connection. Useful for when you need to call a + * proprietary method such as postgresql's lo_* methods. + * + * @return resource The underlying database connection resource. + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Inherits the connection of another database driver. Useful for cloning + * the CMS database connection into an Akeeba Engine database driver. + * + * @param resource $connection + */ + public function setConnection($connection) + { + $this->connection = $connection; + } + + /** + * Get the total number of SQL statements executed by the database driver. + * + * @return integer + * + * @since 11.1 + */ + public function getCount() + { + return $this->count; + } + + /** + * Gets the name of the database used by this connection. + * + * @return string + */ + protected function getDatabase() + { + return $this->_database; + } + + /** + * Returns a PHP date() function compliant date format for the database driver. + * + * @return string The format string. + */ + public function getDateFormat() + { + return 'Y-m-d H:i:s'; + } + + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since 11.1 + */ + public function getLog() + { + return $this->log; + } + + /** + * Get the minimum supported database version. + * + * @return string The minimum version number for the database driver. + * + * @since 12.1 + */ + public function getMinimum() + { + return static::$dbMinimum; + } + + /** + * Get the null or zero representation of a timestamp for the database driver. + * + * @return string Null or zero representation of a timestamp. + */ + public function getNullDate() + { + return $this->nullDate; + } + + /** + * 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. + */ + abstract public function getNumRows($cursor = null); + + /** + * Get the database table prefix + * + * @return string The database prefix + */ + public function getPrefix() + { + return $this->tablePrefix; + } + + /** + * 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. + */ + abstract public function getQuery($new = false); + + /** + * 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. + */ + abstract public function getTableColumns($table, $typeOnly = true); + + /** + * 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. + */ + abstract public function getTableCreate($tables); + + /** + * 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). + */ + abstract public function getTableKeys($tables); + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + */ + abstract public function getTableList(); + + /** + * 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 + */ + abstract public function getTables($abstract = true); + + /** + * Determine whether or not the database engine supports UTF-8 character encoding. + * + * @return boolean True if the database engine supports UTF-8 character encoding. + */ + public function getUTFSupport() + { + return $this->utf; + } + + /** + * Determine whether or not the database engine supports UTF-8 character encoding. + * + * @return boolean True if the database engine supports UTF-8 character encoding. + */ + public function hasUTFSupport() + { + return $this->utf; + } + + /** + * Determines if the database engine supports UTF-8 character encoding. + * + * @return boolean True if supported. + */ + public function hasUTF() + { + return $this->utf; + } + + /** + * Get the version of the database connector + * + * @return string The database connector version. + */ + abstract public function getVersion(); + + /** + * 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. + */ + abstract public function insertid(); + + /** + * 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(); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Only process non-null scalars. + if (is_array($v) or is_object($v) or $v === null) + { + continue; + } + + // Ignore any internal fields. + if ($k[0] == '_') + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->quote($v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + // Set the query and execute the insert. + $this->setQuery($query); + if (!$this->execute()) + { + return false; + } + + // Update the primary key if it exists. + $id = $this->insertid(); + if ($key && $id && is_string($key)) + { + $object->$key = $id; + } + + return true; + } + + /** + * Method to check whether the installed database version is supported by the database driver + * + * @return boolean True if the database version is supported + * + * @since 12.1 + */ + public function isMinimumVersion() + { + return version_compare($this->getVersion(), static::$dbMinimum) >= 0; + } + + /** + * Method to get the first row of the result set from the database query as an associative array + * of ['field_name' => 'row_value']. + * + * @return mixed The return value or null if the query failed. + */ + public function loadAssoc() + { + $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 associative array. + if ($array = $this->fetchAssoc($cursor)) + { + $ret = $array; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an associative array + * of ['field_name' => 'row_value']. The array of rows can optionally be keyed by a field name, but defaults to + * a sequential numeric array. + * + * NOTE: Chosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * @param string $column An optional column name. Instead of the whole row, only this column value will be in + * the result array. + * + * @return mixed The return value or null if the query failed. + */ + public function loadAssocList($key = null, $column = null) + { + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set. + while ($row = $this->fetchAssoc($cursor)) + { + $value = ($column) ? (isset($row[$column]) ? $row[$column] : $row) : $row; + if ($key) + { + $array[$row[$key]] = $value; + } + else + { + $array[] = $value; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * Method to get an array of values from the $offset field in each row of the result set from + * the database query. + * + * @param integer $offset The row offset to use to build the result array. + * + * @return mixed The return value or null if the query failed. + */ + public function loadColumn($offset = 0) + { + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as arrays. + while ($row = $this->fetchArray($cursor)) + { + $array[] = $row[$offset]; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * 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') + { + static $cursor = null; + + // Execute the query and get the result set cursor. + if (is_null($cursor)) + { + if (!($cursor = $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($cursor, $class)) + { + return $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + $cursor = null; + + 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() + { + static $cursor = null; + + // Execute the query and get the result set cursor. + if (is_null($cursor)) + { + if (!($cursor = $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($cursor)) + { + return $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + $cursor = null; + + return false; + } + + /** + * Method to get the first row of 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 return value or null if the query failed. + */ + public function loadObject($class = 'stdClass') + { + $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 object of type $class. + if ($object = $this->fetchObject($cursor, $class)) + { + $ret = $object; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an object. The array + * of objects can optionally be keyed by a field name, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * @param string $class The class name to use for the returned row objects. + * + * @return mixed The return value or null if the query failed. + */ + public function loadObjectList($key = '', $class = 'stdClass') + { + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as objects of type $class. + while ($row = $this->fetchObject($cursor, $class)) + { + if ($key) + { + $array[$row->$key] = $row; + } + else + { + $array[] = $row; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * 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 = $this->fetchArray($cursor)) + { + $ret = $row[0]; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get the first row of the result set from the database query as an array. Columns are indexed + * numerically so the first column in the result set would be accessible via $row[0], etc. + * + * @return mixed The return value or null if the query failed. + */ + public function loadRow() + { + $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 = $this->fetchArray($cursor)) + { + $ret = $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an array. The array + * of objects can optionally be keyed by a field offset, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * + * @return mixed The return value or null if the query failed. + */ + public function loadRowList($key = null) + { + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as arrays. + while ($row = $this->fetchArray($cursor)) + { + if ($key !== null) + { + $array[$row[$key]] = $row; + } + else + { + $array[] = $row; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * 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 abstract function lockTable($tableName); + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + */ + abstract public function query(); + + /** + * An alias for query() + * + * @return mixed A database cursor resource on success, boolean false on failure. + */ + public function execute() + { + return $this->query(); + } + + /** + * Method to quote and optionally escape a string to database requirements for insertion into the database. + * + * @param string $text The string to quote. + * @param boolean $escape True (default) to escape the string, false to leave it unchanged. + */ + public function quote($text, $escape = true) + { + return '\'' . ($escape ? $this->escape($text) : $text) . '\''; + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * @param mixed $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes. + * Each type supports dot-notation name. + * @param mixed $as The AS query part associated to $name. It can be string or array, in latter case it has to be + * same length of $name; if is null there will not be any AS part for string or array element. + */ + public function quoteName($name, $as = null) + { + if (is_string($name)) + { + $quotedName = $this->quoteNameStr(explode('.', $name)); + + $quotedAs = ''; + if (!is_null($as)) + { + settype($as, 'array'); + $quotedAs .= ' AS ' . $this->quoteNameStr($as); + } + + return $quotedName . $quotedAs; + } + else + { + $fin = array(); + + if (is_null($as)) + { + foreach ($name as $str) + { + $fin[] = $this->quoteName($str); + } + } + elseif (is_array($name) && (count($name) == count($as))) + { + $count = count($name); + for ($i = 0; $i < $count; $i++) + { + $fin[] = $this->quoteName($name[$i], $as[$i]); + } + } + + return $fin; + } + } + + /** + * Quote strings coming from quoteName call. + * + * @param array $strArr Array of strings coming from quoteName dot-explosion. + * + * @return string Dot-imploded string of quoted parts. + */ + protected function quoteNameStr($strArr) + { + $parts = array(); + $q = $this->nameQuote; + + foreach ($strArr as $part) + { + if (is_null($part)) + { + continue; + } + + if (strlen($q) == 1) + { + $parts[] = $q . $part . $q; + } + else + { + $parts[] = $q{0} . $part . $q{1}; + } + } + + return implode('.', $parts); + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix class variable. + * + * @param string $query The SQL statement to prepare. + * @param string $prefix The common table prefix. + * + * @return string The processed SQL statement. + */ + 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, "'", $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; + } + + /** + * 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 abstract function renameTable($oldTable, $newTable, $backup = null, $prefix = null); + + /** + * 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. + */ + abstract public function select($database); + + /** + * Sets the database debugging state for the driver. + * + * @param boolean $level True to enable debugging. + * + * @return boolean The old debugging level. + */ + public function setDebug($level) + { + $previous = $this->debug; + $this->debug = (bool)$level; + + return $previous; + } + + /** + * Sets the SQL statement string for later execution. + * + * @param mixed $query The SQL statement to set either as a QueryBase object or a string. + * @param integer $offset The affected row offset to set. + * @param integer $limit The maximum affected rows to set. + * + * @return self This object to support method chaining. + */ + public function setQuery($query, $offset = 0, $limit = 0) + { + $this->sql = $query; + $this->limit = (int)$limit; + $this->offset = (int)$offset; + + return $this; + } + + /** + * Set the connection to use UTF-8 character encoding. + * + * @return boolean True on success. + */ + abstract public function setUTF(); + + /** + * Method to commit a transaction. + * + * @return void + */ + abstract public function transactionCommit(); + + /** + * Method to roll back a transaction. + * + * @return void + */ + abstract public function transactionRollback(); + + /** + * Method to initialize a transaction. + * + * @return void + */ + abstract public function transactionStart(); + + /** + * Method to truncate a table. + * + * @param string $table The table to truncate + * + * @return void + */ + public function truncateTable($table) + { + $this->setQuery('TRUNCATE TABLE ' . $this->quoteName($table)); + $this->query(); + } + + /** + * Updates a row in a table based on an object's properties. + * + * @param string $table The name of the database table to update. + * @param object &$object A reference to an object whose public properties match the table fields. + * @param string $key The name of the primary key. + * @param boolean $nulls True to update null fields or false to ignore them. + * + * @return boolean True on success. + */ + public function updateObject($table, &$object, $key, $nulls = false) + { + $fields = array(); + $where = array(); + + if (is_string($key)) + { + $key = array($key); + } + + if (is_object($key)) + { + $key = (array)$key; + } + + // Create the base update statement. + $statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s'; + + // Iterate over the object variables to build the query fields/value pairs. + foreach (get_object_vars($object) as $k => $v) + { + // Only process scalars that are not internal fields. + if (is_array($v) or is_object($v) or $k[0] == '_') + { + continue; + } + + // Set the primary key to the WHERE clause instead of a field to update. + if (in_array($k, $key)) + { + $where[] = $this->quoteName($k) . '=' . $this->quote($v); + continue; + } + + // Prepare and sanitize the fields and values for the database query. + if ($v === null) + { + // If the value is null and we want to update nulls then set it. + if ($nulls) + { + $val = 'NULL'; + } + // If the value is null and we do not want to update nulls then ignore this field. + else + { + continue; + } + } + // The field is not null so we prep it for update. + else + { + $val = $this->quote($v); + } + + // Add the field to be updated. + $fields[] = $this->quoteName($k) . '=' . $val; + } + + // We don't have any fields to update. + if (empty($fields)) + { + return true; + } + + // Set the query and execute the update. + $this->setQuery(sprintf($statement, implode(",", $fields), implode(' AND ', $where))); + + return $this->execute(); + } + + /** + * Unlocks tables in the database. + * + * @return Base Returns this object to support chaining. + */ + public abstract function unlockTables(); + + /** + * Get the error message + * + * @return string The error message for the most recent query + */ + public function getErrorMsg($escaped = false) + { + if ($escaped) + { + return addslashes($this->errorMsg); + } + else + { + return $this->errorMsg; + } + } + + /** + * Get the error number + * + * @return int The error number for the most recent query + */ + public function getErrorNum() + { + return $this->errorNum; + } + + /** + * Resets the error condition in the driver. Useful to reset the error state after handling a thrown exception. + * + * @return $this for chaining + */ + public function resetErrors() + { + $this->errorNum = 0; + $this->errorMsg = ''; + + parent::resetErrors(); + + 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 getEscaped($text, $extra = false) + { + return $this->escape($text, $extra); + } + + /** + * Retrieves field information about the given tables. + * + * @param mixed $tables A table name or a list of table names. + * @param boolean $typeOnly True to only return field types. + * + * @return array An array of fields by table. + */ + public function getTableFields($tables, $typeOnly = true) + { + $results = array(); + + settype($tables, 'array'); + + foreach ($tables as $table) + { + $results[$table] = $this->getTableColumns($table, $typeOnly); + } + + return $results; + } + + /** + * Method to get an array of values from the $offset field in each row of the result set from + * the database query. + * + * @param integer $offset The row offset to use to build the result array. + * + * @return mixed The return value or null if the query failed. + */ + public function loadResultArray($offset = 0) + { + return $this->loadColumn($offset); + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * @param string $name The identifier name to wrap in quotes. + * + * @return string The quote wrapped name. + */ + public function nameQuote($name) + { + return $this->quoteName($name); + } + + /** + * Returns the abstracted name of a database object + * + * @param string $tableName + * + * @return string + */ + public function getAbstract($tableName) + { + $prefix = $this->getPrefix(); + + // Don't return abstract names for non-CMS tables + if (is_null($prefix)) + { + return $tableName; + } + + switch ($prefix) + { + case '': + // This is more of a hack; it assumes all tables are CMS tables if the prefix is empty. + return '#__' . $tableName; + break; + + default: + // Normal behaviour for 99% of sites + $tableAbstract = $tableName; + if (!empty($prefix)) + { + if (substr($tableName, 0, strlen($prefix)) == $prefix) + { + $tableAbstract = '#__' . substr($tableName, strlen($prefix)); + } + else + { + $tableAbstract = $tableName; + } + } + + return $tableAbstract; + break; + } + } + + /** + * Return the database driver type, e.g. "mysql" for all drivers which can talk to MySQL + * + * @return string + */ + public function getDriverType() + { + return $this->driverType; + } + + /** + * Is this driver supported under the current system configuration? + * + * @return bool + */ + public static function test() + { + return self::isSupported(); + } + + /** + * Is this driver class supported on this server? Child classes are supposed to override this and perform a + * compatibility check. + * + * @return bool True if the driver class is supported on the server + */ + public static function isSupported() + { + return false; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php new file mode 100644 index 00000000..92012e9d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php @@ -0,0 +1,1019 @@ +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; + + if (!empty($port)) + { + $host .= ':' . $port; + } + + // finalize initialization + parent::__construct($options); + + // Avoid overwriting connection info if they're already set + if (is_null($this->host)) + { + $this->host = $host; + } + + if (is_null($this->user)) + { + $this->user = $user; + } + + if (is_null($this->password)) + { + $this->password = $password; + } + + if (is_null($this->_database)) + { + $this->_database = $database; + } + + if (is_null($this->selectDatabase)) + { + $this->selectDatabase = $select; + } + + // Open the connection + if (!is_resource($this->connection) || is_null($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('mysql_connect')) + { + $this->errorNum = 1; + $this->errorMsg = 'The MySQL adapter "mysql" is not available.'; + + return; + } + + if (!($this->connection = @mysql_connect($this->host, $this->user, $this->password, true))) + { + $this->errorNum = 2; + $this->errorMsg = 'Could not connect to MySQL'; + + return; + } + + // Set sql_mode to non_strict mode + mysql_query("SET @@SESSION.sql_mode = '';", $this->connection); + + // If auto-select is enabled select the given database. + 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)) + { + mysql_free_result($this->cursor); + } + if (is_resource($this->connection) || (!is_null($this->connection) && !is_bool($this->connection))) + { + $return = mysql_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 = @mysql_real_escape_string($text, $this->getConnection()); + + 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 test() + { + return (function_exists('mysql_connect')); + } + + /** + * Test to see if the MySQL connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function isSupported() + { + return (function_exists('mysql_connect')); + } + + /** + * Determines if the connection to the server is active. + * + * @return boolean True if connected to the database engine. + */ + public function connected() + { + if (is_resource($this->connection)) + { + return mysql_ping($this->connection); + } + + return false; + } + + /** + * 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 Mysql Returns this object to support chaining. + */ + public function dropTable($tableName, $ifExists = true) + { + $query = $this->getQuery(true); + + $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($tableName)); + + $this->query(); + + 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 mysql_affected_rows($this->connection); + } + + /** + * 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 (string) or boolean false if not supported. + */ + public function getCollation() + { + $this->setQuery('SHOW FULL COLUMNS FROM #__ak_stats'); + $array = $this->loadAssocList(); + + return $array['2']['Collation']; + } + + /** + * 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 mysql_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 QueryMysql($this); + } + else + { + return $this->sql; + } + } + + /** + * 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) + { + // Initialise variables. + $result = array(); + + // Sanitize input to an array and iterate over the list. + settype($tables, 'array'); + foreach ($tables as $table) + { + // Set the query to get the table CREATE statement. + $this->setQuery('SHOW CREATE table ' . $this->quoteName($this->escape($table))); + $row = $this->loadRow(); + + // Populate the result array based on the create statements. + $result[$table] = $row[1]; + } + + return $result; + } + + /** + * 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. + */ + public function getTableColumns($table, $typeOnly = true) + { + $result = array(); + + // Set the query to get the table fields statement. + $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table))); + $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; + } + + /** + * 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) + { + // Get the details columns information. + $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table)); + $keys = $this->loadObjectList(); + + return $keys; + } + + /** + * 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('SHOW TABLES'); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + */ + public function getVersion() + { + return mysql_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 mysql_insert_id($this->connection); + } + + /** + * Locks a table in the database. + * + * @param string $table The name of the table to unlock. + * + * @return Mysql Returns this object to support chaining. + */ + public function lockTable($table) + { + $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->query(); + + return $this; + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + */ + public function query() + { + static $isReconnecting = false; + + 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 .= ' 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 = @mysql_query($query, $this->connection); + + // 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. + $this->errorNum = (int)mysql_errno($this->connection); + $this->errorMsg = (string)mysql_error($this->connection) . ' 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. + $this->errorNum = (int)mysql_errno($this->connection); + $this->errorMsg = (string)mysql_error($this->connection) . ' SQL=' . $query; + + // Throw the normal query exception. + if ($this->errorNum != 0) + { + throw new \RuntimeException($this->errorMsg, $this->errorNum); + } + } + } + + return $this->cursor; + } + + /** + * 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 MySQL. + * @param string $prefix Not used by MySQL. + * + * @return Mysql Returns this object to support chaining. + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->setQuery('RENAME TABLE ' . $oldTable . ' TO ' . $newTable)->query(); + + 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) + { + if (!$database) + { + return false; + } + + if (!mysql_select_db($database, $this->connection)) + { + 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() + { + $result = false; + + if ($this->supportsUtf8mb4()) + { + $result = @mysql_set_charset('utf8mb4', $this->connection); + } + + if (!$result) + { + $result = @mysql_set_charset('utf8', $this->connection); + } + + return $result; + } + + /** + * Method to commit a transaction. + * + * @return void + */ + public function transactionCommit() + { + $this->setQuery('COMMIT'); + $this->execute(); + } + + /** + * Method to roll back a transaction. + * + * @return void + */ + public function transactionRollback() + { + $this->setQuery('ROLLBACK'); + $this->execute(); + } + + /** + * Method to initialize a transaction. + * + * @return void + */ + public function transactionStart() + { + $this->setQuery('START TRANSACTION'); + $this->execute(); + } + + /** + * 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 mysql_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 mysql_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 mysql_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) + { + mysql_free_result($cursor ? $cursor : $this->cursor); + } + + /** + * Unlocks tables in the database. + * + * @return Mysql Returns this object to support chaining. + * + * @since 11.4 + * @throws \Exception + */ + public function unlockTables() + { + $this->setQuery('UNLOCK TABLES')->execute(); + + return $this; + } + + /** + * 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; + } + + $sql = "SHOW TABLES"; + $this->setQuery($sql); + $all_tables = $this->loadColumn(); + + if (!empty($all_tables)) + { + // Start by adding tables and views to the list + foreach ($all_tables as $table_name) + { + if ($abstract) + { + $table_name = $this->getAbstract($table_name); + } + $tables[$table_name] = 'table'; + } + + // Loop all metadatas + foreach ($all_tables as $table_metadata) + { + $table_name = $table_metadata; + $table_abstract = $this->getAbstract($table_metadata); + $type = 'table'; + + if ($abstract) + { + $table_metadata = $table_abstract; + } + + $create = $this->get_create($table_abstract, $table_name, $type); + // Scan for the table engine. + $engine = null; // So that we detect VIEWs correctly + + if ($type == 'table') + { + $engine = 'MyISAM'; // So that even with MySQL 4 hosts we don't screw this up + $engine_keys = array('ENGINE=', 'TYPE='); + foreach ($engine_keys as $engine_key) + { + $start_pos = strrpos($create, $engine_key); + if ($start_pos !== false) + { + // Advance the start position just after the position of the ENGINE keyword + $start_pos += strlen($engine_key); + // Try to locate the space after the engine type + $end_pos = stripos($create, ' ', $start_pos); + if ($end_pos === false) + { + // Uh... maybe it ends with ENGINE=EngineType; + $end_pos = stripos($create, ';'); + } + if ($end_pos !== '') + { + // Grab the string + $engine = substr($create, $start_pos, $end_pos - $start_pos); + } + } + } + $engine = strtoupper($engine); + } + + switch ($engine) + { + // Views -- FIX: They are detected based on their CREATE STATEMENT + case null: + $tables[$table_metadata] = 'view'; + break; + + // Merge tables + case 'MRG_MYISAM': + $tables[$table_metadata] = 'merge'; + 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': + $tables[$table_metadata] = 'temp'; + break; + + // Normal tables + default: + break; + } // switch + } // foreach + } // if !empty + + // If we have MySQL > 5.0 add the list of stored procedures, stored functions + // and triggers + $registry = Factory::getConfiguration(); + $enable_entities = $registry->get('engine.dump.native.advanced_entitites', true); + if ($enable_entities) + { + // 1. Stored procedures + $sql = "SHOW PROCEDURE STATUS WHERE " . $this->quoteName('Db') . "=" . $this->quote($this->_database); + $this->setQuery($sql); + + try + { + $all_entries = $this->loadAssocList(); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + if (count($all_entries)) + { + foreach ($all_entries as $entry) + { + $table_name = $entry['Name']; + if ($abstract) + { + $table_name = $this->getAbstract($table_name); + } + $tables[$table_name] = 'procedure'; + } + } + + // 2. Stored functions + $sql = "SHOW FUNCTION STATUS WHERE " . $this->quoteName('Db') . "=" . $this->quote($this->_database); + $this->setQuery($sql); + + try + { + $all_entries = $this->loadColumn(1); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + // If we have filters, make sure the tables pass the filtering + if (is_array($all_entries)) + { + if (count($all_entries)) + { + foreach ($all_entries as $table_name) + { + if ($abstract) + { + $table_name = $this->getAbstract($table_name); + } + $tables[$table_name] = 'function'; + } + } + } + + // 3. Triggers + $sql = "SHOW TRIGGERS"; + $this->setQuery($sql); + + try + { + $all_entries = $this->loadColumn(); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + // If we have filters, make sure the tables pass the filtering + if (is_array($all_entries)) + { + if (count($all_entries)) + { + foreach ($all_entries as $table_name) + { + if ($abstract) + { + $table_name = $this->getAbstract($table_name); + } + $tables[$table_name] = 'trigger'; + } + } + } + + } + + return $tables; + } + + /** + * Gets the CREATE TABLE command for a given table/view + * + * @param string $table_abstract The abstracted name of the entity + * @param string $table_name The name of the table + * @param string $type The type of the entity to scan. If it's found to differ, the correct type is returned. + * + * @return string The CREATE command, w/out newlines + */ + protected function get_create($table_abstract, $table_name, &$type) + { + $sql = "SHOW CREATE TABLE `$table_abstract`"; + $this->setQuery($sql); + $temp = $this->loadRowList(); + $table_sql = $temp[0][1]; + unset($temp); + + // Smart table type detection + if (in_array($type, array('table', 'merge', 'view'))) + { + // Check for CREATE VIEW + $pattern = '/^CREATE(.*) VIEW (.*)/i'; + $result = preg_match($pattern, $table_sql); + if ($result === 1) + { + // This is a view. + $type = 'view'; + } + else + { + // This is a table. + $type = 'table'; + } + + // Is it a VIEW but we don't have SHOW VIEW privileges? + if (empty($table_sql)) + { + $type = 'view'; + } + } + + $table_sql = str_replace($table_name, $table_abstract, $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); + + // Post-process CREATE VIEW + if ($type == 'view') + { + $pos_view = strpos($table_sql, ' VIEW '); + + if ($pos_view > 7) + { + // Only post process if there are view properties between the CREATE and VIEW keywords + $propstring = substr($table_sql, 7, $pos_view - 7); // Properties string + // Fetch the ALGORITHM={UNDEFINED | MERGE | TEMPTABLE} keyword + $algostring = ''; + $algo_start = strpos($propstring, 'ALGORITHM='); + if ($algo_start !== false) + { + $algo_end = strpos($propstring, ' ', $algo_start); + $algostring = substr($propstring, $algo_start, $algo_end - $algo_start + 1); + } + // Create our modified create statement + $table_sql = 'CREATE OR REPLACE ' . $algostring . substr($table_sql, $pos_view); + } + } + + return $table_sql; + } + + /** + * 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 + */ + protected function supportsUtf8mb4() + { + $client_version = mysql_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', '>='); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysqli.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysqli.php new file mode 100644 index 00000000..858584b1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Mysqli.php @@ -0,0 +1,563 @@ +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((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.+))?$/'; + + // 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\[.*\])(:(?P.+))?$/', $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(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P[^:]+))?$/i', $host, $matches)) + { + $host = $matches['host']; + + if (!empty($matches['port'])) + { + $port = $matches['port']; + } + } + // Empty host, just port, e.g. ':3306' + elseif (preg_match('/^:(?P[^:]+)$/', $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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/None.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/None.php new file mode 100644 index 00000000..5e6a70b8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/None.php @@ -0,0 +1,377 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Pdomysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Pdomysql.php new file mode 100644 index 00000000..ba5fae91 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Pdomysql.php @@ -0,0 +1,726 @@ +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); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Postgresql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Postgresql.php new file mode 100644 index 00000000..b1c16953 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Postgresql.php @@ -0,0 +1,1349 @@ +open(); + + $query = $this->getQuery(true) + ->select(array('table_name', 'table_type')) + ->from('information_schema.tables') + ->where('((table_type=' . $this->quote('BASE TABLE') . ') OR (table_type=' . $this->quote('VIEW') . '))') + ->where( + 'table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ')' + ) + ->order('table_name ASC'); + + $this->setQuery($query); + $all_tables = $this->loadAssocList(); + + if (!empty($all_tables)) + { + foreach ($all_tables as $table) + { + switch ($table['table_type']) + { + case 'BASE TABLE': + $type = 'table'; + break; + + case 'VIEW': + $type = 'view'; + break; + + default: + continue; + } + + $table_name = $table['table_name']; + if ($abstract) + { + $table_name = $this->getAbstract($table_name); + } + + $tables[$table_name] = $type; + } + } + + return $tables; + } + + /** + * Database object constructor + * + * @param array $options List of options used to configure the connection + * + */ + public function __construct($options) + { + $this->driverType = 'postgresql'; + + $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost'; + $options['user'] = (isset($options['user'])) ? $options['user'] : ''; + $options['password'] = (isset($options['password'])) ? $options['password'] : ''; + $options['database'] = (isset($options['database'])) ? $options['database'] : ''; + + $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; + + // Finalize initialization + parent::__construct($options); + + if (!is_resource($this->connection) || is_null($this->connection)) + { + $this->open(); + } + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @throws \RuntimeException + */ + public function open() + { + if ($this->connection) + { + return; + } + + // Make sure the postgresql extension for PHP is installed and enabled. + if (!function_exists('pg_connect')) + { + throw new \RuntimeException('PHP extension pg_connect is not available.'); + } + + // Build the DSN for the connection. + $dsn = ''; + + if (!empty($this->options['host'])) + { + $dsn .= "host={$this->options['host']} "; + } + + $dsn .= "dbname={$this->options['database']} user={$this->options['user']} password={$this->options['password']}"; + + // Attempt to connect to the server. + if (!($this->connection = @pg_connect($dsn))) + { + throw new \RuntimeException('Error connecting to PostgreSQL database.'); + } + + pg_set_error_verbosity($this->connection, PGSQL_ERRORS_DEFAULT); + pg_query('SET standard_conforming_strings=off'); + pg_query('SET escape_string_warning=off'); + } + + /** + * Disconnects the database. + * + * @return void + */ + public function close() + { + // Close the connection. + if (is_resource($this->connection)) + { + pg_close($this->connection); + } + + $this->connection = null; + } + + /** + * 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) + { + $this->open(); + + $result = pg_escape_string($this->connection, $text); + + if ($extra) + { + $result = addcslashes($result, '%_'); + } + + return $result; + } + + /** + * Test to see if the PostgreSQL connector is available + * + * @return boolean True on success, false otherwise. + */ + public static function test() + { + return (function_exists('pg_connect')); + } + + /** + * Determines if the connection to the server is active. + * + * @return boolean + */ + public function connected() + { + $this->open(); + + if (is_resource($this->connection)) + { + return pg_ping($this->connection); + } + + return false; + } + + /** + * 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 boolean true + * + * @throws \RuntimeException + */ + public function dropTable($tableName, $ifExists = true) + { + $this->open(); + + $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName)); + $this->execute(); + + return true; + } + + /** + * Get the number of affected rows for the previous executed SQL statement. + * + * @return int The number of affected rows in the previous operation + */ + public function getAffectedRows() + { + $this->open(); + + return pg_affected_rows($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. + * + * @throws \RuntimeException + */ + public function getCollation() + { + $this->open(); + + $this->setQuery('SHOW LC_COLLATE'); + $array = $this->loadAssocList(); + + return $array[0]['lc_collate']; + } + + /** + * Get the number of returned rows for the previous executed SQL statement. + * + * @param resource $cur An optional database cursor resource to extract the row count from. + * + * @return integer The number of returned rows. + */ + public function getNumRows($cur = null) + { + $this->open(); + + return pg_num_rows((int)$cur ? $cur : $this->cursor); + } + + /** + * Get the current or query, or new QueryBase object. + * + * @param boolean $new False to return the last query set, True to return a new QueryBase object. + * + * @return QueryBase The current query object or a new object extending the QueryBase class. + * + * @throws \RuntimeException + */ + public function getQuery($new = false) + { + if ($new) + { + return new QueryPostgresql($this); + } + else + { + return $this->sql; + } + } + + /** + * Shows the table CREATE statement that creates the given tables. + * + * This is unsuported by PostgreSQL. + * + * @param mixed $tables A table name or a list of table names. + * + * @return string An empty char because this function is not supported by PostgreSQL. + * + * @throws \RuntimeException + */ + public function getTableCreate($tables) + { + return ''; + } + + /** + * 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. + * + * @throws \RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->open(); + + $result = array(); + + $tableSub = $this->replacePrefix($table); + + $this->setQuery(' + SELECT a.attname AS "column_name", + pg_catalog.format_type(a.atttypid, a.atttypmod) as "type", + CASE WHEN a.attnotnull IS TRUE + THEN \'NO\' + ELSE \'YES\' + END AS "null", + CASE WHEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) IS NOT NULL + THEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) + END as "Default", + CASE WHEN pg_catalog.col_description(a.attrelid, a.attnum) IS NULL + THEN \'\' + ELSE pg_catalog.col_description(a.attrelid, a.attnum) + END AS "comments" + FROM pg_catalog.pg_attribute a + LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum + LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid + WHERE a.attrelid = + (SELECT oid FROM pg_catalog.pg_class WHERE relname=' . $this->quote($tableSub) . ' + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE + nspname = \'public\') + ) + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum' + ); + + $fields = $this->loadObjectList(); + + if ($typeOnly) + { + foreach ($fields as $field) + { + $result[$field->column_name] = preg_replace("/[(0-9)]/", '', $field->type); + } + } + else + { + foreach ($fields as $field) + { + $result[$field->column_name] = $field; + } + } + + /* Change Postgresql's NULL::* type with PHP's null one */ + foreach ($fields as $field) + { + if (preg_match("/^NULL::*/", $field->Default)) + { + $field->Default = null; + } + } + + return $result; + } + + /** + * 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. + * + * @throws \RuntimeException + */ + public function getTableKeys($table) + { + $this->open(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + if (in_array($table, $tableList)) + { + // Get the details columns information. + $this->setQuery(' + SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique AS "isUnique", + CASE WHEN indisprimary = true THEN + ( SELECT \'ALTER TABLE \' || tablename || \' ADD \' || pg_catalog.pg_get_constraintdef(const.oid, true) + FROM pg_constraint AS const WHERE const.conname= pgClassFirst.relname ) + ELSE pg_catalog.pg_get_indexdef(indexrelid, 0, true) + END AS "Query" + FROM pg_indexes + LEFT JOIN pg_class AS pgClassFirst ON indexname=pgClassFirst.relname + LEFT JOIN pg_index AS pgIndex ON pgClassFirst.oid=pgIndex.indexrelid + WHERE tablename=' . $this->quote($table) . ' ORDER BY indkey' + ); + $keys = $this->loadObjectList(); + + return $keys; + } + + return false; + } + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + * + * @throws \RuntimeException + */ + public function getTableList() + { + $this->open(); + + $query = $this->getQuery(true) + ->select('table_name') + ->from('information_schema.tables') + ->where('table_type=' . $this->quote('BASE TABLE')) + ->where( + 'table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ')' + ) + ->order('table_name ASC'); + + $this->setQuery($query); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the details list of sequences for a table. + * + * @param string $table The name of the table. + * + * @return array An array of sequences specification for the table. + * + * @throws \RuntimeException + */ + public function getTableSequences($table) + { + $this->open(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + if (in_array($table, $tableList)) + { + $name = array( + 's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type', + 'info.minimum_value', 'info.maximum_value', 'info.increment', 'info.cycle_option' + ); + $as = array( + 'sequence', 'schema', 'table', 'column', 'data_type', + 'minimum_value', 'maximum_value', 'increment', 'cycle_option' + ); + + if (version_compare($this->getVersion(), '9.1.0') >= 0) + { + $name[] .= 'info.start_value'; + $as[] .= 'start_value'; + } + + // Get the details columns information. + $query = $this->getQuery(true) + ->select($this->quoteName($name, $as)) + ->from('pg_class AS s') + ->join('LEFT', "pg_depend d ON d.objid=s.oid AND d.classid='pg_class'::regclass AND d.refclassid='pg_class'::regclass") + ->join('LEFT', 'pg_class t ON t.oid=d.refobjid') + ->join('LEFT', 'pg_namespace n ON n.oid=t.relnamespace') + ->join('LEFT', 'pg_attribute a ON a.attrelid=t.oid AND a.attnum=d.refobjsubid') + ->join('LEFT', 'information_schema.sequences AS info ON info.sequence_name=s.relname') + ->where("s.relkind='S' AND d.deptype='a' AND t.relname=" . $this->quote($table)); + $this->setQuery($query); + $seq = $this->loadObjectList(); + + return $seq; + } + + return false; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + */ + public function getVersion() + { + $this->open(); + $version = pg_version($this->connection); + + return $version['server']; + } + + /** + * Method to get the auto-incremented value from the last INSERT statement. + * To be called after the INSERT statement, it's MANDATORY to have a sequence on + * every primary key table. + * + * To get the auto incremented value it's possible to call this function after + * INSERT INTO query, or use INSERT INTO with RETURNING clause. + * + * @example with insertid() call: + * $query = $this->getQuery(true); + * $query->insert('jos_dbtest') + * ->columns('title,start_date,description') + * ->values("'testTitle2nd','1971-01-01','testDescription2nd'"); + * $this->setQuery($query); + * $this->execute(); + * $id = $this->insertid(); + * + * @example with RETURNING clause: + * $query = $this->getQuery(true); + * $query->insert('jos_dbtest') + * ->columns('title,start_date,description') + * ->values("'testTitle2nd','1971-01-01','testDescription2nd'") + * ->returning('id'); + * $this->setQuery($query); + * $id = $this->loadResult(); + * + * @return integer The value of the auto-increment field from the last inserted row. + */ + public function insertid() + { + $this->open(); + $insertQuery = $this->getQuery(false, true); + $table = $insertQuery->__get('insert')->getElements(); + + /* find sequence column name */ + $colNameQuery = $this->getQuery(true); + $colNameQuery->select('column_default') + ->from('information_schema.columns') + ->where( + "table_name=" . $this->quote( + $this->replacePrefix(str_replace('"', '', $table[0])) + ), 'AND' + ) + ->where("column_default LIKE '%nextval%'"); + + $this->setQuery($colNameQuery); + $colName = $this->loadRow(); + $changedColName = str_replace('nextval', 'currval', $colName); + + $insertidQuery = $this->getQuery(true); + $insertidQuery->select($changedColName); + $this->setQuery($insertidQuery); + $insertVal = $this->loadRow(); + + return $insertVal[0]; + } + + /** + * Locks a table in the database. + * + * @param string $tableName The name of the table to unlock. + * + * @return Postgresql Returns this object to support chaining. + * + * @throws \RuntimeException + */ + public function lockTable($tableName) + { + $this->transactionStart(); + $this->setQuery('LOCK TABLE ' . $this->quoteName($tableName) . ' IN ACCESS EXCLUSIVE MODE')->execute(); + + return $this; + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @throws \RuntimeException + */ + public function query() + { + static $isReconnecting = false; + + $this->open(); + + if (empty($this->connection)) + { + // TODO + 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->limit . ' OFFSET ' . $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 = ''; + + // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. + $this->cursor = @pg_query($this->connection, $query); + + // If an error occurred handle it. + if (!$this->cursor) + { + // Get the error number and message before we overwrite the error message + $this->errorNum = (int)pg_result_error_field($this->cursor, PGSQL_DIAG_SQLSTATE) . ' '; // This will never wok as the cursor is false + $this->errorMsg = pg_last_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) + { + // Get the error number and message. + $this->errorNum = (int)pg_result_error_field($this->cursor, PGSQL_DIAG_SQLSTATE) . ' '; // This will never wok as the cursor is false + $this->errorMsg = pg_last_error($this->connection); + + // Throw the normal query exception. + throw new \RuntimeException($this->errorMsg); + } + + // Since we were able to reconnect, run the query again. + $result = $this->query(); + $isReconnecting = false; + + return $result; + } + // The server was not disconnected. + else + { + // Throw the normal query exception. + throw new \RuntimeException($this->errorMsg); + } + } + + return $this->cursor; + } + + /** + * 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 PostgreSQL. + * @param string $prefix Not used by PostgreSQL. + * + * @return Postgresql Returns this object to support chaining. + * + * @throws \RuntimeException + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->open(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + // Origin Table does not exist + if (!in_array($oldTable, $tableList)) + { + // Origin Table not found + throw new \RuntimeException('Table not found in Postgresql database.'); + } + else + { + /* Rename indexes */ + $this->setQuery( + 'SELECT relname + FROM pg_class + WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname=' . $this->quote($oldTable, true) . ' + AND pg_class.oid=pg_index.indrelid );' + ); + + $oldIndexes = $this->loadColumn(); + foreach ($oldIndexes as $oldIndex) + { + $changedIdxName = str_replace($oldTable, $newTable, $oldIndex); + $this->setQuery('ALTER INDEX ' . $this->escape($oldIndex) . ' RENAME TO ' . $this->escape($changedIdxName)); + $this->execute(); + } + + /* Rename sequence */ + $this->setQuery( + 'SELECT relname + FROM pg_class + WHERE relkind = \'S\' + AND relnamespace IN ( + SELECT oid + FROM pg_namespace + WHERE nspname NOT LIKE \'pg_%\' + AND nspname != \'information_schema\' + ) + AND relname LIKE \'%' . $oldTable . '%\' ;' + ); + + $oldSequences = $this->loadColumn(); + foreach ($oldSequences as $oldSequence) + { + $changedSequenceName = str_replace($oldTable, $newTable, $oldSequence); + $this->setQuery('ALTER SEQUENCE ' . $this->escape($oldSequence) . ' RENAME TO ' . $this->escape($changedSequenceName)); + $this->execute(); + } + + /* Rename table */ + $this->setQuery('ALTER TABLE ' . $this->escape($oldTable) . ' RENAME TO ' . $this->escape($newTable)); + $this->execute(); + } + + return true; + } + + /** + * Selects the database, but redundant for PostgreSQL + * + * @param string $database Database name to select. + * + * @return boolean Always true + */ + public function select($database) + { + return true; + } + + /** + * Custom settings for UTF support + * + * @return int Zero on success, -1 on failure + */ + public function setUTF() + { + $this->open(); + + return pg_set_client_encoding($this->connection, 'UTF8'); + } + + /** + * This function return a field value as a prepared string to be used in a SQL statement. + * + * @param array $columns Array of table's column returned by ::getTableColumns. + * @param string $field_name The table field's name. + * @param string $field_value The variable value to quote and return. + * + * @return string The quoted string. + */ + protected function sqlValue($columns, $field_name, $field_value) + { + switch ($columns[$field_name]) + { + case 'boolean': + $val = 'NULL'; + if ($field_value == 't') + { + $val = 'TRUE'; + } + elseif ($field_value == 'f') + { + $val = 'FALSE'; + } + break; + case 'bigint': + case 'bigserial': + case 'integer': + case 'money': + case 'numeric': + case 'real': + case 'smallint': + case 'serial': + case 'numeric,': + $val = strlen($field_value) == 0 ? 'NULL' : $field_value; + break; + case 'date': + case 'timestamp without time zone': + if (empty($field_value)) + { + $field_value = $this->getNullDate(); + } + default: + $val = $this->quote($field_value); + break; + } + + return $val; + } + + /** + * Method to commit a transaction. + * + * @return void + * + * @throws \RuntimeException + */ + public function transactionCommit() + { + $this->open(); + + $this->setQuery('COMMIT'); + $this->execute(); + } + + /** + * Method to roll back a transaction. + * + * @param string $toSavepoint If present rollback transaction to this savepoint + * + * @return void + * + * @throws \RuntimeException + */ + public function transactionRollback($toSavepoint = null) + { + $this->open(); + + $query = 'ROLLBACK'; + if (!is_null($toSavepoint)) + { + $query .= ' TO SAVEPOINT ' . $this->escape($toSavepoint); + } + + $this->setQuery($query); + $this->execute(); + } + + /** + * Method to initialize a transaction. + * + * @return void + * + * @throws \RuntimeException + */ + public function transactionStart() + { + $this->open(); + $this->setQuery('START TRANSACTION'); + $this->execute(); + } + + /** + * 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. + */ + public function fetchArray($cursor = null) + { + return pg_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 pg_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. + */ + public function fetchObject($cursor = null, $class = 'stdClass') + { + return pg_fetch_object(is_null($cursor) ? $this->cursor : $cursor, null, $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) + { + pg_free_result($cursor ? $cursor : $this->cursor); + } + + /** + * 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. + * + * @throws \RuntimeException + */ + public function insertObject($table, &$object, $key = null) + { + $columns = $this->getTableColumns($table); + + $fields = array(); + $values = array(); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Only process non-null scalars. + if (is_array($v) or is_object($v) or $v === null) + { + continue; + } + + // Ignore any internal fields. + if ($k[0] == '_') + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->sqlValue($columns, $k, $v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + $retVal = false; + + if ($key) + { + $query->returning($key); + + // Set the query and execute the insert. + $this->setQuery($query); + + $id = $this->loadResult(); + if ($id) + { + $object->$key = $id; + $retVal = true; + } + } + else + { + // Set the query and execute the insert. + $this->setQuery($query); + + if ($this->execute()) + { + $retVal = true; + } + } + + return $retVal; + } + + /** + * Test to see if the PostgreSQL connector is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isSupported() + { + return (function_exists('pg_connect')); + } + + /** + * Returns an array containing database's table list. + * + * @return array The database's table list. + */ + public function showTables() + { + $this->open(); + + $query = $this->getQuery(true) + ->select('table_name') + ->from('information_schema.tables') + ->where('table_type=' . $this->quote('BASE TABLE')) + ->where( + 'table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ' )' + ); + + $this->setQuery($query); + $tableList = $this->loadColumn(); + + return $tableList; + } + + /** + * Get the substring position inside a string + * + * @param string $substring The string being sought + * @param string $string The string/column being searched + * + * @return int The position of $substring in $string + */ + public function getStringPositionSQL($substring, $string) + { + $this->open(); + + $query = "SELECT POSITION( $substring IN $string )"; + $this->setQuery($query); + $position = $this->loadRow(); + + return $position['position']; + } + + /** + * Generate a random value + * + * @return float The random generated number + */ + public function getRandom() + { + $this->open(); + + $this->setQuery('SELECT RANDOM()'); + $random = $this->loadAssoc(); + + return $random['random']; + } + + /** + * Return the query string to alter the database character set. + * + * @param string $dbName The database name + * + * @return string The query that alter the database query string + */ + protected function getAlterDbCharacterSet($dbName) + { + $query = 'ALTER DATABASE ' . $this->quoteName($dbName) . ' SET CLIENT_ENCODING TO ' . $this->quote('UTF8'); + + return $query; + } + + /** + * Return the query string to create new Database using PostgreSQL's syntax + * + * @param \stdClass $options Object used to pass user and database name to database driver. + * This object must have "db_name" and "db_user" set. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return string The query that creates database, owned by $options['user'] + */ + protected function getCreateDatabaseQuery($options, $utf) + { + $query = 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' OWNER ' . $this->quoteName($options->db_user); + + if ($utf) + { + $query .= ' ENCODING ' . $this->quote('UTF-8'); + } + + return $query; + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix class variable. + * + * @param string $query The SQL statement to prepare. + * @param string $prefix The common table prefix. + * + * @return string The processed SQL statement. + */ + public function replacePrefix($query, $prefix = '#__') + { + $query = trim($query); + $replacedQuery = ''; + + if (strpos($query, '\'')) + { + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'currval')) + { + $query = explode('currval', $query); + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + $query = implode('currval', $query); + } + + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'nextval')) + { + $query = explode('nextval', $query); + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + $query = implode('nextval', $query); + } + + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'setval')) + { + $query = explode('setval', $query); + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + $query = implode('setval', $query); + } + + $explodedQuery = explode('\'', $query); + + for ($nIndex = 0; $nIndex < count($explodedQuery); $nIndex = $nIndex + 2) + { + if (strpos($explodedQuery[$nIndex], $prefix)) + { + $explodedQuery[$nIndex] = str_replace($prefix, $this->tablePrefix, $explodedQuery[$nIndex]); + } + } + + $replacedQuery = implode('\'', $explodedQuery); + } + else + { + $replacedQuery = str_replace($prefix, $this->tablePrefix, $query); + } + + return $replacedQuery; + } + + /** + * Method to release a savepoint. + * + * @param string $savepointName Savepoint's name to release + * + * @return void + */ + public function releaseTransactionSavepoint($savepointName) + { + $this->open(); + $this->setQuery('RELEASE SAVEPOINT ' . $this->escape($savepointName)); + $this->execute(); + } + + /** + * Method to create a savepoint. + * + * @param string $savepointName Savepoint's name to create + * + * @return void + */ + public function transactionSavepoint($savepointName) + { + $this->open(); + $this->setQuery('SAVEPOINT ' . $this->escape($savepointName)); + $this->execute(); + } + + /** + * Unlocks tables in the database, this command does not exist in PostgreSQL, + * it is automatically done on commit or rollback. + * + * @return Postgresql Returns this object to support chaining. + * + * @throws \RuntimeException + */ + public function unlockTables() + { + $this->transactionCommit(); + + return $this; + } + + /** + * Updates a row in a table based on an object's properties. + * + * @param string $table The name of the database table to update. + * @param object &$object A reference to an object whose public properties match the table fields. + * @param string $key The name of the primary key. + * @param boolean $nulls True to update null fields or false to ignore them. + * + * @return boolean True on success. + * + * @throws \RuntimeException + */ + public function updateObject($table, &$object, $key, $nulls = false) + { + $columns = $this->getTableColumns($table); + $fields = array(); + $where = ''; + + // Create the base update statement. + $query = $this->getQuery(true) + ->update($table); + $stmt = '%s WHERE %s'; + + // Iterate over the object variables to build the query fields/value pairs. + foreach (get_object_vars($object) as $k => $v) + { + // Only process scalars that are not internal fields. + if (is_array($v) or is_object($v) or $k[0] == '_') + { + continue; + } + + // Set the primary key to the WHERE clause instead of a field to update. + if ($k == $key) + { + $key_val = $this->sqlValue($columns, $k, $v); + $where = $this->quoteName($k) . '=' . $key_val; + continue; + } + + // Prepare and sanitize the fields and values for the database query. + if ($v === null) + { + // If the value is null and we want to update nulls then set it. + if ($nulls) + { + $val = 'NULL'; + } + // If the value is null and we do not want to update nulls then ignore this field. + else + { + continue; + } + } + // The field is not null so we prep it for update. + else + { + $val = $this->sqlValue($columns, $k, $v); + } + + // Add the field to be updated. + $fields[] = $this->quoteName($k) . '=' . $val; + } + + // We don't have any fields to update. + if (empty($fields)) + { + return true; + } + + // Set the query and execute the update. + $query->set(sprintf($stmt, implode(",", $fields), $where)); + $this->setQuery($query); + + return $this->execute(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Base.php new file mode 100644 index 00000000..592ca0e9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Base.php @@ -0,0 +1,1605 @@ +quote($args[0], isset($args[1]) ? $args[1] : true); + break; + + case 'qn': + return $this->quoteName($args[0]); + break; + + case 'e': + return $this->escape($args[0], isset($args[1]) ? $args[1] : false); + break; + } + } + + /** + * Class constructor. + * + * @param DriverBase $db The database connector resource. + */ + public function __construct(DriverBase $db = null) + { + $this->db = $db; + } + + /** + * Magic function to convert the query to a string. + * + * @return string The completed query. + */ + public function __toString() + { + $query = ''; + + if ($this->sql) + { + return $this->sql; + } + + switch ($this->type) + { + case 'element': + $query .= (string)$this->element; + break; + + 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; + } + + break; + + case 'union': + $query .= (string)$this->union; + break; + + case 'unionAll': + $query .= (string)$this->unionAll; + break; + + case 'delete': + $query .= (string)$this->delete; + $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; + } + + break; + + case 'update': + $query .= (string)$this->update; + + if ($this->join) + { + // Special case for joins + foreach ($this->join as $join) + { + $query .= (string)$join; + } + } + + $query .= (string)$this->set; + + if ($this->where) + { + $query .= (string)$this->where; + } + + break; + + 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->values->getElements(); + + if (!($elements[0] instanceof $this)) + { + $query .= ' VALUES '; + } + + $query .= (string)$this->values; + } + + break; + + case 'call': + $query .= (string)$this->call; + break; + + case 'exec': + $query .= (string)$this->exec; + break; + } + + if ($this instanceof QueryLimitable) + { + $query = $this->processLimit($query, $this->limit, $this->offset); + } + + return $query; + } + + /** + * Magic function to get protected variable value + * + * @param string $name The name of the variable. + * + * @return mixed + */ + public function __get($name) + { + return isset($this->$name) ? $this->$name : null; + } + + /** + * Add a single column, or array of columns to the CALL clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The call method can, however, be called multiple times in the same query. + * + * Usage: + * $query->call('a.*')->call('b.id'); + * $query->call(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return Base Returns this object to allow chaining. + */ + public function call($columns) + { + $this->type = 'call'; + + if (is_null($this->call)) + { + $this->call = new QueryElement('CALL', $columns); + } + else + { + $this->call->append($columns); + } + + 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. + */ + public function castAsChar($value) + { + return $value; + } + + /** + * 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) + { + return 'CHAR_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 Base Returns this object to allow chaining. + */ + public function clear($clause = null) + { + $this->sql = null; + + switch ($clause) + { + case 'select': + $this->select = null; + $this->type = null; + break; + + case 'delete': + $this->delete = null; + $this->type = null; + break; + + case 'update': + $this->update = null; + $this->type = null; + break; + + case 'insert': + $this->insert = null; + $this->type = null; + $this->autoIncrementField = null; + break; + + case 'from': + $this->from = null; + break; + + case 'join': + $this->join = null; + break; + + case 'set': + $this->set = null; + break; + + case 'where': + $this->where = null; + break; + + case 'group': + $this->group = null; + break; + + case 'having': + $this->having = null; + break; + + case 'order': + $this->order = null; + break; + + case 'columns': + $this->columns = null; + break; + + case 'values': + $this->values = null; + break; + + case 'exec': + $this->exec = null; + $this->type = null; + break; + + case 'call': + $this->call = null; + $this->type = null; + break; + + case 'limit': + $this->offset = 0; + $this->limit = 0; + break; + + case 'union': + $this->union = null; + break; + + case 'unionAll': + $this->unionAll = null; + break; + + default: + $this->type = null; + $this->select = null; + $this->delete = null; + $this->update = null; + $this->insert = null; + $this->from = null; + $this->join = null; + $this->set = null; + $this->where = null; + $this->group = null; + $this->having = null; + $this->order = null; + $this->columns = null; + $this->values = null; + $this->autoIncrementField = null; + $this->exec = null; + $this->call = null; + $this->union = null; + $this->unionAll = null; + $this->offset = 0; + $this->limit = 0; + break; + } + + return $this; + } + + /** + * Adds a column, or array of column names that would be used for an INSERT INTO statement. + * + * @param mixed $columns A column name, or array of column names. + * + * @return Base Returns this object to allow chaining. + */ + public function columns($columns) + { + if (is_null($this->columns)) + { + $this->columns = new QueryElement('()', $columns); + } + else + { + $this->columns->append($columns); + } + + return $this; + } + + /** + * 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. + */ + public function concatenate($values, $separator = null) + { + if ($separator) + { + return 'CONCATENATE(' . implode(' || ' . $this->quote($separator) . ' || ', $values) . ')'; + } + else + { + return 'CONCATENATE(' . implode(' || ', $values) . ')'; + } + } + + /** + * Gets the current date and time. + * + * Usage: + * $query->where('published_up < '.$query->currentTimestamp()); + * + * @return string + */ + public function currentTimestamp() + { + return 'CURRENT_TIMESTAMP()'; + } + + /** + * Returns a PHP date() function compliant date format for the database driver. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the getDateFormat method directly. + * + * @return string The format string. + */ + public function dateFormat() + { + if (!($this->db instanceof DriverBase)) + { + throw new QueryException('Invalid database object'); + } + + return $this->db->getDateFormat(); + } + + /** + * Creates a formatted dump of the query for debugging purposes. + * + * Usage: + * echo $query->dump(); + * + * @return string + */ + public function dump() + { + return '
' . str_replace('#__', $this->db->getPrefix(), $this) . '
'; + } + + /** + * Add a table name to the DELETE clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->delete('#__a')->where('id = 1'); + * + * @param string $table The name of the table to delete from. + * + * @return Base Returns this object to allow chaining. + */ + public function delete($table = null) + { + $this->type = 'delete'; + $this->delete = new QueryElement('DELETE', null); + + if (!empty($table)) + { + $this->from($table); + } + + return $this; + } + + /** + * Method to escape a string for usage in an SQL statement. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the escape method directly. + * + * Note that 'e' is an alias for this method as it is in DriverBase. + * + * @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 (!($this->db instanceof DriverBase)) + { + throw new QueryException('Invalid database object'); + } + + return $this->db->escape($text, $extra); + } + + /** + * Add a single column, or array of columns to the EXEC clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The exec method can, however, be called multiple times in the same query. + * + * Usage: + * $query->exec('a.*')->exec('b.id'); + * $query->exec(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return Base Returns this object to allow chaining. + */ + public function exec($columns) + { + $this->type = 'exec'; + + if (is_null($this->exec)) + { + $this->exec = new QueryElement('EXEC', $columns); + } + else + { + $this->exec->append($columns); + } + + return $this; + } + + /** + * Add a table to the FROM clause of the query. + * + * Note that while an array of tables can be provided, it is recommended you use explicit joins. + * + * Usage: + * $query->select('*')->from('#__a'); + * + * @param mixed $tables A string or array of table names. + * This can be a Base object (or a child of it) when used + * as a subquery in FROM clause along with a value for $subQueryAlias. + * @param string $subQueryAlias Alias used when $tables is a Base. + * + * @return Base Returns this object to allow chaining. + */ + public function from($tables, $subQueryAlias = null) + { + if (is_null($this->from)) + { + if ($tables instanceof $this) + { + if (is_null($subQueryAlias)) + { + throw new QueryException('Null subquery defined'); + } + + $tables = '( ' . (string)$tables . ' ) AS ' . $this->quoteName($subQueryAlias); + } + + $this->from = new QueryElement('FROM', $tables); + } + else + { + $this->from->append($tables); + } + + 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. + */ + public function year($date) + { + return 'YEAR(' . $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. + */ + public function month($date) + { + return 'MONTH(' . $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. + */ + public function day($date) + { + return 'DAY(' . $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. + */ + public function hour($date) + { + return 'HOUR(' . $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. + */ + public function minute($date) + { + return 'MINUTE(' . $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. + */ + public function second($date) + { + return 'SECOND(' . $date . ')'; + } + + /** + * Add a grouping column to the GROUP clause of the query. + * + * Usage: + * $query->group('id'); + * + * @param mixed $columns A string or array of ordering columns. + * + * @return Base Returns this object to allow chaining. + */ + public function group($columns) + { + if (is_null($this->group)) + { + $this->group = new QueryElement('GROUP BY', $columns); + } + else + { + $this->group->append($columns); + } + + return $this; + } + + /** + * A conditions to the HAVING clause of the query. + * + * Usage: + * $query->group('id')->having('COUNT(id) > 5'); + * + * @param mixed $conditions A string or array of columns. + * @param string $glue The glue by which to join the conditions. Defaults to AND. + * + * @return Base Returns this object to allow chaining. + */ + public function having($conditions, $glue = 'AND') + { + if (is_null($this->having)) + { + $glue = strtoupper($glue); + $this->having = new QueryElement('HAVING', $conditions, " $glue "); + } + else + { + $this->having->append($conditions); + } + + return $this; + } + + /** + * Add an INNER JOIN clause to the query. + * + * Usage: + * $query->innerJoin('b ON b.id = a.id')->innerJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return Base Returns this object to allow chaining. + */ + public function innerJoin($condition) + { + $this->join('INNER', $condition); + + return $this; + } + + /** + * Add a table name to the INSERT clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->insert('#__a')->set('id = 1'); + * $query->insert('#__a')->columns('id, title')->values('1,2')->values('3,4'); + * $query->insert('#__a')->columns('id, title')->values(array('1,2', '3,4')); + * + * @param mixed $table The name of the table to insert data into. + * @param boolean $incrementField The name of the field to auto increment. + * + * @return Base Returns this object to allow chaining. + */ + public function insert($table, $incrementField = false) + { + $this->type = 'insert'; + $this->insert = new QueryElement('INSERT INTO', $table); + $this->autoIncrementField = $incrementField; + + return $this; + } + + /** + * Add a JOIN clause to the query. + * + * Usage: + * $query->join('INNER', 'b ON b.id = a.id); + * + * @param string $type The type of join. This string is prepended to the JOIN keyword. + * @param string $conditions A string or array of conditions. + * + * @return Base Returns this object to allow chaining. + */ + public function join($type, $conditions) + { + if (is_null($this->join)) + { + $this->join = array(); + } + $this->join[] = new QueryElement(strtoupper($type) . ' JOIN', $conditions); + + return $this; + } + + /** + * Add a LEFT JOIN clause to the query. + * + * Usage: + * $query->leftJoin('b ON b.id = a.id')->leftJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return Base Returns this object to allow chaining. + */ + public function leftJoin($condition) + { + $this->join('LEFT', $condition); + + return $this; + } + + /** + * Get the length of a string in bytes. + * + * Note, use 'charLength' to find the number of characters in a string. + * + * Usage: + * query->where($query->length('a').' > 3'); + * + * @param string $value The string to measure. + * + * @return int + */ + public function length($value) + { + return 'LENGTH(' . $value . ')'; + } + + /** + * Get the null or zero representation of a timestamp for the database driver. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the nullDate method directly. + * + * Usage: + * $query->where('modified_date <> '.$query->nullDate()); + * + * @param boolean $quoted Optionally wraps the null date in database quotes (true by default). + * + * @return string Null or zero representation of a timestamp. + */ + public function nullDate($quoted = true) + { + if (!($this->db instanceof DriverBase)) + { + throw new QueryException('Invalid database object'); + } + + $result = $this->db->getNullDate($quoted); + + if ($quoted) + { + return $this->db->quote($result); + } + + return $result; + } + + /** + * Add a ordering column to the ORDER clause of the query. + * + * Usage: + * $query->order('foo')->order('bar'); + * $query->order(array('foo','bar')); + * + * @param mixed $columns A string or array of ordering columns. + * + * @return Base Returns this object to allow chaining. + */ + public function order($columns) + { + if (is_null($this->order)) + { + $this->order = new QueryElement('ORDER BY', $columns); + } + else + { + $this->order->append($columns); + } + + return $this; + } + + /** + * Add an OUTER JOIN clause to the query. + * + * Usage: + * $query->outerJoin('b ON b.id = a.id')->outerJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return Base Returns this object to allow chaining. + */ + public function outerJoin($condition) + { + $this->join('OUTER', $condition); + + return $this; + } + + /** + * Method to quote and optionally escape a string to database requirements for insertion into the database. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the quote method directly. + * + * Note that 'q' is an alias for this method as it is in DriverBase. + * + * Usage: + * $query->quote('fulltext'); + * $query->q('fulltext'); + * $query->q(array('option', 'fulltext')); + * + * @param mixed $text A string or an array of strings to quote. + * @param boolean $escape True to escape the string, false to leave it unchanged. + * + * @return string The quoted input string. + * + * @throws QueryException if the internal db property is not a valid object. + */ + public function quote($text, $escape = true) + { + if (!($this->db instanceof DriverBase)) + { + throw new QueryException('Invalid database object'); + } + + return $this->db->quote($text, $escape); + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the quoteName method directly. + * + * Note that 'qn' is an alias for this method as it is in DriverBase. + * + * Usage: + * $query->quoteName('#__a'); + * $query->qn('#__a'); + * + * @param mixed $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes. + * Each type supports dot-notation name. + * @param mixed $as The AS query part associated to $name. It can be string or array, in latter case it has + * to be same length of $name; if is null there will not be any AS part for string or array + * element. + * + * @return mixed The quote wrapped name, same type of $name. + * + * @throws QueryException if the internal db property is not a valid object. + */ + public function quoteName($name, $as = null) + { + if (!($this->db instanceof DriverBase)) + { + throw new QueryException('Invalid database object'); + } + + return $this->db->quoteName($name, $as); + } + + /** + * Add a RIGHT JOIN clause to the query. + * + * Usage: + * $query->rightJoin('b ON b.id = a.id')->rightJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return Base Returns this object to allow chaining. + */ + public function rightJoin($condition) + { + $this->join('RIGHT', $condition); + + return $this; + } + + /** + * Add a single column, or array of columns to the SELECT clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The select method can, however, be called multiple times in the same query. + * + * Usage: + * $query->select('a.*')->select('b.id'); + * $query->select(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return Base Returns this object to allow chaining. + */ + public function select($columns) + { + $this->type = 'select'; + + if (is_null($this->select)) + { + $this->select = new QueryElement('SELECT', $columns); + } + else + { + $this->select->append($columns); + } + + return $this; + } + + /** + * Add a single condition string, or an array of strings to the SET clause of the query. + * + * Usage: + * $query->set('a = 1')->set('b = 2'); + * $query->set(array('a = 1', 'b = 2'); + * + * @param mixed $conditions A string or array of string conditions. + * @param string $glue The glue by which to join the condition strings. Defaults to ,. + * Note that the glue is set on first use and cannot be changed. + * + * @return Base Returns this object to allow chaining. + */ + public function set($conditions, $glue = ',') + { + if (is_null($this->set)) + { + $glue = strtoupper($glue); + $this->set = new QueryElement('SET', $conditions, "\n\t$glue "); + } + else + { + $this->set->append($conditions); + } + + return $this; + } + + /** + * Allows a direct query to be provided to the database + * driver's setQuery() method, but still allow queries + * to have bounded variables. + * + * Usage: + * $query->setQuery('select * from #__users'); + * + * @param mixed $query An SQL Query + * + * @return QueryElement Returns this object to allow chaining. + */ + public function setQuery($query) + { + $this->sql = $query; + + return $this; + } + + /** + * Add a table name to the UPDATE clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->update('#__foo')->set(...); + * + * @param string $table A table to update. + * + * @return Base Returns this object to allow chaining. + */ + public function update($table) + { + $this->type = 'update'; + $this->update = new QueryElement('UPDATE', $table); + + return $this; + } + + /** + * Adds a tuple, or array of tuples that would be used as values for an INSERT INTO statement. + * + * Usage: + * $query->values('1,2,3')->values('4,5,6'); + * $query->values(array('1,2,3', '4,5,6')); + * + * @param string $values A single tuple, or array of tuples. + * + * @return Base Returns this object to allow chaining. + */ + public function values($values) + { + if (is_null($this->values)) + { + $this->values = new QueryElement('()', $values, '),('); + } + else + { + $this->values->append($values); + } + + return $this; + } + + /** + * Add a single condition, or an array of conditions to the WHERE clause of the query. + * + * Usage: + * $query->where('a = 1')->where('b = 2'); + * $query->where(array('a = 1', 'b = 2')); + * + * @param mixed $conditions A string or array of where conditions. + * @param string $glue The glue by which to join the conditions. Defaults to AND. + * Note that the glue is set on first use and cannot be changed. + * + * @return Base Returns this object to allow chaining. + */ + public function where($conditions, $glue = 'AND') + { + if (is_null($this->where)) + { + $glue = strtoupper($glue); + $this->where = new QueryElement('WHERE', $conditions, " $glue "); + } + else + { + $this->where->append($conditions); + } + + return $this; + } + + /** + * Method to provide deep copy support to nested objects and + * arrays when cloning. + * + * @return void + */ + public function __clone() + { + foreach ($this as $k => $v) + { + if ($k === 'db') + { + continue; + } + + if (is_object($v) || is_array($v)) + { + $this->$k = unserialize(serialize($v)); + } + } + } + + /** + * Add a query to UNION with the current query. + * Multiple unions each require separate statements and create an array of unions. + * + * Usage: + * $query->union('SELECT name FROM #__foo') + * $query->union('SELECT name FROM #__foo','distinct') + * $query->union(array('SELECT name FROM #__foo','SELECT name FROM #__bar')) + * + * @param mixed $query The Base object or string to union. + * @param boolean $distinct True to only return distinct rows from the union. + * @param string $glue The glue by which to join the conditions. + * + * @return mixed The Base object on success or boolean false on failure. + */ + public function union($query, $distinct = false, $glue = '') + { + // Clear any ORDER BY clause in UNION query + // See http://dev.mysql.com/doc/refman/5.0/en/union.html + if (!is_null($this->order)) + { + $this->clear('order'); + } + + // Set up the DISTINCT flag, the name with parentheses, and the glue. + if ($distinct) + { + $name = 'UNION DISTINCT ()'; + $glue = ')' . PHP_EOL . 'UNION DISTINCT ('; + } + else + { + $glue = ')' . PHP_EOL . 'UNION ('; + $name = 'UNION ()'; + } + + // Get the QueryElement if it does not exist + if (is_null($this->union)) + { + $this->union = new QueryElement($name, $query, "$glue"); + } + // Otherwise append the second UNION. + else + { + $glue = ''; + $this->union->append($query); + } + + return $this; + } + + /** + * Add a query to UNION DISTINCT with the current query. Simply a proxy to Union with the Distinct clause. + * + * Usage: + * $query->unionDistinct('SELECT name FROM #__foo') + * + * @param mixed $query The Base object or string to union. + * @param string $glue The glue by which to join the conditions. + * + * @return mixed The Base object on success or boolean false on failure. + */ + public function unionDistinct($query, $glue = '') + { + $distinct = true; + + // Apply the distinct flag to the union. + return $this->union($query, $distinct, $glue); + } + + /** + * Find and replace sprintf-like tokens in a format string. + * Each token takes one of the following forms: + * %% - A literal percent character. + * %[t] - Where [t] is a type specifier. + * %[n]$[x] - Where [n] is an argument specifier and [t] is a type specifier. + * + * Types: + * a - Numeric: Replacement text is coerced to a numeric type but not quoted or escaped. + * e - Escape: Replacement text is passed to $this->escape(). + * E - Escape (extra): Replacement text is passed to $this->escape() with true as the second argument. + * n - Name Quote: Replacement text is passed to $this->quoteName(). + * q - Quote: Replacement text is passed to $this->quote(). + * Q - Quote (no escape): Replacement text is passed to $this->quote() with false as the second argument. + * r - Raw: Replacement text is used as-is. (Be careful) + * + * Date Types: + * - Replacement text automatically quoted (use uppercase for Name Quote). + * - Replacement text should be a string in date format or name of a date column. + * y/Y - Year + * m/M - Month + * d/D - Day + * h/H - Hour + * i/I - Minute + * s/S - Second + * + * Invariable Types: + * - Takes no argument. + * - Argument index not incremented. + * t - Replacement text is the result of $this->currentTimestamp(). + * z - Replacement text is the result of $this->nullDate(false). + * Z - Replacement text is the result of $this->nullDate(true). + * + * Usage: + * $query->format('SELECT %1$n FROM %2$n WHERE %3$n = %4$a', 'foo', '#__foo', 'bar', 1); + * Returns: SELECT `foo` FROM `#__foo` WHERE `bar` = 1 + * + * Notes: + * The argument specifier is optional but recommended for clarity. + * The argument index used for unspecified tokens is incremented only when used. + * + * @param string $format The formatting string. + * + * @return string Returns a string produced according to the formatting string. + */ + public function format($format) + { + $query = $this; + $args = array_slice(func_get_args(), 1); + array_unshift($args, null); + + $i = 1; + $func = function ($match) use ($query, $args, &$i) + { + if (isset($match[6]) && $match[6] == '%') + { + return '%'; + } + + // No argument required, do not increment the argument index. + switch ($match[5]) + { + case 't': + return $query->currentTimestamp(); + break; + + case 'z': + return $query->nullDate(false); + break; + + case 'Z': + return $query->nullDate(true); + break; + } + + // Increment the argument index only if argument specifier not provided. + $index = is_numeric($match[4]) ? (int)$match[4] : $i++; + + if (!$index || !isset($args[$index])) + { + // TODO - What to do? sprintf() throws a Warning in these cases. + $replacement = ''; + } + else + { + $replacement = $args[$index]; + } + + switch ($match[5]) + { + case 'a': + return 0 + $replacement; + break; + + case 'e': + return $query->escape($replacement); + break; + + case 'E': + return $query->escape($replacement, true); + break; + + case 'n': + return $query->quoteName($replacement); + break; + + case 'q': + return $query->quote($replacement); + break; + + case 'Q': + return $query->quote($replacement, false); + break; + + case 'r': + return $replacement; + break; + + // Dates + case 'y': + return $query->year($query->quote($replacement)); + break; + + case 'Y': + return $query->year($query->quoteName($replacement)); + break; + + case 'm': + return $query->month($query->quote($replacement)); + break; + + case 'M': + return $query->month($query->quoteName($replacement)); + break; + + case 'd': + return $query->day($query->quote($replacement)); + break; + + case 'D': + return $query->day($query->quoteName($replacement)); + break; + + case 'h': + return $query->hour($query->quote($replacement)); + break; + + case 'H': + return $query->hour($query->quoteName($replacement)); + break; + + case 'i': + return $query->minute($query->quote($replacement)); + break; + + case 'I': + return $query->minute($query->quoteName($replacement)); + break; + + case 's': + return $query->second($query->quote($replacement)); + break; + + case 'S': + return $query->second($query->quoteName($replacement)); + break; + } + + return ''; + }; + + /** + * Regexp to find an replace all tokens. + * Matched fields: + * 0: Full token + * 1: Everything following '%' + * 2: Everything following '%' unless '%' + * 3: Argument specifier and '$' + * 4: Argument specifier + * 5: Type specifier + * 6: '%' if full token is '%%' + */ + + return preg_replace_callback('#%(((([\d]+)\$)?([aeEnqQryYmMdDhHiIsStzZ]))|(%))#', $func, $format); + } + + /** + * 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. + * Note: Not all drivers support all units. + * + * @param string $date The SQL-formatted date to add to. May be a date or datetime string. + * @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 + * + * @see http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-add + */ + public function dateAdd($date, $interval, $datePart) + { + return trim("DATE_ADD('" . $date . "', INTERVAL " . $interval . ' ' . $datePart . ')'); + } + + /** + * Add a query to UNION ALL with the current query. + * Multiple unions each require separate statements and create an array of unions. + * + * Usage: + * $query->union('SELECT name FROM #__foo') + * $query->union('SELECT name FROM #__foo','distinct') + * $query->union(array('SELECT name FROM #__foo','SELECT name FROM #__bar')) + * + * @param mixed $query The Base object or string to union. + * @param boolean $distinct True to only return distinct rows from the union. + * @param string $glue The glue by which to join the conditions. + * + * @return mixed The Base object on success or boolean false on failure. + */ + public function unionAll($query, $distinct = false, $glue = '') + { + $glue = ')' . PHP_EOL . 'UNION ALL ('; + $name = 'UNION ALL ()'; + + // Get the QueryElement if it does not exist + if (is_null($this->unionAll)) + { + $this->unionAll = new QueryElement($name, $query, "$glue"); + } + + // Otherwise append the second UNION. + else + { + $glue = ''; + $this->unionAll->append($query); + } + + return $this; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Element.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Element.php new file mode 100644 index 00000000..0667ebba --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Element.php @@ -0,0 +1,116 @@ +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)); + } + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Limitable.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Limitable.php new file mode 100644 index 00000000..0453c36e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Limitable.php @@ -0,0 +1,60 @@ +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); +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Mysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Mysql.php new file mode 100644 index 00000000..7f6cfa3b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Mysql.php @@ -0,0 +1,22 @@ + 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) . ')'; + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Pdomysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Pdomysql.php new file mode 100644 index 00000000..ea96a0b4 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Pdomysql.php @@ -0,0 +1,22 @@ +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 . "'"; + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Preparable.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Preparable.php new file mode 100644 index 00000000..2a531e56 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Preparable.php @@ -0,0 +1,56 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Sqlsrv.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Sqlsrv.php new file mode 100644 index 00000000..389dd326 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Query/Sqlsrv.php @@ -0,0 +1,196 @@ +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 . "'" . ')'; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Queryexception.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Queryexception.php new file mode 100644 index 00000000..e57d1ea2 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Queryexception.php @@ -0,0 +1,16 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Sqlsrv.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Sqlsrv.php new file mode 100644 index 00000000..29ac162c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Driver/Sqlsrv.php @@ -0,0 +1,992 @@ +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 $prefix with the string held is the + * tablePrefix 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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Base.php new file mode 100644 index 00000000..aa572ba4 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Base.php @@ -0,0 +1,877 @@ +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 ''; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native.php new file mode 100644 index 00000000..671cea18 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native.php @@ -0,0 +1,115 @@ +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; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Mysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Mysql.php new file mode 100644 index 00000000..7569e0b7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Mysql.php @@ -0,0 +1,2069 @@ + MySQL database server host name or IP address + * port MySQL database server port (optional) + * username MySQL user name, for authentication + * password MySQL password, for authentication + * database MySQL database + * dumpFile Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) + */ +class Mysql extends Base +{ + /** + * 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 DATABASE()')->loadResult(); + } + catch (\Exception $e) + { + return ''; + } + + return empty($ret) ? '' : $ret; + } + + /** + * The primary key structure of the currently backed up table. The keys contained are: + * - table The name of the table being backed up + * - field The name of the primary key field + * - value The last value of the PK field + * + * @var array + */ + protected $table_autoincrement = array( + 'table' => null, + 'field' => null, + 'value' => null, + ); + + /** + * Implements the constructor of the class + * + * @return Mysql + */ + public function __construct() + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance"); + } + + /** + * Applies the SQL compatibility setting + * + * @return void + */ + protected function enforceSQLCompatibility() + { + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + // Try to enforce SQL_BIG_SELECTS option + try + { + $db->setQuery('SET sql_big_selects=1'); + $db->query(); + } + catch (\Exception $e) + { + // Do nothing; some versions of MySQL don't allow you to use the BIG_SELECTS option. + } + + $db->resetErrors(); + } + + /** + * Performs one more step of dumping database data + * + * @return void + */ + protected function stepDatabaseDump() + { + // Initialize local variables + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + if (!is_object($db) || ($db === false)) + { + $this->setError(__CLASS__ . '::_run() Could not connect to database?!'); + + return; + } + + $outData = ''; // Used for outputting INSERT INTO commands + + $this->enforceSQLCompatibility(); // Apply MySQL compatibility option + + if ($this->getError()) + { + return; + } + + // Touch SQL dump file + $nada = ""; + $this->writeline($nada); + + // Get this table's information + $tableName = $this->nextTable; + $this->setStep($tableName); + $this->setSubstep(''); + $tableAbstract = trim($this->table_name_map[ $tableName ]); + $dump_records = $this->tables_data[ $tableName ]['dump_records']; + + // Restore any previously information about the largest query we had to run + $this->largest_query = Factory::getConfiguration()->get('volatile.database.largest_query', 0); + + // If it is the first run, find number of rows and get the CREATE TABLE command + if ($this->nextRange == 0) + { + if ($this->getError()) + { + return; + } + + $outCreate = ''; + + if (is_array($this->tables_data[ $tableName ])) + { + if (array_key_exists('create', $this->tables_data[ $tableName ])) + { + $outCreate = $this->tables_data[ $tableName ]['create']; + } + } + + if (empty($outCreate) && !empty($tableName)) + { + // The CREATE command wasn't cached. Time to create it. The $type and $dependencies + // variables will be thrown away. + $type = isset($this->tables_data[ $tableName ]['type']) ? $this->tables_data[ $tableName ]['type'] : 'table'; + $dependencies = array(); + $outCreate = $this->get_create($tableAbstract, $tableName, $type, $dependencies); + } + + // Create drop statements if required (the key is defined by the scripting engine) + if (Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0)) + { + if (array_key_exists('create', $this->tables_data[ $tableName ])) + { + $dropStatement = $this->createDrop($this->tables_data[ $tableName ]['create']); + } + else + { + $type = 'table'; + $createStatement = $this->get_create($tableAbstract, $tableName, $type, $dependencies); + $dropStatement = $this->createDrop($createStatement); + } + + if (!empty($dropStatement)) + { + $dropStatement .= "\n"; + + if (!$this->writeDump($dropStatement)) + { + return; + } + } + } + + // Write the CREATE command after any DROP command which might be necessary. + if (!$this->writeDump($outCreate)) + { + return; + } + + if ($dump_records) + { + // We are dumping data from a table, get the row count + $this->getRowCount($tableAbstract); + + // If we can't get the row count we cannot back up this table's data + if (is_null($this->maxRange)) + { + $dump_records = false; + } + } + else + { + /** + * Do NOT move this line to the if-block below. We need to only log this message on tables which are + * filtered, not on tables we simply cannot get the row count information for! + */ + Factory::getLog()->log(LogLevel::INFO, "Skipping dumping data of " . $tableAbstract); + } + + // The table is either filtered or we cannot get the row count. Either way we should not dump any data. + if (!$dump_records) + { + $this->maxRange = 0; + $this->nextRange = 1; + $outData = ''; + $numRows = 0; + $dump_records = false; + } + + // Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server + if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Writing data dump preamble for " . $tableAbstract); + $preamble = $this->getDataDumpPreamble($tableAbstract, $tableName, $this->maxRange); + + if (!empty($preamble)) + { + if (!$this->writeDump($preamble)) + { + return; + } + } + } + + // Get the table's auto increment information + if ($dump_records) + { + $this->setAutoIncrementInfo(); + } + } + + // Load the active database root + $configuration = Factory::getConfiguration(); + $dbRoot = $configuration->get('volatile.database.root', '[SITEDB]'); + + // Get the default and the current (optimal) batch size + $defaultBatchSize = $this->getDefaultBatchSize(); + $batchSize = $configuration->get('volatile.database.batchsize', $defaultBatchSize); + + // Check if we have more work to do on this table + if (($this->nextRange < $this->maxRange)) + { + $timer = Factory::getTimer(); + + // Get the number of rows left to dump from the current table + $sql = $db->getQuery(true) + ->select('*') + ->from($db->nameQuote($tableAbstract)); + + if (!is_null($this->table_autoincrement['field'])) + { + $sql->order($db->qn($this->table_autoincrement['field']) . ' ASC'); + } + + if ($this->nextRange == 0) + { + // Get the optimal batch size for this table and save it to the volatile data + $batchSize = $this->getOptimalBatchSize($tableAbstract, $defaultBatchSize); + $configuration->set('volatile.database.batchsize', $batchSize); + + // First run, get a cursor to all records + $db->setQuery($sql, 0, $batchSize); + Factory::getLog()->log(LogLevel::INFO, "Beginning dump of " . $tableAbstract); + Factory::getLog()->log(LogLevel::DEBUG, "Up to $batchSize records will be read at once."); + } + else + { + // Subsequent runs, get a cursor to the rest of the records + $this->setSubstep($this->nextRange . ' / ' . $this->maxRange); + + // If we have an auto_increment value and the table has over $batchsize records use the indexed select instead of a plain limit + if (!is_null($this->table_autoincrement['field']) && !is_null($this->table_autoincrement['value'])) + { + Factory::getLog() + ->log(LogLevel::INFO, "Continuing dump of " . $tableAbstract . " from record #{$this->nextRange} using auto_increment column {$this->table_autoincrement['field']} and value {$this->table_autoincrement['value']}"); + $sql->where($db->qn($this->table_autoincrement['field']) . ' > ' . $db->q($this->table_autoincrement['value'])); + $db->setQuery($sql, 0, $batchSize); + } + else + { + Factory::getLog() + ->log(LogLevel::INFO, "Continuing dump of " . $tableAbstract . " from record #{$this->nextRange}"); + $db->setQuery($sql, $this->nextRange, $batchSize); + } + } + + $this->query = ''; + $numRows = 0; + $use_abstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1); + + $filters = Factory::getFilters(); + $mustFilter = $filters->hasFilterType('dbobject', 'children'); + + try + { + $cursor = $db->query(); + } + catch (\Exception $exc) + { + // Issue a warning about the failure to dump data + $errno = $exc->getCode(); + $error = $exc->getMessage(); + $this->setWarning("Failed dumping $tableAbstract from record #{$this->nextRange}. MySQL error $errno: $error"); + + // Reset the database driver's state (we will try to dump other tables anyway) + $db->resetErrors(); + $cursor = null; + + // Mark this table as done since we are unable to dump it. + $this->nextRange = $this->maxRange; + } + + while (is_array($myRow = $db->fetchAssoc()) && ($numRows < ($this->maxRange - $this->nextRange))) + { + if ($this->createNewPartIfRequired() == false) + { + /** + * When createNewPartIfRequired returns false it means that we have began adding a SQL part to the + * backup archive but it hasn't finished. If we don't return here, the code below will keep adding + * data to that dump file. Yes, despite being closed. When you call writeDump the file is reopened. + * As a result of writing data of length Y, the file that had a size X now has a size of X + Y. This + * means that the loop in BaseArchiver which tries to add it to the archive will never see its End + * Of File since we are trying to resume the backup from *beyond* the file position that was + * recorded as the file size. The archive can detect a file shrinking but not a file growing! + * Therefore we hit an infinite loop a.k.a. runaway backup. + */ + return; + } + + $numRows ++; + $numOfFields = count($myRow); + + // On MS SQL Server there's always a RowNumber pseudocolumn added at the end, screwing up the backup (GRRRR!) + if ($db->getDriverType() == 'mssql') + { + $numOfFields --; + } + + // If row-level filtering is enabled, please run the filtering + if ($mustFilter) + { + $isFiltered = $filters->isFiltered( + array( + 'table' => $tableAbstract, + 'row' => $myRow + ), + $dbRoot, + 'dbobject', + 'children' + ); + + if ($isFiltered) + { + // Update the auto_increment value to avoid edge cases when the batch size is one + if (!is_null($this->table_autoincrement['field']) && isset($myRow[ $this->table_autoincrement['field'] ])) + { + $this->table_autoincrement['value'] = $myRow[ $this->table_autoincrement['field'] ]; + } + + continue; + } + } + + if ( + (!$this->extendedInserts) || // Add header on simple INSERTs, or... + ($this->extendedInserts && empty($this->query)) //...on extended INSERTs if there are no other data, yet + ) + { + $newQuery = true; + $fieldList = $this->getFieldListSQL(array_keys($myRow), $numOfFields); + + if ($numOfFields > 0) + { + $this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " $fieldList VALUES "; + } + } + else + { + // On other cases, just mark that we should add a comma and start a new VALUES entry + $newQuery = false; + } + + $outData = '('; + + // Step through each of the row's values + $fieldID = 0; + + // Used in running backup fix + $isCurrentBackupEntry = false; + + // Fix 1.2a - NULL values were being skipped + if ($numOfFields > 0) + { + foreach ($myRow as $fieldName => $value) + { + // The ID of the field, used to determine placement of commas + $fieldID ++; + + if ($fieldID > $numOfFields) + { + // This is required for SQL Server backups, do NOT remove! + continue; + } + + // Fix 2.0: Mark currently running backup as successful in the DB snapshot + if ($tableAbstract == '#__ak_stats') + { + if ($fieldID == 1) + { + // Compare the ID to the currently running + $statistics = Factory::getStatistics(); + $isCurrentBackupEntry = ($value == $statistics->getId()); + } + elseif ($fieldID == 6) + { + // Treat the status field + $value = $isCurrentBackupEntry ? 'complete' : $value; + } + } + + // Post-process the value + if (is_null($value)) + { + $outData .= "NULL"; // Cope with null values + } + else + { + // Accommodate for runtime magic quotes + if (function_exists('get_magic_quotes_runtime')) + { + $value = @get_magic_quotes_runtime() ? stripslashes($value) : $value; + } + + $value = $db->quote($value); + + if ($this->postProcessValues) + { + $value = $this->postProcessQuotedValue($value); + } + + $outData .= $value; + } + + if ($fieldID < $numOfFields) + { + $outData .= ', '; + } + } + } + + $outData .= ')'; + + if ($numOfFields) + { + // If it's an existing query and we have extended inserts + if ($this->extendedInserts && !$newQuery) + { + // Check the existing query size + $query_length = strlen($this->query); + $data_length = strlen($outData); + + if (($query_length + $data_length) > $this->packetSize) + { + // We are about to exceed the packet size. Write the data so far. + $this->query .= ";\n"; + + if (!$this->writeDump($this->query)) + { + return; + } + + // Then, start a new query + $this->query = ''; + $this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " VALUES "; + $this->query .= $outData; + } + else + { + // We have room for more data. Append $outData to the query. + $this->query .= ', '; + $this->query .= $outData; + } + } + // If it's a brand new insert statement in an extended INSERTs set + elseif ($this->extendedInserts && $newQuery) + { + // Append the data to the INSERT statement + $this->query .= $outData; + // Let's see the size of the dumped data... + $query_length = strlen($this->query); + + if ($query_length >= $this->packetSize) + { + // This was a BIG query. Write the data to disk. + $this->query .= ";\n"; + + if (!$this->writeDump($this->query)) + { + return; + } + + // Then, start a new query + $this->query = ''; + } + } + // It's a normal (not extended) INSERT statement + else + { + // Append the data to the INSERT statement + $this->query .= $outData; + // Write the data to disk. + $this->query .= ";\n"; + + if (!$this->writeDump($this->query)) + { + return; + } + + // Then, start a new query + $this->query = ''; + } + } + $outData = ''; + + // Update the auto_increment value to avoid edge cases when the batch size is one + if (!is_null($this->table_autoincrement['field'])) + { + $this->table_autoincrement['value'] = $myRow[ $this->table_autoincrement['field'] ]; + } + + unset($myRow); + + // Check for imminent timeout + if ($timer->getTimeLeft() <= 0) + { + Factory::getLog() + ->log(LogLevel::DEBUG, "Breaking dump of $tableAbstract after $numRows rows; will continue on next step"); + + break; + } + } + + $db->freeResult($cursor); + + // Advance the _nextRange pointer + $this->nextRange += ($numRows != 0) ? $numRows : 1; + + $this->setStep($tableName); + $this->setSubstep($this->nextRange . ' / ' . $this->maxRange); + } + + // Finalize any pending query + // WARNING! If we do not do that now, the query will be emptied in the next operation and all + // accumulated data will go away... + if (!empty($this->query)) + { + $this->query .= ";\n"; + + if (!$this->writeDump($this->query)) + { + return; + } + + $this->query = ''; + } + + // Check for end of table dump (so that it happens inside the same operation) + if (!($this->nextRange < $this->maxRange)) + { + // Tell the user we are done with the table + Factory::getLog()->log(LogLevel::DEBUG, "Done dumping " . $tableAbstract); + + // Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server + if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0)) + { + Factory::getLog()->log(LogLevel::DEBUG, "Writing data dump epilogue for " . $tableAbstract); + $epilogue = $this->getDataDumpEpilogue($tableAbstract, $tableName, $this->maxRange); + + if (!empty($epilogue)) + { + if (!$this->writeDump($epilogue)) + { + return; + } + } + } + + if (count($this->tables) == 0) + { + // We have finished dumping the database! + Factory::getLog()->log(LogLevel::INFO, "End of database detected; flushing the dump buffers..."); + $null = null; + $this->writeDump($null); + Factory::getLog()->log(LogLevel::INFO, "Database has been successfully dumped to SQL file(s)"); + $this->setState('postrun'); + $this->setStep(''); + $this->setSubstep(''); + $this->nextTable = ''; + $this->nextRange = 0; + + // At the end of the database dump, if any query was longer than 1Mb, let's put a warning file in the installation folder + if ($this->largest_query >= 1024 * 1024) + { + $archive = Factory::getArchiverEngine(); + $archive->addVirtualFile('large_tables_detected', $this->installerSettings->installerroot, $this->largest_query); + } + } + elseif (count($this->tables) != 0) + { + // Switch tables + $this->nextTable = array_shift($this->tables); + $this->nextRange = 0; + $this->setStep($this->nextTable); + $this->setSubstep(''); + } + } + } + + /** + * Gets the row count for table $tableAbstract. Also updates the $this->maxRange variable. + * + * @param string $tableAbstract The abstract name of the table (works with canonical names too, though) + * + * @return void + */ + protected function getRowCount($tableAbstract) + { + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + $sql = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->nameQuote($tableAbstract)); + + $errno = 0; + $error = ''; + + try + { + $db->setQuery($sql); + $this->maxRange = $db->loadResult(); + + if (is_null($this->maxRange)) + { + $errno = $db->getErrorNum(); + $error = $db->getErrorMsg(false); + } + } + catch (\Exception $e) + { + $this->maxRange = null; + $errno = $e->getCode(); + $error = $e->getMessage(); + } + + if (is_null($this->maxRange)) + { + $this->setWarning("Cannot get number of rows of $tableAbstract. MySQL error $errno: $error"); + + return; + } + + Factory::getLog()->log(LogLevel::DEBUG, "Rows on " . $tableAbstract . " : " . $this->maxRange); + } + +// ============================================================================= +// Dependency processing - the Twilight Zone starts here +// ============================================================================= + + /** + * 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() + { + // Makes the MySQL connection compatible with our class + $this->enforceSQLCompatibility(); + + $configuration = Factory::getConfiguration(); + $notracking = $configuration->get('engine.dump.native.nodependencies', 0); + + // First, get a map of table names <--> abstract names + $this->get_tables_mapping(); + + if ($this->getError()) + { + return; + } + + if ($notracking) + { + // Do not process table & view dependencies + $this->get_tables_data_without_dependencies(); + if ($this->getError()) + { + return; + } + } + // Process table & view dependencies (default) + else + { + // Find the type and CREATE command of each table/view in the database + $this->get_tables_data(); + + if ($this->getError()) + { + return; + } + + // Process dependencies and rearrange tables respecting them + $this->process_dependencies(); + + if ($this->getError()) + { + return; + } + + // Remove dependencies array + $this->dependencies = array(); + } + } + + /** + * Generates a mapping between table names as they're stored in the database + * and their abstract representation. Updates $this->table_name_map + * + * @return void + */ + protected function get_tables_mapping() + { + // Get a database connection + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Finding tables to include in the backup set"); + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + // Reset internal tables + $this->table_name_map = array(); + + // Get the list of all database tables + $sql = "SHOW TABLES"; + $db->setQuery($sql); + $all_tables = $db->loadResultArray(); + + $registry = Factory::getConfiguration(); + $root = $registry->get('volatile.database.root', '[SITEDB]'); + + // If we have filters, make sure the tables pass the filtering + $filters = Factory::getFilters(); + + foreach ($all_tables as $table_name) + { + 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)"); + } + } + else + { + Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped."); + } + } + + // 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. + $enable_entities = $registry->get('engine.dump.native.advanced_entitites', true); + $notracking = $registry->get('engine.dump.native.nodependencies', 0); + + if (!$enable_entities) + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you told me not to)"); + } + elseif ($notracking != 0) + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you have disabled dependency tracking, therefore I can't handle advanced entities)"); + } + + if ($enable_entities && ($notracking == 0)) + { + // Cache the database name if this is the main site's database + + // 1. Stored procedures + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored PROCEDUREs"); + $sql = "SHOW PROCEDURE STATUS WHERE `Db`=" . $db->quote($this->database); + $db->setQuery($sql); + + try + { + $all_entries = $db->loadResultArray(1); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + // If we have filters, make sure the tables pass the filtering + if (is_array($all_entries)) + { + if (count($all_entries)) + { + foreach ($all_entries as $entity_name) + { + $entity_abstract = $this->getAbstract($entity_name); + + if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities + { + if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all')) + { + $this->table_name_map[$entity_name] = $entity_abstract; + } + } + } + } + } + + // 2. Stored functions + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored FUNCTIONs"); + $sql = "SHOW FUNCTION STATUS WHERE `Db`=" . $db->quote($this->database); + $db->setQuery($sql); + + try + { + $all_entries = $db->loadResultArray(1); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + // If we have filters, make sure the tables pass the filtering + if (is_array($all_entries)) + { + if (count($all_entries)) + { + foreach ($all_entries as $entity_name) + { + $entity_abstract = $this->getAbstract($entity_name); + + if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities + { + // Apply exclusion filters if set + if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all')) + { + $this->table_name_map[$entity_name] = $entity_abstract; + } + } + } + } + } + + // 3. Triggers + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored TRIGGERs"); + $sql = "SHOW TRIGGERS"; + $db->setQuery($sql); + + try + { + $all_entries = $db->loadResultArray(); + } + catch (\Exception $e) + { + $all_entries = array(); + } + + // If we have filters, make sure the tables pass the filtering + if (is_array($all_entries)) + { + if (count($all_entries)) + { + foreach ($all_entries as $entity_name) + { + $entity_abstract = $this->getAbstract($entity_name); + + if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities + { + // Apply exclusion filters if set + if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all')) + { + $this->table_name_map[$entity_name] = $entity_abstract; + } + } + } + } + } + } // if MySQL 5 + } + + /** + * Populates the _tables array with the metadata of each table and generates + * dependency information for views and merge tables. Updates $this->tables_data. + * + * @return void + */ + protected function get_tables_data() + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Starting CREATE TABLE and dependency scanning"); + + // Get a database connection + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got database connection"); + + // Reset internal tables + $this->tables_data = array(); + $this->dependencies = array(); + + // Get a list of tables where their engine type is shown + $sql = 'SHOW TABLES'; + $db->setQuery($sql); + $metadata_list = $db->loadRowList(); + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got SHOW TABLES"); + + // Get filters and filter root + $registry = Factory::getConfiguration(); + $root = $registry->get('volatile.database.root', '[SITEDB]'); + $filters = Factory::getFilters(); + + foreach ($metadata_list as $table_metadata) + { + // Skip over tables not included in the backup set + if (!array_key_exists($table_metadata[0], $this->table_name_map)) + { + continue; + } + + // Basic information + $table_name = $table_metadata[0]; + $table_abstract = $this->table_name_map[$table_metadata[0]]; + $new_entry = array( + 'type' => 'table', + 'dump_records' => true + ); + + // Get the CREATE command + $dependencies = array(); + $new_entry['create'] = $this->get_create($table_abstract, $table_name, $new_entry['type'], $dependencies); + $new_entry['dependencies'] = $dependencies; + + if ($new_entry['type'] == 'view') + { + $new_entry['dump_records'] = false; + } + else + { + $new_entry['dump_records'] = true; + } + + // Scan for the table engine. + $engine = null; // So that we detect VIEWs correctly + + if ($new_entry['type'] == 'table') + { + $engine = 'MyISAM'; // So that even with MySQL 4 hosts we don't screw this up + $engine_keys = array('ENGINE=', 'TYPE='); + + foreach ($engine_keys as $engine_key) + { + $start_pos = strrpos($new_entry['create'], $engine_key); + + if ($start_pos !== false) + { + // Advance the start position just after the position of the ENGINE keyword + $start_pos += strlen($engine_key); + // Try to locate the space after the engine type + $end_pos = stripos($new_entry['create'], ' ', $start_pos); + + if ($end_pos === false) + { + // Uh... maybe it ends with ENGINE=EngineType; + $end_pos = stripos($new_entry['create'], ';', $start_pos); + } + + if ($end_pos !== false) + { + // Grab the string + $engine = substr($new_entry['create'], $start_pos, $end_pos - $start_pos); + + if (empty($engine)) + { + Factory::getLog()->log(LogLevel::DEBUG, "*** DEBUG *** $table_name - engine $engine"); + Factory::getLog()->log(LogLevel::DEBUG, $new_entry['create']); + Factory::getLog()->log(LogLevel::DEBUG, "start $start_pos - end $end_pos"); + } + } + } + } + + $engine = strtoupper($engine); + } + + switch ($engine) + { + /* + // Views -- They are detected based on their CREATE statement + case null: + $new_entry['type'] = 'view'; + $new_entry['dump_records'] = false; + break; + */ + + // 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 and VIEWs + 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; + } + + $this->tables_data[$table_name] = $new_entry; + } + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got table list"); + + // If we have MySQL > 5.0 add stored procedures, stored functions and triggers + $enable_entities = $registry->get('engine.dump.native.advanced_entitites', true); + + if ($enable_entities) + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing MySQL entities"); + // Get a list of procedures + $sql = 'SHOW PROCEDURE STATUS WHERE `Db`=' . $db->quote($this->database); + $db->setQuery($sql); + + try + { + $metadata_list = $db->loadRowList(); + } + catch (\Exception $e) + { + $metadata_list = null; + } + + if (is_array($metadata_list)) + { + if (count($metadata_list)) + { + foreach ($metadata_list as $entity_metadata) + { + // Skip over entities not included in the backup set + if (!array_key_exists($entity_metadata[1], $this->table_name_map)) + { + continue; + } + + // Basic information + $entity_name = $entity_metadata[1]; + $entity_abstract = $this->table_name_map[$entity_metadata[1]]; + $new_entry = array( + 'type' => 'procedure', + 'dump_records' => false + ); + + // There's no point trying to add a non-procedure entity + if ($entity_metadata[2] != 'PROCEDURE') + { + continue; + } + + $dependencies = array(); + $new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies); + $new_entry['dependencies'] = $dependencies; + $this->tables_data[$entity_name] = $new_entry; + } + } + } // foreach + + // Get a list of functions + $sql = 'SHOW FUNCTION STATUS WHERE `Db`=' . $db->quote($this->database); + $db->setQuery($sql); + + try + { + $metadata_list = $db->loadRowList(); + } + catch (\Exception $e) + { + $metadata_list = null; + } + + if (is_array($metadata_list)) + { + if (count($metadata_list)) + { + foreach ($metadata_list as $entity_metadata) + { + // Skip over entities not included in the backup set + if (!array_key_exists($entity_metadata[1], $this->table_name_map)) + { + continue; + } + + // Basic information + $entity_name = $entity_metadata[1]; + $entity_abstract = $this->table_name_map[$entity_metadata[1]]; + $new_entry = array( + 'type' => 'function', + 'dump_records' => false + ); + + // There's no point trying to add a non-function entity + if ($entity_metadata[2] != 'FUNCTION') + { + continue; + } + + $dependencies = array(); + $new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies); + $new_entry['dependencies'] = $dependencies; + $this->tables_data[$entity_name] = $new_entry; + } + } + } // foreach + + // Get a list of triggers + $sql = 'SHOW TRIGGERS'; + $db->setQuery($sql); + + try + { + $metadata_list = $db->loadRowList(); + } + catch (\Exception $e) + { + $metadata_list = null; + } + + if (is_array($metadata_list)) + { + if (count($metadata_list)) + { + foreach ($metadata_list as $entity_metadata) + { + // Skip over entities not included in the backup set + if (!array_key_exists($entity_metadata[0], $this->table_name_map)) + { + continue; + } + + // Basic information + $entity_name = $entity_metadata[0]; + $entity_abstract = $this->table_name_map[$entity_metadata[0]]; + $new_entry = array( + 'type' => 'trigger', + 'dump_records' => false + ); + + $dependencies = array(); + $new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies); + $new_entry['dependencies'] = $dependencies; + $this->tables_data[$entity_name] = $new_entry; + } + } + } // foreach + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got MySQL entities list"); + } + + /** + // Only store unique values + if(count($dependencies) > 0) + $dependencies = array_unique($dependencies); + /**/ + } + + /** + * Populates the _tables array with the metadata of each table. + * Updates $this->tables_data and $this->tables. + * + * @return void + */ + protected function get_tables_data_without_dependencies() + { + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Pushing table data (without dependency tracking)"); + + // Reset internal tables + $this->tables_data = array(); + $this->dependencies = array(); + + // Get filters and filter root + $registry = Factory::getConfiguration(); + $root = $registry->get('volatile.database.root', '[SITEDB]'); + $filters = Factory::getFilters(); + + foreach ($this->table_name_map as $table_name => $table_abstract) + { + $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; + } + + $this->tables_data[$table_name] = $new_entry; + $this->tables[] = $table_name; + } // foreach + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got table list"); + } + + /** + * Gets the CREATE TABLE command for a given table/view/procedure/function/trigger + * + * @param string $table_abstract The abstracted name of the entity + * @param string $table_name The name of the table + * @param string $type The type of the entity to scan. If it's found to differ, the correct type is returned. + * @param array $dependencies The dependencies of this table + * + * @return string The CREATE command, w/out newlines + */ + protected function get_create($table_abstract, $table_name, &$type, &$dependencies) + { + $configuration = Factory::getConfiguration(); + $notracking = $configuration->get('engine.dump.native.nodependencies', 0); + + $db = $this->getDB(); + + if ($this->getError()) + { + return; + } + + switch ($type) + { + case 'table': + case 'merge': + case 'view': + default: + $sql = "SHOW CREATE TABLE `$table_abstract`"; + break; + + case 'procedure': + $sql = "SHOW CREATE PROCEDURE `$table_abstract`"; + break; + + case 'function': + $sql = "SHOW CREATE FUNCTION `$table_abstract`"; + break; + + case 'trigger': + $sql = "SHOW CREATE TRIGGER `$table_abstract`"; + break; + } + + $db->setQuery($sql); + + try + { + $temp = $db->loadRowList(); + } + catch (\Exception $e) + { + // If the query failed we don't have the necessary SHOW privilege. Log the error and fake an empty reply. + $entityType = ($type == 'merge') ? 'table' : $type; + $msg = $e->getMessage(); + $this->setWarning("Cannot get the structure of $entityType $table_abstract. Database returned error $msg running $sql Please check your database privileges. Your database backup may be incomplete."); + + $db->resetErrors(); + + $temp = array( + array('', '', '') + ); + } + + if (in_array($type, array('procedure', 'function', 'trigger'))) + { + $table_sql = $temp[0][2]; + + // MySQL adds the database name into everything. We have to remove it. + $dbName = $db->qn($this->database) . '.`'; + $table_sql = str_replace($dbName, '`', $table_sql); + + // These can contain comment lines, starting with a double dash. Remove them. + $table_sql = $this->removeMySQLComments($table_sql); + $table_sql = trim($table_sql); + $lines = explode("\n", $table_sql); + $lines = array_map('trim', $lines); + + $table_sql = implode(' ', $lines); + $table_sql = trim($table_sql); + + /** + * Remove the definer from the CREATE PROCEDURE/TRIGGER/FUNCTION. For example, MySQL returns this: + * CREATE DEFINER=`myuser`@`localhost` PROCEDURE `abc_myProcedure`() ... + * If you're restoring on a different machine the definer will probably be invalid, therefore we need to + * remove it from the (portable) output. + */ + $pattern = '/^CREATE(.*) ' . strtoupper($type) . ' (.*)/i'; + $result = preg_match($pattern, $table_sql, $matches); + $table_sql = 'CREATE ' . strtoupper($type) . ' ' . $matches[2]; + + if (substr($table_sql, -1) != ';') + { + $table_sql .= ';'; + } + } + else + { + $table_sql = $temp[0][1]; + } + unset($temp); + + // Smart table type detection + if (in_array($type, array('table', 'merge', 'view'))) + { + // Check for CREATE VIEW + $pattern = '/^CREATE(.*) VIEW (.*)/i'; + $result = preg_match($pattern, $table_sql, $matches); + + if ($result === 1) + { + // This is a view. + $type = 'view'; + + /** + * Newer MySQL versions add the definer and other information in the CREATE VIEW output, e.g. + * CREATE ALGORITHM=UNDEFINED DEFINER=`muyser`@`localhost` SQL SECURITY DEFINER VIEW `abc_myview` AS ... + * We need to remove that to prevent restoration troubles. + */ + $table_sql = 'CREATE VIEW ' . $matches[2]; + } + else + { + // This is a table. + $type = 'table'; + + // # Fix 3.2.1: USING BTREE / USING HASH in indices causes issues migrating from MySQL 5.1+ hosts to + // MySQL 5.0 hosts + if ($configuration->get('engine.dump.native.nobtree', 1)) + { + $table_sql = str_replace(' USING BTREE', ' ', $table_sql); + $table_sql = str_replace(' USING HASH', ' ', $table_sql); + } + + // Translate TYPE= to ENGINE= + $table_sql = str_replace('TYPE=', 'ENGINE=', $table_sql); + } + + // Is it a VIEW but we don't have SHOW VIEW privileges? + if (empty($table_sql)) + { + $type = 'view'; + } + } + + /** + * Replace table name and names of referenced tables with their abstracted forms and populate dependency tables + * at the same time. + */ + + // On DB only backup we don't want any replacing to take place, do we? + if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1)) + { + $old_table_sql = $table_sql; + } + + // Replace the table name with the abstract version. + // We have to quote the table name. If we don't we'll get wrong results. Imagine that you have a column whose name starts + // with the string literal of the table name itself. + // Example: table `poll`, column `poll_id` would become #__poll, #__poll_id + // By quoting before we make sure this won't happen. + $table_sql = str_replace($db->quoteName($table_name), $db->quoteName($table_abstract), $table_sql); + + // Return dependency information only if dependency tracking is enabled + if (!$notracking) + { + // Even on simple tables, we may have foreign key references. + // As a result, we need to replace those referenced table names + // as well. On views and merge arrays, we have referenced tables + // by definition. + $dependencies = array(); + + // Now, loop for all table entries + 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); + } + } + + // Finally, replace the prefix if it's not empty (used in constraints) + if (!empty($this->prefix)) + { + $table_sql = str_replace('`' . $this->prefix, '`#__', $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); + + /** + * Views, procedures, functions and triggers may contain the database name followed by the table name, always + * quoted e.g. `db`.`table_name` We need to replace all these instances with just the table name. The only + * reliable way to do that is to look for "`db`.`" and replace it with "`" + */ + if (in_array($type, array('view', 'procedure', 'function', 'trigger'))) + { + $dbName = $db->qn($this->getDatabaseName()); + $dummyQuote = $db->qn('foo'); + $findWhat = $dbName . '.' . substr($dummyQuote, 0, 1); + $replaceWith = substr($dummyQuote, 0, 1); + $table_sql = str_replace($findWhat, $replaceWith, $table_sql); + } + + // Post-process CREATE VIEW + if ($type == 'view') + { + $pos_view = strpos($table_sql, ' VIEW '); + + if ($pos_view > 7) + { + // Only post process if there are view properties between the CREATE and VIEW keywords + $propstring = substr($table_sql, 7, $pos_view - 7); // Properties string + // Fetch the ALGORITHM={UNDEFINED | MERGE | TEMPTABLE} keyword + $algostring = ''; + $algo_start = strpos($propstring, 'ALGORITHM='); + + if ($algo_start !== false) + { + $algo_end = strpos($propstring, ' ', $algo_start); + $algostring = substr($propstring, $algo_start, $algo_end - $algo_start + 1); + } + + // Create our modified create statement + $table_sql = 'CREATE OR REPLACE ' . $algostring . substr($table_sql, $pos_view); + } + } + elseif ($type == 'procedure') + { + $pos_entity = stripos($table_sql, ' PROCEDURE '); + + if ($pos_entity !== false) + { + $table_sql = 'CREATE' . substr($table_sql, $pos_entity); + } + } + elseif ($type == 'function') + { + $pos_entity = stripos($table_sql, ' FUNCTION '); + + if ($pos_entity !== false) + { + $table_sql = 'CREATE' . substr($table_sql, $pos_entity); + } + } + elseif ($type == 'trigger') + { + $pos_entity = stripos($table_sql, ' TRIGGER '); + + if ($pos_entity !== false) + { + $table_sql = 'CREATE' . substr($table_sql, $pos_entity); + } + } + + // Add DROP statements for DB only backup + if (Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0)) + { + if (($type == 'table') || ($type == 'merge')) + { + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $table_name = str_replace(array("\r", "\n"), array('', ''), $table_name); + + // Table or merge tables, get a DROP TABLE statement + $drop = "DROP TABLE IF EXISTS " . $db->quoteName($table_name) . ";\n"; + } + elseif ($type == 'view') + { + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $table_name = str_replace(array("\r", "\n"), array('', ''), $table_name); + + // Views get a DROP VIEW statement + $drop = "DROP VIEW IF EXISTS " . $db->quoteName($table_name) . ";\n"; + } + elseif ($type == 'procedure') + { + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $table_name = str_replace(array("\r", "\n"), array('', ''), $table_name); + + // Procedures get a DROP PROCEDURE statement and proper delimiter strings + $drop = "DROP PROCEDURE IF EXISTS " . $db->quoteName($table_name) . ";\n"; + $drop .= "DELIMITER // "; + $table_sql = str_replace("\r", " ", $table_sql); + $table_sql = str_replace("\t", " ", $table_sql); + $table_sql = rtrim($table_sql, ";\n") . " // DELIMITER ;\n"; + } + elseif ($type == 'function') + { + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $table_name = str_replace(array("\r", "\n"), array('', ''), $table_name); + + // Procedures get a DROP FUNCTION statement and proper delimiter strings + $drop = "DROP FUNCTION IF EXISTS " . $db->quoteName($table_name) . ";\n"; + $drop .= "DELIMITER // "; + $table_sql = str_replace("\r", " ", $table_sql); + $table_sql = rtrim($table_sql, ";\n") . "// DELIMITER ;\n"; + } + elseif ($type == 'trigger') + { + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $table_name = str_replace(array("\r", "\n"), array('', ''), $table_name); + + // Procedures get a DROP TRIGGER statement and proper delimiter strings + $drop = "DROP TRIGGER IF EXISTS " . $db->quoteName($table_name) . ";\n"; + $drop .= "DELIMITER // "; + $table_sql = str_replace("\r", " ", $table_sql); + $table_sql = str_replace("\t", " ", $table_sql); + $table_sql = rtrim($table_sql, ";\n") . "// DELIMITER ;\n"; + } + + $table_sql = $drop . $table_sql; + } + + return $table_sql; + } + + /** + * Process all table dependencies + * + * @return void + */ + protected function process_dependencies() + { + if (count($this->table_name_map) > 0) + { + foreach ($this->table_name_map as $table_name => $table_abstract) + { + $this->push_table($table_name); + } + } + + Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processed dependencies"); + } + + /** + * Pushes a table in the _tables stack, making sure it will appear after + * its dependencies and other tables/views depending on it will eventually + * appear after it. It's a complicated chicken-and-egg problem. Just make + * sure you don't have any bloody circular references!! + * + * @param string $table_name Canonical name of the table to push + * @param array $stack When called recursive, other views/tables previously processed in order to detect *ahem* dependency loops... + * + * @return void + */ + protected function push_table($table_name, $stack = array(), $currentRecursionDepth = 0) + { + // Load information + $table_data = $this->tables_data[$table_name]; + + if (array_key_exists('dependencies', $table_data)) + { + $referenced = $table_data['dependencies']; + } + else + { + $referenced = array(); + } + + unset($table_data); + + // Try to find the minimum insert position, so as to appear after the last referenced table + $insertpos = false; + + if (count($referenced)) + { + foreach ($referenced as $referenced_table) + { + if (count($this->tables)) + { + $newpos = array_search($referenced_table, $this->tables); + + if ($newpos !== false) + { + if ($insertpos === false) + { + $insertpos = $newpos; + } + else + { + $insertpos = max($insertpos, $newpos); + } + } + } + } + } + + // Add to the _tables array + if (count($this->tables) && ($insertpos !== false)) + { + array_splice($this->tables, $insertpos + 1, 0, $table_name); + } + else + { + $this->tables[] = $table_name; + } + + // Here's what... Some other table/view might depend on us, so we must appear + // before it (actually, it must appear after us). So, we scan for such + // tables/views and relocate them + if (count($this->dependencies)) + { + if (array_key_exists($table_name, $this->dependencies)) + { + foreach ($this->dependencies[$table_name] as $depended_table) + { + // First, make sure that either there is no stack, or the + // depended table doesn't belong it. In any other case, we + // were fooled to follow an endless dependency loop and we + // will simply bail out and let the user sort things out. + if (count($stack) > 0) + { + if (in_array($depended_table, $stack)) + { + continue; + } + } + + $my_position = array_search($table_name, $this->tables); + $remove_position = array_search($depended_table, $this->tables); + + if (($remove_position !== false) && ($remove_position < $my_position)) + { + $stack[] = $table_name; + array_splice($this->tables, $remove_position, 1); + + // Where should I put the other table/view now? Don't tell me. + // I have to recurse... + if ($currentRecursionDepth < 19) + { + $this->push_table($depended_table, $stack, ++$currentRecursionDepth); + } + else + { + // We're hitting a circular dependency. We'll add the removed $depended_table + // in the penultimate position of the table and cross our virtual fingers... + array_splice($this->tables, count($this->tables) - 1, 0, $depended_table); + } + } + } + } + } + } + + /** + * Creates a drop query from a CREATE query + * + * @param string $query 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, 0, $pos); + } + + unset($restOfQuery); + + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $tableName = str_replace(array("\r", "\n"), array('', ''), $tableName); + + // Try to drop the table anyway + $dropQuery = 'DROP TABLE IF EXISTS ' . $db->nameQuote($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, 0, $pos); + } + + unset($restOfQuery); + + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $tableName = str_replace(array("\r", "\n"), array('', ''), $tableName); + + $dropQuery = 'DROP VIEW IF EXISTS ' . $db->nameQuote($tableName) . ';'; + } + // CREATE PROCEDURE pre-processing + elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'PROCEDURE ') !== false)) + { + // Try to get the procedure name + $entity_keyword = ' PROCEDURE '; + $entity_pos = strpos($query, $entity_keyword); + $restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string + + // Is there a backtick? + if (substr($restOfQuery, 0, 1) == '`') + { + // There is... Good, we'll just find the matching backtick + $pos = strpos($restOfQuery, '`', 1); + $entity_name = substr($restOfQuery, 1, $pos - 1); + } + else + { + // Nope, let's assume the entity name ends in the next blank character + $pos = strpos($restOfQuery, ' ', 1); + $entity_name = substr($restOfQuery, 0, $pos); + } + + unset($restOfQuery); + + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name); + + $dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;'; + } + // CREATE FUNCTION pre-processing + elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'FUNCTION ') !== false)) + { + // Try to get the procedure name + $entity_keyword = ' FUNCTION '; + $entity_pos = strpos($query, $entity_keyword); + $restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string + + // Is there a backtick? + if (substr($restOfQuery, 0, 1) == '`') + { + // There is... Good, we'll just find the matching backtick + $pos = strpos($restOfQuery, '`', 1); + $entity_name = substr($restOfQuery, 1, $pos - 1); + } + else + { + // Nope, let's assume the entity name ends in the next blank character + $pos = strpos($restOfQuery, ' ', 1); + $entity_name = substr($restOfQuery, 0, $pos); + } + + unset($restOfQuery); + + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name); + + // Try to drop the entity anyway + $dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;'; + } + // CREATE TRIGGER pre-processing + elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'TRIGGER ') !== false)) + { + // Try to get the procedure name + $entity_keyword = ' TRIGGER '; + $entity_pos = strpos($query, $entity_keyword); + $restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string + + // Is there a backtick? + if (substr($restOfQuery, 0, 1) == '`') + { + // There is... Good, we'll just find the matching backtick + $pos = strpos($restOfQuery, '`', 1); + $entity_name = substr($restOfQuery, 1, $pos - 1); + } + else + { + // Nope, let's assume the entity name ends in the next blank character + $pos = strpos($restOfQuery, ' ', 1); + $entity_name = substr($restOfQuery, 0, $pos); + } + + unset($restOfQuery); + + // Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones + $entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name); + + // Try to drop the entity anyway + $dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;'; + } + + return $dropQuery; + } + + /** + * Try to find an auto_increment field for the table being currently backed up and populate the + * $this->table_autoincrement table. Updates $this->table_autoincrement. + * + * @return void + */ + protected function setAutoIncrementInfo() + { + $this->table_autoincrement = array( + 'table' => $this->nextTable, + 'field' => null, + 'value' => null, + ); + + $db = $this->getDB(); + + $query = 'SHOW COLUMNS FROM ' . $db->qn($this->nextTable) . ' WHERE ' . $db->qn('Extra') . ' = ' . + $db->q('auto_increment') . ' AND ' . $db->qn('Null') . ' = ' . $db->q('NO'); + $keyInfo = $db->setQuery($query)->loadAssocList(); + + if (!empty($keyInfo)) + { + $row = array_shift($keyInfo); + $this->table_autoincrement['field'] = $row['Field']; + } + } + + /** + * Removes MySQL comments from the SQL command + * + * @param string $sql Potentially commented SQL + * + * @return string SQL without comments + * + * @see http://stackoverflow.com/questions/9690448/regular-expression-to-remove-comments-from-sql-statement + */ + protected function removeMySQLComments($sql) + { + $sqlComments = '@(([\'"]).*?[^\\\]\2)|((?:\#|--).*?$|/\*(?:[^/*]|/(?!\*)|\*(?!/)|(?R))*\*\/)\s*|(?<=;)\s+@ms'; + + return preg_replace($sqlComments, '$1', $sql); + } + + /** + * Get the default database dump batch size from the configuration + * + * @return int + */ + protected function getDefaultBatchSize() + { + static $batchSize = null; + + if (is_null($batchSize)) + { + $configuration = Factory::getConfiguration(); + $batchSize = intval($configuration->get('engine.dump.common.batchsize', 1000)); + + if ($batchSize <= 0) + { + $batchSize = 1000; + } + } + + return $batchSize; + } + + /** + * Get the optimal row batch size for a given table based on the available memory + * + * @param string $tableAbstract The abstract table name, e.g. #__foobar + * @param int $defaultBatchSize The default row batch size in the application configuration + * + * @return int + */ + protected function getOptimalBatchSize($tableAbstract, $defaultBatchSize) + { + $db = $this->getDB(); + + try + { + $info = $db->setQuery('SHOW TABLE STATUS LIKE ' . $db->q($tableAbstract))->loadAssoc(); + } + catch (\Exception $e) + { + return $defaultBatchSize; + } + + if (!isset($info['Avg_row_length']) || empty($info['Avg_row_length'])) + { + return $defaultBatchSize; + } + + // That's the average row size as reported by MySQL. + $avgRow = str_replace(array(',', '.'), array('', ''), $info['Avg_row_length']); + // The memory available for manipulating data is less than the free memory + $memoryLimit = $this->getMemoryLimit(); + $memoryLimit = empty($memoryLimit) ? 33554432 : $memoryLimit; + $usedMemory = memory_get_usage(); + $memoryLeft = 0.75 * ($memoryLimit - $usedMemory); + // The 3.25 factor is empirical and leans on the safe side. + $maxRows = (int) ($memoryLeft / (3.25 * $avgRow)); + + return max(1, min($maxRows, $defaultBatchSize)); + } + + /** + * 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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/None.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/None.php new file mode 100644 index 00000000..cdc2cf13 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/None.php @@ -0,0 +1,35 @@ +log(LogLevel::INFO, "There is no native engine for backing up databases with the None driver. Using the Reverse Engineering class instead."); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Postgresql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Postgresql.php new file mode 100644 index 00000000..27eff723 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Postgresql.php @@ -0,0 +1,35 @@ +log(LogLevel::INFO, "There is no native engine for backing up PostgreSQL databases. Using the Reverse Engineering class instead."); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlite.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlite.php new file mode 100644 index 00000000..d2ea34e7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlite.php @@ -0,0 +1,35 @@ +log(LogLevel::INFO, "There is no native engine for backing up SQLite databases. Using the Reverse Engineering class instead."); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlsrv.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlsrv.php new file mode 100644 index 00000000..0f882602 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Native/Sqlsrv.php @@ -0,0 +1,35 @@ +log(LogLevel::INFO, "There is no native engine for backing up Microsoft SQL Server databases. Using the Reverse Engineering class instead."); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse.php new file mode 100644 index 00000000..d47423d5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse.php @@ -0,0 +1,107 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Mysql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Mysql.php new file mode 100644 index 00000000..465abc89 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Mysql.php @@ -0,0 +1,597 @@ + MySQL database server host name or IP address + * port MySQL database server port (optional) + * username MySQL user name, for authentication + * password MySQL password, for authentication + * database MySQL database + * dumpFile 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; + } + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/None.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/None.php new file mode 100644 index 00000000..7cd8e6b6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/None.php @@ -0,0 +1,72 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Postgresql.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Postgresql.php new file mode 100644 index 00000000..a53089c9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Postgresql.php @@ -0,0 +1,747 @@ + PostgreSQL database server host name or IP address + * port PostgreSQL database server port (optional) + * username PostgreSQL user name, for authentication + * password PostgreSQL password, for authentication + * database PostgreSQL database + * dumpFile 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 + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlite.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlite.php new file mode 100644 index 00000000..4ccb43de --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlite.php @@ -0,0 +1,76 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlsrv.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlsrv.php new file mode 100644 index 00000000..30ef64c3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/Reverse/Sqlsrv.php @@ -0,0 +1,922 @@ + SQL database server host name or IP address + * port SQL database server port (optional) + * username SQL user name, for authentication + * password SQL password, for authentication + * database SQL database + * dumpFile 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 + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/native.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/native.ini new file mode 100644 index 00000000..48b9d680 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/native.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/reverse.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/reverse.ini new file mode 100644 index 00000000..dfee560b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Dump/reverse.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Factory.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Factory.php new file mode 100644 index 00000000..8b62b050 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Factory.php @@ -0,0 +1,1017 @@ +objectList[$class_name])) + { + $self->objectList[$class_name] = false; + + if (class_exists($class_name, true)) + { + $self->objectList[$class_name] = new $class_name; + } + } + + return $self->objectList[$class_name]; + } + + /** + * Internal function which removes the object of the class named $class_name + * + * @param string $class_name + * + * @return void + */ + protected static function unsetObjectInstance($class_name) + { + $self = self::getInstance(); + + if (isset($self->objectList[$class_name])) + { + $self->objectList[$class_name] = null; + unset($self->objectList[$class_name]); + } + } + + /** + * Internal function which instantiates an object of a class named $class_name. This is a temporary instance which + * will not survive serialisation and subsequent unserialisation. + * + * @param string $class_name + * + * @return object + */ + protected static function &getTempObjectInstance($class_name) + { + $self = self::getInstance(); + + if (!isset($self->temporaryObjectList[$class_name])) + { + $self->temporaryObjectList[$class_name] = false; + + if (class_exists($class_name, true)) + { + $self->temporaryObjectList[$class_name] = new $class_name; + } + } + + return $self->temporaryObjectList[$class_name]; + } + + /** + * Internal function which removes the object of the class named $class_name. This is a temporary instance which + * will not survive serialisation and subsequent unserialisation. + * + * @param string $class_name The class name of the object to remove + * + * @return void + */ + protected static function unsetTempObjectInstance($class_name) + { + $self = self::getInstance(); + + if (isset($self->temporaryObjectList[$class_name])) + { + $self->temporaryObjectList[$class_name] = null; + unset($self->temporaryObjectList[$class_name]); + } + } + + // ======================================================================== + // Public factory interface + // ======================================================================== + + /** + * Gets a serialized snapshot of the Factory for safekeeping (hibernate) + * + * @return string The serialized snapshot of the Factory + */ + public static function serialize() + { + // Call _onSerialize in all classes known to the factory + $self = self::getInstance(); + + if (!empty($self->objectList)) + { + foreach ($self->objectList as $class_name => $object) + { + $o = $self->objectList[$class_name]; + + if (method_exists($o, '_onSerialize')) + { + call_user_func(array($o, '_onSerialize')); + } + } + } + + // Serialize the factory + return serialize(self::getInstance()); + } + + /** + * Regenerates the full Factory state from a serialized snapshot (resume) + * + * @param string $serialized_data The serialized snapshot to resume from + * + * @return void + */ + public static function unserialize($serialized_data) + { + self::getInstance($serialized_data); + } + + /** + * Reset the internal factory state, freeing all previously created objects + * + * @return void + */ + public static function nuke() + { + $self = self::getInstance(); + + foreach ($self->objectList as $key => $object) + { + $self->objectList[$key] = null; + } + + $self->objectList = array(); + } + + /** + * Saves the engine state to temporary storage + * + * @param string $tag The backup origin to save. Leave empty to get from already loaded Kettenrad instance. + * @param string $backupId The backup ID to save. Leave empty to get from already loaded Kettenrad instance. + * + * @return void + * + * @throws \RuntimeException When the state save fails for any reason + */ + public static function saveState($tag = null, $backupId = null) + { + $kettenrad = self::getKettenrad(); + + if (empty($tag)) + { + $tag = $kettenrad->getTag(); + } + + if (empty($backupId)) + { + $backupId = $kettenrad->getBackupId(); + } + + $saveTag = $tag . (empty($backupId) ? '' : ('.' . $backupId)); + + $ret = $kettenrad->getStatusArray(); + + if ($ret['HasRun'] == 1) + { + Factory::getLog()->log(LogLevel::DEBUG, "Will not save a finished Kettenrad instance"); + + return; + } + + Factory::getLog()->log(LogLevel::DEBUG, "Saving Kettenrad instance $tag"); + + // Save a Factory snapshot + $factoryStorage = self::getFactoryStorage(); + $engine = self::getConfiguration()->get('akeeba.core.usedbstorage', 0) ? 'db' : 'file'; + $factoryStorage->setStorageEngine($engine); + + $logger = self::getLog(); + + $serializedFactoryData = self::serialize(); + $result = $factoryStorage->set($serializedFactoryData, $saveTag); + + if (!$result) + { + $saveKey = $factoryStorage->get_storage_filename($saveTag); + $errorMessage = "Cannot save factory state in $engine storage, storage key $saveKey"; + $logger->error($errorMessage); + + throw new \RuntimeException($errorMessage); + } + } + + /** + * Loads the engine state from the storage (if it exists). + * + * When failIfMissing is true (default) an exception will be thrown if the memory file / database record is no + * longer there. This is a clear indication of an issue with the storage engine, e.g. the host deleting the memory + * files in the middle of the backup step. Therefore we'll switch the storage engine type before throwing the + * exception. + * + * When failIfMissing is false we do NOT throw an exception. Instead, we do a hard reset of the backup factory. This + * is required by the resetState method when we ask it to reset multiple origins at once. + * + * @param string $tag The backup origin to load + * @param string $backupId The backup ID to load + * @param bool $failIfMissing Throw an exception if the memory data is no longer there + * + * @return void + */ + public static function loadState($tag = null, $backupId = null, $failIfMissing = true) + { + if (is_null($tag) && defined('AKEEBA_BACKUP_ORIGIN')) + { + $tag = AKEEBA_BACKUP_ORIGIN; + } + + if (is_null($backupId) && defined('AKEEBA_BACKUP_ID')) + { + $tag = AKEEBA_BACKUP_ID; + } + + $loadTag = $tag . (empty($backupId) ? '' : ('.' . $backupId)); + + // In order to load anything, we need to have the correct profile loaded. Let's assume + // that the latest backup record in this tag has the correct profile number set. + $config = self::getConfiguration(); + + if (empty($config->activeProfile)) + { + $profile = Platform::getInstance()->get_active_profile(); + + if (empty($profile) || ($profile <= 1)) + { + // Only bother loading a configuration if none has been already loaded + $statList = Platform::getInstance()->get_statistics_list(array( + 'filters' => array( + array('field' => 'tag', 'value' => $tag) + ), 'order' => array( + 'by' => 'id', 'order' => 'DESC' + ) + ) + ); + + if (is_array($statList)) + { + $stat = array_pop($statList); + $profile = $stat['profile_id']; + } + } + + Platform::getInstance()->load_configuration($profile); + } + + $profile = $config->activeProfile; + + Factory::getLog()->open($loadTag); + Factory::getLog()->log(LogLevel::DEBUG, "Kettenrad :: Attempting to load from database ($tag) [$loadTag]"); + + $serialized_factory = self::getFactoryStorage()->get($loadTag); + + if ($serialized_factory === false) + { + if ($failIfMissing) + { + // Find the new storage engine we need to use + $previousEngine = self::getFactoryStorage()->getStorageEngine(); + $newEngine = ($previousEngine == 'file') ? 'db' : 'file'; + + Factory::getLog()->log(LogLevel::DEBUG, "Failed loading temporary data using $previousEngine storage engine. Switching to $newEngine. The backup has failed and MUST be restarted."); + + // Switch the engine + Factory::getConfiguration()->reset(); + Platform::getInstance()->load_configuration($profile); + $config = Factory::getConfiguration(); + $config->set('akeeba.core.usedbstorage', ($newEngine == 'db')); + Platform::getInstance()->save_configuration(); + + throw new \RuntimeException("Akeeba Engine detected a problem while saving temporary data. Please restart your backup.", 500); + } + + // There is no serialized factory. Nuke the in-memory factory. + Factory::getLog()->log(LogLevel::DEBUG, " -- Stored Akeeba Factory ($tag) [$loadTag] not found - hard reset"); + self::nuke(); + Platform::getInstance()->load_configuration($profile); + } + + Factory::getLog()->log(LogLevel::DEBUG, " -- Loaded stored Akeeba Factory ($tag) [$loadTag]"); + self::unserialize($serialized_factory); + + unset($serialized_factory); + } + + /** + * Resets the engine state, wiping out any pending backups and/or stale temporary data. The configuration parameters + * are: + * global bool True to reset all origins, false to only reset the current origin (default: true) + * log bool True to log our actions (default: false) + * maxrun int Only backup records older than this number of seconds will be reset (default: 180) + * + * @param array $config Configuration parameters for the reset operation + * + * @return void + */ + public static function resetState($config = array()) + { + $default_config = array( + 'global' => true, // Reset all origins when true + 'log' => false, // Log our actions + 'maxrun' => 180, // Consider "pending" backups as failed after this many seconds + ); + + $config = (object)array_merge($default_config, $config); + + // Pause logging if so desired + if (!$config->log) + { + Factory::getLog()->pause(); + } + + $originTag = null; + + if (!$config->global) + { + // If we're not resetting globally, get a list of running backups per tag + $originTag = Platform::getInstance()->get_backup_origin(); + } + + // Cache the factory before proceeding + $factory = self::serialize(); + + $runningList = Platform::getInstance()->get_running_backups($originTag); + + // Origins we have to clean + $origins = array( + Platform::getInstance()->get_backup_origin() + ); + + // 1. Detect failed backups + if (is_array($runningList) && !empty($runningList)) + { + // The current timestamp + $now = time(); + + // Mark running backups as failed + foreach ($runningList as $running) + { + if (empty($originTag)) + { + // Check the timestamp of the log file to decide if it's stuck, + // but only if a tag is not set + $tstamp = Factory::getLog()->getLastTimestamp($running['origin']); + + if (!is_null($tstamp)) + { + // We can only check the timestamp if it's returned. If not, we assume the backup is stale + $difference = abs($now - $tstamp); + + // Backups less than maxrun seconds old are not considered stale (default: 3 minutes) + if ($difference < $config->maxrun) + { + continue; + } + } + } + + $filenames = Factory::getStatistics()->get_all_filenames($running, false); + $totalSize = 0; + + // Process if there are files to delete... + if (!is_null($filenames)) + { + // Delete the failed backup's archive, if exists + foreach ($filenames as $failedArchive) + { + if (file_exists($failedArchive)) + { + $totalSize += (int)@filesize($failedArchive); + Platform::getInstance()->unlink($failedArchive); + } + } + } + + // Mark the backup failed + if (!$running['total_size']) + { + $running['total_size'] = $totalSize; + } + + $running['status'] = 'fail'; + $running['multipart'] = 0; + $dummy = null; + Platform::getInstance()->set_or_update_statistics($running['id'], $running, $dummy); + + $backupId = isset($running['backupid']) ? ('.' . $running['backupid']) : ''; + + $origins[] = $running['origin'] . $backupId; + } + } + + if (!empty($origins)) + { + $origins = array_unique($origins); + + foreach ($origins as $originTag) + { + self::loadState($originTag, null, false); + // Remove temporary files + Factory::getTempFiles()->deleteTempFiles(); + // Delete any stale temporary data + self::getFactoryStorage()->reset($originTag); + } + } + + // Reload the factory + self::unserialize($factory); + unset($factory); + + // Unpause logging if it was previously paused + if (!$config->log) + { + Factory::getLog()->unpause(); + } + } + + // ======================================================================== + // Core objects which are part of the engine state + // ======================================================================== + + /** + * Returns an Akeeba Configuration object + * + * @return \Akeeba\Engine\Configuration The Akeeba Configuration object + */ + public static function &getConfiguration() + { + return self::getObjectInstance('\\Akeeba\\Engine\\Configuration'); + } + + /** + * Returns a statistics object, used to track current backup's progress + * + * @return \Akeeba\Engine\Util\Statistics + */ + public static function &getStatistics() + { + return self::getObjectInstance('\\Akeeba\\Engine\\Util\\Statistics'); + } + + /** + * Returns the currently configured archiver engine + * + * @param bool $reset Should I try to forcible create a new instance? + * + * @return \Akeeba\Engine\Archiver\Base + */ + public static function &getArchiverEngine($reset = false) + { + static $class_name; + + if ($reset) + { + $class_name = null; + } + + if (empty($class_name)) + { + $registry = self::getConfiguration(); + $engine = $registry->get('akeeba.advanced.archiver_engine'); + $class_name = '\\Akeeba\\Engine\\Archiver\\' . ucfirst($engine); + + if (!class_exists($class_name, true)) + { + $class_name = '\\Akeeba\\Engine\\Archiver\\Jpa'; + } + } + + if ($reset) + { + self::unsetObjectInstance($class_name); + } + + return self::getObjectInstance($class_name); + } + + /** + * Returns the currently configured dump engine + * + * @param boolean $reset Should I try to forcible create a new instance? + * + * @return \Akeeba\Engine\Dump\Base + */ + public static function &getDumpEngine($reset = false) + { + static $class_name; + + if (empty($class_name)) + { + $registry = self::getConfiguration(); + $engine = $registry->get('akeeba.advanced.dump_engine'); + $class_name = '\\Akeeba\\Engine\\Dump\\' . ucfirst($engine); + + if (!class_exists($class_name, true)) + { + $class_name = '\\Akeeba\\Engine\\Dump\\Native'; + } + } + + if ($reset) + { + self::unsetObjectInstance($class_name); + } + + return self::getObjectInstance($class_name); + } + + /** + * Returns the filesystem scanner engine instance + * + * @param bool $reset Should I try to forcible create a new instance? + * + * @return \Akeeba\Engine\Scan\Base The scanner engine + */ + public static function &getScanEngine($reset = false) + { + static $class_name; + + if ($reset) + { + $class_name = null; + } + + if (empty($class_name)) + { + $registry = self::getConfiguration(); + $engine = $registry->get('akeeba.advanced.scan_engine'); + $class_name = '\\Akeeba\\Engine\\Scan\\' . ucfirst($engine); + + if (!class_exists($class_name, true)) + { + $class_name = '\\Akeeba\\Engine\\Scan\\Large'; + } + } + + if ($reset) + { + self::unsetObjectInstance($class_name); + } + + return self::getObjectInstance($class_name); + } + + /** + * Returns the current post-processing engine. If no class is specified we + * return the post-processing engine configured in akeeba.advanced.postproc_engine + * + * @param string $engine The name of the post-processing class to forcibly return + * + * @return \Akeeba\Engine\Postproc\Base + */ + public static function &getPostprocEngine($engine = null) + { + $class_name = '\\Akeeba\\Engine\\Postproc\\Fake'; + + if (!is_null($engine)) + { + $class_name = '\\Akeeba\\Engine\\Postproc\\' . ucfirst($engine); + } + + if (is_null($engine) || !class_exists($class_name, true)) + { + $registry = self::getConfiguration(); + $engine = $registry->get('akeeba.advanced.postproc_engine'); + $class_name = '\\Akeeba\\Engine\\Postproc\\' . ucfirst($engine); + + if (!class_exists($class_name, true)) + { + $class_name = '\\Akeeba\\Engine\\Postproc\\None'; + } + } + + return self::getObjectInstance($class_name); + } + + /** + * Returns an instance of the Filters feature class + * + * @return \Akeeba\Engine\Core\Filters The Filters feature class' object instance + */ + public static function &getFilters() + { + return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Filters'); + } + + /** + * Returns an instance of the specified filter group class. Do note that it does not + * work with platform filter classes. They are handled internally by AECoreFilters. + * + * @param string $filter_name The filter class to load, without AEFilter prefix + * + * @return \Akeeba\Engine\Filter\Base The filter class' object instance + */ + public static function &getFilterObject($filter_name) + { + return self::getObjectInstance('\\Akeeba\\Engine\\Filter\\' . ucfirst($filter_name)); + } + + /** + * Loads an engine domain class and returns its associated object + * + * @param string $domain_name The name of the domain, e.g. installer for AECoreDomainInstaller + * + * @return \Akeeba\Engine\Base\Part + */ + public static function &getDomainObject($domain_name) + { + return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Domain\\' . ucfirst($domain_name)); + } + + /** + * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase() + * + * @param array $options Options to use when instantiating the database connection + * + * @return \Akeeba\Engine\Driver\Base + */ + public static function &getDatabase($options = null) + { + if (is_null($options)) + { + $options = Platform::getInstance()->get_platform_database_options(); + } + + if (isset($options['username']) && !isset($options['user'])) + { + $options['user'] = $options['username']; + } + + return Database::getDatabase($options); + } + + /** + * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase() + * + * @param array $options Options to use when instantiating the database connection + * + * @return \Akeeba\Engine\Driver\Base + */ + public static function unsetDatabase($options = null) + { + if (is_null($options)) + { + $options = Platform::getInstance()->get_platform_database_options(); + } + + $db = Database::getDatabase($options); + $db->close(); + + Database::unsetDatabase($options); + } + + /** + * Get the a reference to the Akeeba Engine's timer + * + * @return \Akeeba\Engine\Core\Timer + */ + public static function &getTimer() + { + return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Timer'); + } + + /** + * Get a reference to Akeeba Engine's main controller called Kettenrad + * + * @return \Akeeba\Engine\Core\Kettenrad + */ + public static function &getKettenrad() + { + return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Kettenrad'); + } + + // ======================================================================== + // Temporary objects which are not part of the engine state + // ======================================================================== + + /** + * Returns an instance of the factory storage class (formerly Tempvars) + * + * @return \Akeeba\Engine\Util\FactoryStorage + */ + public static function &getFactoryStorage() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FactoryStorage'); + } + + /** + * Returns an instance of the encryption class + * + * @return \Akeeba\Engine\Util\Encrypt + */ + public static function &getEncryption() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\Encrypt'); + } + + /** + * Returns an instance of the CRC32 calculations class + * + * @return \Akeeba\Engine\Util\CRC32 + */ + public static function &getCRC32Calculator() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\CRC32'); + } + + /** + * Returns an instance of the crypto-safe random value generator class + * + * @return \Akeeba\Engine\Util\RandomValue + */ + public static function &getRandval() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\RandomValue'); + } + + /** + * Returns an instance of the filesystem tools class + * + * @return \Akeeba\Engine\Util\FileSystem + */ + public static function &getFilesystemTools() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FileSystem'); + } + + /** + * Returns an instance of the filesystem tools class + * + * @return \Akeeba\Engine\Util\FileLister + */ + public static function &getFileLister() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FileLister'); + } + + /** + * Returns an instance of the engine parameters provider which provides information on scripting, GUI configuration + * elements and engine parts + * + * @return \Akeeba\Engine\Util\EngineParameters + */ + public static function &getEngineParamsProvider() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\EngineParameters'); + } + + /** + * Returns an instance of the log object + * + * @return \Akeeba\Engine\Util\Logger + */ + public static function &getLog() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\Logger'); + } + + /** + * Returns an instance of the configuration checks object + * + * @return \Akeeba\Engine\Util\ConfigurationCheck + */ + public static function &getConfigurationChecks() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\ConfigurationCheck'); + } + + /** + * Returns an instance of the secure settings handling object + * + * @return \Akeeba\Engine\Util\SecureSettings + */ + public static function &getSecureSettings() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\SecureSettings'); + } + + /** + * Returns an instance of the secure settings handling object + * + * @return \Akeeba\Engine\Util\TemporaryFiles + */ + public static function &getTempFiles() + { + return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\TemporaryFiles'); + } + + /** + * Get the connector object for push messages + * + * @return PushMessages + */ + public static function &getPush() + { + return self::getObjectInstance('Akeeba\\Engine\\Util\\PushMessages'); + } + + // ======================================================================== + // Handy functions + // ======================================================================== + + /** + * Returns the absolute path to Akeeba Engine's installation + * + * @return string + */ + public static function getAkeebaRoot() + { + if (empty(self::$root)) + { + self::$root = __DIR__; + } + + return self::$root; + } + + // ======================================================================== + // Used in unit testing + // ======================================================================== + + /** + * Force an object instance which will survive serialisation. This is supposed to be used only in Unit Tests. + * + * @param string $class_name The class name used internally by the Factory + * @param object|null $object The object to push. Use null to unset the object + * + * @return void + * + * @throws \Exception when you try using it outside of Unit Tests + */ + public static function forceObjectInstance($class_name, $object) + { + if (!interface_exists('PHPUnit_Exception', false)) + { + $method = __METHOD__; + + throw new \Exception("You can only use $method in Unit Tests", 500); + } + + $self = self::getInstance(); + + if (is_null($object) && isset($self->objectList[$class_name])) + { + unset($self->objectList[$class_name]); + + return; + } + + $self->objectList[$class_name] = $object; + } + + /** + * Force an object instance which will not survive serialisation. This is supposed to be used only in Unit Tests. + * + * @param string $class_name The class name used internally by the Factory + * @param object|null $object The object to push. Use null to unset the object + * + * @return void + * + * @throws \Exception when you try using it outside of Unit Tests + */ + public static function forceTempObjectInstance($class_name, $object) + { + if (!interface_exists('PHPUnit_Exception', false)) + { + $method = __METHOD__; + + throw new \Exception("You can only use $method in Unit Tests", 500); + } + + $self = self::getInstance(); + + if (is_null($object) && isset($self->temporaryObjectList[ $class_name ])) + { + unset($self->temporaryObjectList[ $class_name ]); + + return; + } + + $self->temporaryObjectList[ $class_name ] = $object; + } +} + +/** + * Timeout handler. It is registered as a global PHP shutdown function. + * + * If a PHP reports a timeout we will log this before letting PHP kill us. + */ +function AkeebaTimeoutTrap() +{ + if (connection_status() >= 2) + { + Factory::getLog()->log(LogLevel::ERROR, 'Akeeba Engine has timed out'); + } +} + +register_shutdown_function("\\Akeeba\\Engine\\AkeebaTimeoutTrap"); diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Base.php new file mode 100644 index 00000000..4d6babdd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Base.php @@ -0,0 +1,694 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Directories.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Directories.php new file mode 100644 index 00000000..750adfd0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Directories.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Extradirs.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Extradirs.php new file mode 100644 index 00000000..1f9d5d95 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Extradirs.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Files.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Files.php new file mode 100644 index 00000000..b621f01c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Files.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Incremental.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Incremental.php new file mode 100644 index 00000000..9d8f7077 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Incremental.php @@ -0,0 +1,147 @@ +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; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Multidb.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Multidb.php new file mode 100644 index 00000000..0b326d10 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Multidb.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexdirectories.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexdirectories.php new file mode 100644 index 00000000..fd6d6722 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexdirectories.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexfiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexfiles.php new file mode 100644 index 00000000..08d6af31 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexfiles.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipdirs.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipdirs.php new file mode 100644 index 00000000..b0f23806 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipdirs.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipfiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipfiles.php new file mode 100644 index 00000000..c74889dd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regexskipfiles.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextabledata.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextabledata.php new file mode 100644 index 00000000..9e1c425f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextabledata.php @@ -0,0 +1,45 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextables.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextables.php new file mode 100644 index 00000000..e824fc7e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Regextables.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipdirs.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipdirs.php new file mode 100644 index 00000000..04bce0fe --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipdirs.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipfiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipfiles.php new file mode 100644 index 00000000..20abac41 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Skipfiles.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/README.html b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/README.html new file mode 100644 index 00000000..18093440 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/README.html @@ -0,0 +1,28 @@ + + + + + + AkeebaBackup :: Filter Stack + + +

What is this directory?

+ +

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: filtername.ini and filtername.php. The former + contains the filter-specific configuration options and the later contains the actual filter code.

+ +

+ 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.filtername.enabled which controls the loading of this particular filter. +

+ +

+ Optional filters are always named Akeeba\Engine\Filter\Stack\StackFiltername so that the autoloader can + find them. For the same reason their filename must be StackFiltername.php Please watch out for the + letter case in the names, it's important. +

+ + diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackDateconditional.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackDateconditional.php new file mode 100644 index 00000000..881e9982 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackDateconditional.php @@ -0,0 +1,74 @@ +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; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackErrorlogs.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackErrorlogs.php new file mode 100644 index 00000000..22a1a692 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackErrorlogs.php @@ -0,0 +1,54 @@ +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; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackHoststats.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackHoststats.php new file mode 100644 index 00000000..7f2cd326 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/StackHoststats.php @@ -0,0 +1,50 @@ +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; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/dateconditional.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/dateconditional.ini new file mode 100644 index 00000000..d01dcf59 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/dateconditional.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/errorlogs.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/errorlogs.ini new file mode 100644 index 00000000..8b15d9b8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/errorlogs.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/hoststats.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/hoststats.ini new file mode 100644 index 00000000..77e3e1a3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Stack/hoststats.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tabledata.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tabledata.php new file mode 100644 index 00000000..000ecc14 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tabledata.php @@ -0,0 +1,44 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tables.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tables.php new file mode 100644 index 00000000..6d22507b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Filter/Tables.php @@ -0,0 +1,46 @@ +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(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform.php new file mode 100644 index 00000000..cee2b1f6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform.php @@ -0,0 +1,352 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Base.php new file mode 100644 index 00000000..ee664cd5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Base.php @@ -0,0 +1,1005 @@ +platformName); + } + + public function isThisPlatform() + { + return true; + } + + public function register_autoloader() + { + } + + /** + * Saves the current configuration to the database table + * + * @param int $profile_id The profile where to save the configuration to, defaults to current profile + * + * @return bool True if everything was saved properly + */ + public function save_configuration($profile_id = null) + { + // Load the database class + $db = Factory::getDatabase($this->get_platform_database_options()); + + if (!$db->connected()) + { + return false; + } + + // Get the active profile number, if no profile was specified + if (is_null($profile_id)) + { + $profile_id = $this->get_active_profile(); + } + + // Get an INI format registry dump + $registry = Factory::getConfiguration(); + $dump_profile = $registry->exportAsINI(); + + // Encrypt the registry dump if required + $secureSettings = Factory::getSecureSettings(); + $dump_profile = $secureSettings->encryptSettings($dump_profile); + + // Does the record already exist? + $exists = true; + $sql = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->qn($this->tableNameProfiles)) + ->where($db->qn('id') . ' = ' . $db->q($profile_id)); + + try + { + $count = $db->setQuery($sql)->loadResult(); + $exists = ($count > 0); + } + catch (\Exception $e) + { + $exists = true; + } + + if ($exists) + { + $sql = $db->getQuery(true) + ->update($db->qn($this->tableNameProfiles)) + ->set($db->qn('configuration') . ' = ' . $db->q($dump_profile)) + ->where($db->qn('id') . ' = ' . $db->q($profile_id)); + } + else + { + $sql = $db->getQuery(true) + ->insert($db->qn($this->tableNameProfiles)) + ->columns(array($db->qn('id'), $db->qn('description'), $db->qn('configuration'), + $db->qn('filters'), $db->qn('quickicon'))) + ->values( + $db->q(1) . ', ' . + $db->q("Default backup profile") . ', ' . + $db->q($dump_profile) . ', ' . + $db->q('') . ', ' . + $db->q(1) + ); + } + + $db->setQuery($sql); + + try + { + $result = $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + return ($result == true); + } + + /** + * Loads the current configuration off the database table + * + * @param int $profile_id The profile where to read the configuration from, defaults to current profile + * + * @return bool True if everything was read properly + * + * @throws DecryptionException When the settings cannot be decrypted + */ + public function load_configuration($profile_id = null) + { + // Load the database class + $db = Factory::getDatabase($this->get_platform_database_options()); + + // Get the active profile number, if no profile was specified + if (is_null($profile_id)) + { + $profile_id = $this->get_active_profile(); + } + + // Initialize the registry + $registry = Factory::getConfiguration(); + $registry->reset(); + + // Is the database connected? + if (!$db->connected()) + { + return false; + } + + // Load the INI format local configuration dump off the database + $sql = $db->getQuery(true) + ->select($db->qn('configuration')) + ->from($db->qn($this->tableNameProfiles)) + ->where($db->qn('id') . ' = ' . $db->q($profile_id)); + + $db->setQuery($sql); + $databaseData = $db->loadResult(); + + /** + * If the profile is not the default and we can't load anything let's switch back to the default profile. + * + * You will end up here when you have opened the application in two different browsers and Browser A is used to + * delete the active profile you were using with Browser B. If we were not to load the default profile Browser B + * would try to save the default configuration data to the deleted profile. However, since the profile does not + * exist in the database any more the load_configuration at the end of the following if-block would trigger the + * same code path, recursively, infinitely until you reached the maximum nesting level in PHP, run out of memory + * or hit the execution time limit. + */ + if ((empty($databaseData) || is_null($databaseData)) && ($profile_id != 1)) + { + return $this->load_configuration(1); + } + + if (empty($databaseData) || is_null($databaseData)) + { + // No configuration was saved yet - store the defaults + $saved = $this->save_configuration($profile_id); + + // If this is the case we probably don't have the necesary table. Throw an exception. + if (!$saved) + { + throw new \RuntimeException("Could not save data to backup profile #$profile_id", 500); + } + + return $this->load_configuration($profile_id); + } + + // Configuration found. Convert to array format. + if (function_exists('get_magic_quotes_runtime')) + { + if (@get_magic_quotes_runtime()) + { + $databaseData = stripslashes($databaseData); + } + } + + // Decrypt the data if required + $secureSettings = Factory::getSecureSettings(); + $noData = empty($databaseData); + $signature = ($noData || (strlen($databaseData) < 12)) ? '' : substr($databaseData, 0, 12); + $parsedData = array(); + + /** + * Special case: profile data is encrypted but encryption is set to false. This means that the user has just + * asked for the encryption to be disabled. We have to NOT load the settings so that the application has the + * chance to decode the data and write the decoded data back to the database. + */ + + if (!$secureSettings->supportsEncryption() && in_array($signature, array('###AES128###', '###CTR128###'))) + { + $dataArray = array('volatile' => array('fake_decrypt_flag' => 1)); + } + else + { + $databaseData = $secureSettings->decryptSettings($databaseData); + $dataArray = ParseIni::parse_ini_file($databaseData, true, true); + + // Did the decryption fail and we were asked to throw an exception? + if ($this->decryptionException && !$noData) + { + // No decrypted data + if (empty($databaseData)) + { + throw new DecryptionException; + } + + // Corrupt data + if (!strstr($databaseData, '[akeeba]')) + { + throw new DecryptionException; + } + } + } + + unset($databaseData); + + foreach ($dataArray as $section => $row) + { + if ($section == 'volatile') + { + continue; + } + + if (is_array($row) && !empty($row)) + { + foreach ($row as $key => $value) + { + $parsedData["$section.$key"] = $value; + } + } + } + + unset($dataArray); + + // Import the configuration array + $protected_keys = $registry->getProtectedKeys(); + $registry->resetProtectedKeys(); + $registry->mergeArray($parsedData, false, false); + + // Old profiles have advanced.proc_engine instead of advanced.postproc_engine. Migrate them. + $procEngine = $registry->get('akeeba.advanced.proc_engine', null); + + if (!empty($procEngine)) + { + $registry->set('akeeba.advanced.postproc_engine', $procEngine); + $registry->set('akeeba.advanced.proc_engine', null); + } + + // Apply config overrides + if (is_array($this->configOverrides) && !empty($this->configOverrides)) + { + $registry->mergeArray($this->configOverrides, false, false); + } + + $registry->setProtectedKeys($protected_keys); + $registry->activeProfile = $profile_id; + + return true; + } + + public function get_stock_directories() + { + return array(); + } + + public function get_site_root() + { + return ''; + } + + public function get_installer_images_path() + { + return ''; + } + + public function get_active_profile() + { + return 1; + } + + public function get_profile_name($id = null) + { + return ''; + } + + public function get_backup_origin() + { + return 'backend'; + } + + public function get_timestamp_database($date = 'now') + { + return ''; + } + + public function get_local_timestamp($format) + { + $dateNow = new \DateTime('now', new \DateTimeZone('UTC')); + + return $dateNow->format($format); + } + + public function get_host() + { + return ''; + } + + public function get_site_name() + { + return ''; + } + + public function get_default_database_driver($use_platform = true) + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + + /** + * Creates or updates the statistics record of the current backup attempt + * + * @param int $id Backup record ID, use null for new record + * @param array $data The data to store + * @param \Akeeba\Engine\Base\BaseObject $caller The calling object + * + * @return int|null|bool 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) + { + // No valid data? + if (!is_array($data)) + { + return null; + } + + // No data at all? + if (empty($data)) + { + return null; + } + + $db = Factory::getDatabase($this->get_platform_database_options()); + + $tableFields = $db->getTableColumns($this->tableNameStats); + $tableFields = array_keys($tableFields); + + if (is_null($id)) + { + // Create a new record + $sql_fields = array(); + $sql_values = ''; + + foreach ($data as $key => $value) + { + if (!in_array($key, $tableFields)) + { + continue; + } + + $sql_fields[] = $db->qn($key); + $sql_values .= (!empty($sql_values) ? ',' : '') . $db->quote($value); + } + + $sql = $db->getQuery(true) + ->insert($db->quoteName($this->tableNameStats)) + ->columns($sql_fields) + ->values($sql_values); + + $db->setQuery($sql); + + try + { + $db->query(); + } + catch (\Exception $exc) + { + if (is_object($caller) && ($caller instanceof BaseObject)) + { + $caller->setError($exc->getMessage()); + } + + return false; + } + + return $db->insertid(); + } + else + { + $sql_set = array(); + foreach ($data as $key => $value) + { + if ($key == 'id') + { + continue; + } + + $sql_set[] = $db->qn($key) . '=' . $db->q($value); + } + $sql = $db->getQuery(true) + ->update($db->qn($this->tableNameStats)) + ->set($sql_set) + ->where($db->qn('id') . '=' . $db->q($id)); + $db->setQuery($sql); + + try + { + $db->query(); + } + catch (\Exception $exc) + { + if (is_object($caller) && ($caller instanceof BaseObject)) + { + $caller->setError($exc->getMessage()); + } + + return false; + } + + return null; + } + } + + + /** + * Loads and returns a backup statistics record as a hash array + * + * @param int $id Backup record ID + * + * @return array + */ + public function get_statistics($id) + { + $db = Factory::getDatabase($this->get_platform_database_options()); + $query = $db->getQuery(true) + ->select('*') + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $db->setQuery($query); + + return $db->loadAssoc(true); + } + + + /** + * Completely removes a backup statistics record + * + * @param int $id Backup record ID + * + * @return bool True on success + */ + public function delete_statistics($id) + { + $db = Factory::getDatabase($this->get_platform_database_options()); + $query = $db->getQuery(true) + ->delete($db->qn($this->tableNameStats)) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $db->setQuery($query); + + $result = true; + try + { + $db->query(); + } + catch (\Exception $exc) + { + $result = false; + } + + return $result; + } + + + /** + * 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) + * + * @return array + */ + function &get_statistics_list($config = array()) + { + $defaultConfiguration = array( + 'limitstart' => 0, + 'limit' => 0, + 'filters' => array(), + 'order' => null + ); + $config = (object)array_merge($defaultConfiguration, $config); + + $db = Factory::getDatabase($this->get_platform_database_options()); + + $query = $db->getQuery(true); + + if (!empty($config->filters)) + { + if (is_array($config->filters)) + { + if (!empty($config->filters)) + { + // Parse the filters array + foreach ($config->filters as $f) + { + $clause = $db->qn($f['field']); + if (array_key_exists('operand', $f)) + { + $clause .= ' ' . strtoupper($f['operand']) . ' '; + if ($f['operand'] == 'BETWEEN') + { + $clause .= $db->q($f['value']) . ' AND ' . $db->q($f['value2']); + } + elseif ($f['operand'] == 'LIKE') + { + $clause .= '\'%' . $db->escape($f['value']) . '%\''; + } + else + { + $clause .= $db->q($f['value']); + } + } + else + { + $clause .= ' = ' . $db->q($f['value']); + } + + $query->where($clause); + } + } + } + else + { + // Legacy mode: profile ID given + $query->where($db->qn('profile_id') . ' = ' . $db->q($config->filters)); + } + } + + if (empty($config->order) || !is_array($config->order)) + { + $config->order = array( + 'by' => 'id', + 'order' => 'DESC' + ); + } + + $query->select('*') + ->from($db->qn($this->tableNameStats)) + ->order($db->qn($config->order['by']) . " " . strtoupper($config->order['order'])); + + $db->setQuery($query, $config->limitstart, $config->limit); + + $list = $db->loadAssocList(); + + return $list; + } + + /** + * 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 int + */ + function get_statistics_count($filters = null) + { + $db = Factory::getDatabase($this->get_platform_database_options()); + + $query = $db->getQuery(true); + + if (!empty($filters)) + { + if (is_array($filters)) + { + if (!empty($filters)) + { + // Parse the filters array + foreach ($filters as $f) + { + $clause = $db->quoteName($f['field']); + if (array_key_exists('operand', $f)) + { + $clause .= ' ' . strtoupper($f['operand']) . ' '; + } + else + { + $clause .= ' = '; + } + if ($f['operand'] == 'BETWEEN') + { + $clause .= $db->q($f['value']) . ' AND ' . $db->q($f['value2']); + } + elseif ($f['operand'] == 'LIKE') + { + $clause .= '\'%' . $db->escape($f['value']) . '%\''; + } + else + { + $clause .= $db->q($f['value']); + } + $query->where($clause); + } + } + } + else + { + // Legacy mode: profile ID given + $query->where($db->qn('profile_id') . ' = ' . $db->q($filters)); + } + } + + $query->select('COUNT(*)') + ->from($db->quoteName($this->tableNameStats)); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Returns an array with the specifics of running backups + * + * @param string $tag + * + * @throws \Akeeba\Engine\Driver\QueryException + * + * @return array Array list of associative arrays + */ + public function get_running_backups($tag = null) + { + $db = Factory::getDatabase($this->get_platform_database_options()); + $query = $db->getQuery(true) + ->select('*') + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('status') . ' = ' . $db->q('run')) + ->where(' NOT ' . $db->qn('archivename') . ' = ' . $db->q('')); + if (!empty($tag)) + { + $query->where($db->qn('origin') . ' LIKE ' . $db->q($tag . '%')); + } + $db->setQuery($query); + + return $db->loadAssocList(); + } + + /** + * 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 bool $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 + * + * @throws \Akeeba\Engine\Driver\QueryException + * + * @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') + { + $db = Factory::getDatabase($this->get_platform_database_options()); + + $query2 = $db->getQuery(true) + ->select('MAX(' . $db->qn('id') . ') AS ' . $db->qn('id')) + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('status') . ' = ' . $db->q('complete')) + ->group($db->qn('absolute_path')); + + $query = $db->getQuery(true) + ->select($db->qn('id')) + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('filesexist') . ' = ' . $db->q(1)) + ->where($db->qn('id') . ' IN (' . $query2 . ')') + ->where('NOT ' . $db->qn('absolute_path') . ' = ' . $db->q('')) + ->order($db->qn('id') . ' ' . $ordering); + + if ($useprofile) + { + $profile_id = $this->get_active_profile(); + $query->where($db->qn('profile_id') . " = " . $db->q($profile_id)); + } + + if (!empty($tagFilters)) + { + $operator = ''; + $first = array_shift($tagFilters); + if ($first == 'NOT') + { + $operator = 'NOT'; + } + else + { + array_unshift($tagFilters, $first); + } + + $quotedTags = array(); + foreach ($tagFilters as $tag) + { + $quotedTags[] = $db->q($tag); + } + $filter = implode(', ', $quotedTags); + unset($quotedTags); + $query->where($operator . ' ' . $db->quoteName('tag') . ' IN (' . $filter . ')'); + } + + $db->setQuery($query); + $array = $db->loadColumn(); + + return $array; + } + + /** + * Invalidates older records sharing the same $archivename + * + * @param string $archivename + */ + public function remove_duplicate_backup_records($archivename) + { + Factory::getLog()->log(LogLevel::DEBUG, "Removing any old records with $archivename filename"); + $db = Factory::getDatabase($this->get_platform_database_options()); + + $query = $db->getQuery(true) + ->select($db->qn('id')) + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('archivename') . ' = ' . $db->q($archivename)) + ->order($db->qn('id') . ' DESC'); + + $db->setQuery($query); + $array = $db->loadColumn(); + + Factory::getLog()->log(LogLevel::DEBUG, count($array) . " records found"); + + // No records?! Quit. + if (empty($array)) + { + return; + } + // Only one record. Quit. + if (count($array) == 1) + { + return; + } + + // Shift the first (latest) element off the array + $currentID = array_shift($array); + + // Invalidate older records + $this->invalidate_backup_records($array); + } + + /** + * Marks the specified backup records as having no files + * + * @param array $ids Array of backup record IDs to ivalidate + */ + public function invalidate_backup_records($ids) + { + if (empty($ids)) + { + return false; + } + $db = Factory::getDatabase($this->get_platform_database_options()); + $temp = array(); + foreach ($ids as $id) + { + $temp[] = $db->q($id); + } + $list = implode(',', $temp); + $sql = $db->getQuery(true) + ->update($db->qn($this->tableNameStats)) + ->set($db->qn('filesexist') . ' = ' . $db->q('0')) + ->where($db->qn('id') . ' IN (' . $list . ')');; + $db->setQuery($sql); + + try + { + $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + return true; + } + + /** + * Gets a list of records with remotely stored files in the selected remote storage + * provider and profile. + * + * @param $profile int (optional) The profile to use. Skip or use null for active profile. + * @param $engine string (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) + { + $config = Factory::getConfiguration(); + $result = array(); + + if (is_null($profile)) + { + $profile = $this->get_active_profile(); + } + if (is_null($engine)) + { + $engine = $config->get('akeeba.advanced.postproc_engine', ''); + } + + if (empty($engine)) + { + return $result; + } + + $db = Factory::getDatabase($this->get_platform_database_options()); + $sql = $db->getQuery(true) + ->select('*') + ->from($db->qn($this->tableNameStats)) + ->where($db->qn('profile_id') . ' = ' . $db->q($profile)) + ->where($db->qn('remote_filename') . ' LIKE ' . $db->q($engine . '://%')) + ->order($db->qn('id') . ' ASC'); + + $db->setQuery($sql); + + return $db->loadAssocList(); + } + + /** + * Returns the filter data for the entire filter group collection + * + * @return array + */ + public function &load_filters() + { + // Load the filter data from the database + $profile_id = $this->get_active_profile(); + $db = Factory::getDatabase($this->get_platform_database_options()); + + // Load the INI format local configuration dump off the database + $sql = $db->getQuery(true) + ->select($db->qn('filters')) + ->from($db->qn($this->tableNameProfiles)) + ->where($db->qn('id') . ' = ' . $db->q($profile_id)); + $db->setQuery($sql); + $all_filter_data = $db->loadResult(); + + if (is_null($all_filter_data) || empty($all_filter_data)) + { + $all_filter_data = array(); + } + else + { + if (function_exists('get_magic_quotes_runtime')) + { + if (@get_magic_quotes_runtime()) + { + $all_filter_data = stripslashes($all_filter_data); + } + } + $all_filter_data = @unserialize($all_filter_data); + if (empty($all_filter_data)) + { + $all_filter_data = array(); + } // Catch unserialization errors + } + + return $all_filter_data; + } + + /** + * Saves the nested filter data array $filter_data to the database + * + * @param array $filter_data The filter data to save + * + * @return bool True on success + */ + public function save_filters(&$filter_data) + { + $profile_id = $this->get_active_profile(); + $db = Factory::getDatabase($this->get_platform_database_options()); + + $sql = $db->getQuery(true) + ->update($db->qn($this->tableNameProfiles)) + ->set($db->qn('filters') . '=' . $db->q(serialize($filter_data))) + ->where($db->qn('id') . ' = ' . $db->q($profile_id)); + $db->setQuery($sql); + + try + { + $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + return true; + } + + public function get_platform_database_options() + { + return array(); + } + + public function translate($key) + { + return ''; + } + + public function load_version_defines() + { + } + + public function getPlatformVersion() + { + return array( + 'name' => 'Platform', + 'version' => 'unknown' + ); + } + + public function log_platform_special_directories() + { + } + + public function get_platform_configuration_option($key, $default) + { + return ''; + } + + public function get_administrator_emails() + { + return array(); + } + + public function send_email($to, $subject, $body, $attachFile = null) + { + return false; + } + + public function unlink($file) + { + return @unlink($file); + } + + public function move($from, $to) + { + $result = @rename($from, $to); + if (!$result) + { + $result = @copy($from, $to); + if ($result) + { + $result = $this->unlink($from); + } + } + + return $result; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Exception/DecryptionException.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Exception/DecryptionException.php new file mode 100644 index 00000000..3bc7ebe8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/Exception/DecryptionException.php @@ -0,0 +1,37 @@ +translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION'); + } + + parent::__construct($message, $code, $previous); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/PlatformInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/PlatformInterface.php new file mode 100644 index 00000000..d23a8285 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Platform/PlatformInterface.php @@ -0,0 +1,392 @@ +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; + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/None.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/None.php new file mode 100644 index 00000000..b6459a91 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/None.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/email.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/email.ini new file mode 100644 index 00000000..05dd4699 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/email.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/none.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/none.ini new file mode 100644 index 00000000..8a6dcef8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Postproc/none.ini @@ -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 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/AbstractLogger.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/AbstractLogger.php new file mode 100644 index 00000000..00f90345 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/AbstractLogger.php @@ -0,0 +1,120 @@ +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); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/InvalidArgumentException.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/InvalidArgumentException.php new file mode 100644 index 00000000..67f852d1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/LoggerInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/LoggerInterface.php new file mode 100644 index 00000000..476bb962 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/LoggerInterface.php @@ -0,0 +1,114 @@ +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); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/NullLogger.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/NullLogger.php new file mode 100644 index 00000000..553a3c59 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Psr/Log/NullLogger.php @@ -0,0 +1,27 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/Base.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/Base.php new file mode 100644 index 00000000..8a1fb309 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/Base.php @@ -0,0 +1,39 @@ +get('volatile.breakflag', false); + + // Reset break flag before continuing + $breakflag = false; + + // Initialize variables + $arr = array(); + $false = false; + + if (!@is_dir($folder) && !@is_dir($folder . '/')) + { + return $false; + } + + $counter = 0; + $registry = Factory::getConfiguration(); + $maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100); + + $allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process; + + if (!@is_dir($folder)) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + if (!@is_readable($folder)) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + $di = new \DirectoryIterator($folder); + $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR; + + /** @var \DirectoryIterator $file */ + foreach ($di as $file) + { + if ($breakflag) + { + break; + } + + /** + * If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we + * will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as + * unreadable without suffering a PHP Fatal Error. + */ + try { + $file->isLink(); + } + catch (\RuntimeException $e) + { + $this->setWarning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname())); + + continue; + } + + if ($file->isDot()) + { + continue; + } + + if ($file->isDir()) + { + continue; + } + + $dir = $folder . $ds . $file->getFilename(); + $data = $dir; + + if (_AKEEBA_IS_WINDOWS) + { + $data = Factory::getFilesystemTools()->TranslateWinPath($dir); + } + + if ($data) + { + $arr[] = $data; + } + + $counter++; + + if ($counter >= $maxCounter) + { + $breakflag = $allowBreakflag; + } + } + + // Save break flag status + $registry->set('volatile.breakflag', $breakflag); + + return $arr; + } + + public function &getFolders($folder, &$position) + { + // Was the breakflag set BEFORE starting? -- This workaround is required due to PHP5 defaulting to assigning variables by reference + $registry = Factory::getConfiguration(); + $breakflag_before_process = $registry->get('volatile.breakflag', false); + + // Reset break flag before continuing + $breakflag = false; + + // Initialize variables + $arr = array(); + $false = false; + + if (!is_dir($folder) && !is_dir($folder . '/')) + { + return $false; + } + + $counter = 0; + $registry = Factory::getConfiguration(); + $maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100); + + $allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process; + + if (!@is_readable($folder)) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + $di = new \DirectoryIterator($folder); + $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR; + + /** @var \DirectoryIterator $file */ + foreach ($di as $file) + { + if ($breakflag) + { + break; + } + + /** + * If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we + * will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as + * unreadable without suffering a PHP Fatal Error. + */ + try { + $file->isLink(); + } + catch (\RuntimeException $e) + { + $this->setWarning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname())); + + continue; + } + + if ($file->isDot()) + { + continue; + } + + if (!$file->isDir()) + { + continue; + } + + $dir = $folder . $ds . $file->getFilename(); + $data = $dir; + + if (_AKEEBA_IS_WINDOWS) + { + $data = Factory::getFilesystemTools()->TranslateWinPath($dir); + } + + if ($data) + { + $arr[] = $data; + } + + $counter++; + + if ($counter >= $maxCounter) + { + $breakflag = $allowBreakflag; + } + } + + // Save break flag status + $registry->set('volatile.breakflag', $breakflag); + + return $arr; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/smart.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/smart.ini new file mode 100644 index 00000000..d1da3660 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Scan/smart.ini @@ -0,0 +1,30 @@ +; Akeeba 'Smart' Scan Engine +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: smart.ini 604 2011-05-17 08:29:51Z nikosdion $ + +; Engine information +[_information] +title = COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_TITLE +description = COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_DESCRIPTION + +[engine.scan.smart.large_dir_threshold] +default = 100 +type = integer +min = 0 +max = 500 +shortcuts = "20|50|100|200|300|400|500" +scale = 1 +uom = +title = COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_TITLE +description = COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_DESCRIPTION + +[engine.scan.common.largefile] +default = 10485760 +type = integer +min = 1048576 +max = 1048576000 +shortcuts = "1048576|2097152|5242880|10485760|15728640|20971520|26214400|31457280|41943040|52428800|78643200|104857600" +scale = 1048576 +uom = MB +title = COM_AKEEBA_CONFIG_LARGEFILE_TITLE +description = COM_AKEEBA_CONFIG_LARGEFILE_DESCRIPTION diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AbstractAdapter.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AbstractAdapter.php new file mode 100644 index 00000000..b80f96f5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AbstractAdapter.php @@ -0,0 +1,91 @@ + $size) + { + if (function_exists('mb_substr')) + { + return mb_substr($key, 0, $size, 'ASCII'); + } + + return substr($key, 0, $size); + } + + return $key . str_repeat("\0", ($size - $keyLength)); + } + + /** + * Returns null bytes to append to the string so that it's zero padded to the specified block size + * + * @param string $string The binary string which will be zero padded + * @param int $blockSize The block size + * + * @return string The zero bytes to append to the string to zero pad it to $blockSize + */ + protected function getZeroPadding($string, $blockSize) + { + $stringSize = strlen($string); + + if (function_exists('mb_strlen')) + { + $stringSize = mb_strlen($string, 'ASCII'); + } + + if ($stringSize == $blockSize) + { + return ''; + } + + if ($stringSize < $blockSize) + { + return str_repeat("\0", $blockSize - $stringSize); + } + + $paddingBytes = $stringSize % $blockSize; + + return str_repeat("\0", $blockSize - $paddingBytes); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AdapterInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AdapterInterface.php new file mode 100644 index 00000000..65b05dd1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/AdapterInterface.php @@ -0,0 +1,84 @@ +cipherType = MCRYPT_RIJNDAEL_128; + break; + + case '192': + $this->cipherType = MCRYPT_RIJNDAEL_192; + break; + + case '256': + $this->cipherType = MCRYPT_RIJNDAEL_256; + break; + } + + switch (strtolower($mode)) + { + case 'ecb': + $this->cipherMode = MCRYPT_MODE_ECB; + break; + + default: + case 'cbc': + $this->cipherMode = MCRYPT_MODE_CBC; + break; + } + + } + + public function encrypt($plainText, $key, $iv = null) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = $this->resizeKey($iv, $iv_size); + + if (empty($iv)) + { + $randVal = new RandomValue(); + $iv = $randVal->generate($iv_size); + } + + $cipherText = mcrypt_encrypt($this->cipherType, $key, $plainText, $this->cipherMode, $iv); + $cipherText = $iv . $cipherText; + + return $cipherText; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('mcrypt_get_key_size')) + { + return false; + } + + if (!function_exists('mcrypt_get_iv_size')) + { + return false; + } + + if (!function_exists('mcrypt_create_iv')) + { + return false; + } + + if (!function_exists('mcrypt_encrypt')) + { + return false; + } + + if (!function_exists('mcrypt_decrypt')) + { + return false; + } + + if (!function_exists('mcrypt_list_algorithms')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = mcrypt_list_algorithms(); + + if (!in_array('rijndael-128', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-192', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-256', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + public function getBlockSize() + { + return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/OpenSSL.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/OpenSSL.php new file mode 100644 index 00000000..68507300 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/AesAdapter/OpenSSL.php @@ -0,0 +1,179 @@ +openSSLOptions = 1; + + // PHP 5.4 - Do it THE RIGHT WAY(tm) + if (version_compare(PHP_VERSION, '5.4.0', 'ge')) + { + $this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + } + + public function setEncryptionMode($mode = 'cbc', $strength = 128) + { + static $availableAlgorithms = null; + static $defaultAlgo = 'aes-128-cbc'; + + if (!is_array($availableAlgorithms)) + { + $availableAlgorithms = openssl_get_cipher_methods(); + + foreach (array('aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc', + 'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb') as $algo) + { + if (in_array($algo, $availableAlgorithms)) + { + $defaultAlgo = $algo; + break; + } + } + } + + $strength = (int) $strength; + $mode = strtolower($mode); + + if (!in_array($strength, array(128, 192, 256))) + { + $strength = 256; + } + + if (!in_array($mode, array('cbc', 'ebc'))) + { + $mode = 'cbc'; + } + + $algo = 'aes-' . $strength . '-' . $mode; + + if (!in_array($algo, $availableAlgorithms)) + { + $algo = $defaultAlgo; + } + + $this->method = $algo; + } + + public function encrypt($plainText, $key, $iv = null) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = $this->resizeKey($iv, $iv_size); + + if (empty($iv)) + { + $randVal = new RandomValue(); + $iv = $randVal->generate($iv_size); + } + + $plainText .= $this->getZeroPadding($plainText, $iv_size); + $cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv); + $cipherText = $iv . $cipherText; + + return $cipherText; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('openssl_get_cipher_methods')) + { + return false; + } + + if (!function_exists('openssl_random_pseudo_bytes')) + { + return false; + } + + if (!function_exists('openssl_cipher_iv_length')) + { + return false; + } + + if (!function_exists('openssl_encrypt')) + { + return false; + } + + if (!function_exists('openssl_decrypt')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = openssl_get_cipher_methods(); + + if (!in_array('aes-128-cbc', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + /** + * @return int + */ + public function getBlockSize() + { + return openssl_cipher_iv_length($this->method); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Buffer.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Buffer.php new file mode 100644 index 00000000..19d12a0e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Buffer.php @@ -0,0 +1,193 @@ +name = $url["host"]; + $this->_buffers[$this->name] = null; + $this->position = 0; + + return true; + } + + /** + * Read stream + * + * @param integer $count How many bytes of data from the current position should be returned. + * + * @return mixed The data from the stream up to the specified number of bytes (all data if + * the total number of bytes in the stream is less than $count. Null if + * the stream is empty. + * + * @see streamWrapper::stream_read + */ + public function stream_read($count) + { + $ret = substr($this->_buffers[$this->name], $this->position, $count); + $this->position += strlen($ret); + + return $ret; + } + + /** + * Write stream + * + * @param string $data The data to write to the stream. + * + * @return integer + * + * @see streamWrapper::stream_write + */ + public function stream_write($data) + { + $left = substr($this->_buffers[$this->name], 0, $this->position); + $right = substr($this->_buffers[$this->name], $this->position + strlen($data)); + $this->_buffers[$this->name] = $left . $data . $right; + $this->position += strlen($data); + + return strlen($data); + } + + /** + * Function to get the current position of the stream + * + * @return integer + * + * @see streamWrapper::stream_tell + */ + public function stream_tell() + { + return $this->position; + } + + /** + * Function to test for end of file pointer + * + * @return boolean True if the pointer is at the end of the stream + * + * @see streamWrapper::stream_eof + */ + public function stream_eof() + { + return $this->position >= strlen($this->_buffers[$this->name]); + } + + /** + * The read write position updates in response to $offset and $whence + * + * @param integer $offset The offset in bytes + * @param integer $whence Position the offset is added to + * Options are SEEK_SET, SEEK_CUR, and SEEK_END + * + * @return boolean True if updated + * + * @see streamWrapper::stream_seek + */ + public function stream_seek($offset, $whence) + { + switch ($whence) + { + case SEEK_SET: + if ($offset < strlen($this->_buffers[$this->name]) && $offset >= 0) + { + $this->position = $offset; + + return true; + } + else + { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) + { + $this->position += $offset; + + return true; + } + else + { + return false; + } + break; + + case SEEK_END: + if (strlen($this->_buffers[$this->name]) + $offset >= 0) + { + $this->position = strlen($this->_buffers[$this->name]) + $offset; + + return true; + } + else + { + return false; + } + break; + + default: + return false; + } + } +} + +// Register the stream +stream_wrapper_register("buffer", "\\Akeeba\\Engine\\Util\\Buffer"); diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/CRC32.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/CRC32.php new file mode 100644 index 00000000..a9f32621 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/CRC32.php @@ -0,0 +1,117 @@ +crc32_file_php512($filename); + Factory::getLog()->log(LogLevel::DEBUG, "File $filename - CRC32 = " . dechex($res) . " [HASH_FILE]"); + } + else if (function_exists("file_get_contents") && (@filesize($filename) <= $AkeebaPackerZIP_CHUNK_SIZE)) + { + $res = $this->crc32_file_getcontents($filename); + Factory::getLog()->log(LogLevel::DEBUG, "File $filename - CRC32 = " . dechex($res) . " [FILE_GET_CONTENTS]"); + } + else + { + $res = 0; + Factory::getLog()->log(LogLevel::DEBUG, "File $filename - CRC32 = " . dechex($res) . " [FAKE - CANNOT CALCULATE]"); + } + + if ($res === false) + { + $res = 0; + + $this->setWarning("File $filename - NOT READABLE: CRC32 IS WRONG!"); + } + + return $res; + } + + /** + * Very efficient CRC32 calculation for PHP 5.1.2 and greater, requiring + * the 'hash' PECL extension + * + * @param string $filename Absolute filepath + * + * @return integer The CRC32 + */ + protected function crc32_file_php512($filename) + { + // Detection of buggy PHP hosts + static $mustInvert = null; + + if (is_null($mustInvert)) + { + $test_crc = @hash('crc32b', 'test', false); + $mustInvert = (strtolower($test_crc) == '0c7e7fd8'); // Normally, it's D87F7E0C :) + + if ($mustInvert) + { + Factory::getLog()->log(LogLevel::WARNING, 'Your server has a buggy PHP version which produces inverted CRC32 values. Attempting a workaround. ZIP files may appear as corrupt.'); + } + } + + $res = @hash_file('crc32b', $filename, false); + + if ($mustInvert) + { + // Workaround for buggy PHP versions (I think before 5.1.8) which produce inverted CRC32 sums + $res2 = substr($res, 6, 2) . substr($res, 4, 2) . substr($res, 2, 2) . substr($res, 0, 2); + $res = $res2; + } + $res = hexdec($res); + + return $res; + } + + /** + * A compatible CRC32 calculation using file_get_contents, utilizing immense amounts of RAM + * + * @param string $filename + * + * @return integer + */ + protected function crc32_file_getcontents($filename) + { + return crc32(@file_get_contents($filename)); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Complexify.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Complexify.php new file mode 100644 index 00000000..983ab99f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Complexify.php @@ -0,0 +1,390 @@ +bannedPasswords = self::$BANLIST; + + foreach ($options as $opt => $val) + { + if ($opt === 'banmode') + { + trigger_error('The lowercase banmode option is deprecated. Use banMode instead.', E_USER_DEPRECATED); + $opt = 'banMode'; + } + + $this->{$opt} = $val; + } + } + + /** + * Determine the complexity added from a character set if it is used in a string + * + * @param string $str String to check + * @param int [2] $charset Array of unicode code points representing the lower and upper bound of the + * character range + * + * @return int 0 if there are no characters from the character set, size of the character set if there are any + * characters used in the string + */ + private function additionalComplexityForCharset($str, $charset) + { + $len = mb_strlen($str, $this->encoding); + for ($i = 0; $i < $len; $i++) + { + $c = + unpack('Nord', mb_convert_encoding(mb_substr($str, $i, 1, $this->encoding), 'UCS-4BE', $this->encoding)); + if ($charset[0] <= $c['ord'] && $c['ord'] <= $charset[1]) + { + return $charset[1] - $charset[0] + 1; + } + } + + return 0; + } + + /** + * Check if a string is in the banned password list + * + * @param string $str String to check + * + * @return bool TRUE if $str is a banned password, or if it is a substring of a banned password and + * $this->banMode is 'strict' + */ + private function inBanlist($str) + { + if ($str == '') + { + return false; + } + + $str = mb_convert_case($str, MB_CASE_LOWER, $this->encoding); + + if ($this->banMode === 'strict') + { + for ($i = 0; $i < count($this->bannedPasswords); $i++) + { + if (mb_strpos($this->bannedPasswords[ $i ], $str, 0, $this->encoding) !== false) + { + return true; + } + } + + return false; + } + + return in_array($str, $this->bannedPasswords); + } + + /** + * Check the complexity of a password + * + * @param string $password The password to check + * + * @return object StdClass object with properties "valid", "complexity", and "error" + * - valid: TRUE if the password is complex enough, FALSE if it is not + * - complexity: The complexity of the password as a percent + * - errors: Array containing descriptions of what made the password fail. Possible values are: banned, toosimple, + * tooshort + */ + public function evaluateSecurity($password) + { + $complexity = 0; + $error = array(); + + // Reset complexity to 0 when banned password is found + if (!$this->inBanlist($password)) + { + // Add character complexity + foreach (self::$CHARSETS as $charset) + { + $complexity += $this->additionalComplexityForCharset($password, $charset); + } + } + else + { + array_push($error, 'banned'); + $complexity = 1; + } + + // Use natural log to produce linear scale + $complexity = log(pow($complexity, mb_strlen($password, $this->encoding))) * (1 / $this->strengthScaleFactor); + + if ($complexity <= self::$MIN_COMPLEXITY) + { + array_push($error, 'toosimple'); + } + + if (mb_strlen($password, $this->encoding) < $this->minimumChars) + { + array_push($error, 'tooshort'); + } + + // Scale to percentage, so it can be used for a progress bar + $complexity = ($complexity / self::$MAX_COMPLEXITY) * 100; + $complexity = ($complexity > 100) ? 100 : $complexity; + + return (object)array('valid' => count($error) === 0, 'complexity' => $complexity, 'errors' => $error); + } + + /** + * Checks if a password is strong enough for use on a live site. Used to check the front-end Secret Word. + * + * @param string $password The password to check + * @param bool $throwExceptions Throw an exception if the password is not strong enough? + * + * @return bool + */ + public static function isStrongEnough($password, $throwExceptions = true) + { + $complexify = new self(); + + $res = (object) array( + 'valid' => strlen($password) >= 32, + 'complexity' => 50, + 'errors' => (strlen($password) >= 32) ? array() : array('tooshort') + ); + + if (function_exists('mb_strlen') && function_exists('mb_convert_encoding') && + function_exists('mb_substr') && function_exists('mb_convert_case')) + { + $res = $complexify->evaluateSecurity($password); + } + + + if ($res->valid) + { + return true; + } + + if (!$throwExceptions) + { + return false; + } + + $error = count($res->errors) ? array_shift($res->errors) : 'toosimple'; + + $errorMessage = Platform::getInstance()->translate('COM_AKEEBA_CPANEL_ERR_FESECRETWORD_' . $error); + + throw new \RuntimeException($errorMessage, 403); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ConfigurationCheck.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ConfigurationCheck.php new file mode 100644 index 00000000..3ee6f8be --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ConfigurationCheck.php @@ -0,0 +1,606 @@ + '001', 'severity' => 'critical', 'callback' => array(null, 'q001'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q001'), + array('code' => '003', 'severity' => 'critical', 'callback' => array(null, 'q003'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q003'), + array('code' => '004', 'severity' => 'critical', 'callback' => array(null, 'q004'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q004'), + + array('code' => '101', 'severity' => 'high', 'callback' => array(null, 'q101'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q101'), + array('code' => '103', 'severity' => 'high', 'callback' => array(null, 'q103'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q103'), + array('code' => '104', 'severity' => 'high', 'callback' => array(null, 'q104'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q104'), + array('code' => '106', 'severity' => 'high', 'callback' => array(null, 'q106'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q106'), + + array('code' => '201', 'severity' => 'medium', 'callback' => array(null, 'q201'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q201'), + array('code' => '202', 'severity' => 'medium', 'callback' => array(null, 'q202'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q202'), + array('code' => '204', 'severity' => 'medium', 'callback' => array(null, 'q204'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q204'), + + array('code' => '203', 'severity' => 'low', 'callback' => array(null, 'q203'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q203'), + array('code' => '401', 'severity' => 'low', 'callback' => array(null, 'q401'), 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q401'), + ); + + /** + * The public constructor replaces the missing object reference in the configuration check callbacks + */ + function __construct() + { + $temp = array(); + + foreach ($this->configurationChecks as $check) + { + $check['callback'] = array($this, $check['callback'][1]); + $temp[] = $check; + } + + $this->configurationChecks = $temp; + } + + /** + * Returns the output & temporary folder writable status + * + * @return array A hash array with the writable status + */ + public function getFolderStatus() + { + static $status = null; + + if (is_null($status)) + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + + // Get output writable status + $registry = Factory::getConfiguration(); + $outdir = $registry->get('akeeba.basic.output_directory'); + + foreach ($stock_dirs as $macro => $replacement) + { + $outdir = str_replace($macro, $replacement, $outdir); + } + + $status['output'] = @is_writable($outdir); + } + + return $status; + } + + /** + * Returns the overall status. It's true when both the temporary and output directories are writable and there are + * no critical configuration check failures. + * + * @return boolean + */ + public function getShortStatus() + { + // Base the status on directory writeable status + $status = $this->getFolderStatus(); + $ret = $status['output']; + + // Scan for high severity configuration check errors + $detailedStatus = $this->getDetailedStatus(); + + if (!empty($detailedStatus)) + { + foreach ($detailedStatus as $configCheck) + { + if ($configCheck['severity'] == 'critical') + { + $ret = false; + } + } + } + + // Return status + return $ret; + } + + /** + * Add a configuration check definition + * + * @param string $code The configuration check code (three digit number) + * @param string $severity The severity (low, medium, high, critical) + * @param string $description The description key for this configuration check + * @param null $callback The callback used to determine the status of the configuration check + * + * @return void + */ + public function addConfigurationCheckDefinition($code, $severity = 'low', $description = null, $callback = null) + { + if (!is_callable($callback)) + { + $callback = array($this, 'q' . $code); + } + + if (empty($description)) + { + $description = 'COM_AKEEBA_CPANEL_WARNING_Q' . $code; + } + + $newConfigurationCheck = array( + 'code' => $code, + 'severity' => $severity, + 'description' => $description, + 'callback' => $callback, + ); + + $this->configurationChecks[$code] = $newConfigurationCheck; + } + + /** + * Remove a configuration check definition + * + * @param string $code The code of the configuration check to remove + * + * @return void + */ + public function removeConfigurationCheckDefinition($code) + { + if (isset($this->configurationChecks[$code])) + { + unset($this->configurationChecks[$code]); + } + } + + /** + * Clear the configuration check definitions + * + * @return void + */ + public function clearConfigurationCheckDefinitions() + { + $this->configurationChecks = array(); + } + + /** + * Runs the configuration check scripts. These are potential problems related to server + * configuration, out of Akeeba's control. They are intended to give the user a + * chance to fix them before they cause the backup to fail. + * + * Numbering scheme: + * Q0xx No-go errors + * Q1xx Critical system configuration errors + * Q2xx Medium and low system configuration warnings + * Q3xx Critical software configuration errors + * Q4xx Medium and low component configuration warnings + * + * @param boolean $low_priority Should I include low priority quirks? + * @param string $help_url_template The sprintf template from creating a help URL from a config check code + * + * @return array + */ + public function getDetailedStatus($low_priority = false, $help_url_template = 'https://www.akeebabackup.com/documentation/warnings/q%s.html') + { + static $detailedStatus = null; + + if (is_null($detailedStatus)) + { + $detailedStatus = array(); + + foreach ($this->configurationChecks as $quirkDef) + { + if (!$low_priority && ($quirkDef['severity'] == 'low')) + { + continue; + } + + $this->checkConfiguration($detailedStatus, $quirkDef, $help_url_template); + } + } + + return $detailedStatus; + } + + /** + * Make a configuration check and adds it to the list if it raises a warning / error + * + * @param array $detailedStatus The configuration checks status array + * @param array $quirkDef The configuration check definition + * @param string $help_url_template The sprintf template from creating a help URL from a quirk code + * + * @return void + */ + protected function checkConfiguration(&$detailedStatus, $quirkDef, $help_url_template) + { + if (call_user_func($quirkDef['callback'])) + { + $description = Platform::getInstance()->translate($quirkDef['description']); + + $detailedStatus[(string)$quirkDef['code']] = array( + 'code' => $quirkDef['code'], + 'severity' => $quirkDef['severity'], + 'description' => $description, + 'help_url' => sprintf($help_url_template, $quirkDef['code']), + ); + } + } + + /** + * Q001 - HIGH - Output directory unwriteable + * + * @return bool + */ + private function q001() + { + $status = $this->getFolderStatus(); + + return !$status['output']; + } + + /** + * Q003 - HIGH - Backup output or temporary set to site's root + * + * @return bool + */ + private function q003() + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + + $registry = Factory::getConfiguration(); + $outdir = $registry->get('akeeba.basic.output_directory'); + + foreach ($stock_dirs as $macro => $replacement) + { + $outdir = str_replace($macro, $replacement, $outdir); + } + + $outdir_real = @realpath($outdir); + + if (!empty($outdir_real)) + { + $outdir = $outdir_real; + } + + $siteroot = Platform::getInstance()->get_site_root(); + $siteroot_real = @realpath($siteroot); + + if (!empty($siteroot_real)) + { + $siteroot = $siteroot_real; + } + + return ($siteroot == $outdir); + } + + /** + * Q004 - HIGH - Free memory too low + * + * @return bool + */ + private function q004() + { + // If we can't figure this out, don't report a problem. It doesn't + // really matter, as the backup WILL crash eventually. + if (!function_exists('ini_get')) + { + return false; + } + + $memLimit = ini_get("memory_limit"); + $memLimit = $this->_return_bytes($memLimit); + + if ($memLimit <= 0) + { + return false; + } + + // No limit? + $availableRAM = $memLimit - memory_get_usage(); + + // We need at least 12Mb of free memory + return ($availableRAM <= (12 * 1024 * 1024)); + } + + /** + * Q101 - HIGH - open_basedir on output directory + * + * @return bool + */ + private function q101() + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + + // Get output writable status + $registry = Factory::getConfiguration(); + $outdir = $registry->get('akeeba.basic.output_directory'); + + foreach ($stock_dirs as $macro => $replacement) + { + $outdir = str_replace($macro, $replacement, $outdir); + } + + return $this->checkOpenBasedirs($outdir); + } + + /** + * Q103 - HIGH - Less than 10" of max_execution_time with PHP Safe Mode enabled + * + * @return bool + */ + private function q103() + { + $exectime = ini_get('max_execution_time'); + $safemode = ini_get('safe_mode'); + + if (!$safemode) + { + return false; + } + + if (!is_numeric($exectime)) + { + return false; + } + + if ($exectime <= 0) + { + return false; + } + + return $exectime < 10; + } + + /** + * Q104 - HIGH - Temp directory is the same as the site's root + * + * @return bool + */ + private function q104() + { + + $siteroot = Platform::getInstance()->get_site_root(); + $siteroot_real = @realpath($siteroot); + + if (!empty($siteroot_real)) + { + $siteroot = $siteroot_real; + } + + $stockDirs = Platform::getInstance()->get_stock_directories(); + $temp_directory = $stockDirs['[SITETMP]']; + $temp_directory = @realpath($temp_directory); + + if (empty($temp_directory)) + { + $temp_directory = $siteroot; + } + + return ($siteroot == $temp_directory); + } + + /** + * Q106 - HIGH - Table name prefix contains uppercase characters + * + * @return bool + */ + private function q106() + { + $filters = Factory::getFilters(); + $databases = $filters->getInclusions('db'); + + foreach ($databases as $db) + { + if (!isset($db['prefix'])) + { + continue; + } + + if (preg_match('/[A-Z]/', $db['prefix'])) + { + return true; + } + } + + return false; + } + + /** + * Q201 - MEDIUM - Outdated PHP version. + * + * We currently check for PHP lower than 5.5. + * + * @return bool + */ + private function q201() + { + return version_compare(PHP_VERSION, '5.5.0', 'lt'); + } + + /** + * Q202 - MED - CRC problems with hash extension not present + * + * @return bool + */ + private function q202() + { + $registry = Factory::getConfiguration(); + $archiver = $registry->get('akeeba.advanced.archiver_engine'); + + if ($archiver != 'zip') + { + return false; + } + + return !function_exists('hash_file'); + } + + /** + * Q203 - MED - Default output directory in use + * + * @return bool + */ + private function q203() + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + + $registry = Factory::getConfiguration(); + $outdir = $registry->get('akeeba.basic.output_directory'); + + foreach ($stock_dirs as $macro => $replacement) + { + $outdir = str_replace($macro, $replacement, $outdir); + } + + $default = $stock_dirs['[DEFAULT_OUTPUT]']; + + $outdir = Factory::getFilesystemTools()->TranslateWinPath($outdir); + $default = Factory::getFilesystemTools()->TranslateWinPath($default); + + return $outdir == $default; + } + + /** + * Q204 - MED - Disabled functions may affect operation + * + * @return bool + */ + private function q204() + { + $disabled = ini_get('disabled_functions'); + + return (!empty($disabled)); + } + + /** + * Q401 - LOW - ZIP format selected + * + * @return bool + */ + private function q401() + { + $registry = Factory::getConfiguration(); + $archiver = $registry->get('akeeba.advanced.archiver_engine'); + + return $archiver == 'zip'; + } + + /** + * Checks if a path is restricted by open_basedirs + * + * @param string $check The path to check + * + * @return bool True if the path is restricted (which is bad) + */ + public function checkOpenBasedirs($check) + { + static $paths; + + if (empty($paths)) + { + $open_basedir = ini_get('open_basedir'); + + if (empty($open_basedir)) + { + return false; + } + + $delimiter = strpos($open_basedir, ';') !== false ? ';' : ':'; + $paths_temp = explode($delimiter, $open_basedir); + + // Some open_basedirs are using environemtn variables + $paths = array(); + + foreach ($paths_temp as $path) + { + if (array_key_exists($path, $_ENV)) + { + $paths[] = $_ENV[$path]; + } + else + { + $paths[] = $path; + } + } + } + + if (empty($paths)) + { + return false; // no restrictions + } + else + { + $newcheck = @realpath($check); // Resolve symlinks, like PHP does + + if (!($newcheck === false)) + { + $check = $newcheck; + } + + $included = false; + + foreach ($paths as $path) + { + $newpath = @realpath($path); + + if (!($newpath === false)) + { + $path = $newpath; + } + + if (strlen($check) >= strlen($path)) + { + // Only check if the path to check is longer than the inclusion path. + // Otherwise, I guarantee it's not included!! + // If the path to check begins with an inclusion path, it's permitted. Easy, huh? + if (substr($check, 0, strlen($path)) == $path) + { + $included = true; + } + } + } + + return !$included; + } + } + + private function _return_bytes($setting) + { + $val = trim($setting); + $last = strtolower(substr($val, -1)); + $val = substr($val, 0, -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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Encrypt.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Encrypt.php new file mode 100644 index 00000000..20e7311e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Encrypt.php @@ -0,0 +1,996 @@ +AddRoundKey($state, $w, 0, $Nb); + + for ($round = 1; $round < $Nr; $round++) + { + // apply Nr rounds + $state = $this->SubBytes($state, $Nb); + $state = $this->ShiftRows($state, $Nb); + $state = $this->MixColumns($state, $Nb); + $state = $this->AddRoundKey($state, $w, $round, $Nb); + } + + $state = $this->SubBytes($state, $Nb); + $state = $this->ShiftRows($state, $Nb); + $state = $this->AddRoundKey($state, $w, $Nr, $Nb); + + $output = array(4 * $Nb); // convert state to 1-d array before returning [�3.4] + + for ($i = 0; $i < 4 * $Nb; $i++) + { + $output[$i] = $state[$i % 4][(int)floor($i / 4)]; + } + + return $output; + } + + protected function AddRoundKey($state, $w, $rnd, $Nb) + { + // xor Round Key into state S [�5.1.4] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $state[$r][$c] ^= $w[$rnd * 4 + $c][$r]; + } + } + + return $state; + } + + protected function SubBytes($s, $Nb) + { + // apply SBox to state S [�5.1.1] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $s[$r][$c] = $this->Sbox[$s[$r][$c]]; + } + } + + return $s; + } + + protected function ShiftRows($s, $Nb) + { + // shift row r of state S left by r bytes [�5.1.2] + $t = array(4); + + for ($r = 1; $r < 4; $r++) + { + // shift into temp copy + for ($c = 0; $c < 4; $c++) + { + $t[$c] = $s[$r][($c + $r) % $Nb]; + } + + // and copy back + for ($c = 0; $c < 4; $c++) + { + $s[$r][$c] = $t[$c]; + } + + } + // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES): + + return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf + } + + protected function MixColumns($s, $Nb) + { + // combine bytes of each col of state S [�5.1.3] + for ($c = 0; $c < 4; $c++) + { + $a = array(4); // 'a' is a copy of the current column from 's' + $b = array(4); // 'b' is a�{02} in GF(2^8) + + for ($i = 0; $i < 4; $i++) + { + $a[$i] = $s[$i][$c]; + $b[$i] = $s[$i][$c] & 0x80 ? $s[$i][$c] << 1 ^ 0x011b : $s[$i][$c] << 1; + } + + // a[n] ^ b[n] is a�{03} in GF(2^8) + $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3 + $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3 + $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3 + $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3 + } + + return $s; + } + + /** + * Key expansion for Rijndael Cipher(): performs key expansion on cipher key + * to generate a key schedule + * + * @param array $key cipher key byte-array (16 bytes) + * + * @return array key schedule as 2D byte-array (Nr+1 x Nb bytes) + */ + public function KeyExpansion($key) + { + // generate Key Schedule from Cipher Key [�5.2] + $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) + $Nk = count($key) / 4; // key length (in words): 4/6/8 for 128/192/256-bit keys + $Nr = $Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys + + $w = array(); + $temp = array(); + + for ($i = 0; $i < $Nk; $i++) + { + $r = array($key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]); + $w[$i] = $r; + } + + for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++) + { + $w[(int)$i] = array(); + + for ($t = 0; $t < 4; $t++) + { + $temp[$t] = $w[(int)$i - 1][$t]; + } + + if ($i % $Nk == 0) + { + $temp = $this->SubWord($this->RotWord($temp)); + + for ($t = 0; $t < 4; $t++) + { + $temp[$t] ^= $this->Rcon[(int)($i / $Nk)][$t]; + } + } + elseif ($Nk > 6 && $i % $Nk == 4) + { + $temp = $this->SubWord($temp); + } + + for ($t = 0; $t < 4; $t++) + { + $w[(int)$i][$t] = $w[(int)$i - $Nk][$t] ^ $temp[$t]; + } + } + + return $w; + } + + protected function SubWord($w) + { + // apply SBox to 4-byte word w + for ($i = 0; $i < 4; $i++) + { + $w[$i] = $this->Sbox[$w[$i]]; + } + + return $w; + } + + protected function RotWord($w) + { + // rotate 4-byte word w left by one byte + $tmp = $w[0]; + + for ($i = 0; $i < 3; $i++) + { + $w[$i] = $w[$i + 1]; + } + + $w[3] = $tmp; + + return $w; + } + + /* + * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints + * + * @param a number to be shifted (32-bit integer) + * @param b number of bits to shift a to the right (0..31) + * @return a right-shifted and zero-filled by b bits + */ + protected function urs($a, $b) + { + $a &= 0xffffffff; + $b &= 0x1f; // (bounds check) + + if ($a & 0x80000000 && $b > 0) + { + // if left-most bit set + $a = ($a >> 1) & 0x7fffffff; // right-shift one bit & clear left-most bit + $a = $a >> ($b - 1); // remaining right-shifts + } + else + { + // otherwise + $a = ($a >> $b); // use normal right-shift + } + + return $a; + } + + /** + * Encrypt a text using AES encryption in Counter mode of operation + * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf + * + * Unicode multi-byte character safe + * + * @param string $plaintext source text to be encrypted + * @param string $password the password to use to generate a key + * @param int $nBits number of bits to be used in the key (128, 192, or 256) + * + * @return string encrypted text + */ + public function AESEncryptCtr($plaintext, $password, $nBits) + { + $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES + + // standard allows 128/192/256 bit keys + if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) + { + return ''; + } + + // note PHP (5) gives us plaintext and password in UTF8 encoding! + + // use AES itself to encrypt password to get cipher key (using plain password as source for + // key expansion) - gives us well encrypted key + $nBytes = $nBits / 8; // no bytes in key + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $key = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes)); + $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long + + // initialise counter block (NIST SP800-38A �B.2): millisecond time-stamp for nonce in + // 1st 8 bytes, block counter in 2nd 8 bytes + $counterBlock = array(); + $nonce = floor(microtime(true) * 1000); // timestamp: milliseconds since 1-Jan-1970 + $nonceSec = floor($nonce / 1000); + $nonceMs = $nonce % 1000; + + // encode nonce with seconds in 1st 4 bytes, and (repeated) ms part filling 2nd 4 bytes + for ($i = 0; $i < 4; $i++) + { + $counterBlock[$i] = $this->urs($nonceSec, $i * 8) & 0xff; + } + + for ($i = 0; $i < 4; $i++) + { + $counterBlock[$i + 4] = $nonceMs & 0xff; + } + + // and convert it to a string to go on the front of the ciphertext + $ctrTxt = ''; + + for ($i = 0; $i < 8; $i++) + { + $ctrTxt .= chr($counterBlock[$i]); + } + + // generate key schedule - an expansion of the key into distinct Key Rounds for each round + $keySchedule = $this->KeyExpansion($key); + + $blockCount = ceil(strlen($plaintext) / $blockSize); + $ciphertxt = array(); // ciphertext as array of strings + + for ($b = 0; $b < $blockCount; $b++) + { + // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) + // done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB) + for ($c = 0; $c < 4; $c++) + { + $counterBlock[15 - $c] = $this->urs($b, $c * 8) & 0xff; + } + + for ($c = 0; $c < 4; $c++) + { + $counterBlock[15 - $c - 4] = $this->urs($b / 0x100000000, $c * 8); + } + + $cipherCntr = $this->Cipher($counterBlock, $keySchedule); // -- encrypt counter block -- + + // block size is reduced on final block + $blockLength = $b < $blockCount - 1 ? $blockSize : (strlen($plaintext) - 1) % $blockSize + 1; + $cipherByte = array(); + + for ($i = 0; $i < $blockLength; $i++) + { // -- xor plaintext with ciphered counter byte-by-byte -- + $cipherByte[$i] = $cipherCntr[$i] ^ ord(substr($plaintext, $b * $blockSize + $i, 1)); + $cipherByte[$i] = chr($cipherByte[$i]); + } + + $ciphertxt[$b] = implode('', $cipherByte); // escape troublesome characters in ciphertext + } + + // implode is more efficient than repeated string concatenation + $ciphertext = $ctrTxt . implode('', $ciphertxt); + $ciphertext = base64_encode($ciphertext); + + return $ciphertext; + } + + /** + * Decrypt a text encrypted by AES in counter mode of operation + * + * @param string $ciphertext source text to be decrypted + * @param string $password the password to use to generate a key + * @param int $nBits number of bits to be used in the key (128, 192, or 256) + * + * @return string decrypted text + */ + public function AESDecryptCtr($ciphertext, $password, $nBits) + { + $blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES + + // standard allows 128/192/256 bit keys + if (!($nBits == 128 || $nBits == 192 || $nBits == 256)) + { + return ''; + } + + $ciphertext = base64_decode($ciphertext); + + // use AES to encrypt password (mirroring encrypt routine) + $nBytes = $nBits / 8; // no bytes in key + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $key = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes)); + $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long + + // recover nonce from 1st element of ciphertext + $counterBlock = array(); + $ctrTxt = substr($ciphertext, 0, 8); + + for ($i = 0; $i < 8; $i++) + { + $counterBlock[$i] = ord(substr($ctrTxt, $i, 1)); + } + + // generate key schedule + $keySchedule = $this->KeyExpansion($key); + + // separate ciphertext into blocks (skipping past initial 8 bytes) + $nBlocks = ceil((strlen($ciphertext) - 8) / $blockSize); + $ct = array(); + + for ($b = 0; $b < $nBlocks; $b++) + { + $ct[$b] = substr($ciphertext, 8 + $b * $blockSize, 16); + } + + $ciphertext = $ct; // ciphertext is now array of block-length strings + + // plaintext will get generated block-by-block into array of block-length strings + $plaintxt = array(); + + for ($b = 0; $b < $nBlocks; $b++) + { + // set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) + for ($c = 0; $c < 4; $c++) + { + $counterBlock[15 - $c] = $this->urs($b, $c * 8) & 0xff; + } + + for ($c = 0; $c < 4; $c++) + { + $counterBlock[15 - $c - 4] = $this->urs(($b + 1) / 0x100000000 - 1, $c * 8) & 0xff; + } + + $cipherCntr = $this->Cipher($counterBlock, $keySchedule); // encrypt counter block + + $plaintxtByte = array(); + + for ($i = 0; $i < strlen($ciphertext[$b]); $i++) + { + // -- xor plaintext with ciphered counter byte-by-byte -- + $plaintxtByte[$i] = $cipherCntr[$i] ^ ord(substr($ciphertext[$b], $i, 1)); + $plaintxtByte[$i] = chr($plaintxtByte[$i]); + } + + $plaintxt[$b] = implode('', $plaintxtByte); + } + + // join array of blocks into single plaintext string + $plaintext = implode('', $plaintxt); + + return $plaintext; + } + + /** + * AES encryption in CBC mode. This is the standard mode (the CTR methods + * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). + * The data length is tucked as a 32-bit unsigned integer (little endian) + * after the ciphertext. It supports AES-128 only. + * + * @since 3.0.1 + * @author Nicholas K. Dionysopoulos + * + * @param string $plaintext The data to encrypt + * @param string $password Encryption password + * + * @return string The ciphertext + */ + public function AESEncryptCBC($plaintext, $password) + { + $adapter = $this->getAdapter(); + + if (!$adapter->isSupported()) + { + return false; + } + + // Get encryption parameters + $rand = new RandomValue(); + $params = $this->getKeyDerivationParameters(); + $useStaticSalt = $params['useStaticSalt']; + $keySizeBytes = $params['keySize']; + + if ($useStaticSalt) + { + $key = $this->getStaticSaltExpandedKey($password); + } + else + { + // Create a salt and derive a key from the password using PBKDF2 + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $salt = $rand->generate(64); + $key = $this->pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); + } + + + // Also create a new, random IV + $iv = $rand->generate($keySizeBytes); + + // The ciphertext is the encrypted string... + $ciphertext = $adapter->encrypt($plaintext, $key, $iv); + + // ...minus the IV which was placed in front + $ciphertext = substr($ciphertext, $keySizeBytes); + + if (!$useStaticSalt) + { + // ...plus the PBKDF2 setup values at the end (68 bytes)... + $ciphertext .= 'JPST' . $salt; + } + + // ...plus the IV at the end (20 bytes)... + $ciphertext .= 'JPIV' . $iv; + + // ...plus the plaintext length (4 bytes). + $ciphertext .= pack('V', strlen($plaintext)); + + return $ciphertext; + } + + /** + * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static + * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block + * to minimize the risk of attacks against the password. + * + * @return array + */ + public function getKeyDerivationParameters() + { + return array( + 'keySize' => 16, + 'algorithm' => $this->pbkdf2Algorithm, + 'iterations' => $this->pbkdf2Iterations, + 'useStaticSalt' => $this->pbkdf2UseStaticSalt, + 'staticSalt' => $this->pbkdf2StaticSalt, + ); + } + + /** + * AES decryption in CBC mode. This is the standard mode (the CTR methods + * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). + * + * It supports AES-128 only. It assumes that the last 4 bytes + * contain a little-endian unsigned long integer representing the unpadded + * data length. + * + * @since 3.0.1 + * @author Nicholas K. Dionysopoulos + * + * @param string $ciphertext The data to encrypt + * @param string $password Encryption password + * + * @return string The plaintext + */ + public function AESDecryptCBC($ciphertext, $password) + { + $adapter = $this->getAdapter(); + + if (!$adapter->isSupported()) + { + return false; + } + + // Read the data size + $data_size = unpack('V', substr($ciphertext, -4)); + + // Do I have a PBKDF2 salt? + $salt = substr($ciphertext, -92, 68); + $rightStringLimit = -4; + + $params = $this->getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $useStaticSalt = $params['useStaticSalt']; + + if (substr($salt, 0, 4) == 'JPST') + { + // We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes + // (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the + // uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the + // format specification). + $salt = substr($salt, 4); + $rightStringLimit -= 68; + + $key = $this->pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); + } + elseif ($useStaticSalt) + { + // We have a static salt. Use it for PBKDF2. + $key = $this->getStaticSaltExpandedKey($password); + } + else + { + // Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD. + $key = $this->expandKey($password); + } + + // Try to get the IV from the data + $iv = substr($ciphertext, -24, 20); + + if (substr($iv, 0, 4) == 'JPIV') + { + // We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes + // (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length) + $iv = substr($iv, 4); + $rightStringLimit -= 20; + } + else + { + // No stored IV. Do it the dumb way. + $iv = $this->createTheWrongIV($password); + } + + // Decrypt + $plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key); + + // Trim padding, if necessary + if (strlen($plaintext) > $data_size) + { + $plaintext = substr($plaintext, 0, $data_size); + } + + return $plaintext; + } + + /** + * That's the old way of creating an IV that's definitely not cryptographically sound. + * + * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA + * + * @param string $password The raw password from which we create an IV in a super bozo way + * + * @return string A 16-byte IV string + * + * @since 4.6.0 + * @author Nicholas K. Dionysopoulos + */ + function createTheWrongIV($password) + { + static $ivs = array(); + + $key = md5($password); + + if (!isset($ivs[$key])) + { + // Create an Initialization Vector (IV) based on the password, using the same technique as for the key + $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $iv = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes)); + $newIV = ''; + + foreach ($iv as $int) + { + $newIV .= chr($int); + } + + $ivs[$key] = $newIV; + } + + return $ivs[$key]; + } + + /** + * Expand the password to an appropriate 128-bit encryption key. THIS CODE IS OBSOLETE. DO NOT USE. + * + * @param string $password + * + * @return string + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public function expandKey($password) + { + // Try to fetch cached key or create it if it doesn't exist + $nBits = 128; + $lookupKey = md5($password . '-' . $nBits); + + if (array_key_exists($lookupKey, $this->passwords)) + { + $key = $this->passwords[$lookupKey]; + + return $key; + } + + // use AES itself to encrypt password to get cipher key (using plain password as source for + // key expansion) - gives us well encrypted key. + $nBytes = $nBits / 8; // Number of bytes in key + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $key = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes)); + $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long + $newKey = ''; + + foreach ($key as $int) + { + $newKey .= chr($int); + } + + $key = $newKey; + + $this->passwords[$lookupKey] = $key; + + return $key; + } + + /** + * Returns the correct AES-128 CBC encryption adapter + * + * @return AdapterInterface + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public function getAdapter() + { + static $adapter = null; + + if (is_object($adapter) && ($adapter instanceof AdapterInterface)) + { + return $adapter; + } + + $adapter = new OpenSSL(); + + if (!$adapter->isSupported()) + { + $adapter = new Mcrypt(); + } + + return $adapter; + } + + /** + * Returns the length of a string in BYTES, not characters + * + * @param string $string The string to get the length for + * + * @return int The size in BYTES + */ + public function stringLength($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Attempt to use mbstring for getting parts of strings + * + * @param string $string + * @param int $start + * @param int|null $length + * + * @return string + */ + public function subString($string, $start, $length = null) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : + substr($string, $start, $length); + } + + /** + * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt + * + * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt + * + * This implementation of PBKDF2 was originally created by https://defuse.ca + * With improvements by http://www.variations-of-shadow.com + * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster) + * + * @param string $password The password. + * @param string $salt A salt that is unique to the password. + * @param string $algorithm The hash algorithm to use. Default is sha1. + * @param int $count Iteration count. Higher is better, but slower. Default: 1000. + * @param int $key_length The length of the derived key in bytes. + * + * @return string A string of $key_length bytes + */ + public function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16) + { + if (function_exists("hash_pbkdf2")) + { + return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true); + } + + $hash_length = $this->stringLength(hash($algorithm, "", true)); + $block_count = ceil($key_length / $hash_length); + + $output = ""; + + for ($i = 1; $i <= $block_count; $i++) + { + // $i encoded as 4 bytes, big endian. + $last = $salt . pack("N", $i); + + // First iteration + $xorResult = hash_hmac($algorithm, $last, $password, true); + $last = $xorResult; + + // Perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) + { + $last = hash_hmac($algorithm, $last, $password, true); + $xorResult ^= $last; + } + + $output .= $xorResult; + } + + return $this->subString($output, 0, $key_length); + } + + /** + * @return string + */ + public function getPbkdf2Algorithm() + { + return $this->pbkdf2Algorithm; + } + + /** + * @param string $pbkdf2Algorithm + * @return Encrypt + */ + public function setPbkdf2Algorithm($pbkdf2Algorithm) + { + $this->pbkdf2Algorithm = $pbkdf2Algorithm; + + return $this; + } + + /** + * @return int + */ + public function getPbkdf2Iterations() + { + return $this->pbkdf2Iterations; + } + + /** + * @param int $pbkdf2Iterations + * @return Encrypt + */ + public function setPbkdf2Iterations($pbkdf2Iterations) + { + $this->pbkdf2Iterations = $pbkdf2Iterations; + + return $this; + } + + /** + * @return int + */ + public function getPbkdf2UseStaticSalt() + { + return $this->pbkdf2UseStaticSalt; + } + + /** + * @param int $pbkdf2UseStaticSalt + * @return Encrypt + */ + public function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt) + { + $this->pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt; + + return $this; + } + + /** + * @return string + */ + public function getPbkdf2StaticSalt() + { + return $this->pbkdf2StaticSalt; + } + + /** + * @param string $pbkdf2StaticSalt + * @return Encrypt + */ + public function setPbkdf2StaticSalt($pbkdf2StaticSalt) + { + $this->pbkdf2StaticSalt = $pbkdf2StaticSalt; + + return $this; + } + + /** + * Get the expanded key from the user supplied password using a static salt. The results are cached for performance + * reasons. + * + * @param string $password The user-supplied password, UTF-8 encoded. + * + * @return string The expanded key + */ + public function getStaticSaltExpandedKey($password) + { + $params = $this->getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $staticSalt = $params['staticSalt']; + + $lookupKey = "PBKDF2-$algorithm-$iterations-" . md5($password . $staticSalt); + + if (!array_key_exists($lookupKey, $this->passwords)) + { + $this->passwords[$lookupKey] = $this->pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes); + } + + return $this->passwords[$lookupKey]; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/EngineParameters.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/EngineParameters.php new file mode 100644 index 00000000..9149b3ed --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/EngineParameters.php @@ -0,0 +1,937 @@ +scripting)) + { + $ini_file_name = Factory::getAkeebaRoot() . '/Core/scripting.ini'; + + if (@file_exists($ini_file_name)) + { + $raw_data = ParseIni::parse_ini_file($ini_file_name, false); + $domain_keys = explode('|', $raw_data['volatile.akeebaengine.domains']); + $domains = array(); + + foreach ($domain_keys as $key) + { + $record = array( + 'domain' => $raw_data['volatile.domain.' . $key . '.domain'], + 'class' => $raw_data['volatile.domain.' . $key . '.class'], + 'text' => $raw_data['volatile.domain.' . $key . '.text'] + ); + $domains[$key] = $record; + } + + $script_keys = explode('|', $raw_data['volatile.akeebaengine.scripts']); + $scripts = array(); + + foreach ($script_keys as $key) + { + $record = array( + 'chain' => explode('|', $raw_data['volatile.scripting.' . $key . '.chain']), + 'text' => $raw_data['volatile.scripting.' . $key . '.text'] + ); + $scripts[$key] = $record; + } + + $this->scripting = array( + 'domains' => $domains, + 'scripts' => $scripts, + 'data' => $raw_data + ); + } + else + { + $this->scripting = array(); + } + } + + return $this->scripting; + } + + /** + * Imports the volatile scripting parameters to the registry + * + * @return void + */ + public function importScriptingToRegistry() + { + $scripting = $this->loadScripting(); + $configuration = Factory::getConfiguration(); + $configuration->mergeArray($scripting['data'], false); + } + + /** + * Returns a volatile scripting parameter for the active backup type + * + * @param string $key The relative key, e.g. core.createarchive + * @param mixed $default Default value + * + * @return mixed The scripting parameter's value + */ + public function getScriptingParameter($key, $default = null) + { + $configuration = Factory::getConfiguration(); + + if (is_null($this->activeType)) + { + $this->activeType = $configuration->get('akeeba.basic.backup_type', 'full'); + } + + return $configuration->get('volatile.scripting.' . $this->activeType . '.' . $key, $default); + } + + /** + * Returns an array with domain keys and domain class names for the current + * backup type. The idea is that shifting this array walks through the backup + * process. When the array is empty, the backup is done. + * + * Each element of the array is an array with two keys: domain and class. + * + * @return array + */ + public function getDomainChain() + { + $configuration = Factory::getConfiguration(); + $script = $configuration->get('akeeba.basic.backup_type', 'full'); + + $scripting = $this->loadScripting(); + $domains = $scripting['domains']; + $keys = $scripting['scripts'][$script]['chain']; + + $result = array(); + foreach ($keys as $domain_key) + { + $result[] = array( + 'domain' => $domains[$domain_key]['domain'], + 'class' => $domains[$domain_key]['class'] + ); + } + + return $result; + } + + /** + * Append a path to the end of the paths list for a specific section + * + * @param string $path Absolute filesystem path to add + * @param string $section The section to add it to (gui, engine, installer, filters) + * + * @return void + */ + public function addPath($path, $section = 'gui') + { + $path = Factory::getFilesystemTools()->TranslateWinPath($path); + + // If the array is empty, populate with the defaults + if (!array_key_exists($section, $this->enginePartPaths)) + { + $this->getEnginePartPaths($section); + } + + // If the path doesn't already exist, add it + if (!in_array($path, $this->enginePartPaths[$section])) + { + $this->enginePartPaths[$section][] = $path; + } + } + + /** + * Add a path to the beginning of the paths list for a specific section + * + * @param string $path Absolute filesystem path to add + * @param string $section The section to add it to (gui, engine, installer, filters) + * + * @return void + */ + public function prependPath($path, $section = 'gui') + { + $path = Factory::getFilesystemTools()->TranslateWinPath($path); + + // If the array is empty, populate with the defaults + if (!array_key_exists($section, $this->enginePartPaths)) + { + $this->getEnginePartPaths($section); + } + + // If the path doesn't already exist, add it + if (!in_array($path, $this->enginePartPaths[$section])) + { + array_unshift($this->enginePartPaths[$section], $path); + } + } + + /** + * Get the paths for a specific section + * + * @param string $section The section to get the path list for (engine, installer, gui, filter) + * + * @return array + */ + public function getEnginePartPaths($section = 'gui') + { + // Create the key if it's not already present + if (!array_key_exists($section, $this->enginePartPaths)) + { + $this->enginePartPaths[$section] = array(); + } + + // Add the defaults if the list is empty + if (empty($this->enginePartPaths[$section])) + { + switch ($section) + { + case 'engine': + $this->enginePartPaths[$section] = array( + Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot()), + ); + break; + + case 'installer': + $this->enginePartPaths[$section] = array( + Factory::getFilesystemTools()->TranslateWinPath(Platform::getInstance()->get_installer_images_path()) + ); + break; + + case 'gui': + // Add core GUI definitions + $this->enginePartPaths[$section] = array( + Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Core') + ); + + // Add platform GUI definition files + $platform_paths = Platform::getInstance()->getPlatformDirectories(); + + foreach ($platform_paths as $p) + { + $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config'); + + $pro = defined('AKEEBA_PRO') && AKEEBA_PRO; + $pro = defined('AKEEBABACKUP_PRO') ? (AKEEBABACKUP_PRO ? true : false) : $pro; + + if ($pro) + { + $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config/Pro'); + } + } + break; + + case 'filter': + $this->enginePartPaths[$section] = array( + Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Platform/Filter/Stack'), + Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Filter/Stack'), + ); + + $platform_paths = Platform::getInstance()->getPlatformDirectories(); + + foreach ($platform_paths as $p) + { + $this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Filter/Stack'); + } + + break; + } + } + + return $this->enginePartPaths[$section]; + } + + /** + * Returns a hash list of Akeeba engines and their data. Each entry has the engine + * name as key and contains two arrays, under the 'information' and 'parameters' keys. + * + * @param string $engine_type The engine type to return information for + * + * @return array + */ + public function getEnginesList($engine_type) + { + $engine_type = ucfirst($engine_type); + + // Try to serve cached data first + if (isset($this->engine_list[$engine_type])) + { + return $this->engine_list[$engine_type]; + } + + // Find absolute path to normal and plugins directories + $temp = $this->getEnginePartPaths('engine'); + $path_list = array(); + + foreach ($temp as $path) + { + $path_list[] = $path . '/' . $engine_type; + } + + // Initialize the array where we store our data + $this->engine_list[$engine_type] = array(); + + // Loop for the paths where engines can be found + foreach ($path_list as $path) + { + if (!@is_dir($path)) + { + continue; + } + + if (!@is_readable($path)) + { + continue; + } + + $di = new \DirectoryIterator($path); + + /** @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') + { + continue; + } + + $bare_name = ucfirst($file->getBasename('.ini')); + + // 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 + if (preg_match('/[^A-Za-z0-9]/', $bare_name)) + { + continue; + } + + $information = array(); + $parameters = array(); + + $this->parseEngineINI($file->getRealPath(), $information, $parameters); + + $this->engine_list[$engine_type][lcfirst($bare_name)] = array + ( + 'information' => $information, + 'parameters' => $parameters + ); + } + } + + return $this->engine_list[$engine_type]; + } + + /** + * Parses the GUI INI files and returns an array of groups and their data + * + * @return array + */ + public function getGUIGroups() + { + // Try to serve cached data first + if (!empty($this->gui_list) && is_array($this->gui_list)) + { + if (count($this->gui_list) > 0) + { + return $this->gui_list; + } + } + + // Find absolute path to normal and plugins directories + $path_list = $this->getEnginePartPaths('gui'); + + // Initialize the array where we store our data + $this->gui_list = array(); + + // Loop for the paths where engines can be found + foreach ($path_list as $path) + { + if (!@is_dir($path)) + { + continue; + } + + if (!@is_readable($path)) + { + continue; + } + + $allINIs = array(); + $di = new \DirectoryIterator($path); + + /** @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') + { + continue; + } + + $allINIs[] = $file->getRealPath(); + } + + if (empty($allINIs)) + { + continue; + } + + // Sort GUI files alphabetically + asort($allINIs); + + // Include each GUI def file + foreach ($allINIs as $filename) + { + $information = array(); + $parameters = array(); + + $this->parseInterfaceINI($filename, $information, $parameters); + + // This effectively skips non-GUI INIs (e.g. the scripting INI) + if (!empty($information['description'])) + { + if (!isset($information['merge'])) + { + $information['merge'] = 0; + } + + $group_name = substr(basename($filename), 0, -4); + + $def = array( + 'information' => $information, + 'parameters' => $parameters + ); + + if (!$information['merge'] || !isset($this->gui_list[$group_name])) + { + $this->gui_list[$group_name] = $def; + } + else + { + $this->gui_list[$group_name]['information'] = array_merge($this->gui_list[$group_name]['information'], $def['information']); + $this->gui_list[$group_name]['parameters'] = array_merge($this->gui_list[$group_name]['parameters'], $def['parameters']); + } + } + } + } + + ksort($this->gui_list); + + // Push stack filter settings to the 03.filters section + $path_list = $this->getEnginePartPaths('filter'); + + // Loop for the paths where optional filters can be found + foreach ($path_list as $path) + { + if (!@is_dir($path)) + { + continue; + } + + if (!@is_readable($path)) + { + continue; + } + + // Store INI names in temp array because we'll sort based on filename (GUI order IS IMPORTANT!!) + $allINIs = array(); + + $di = new \DirectoryIterator($path); + + /** @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') + { + continue; + } + + $allINIs[] = $file->getRealPath(); + } + + if (empty($allINIs)) + { + continue; + } + + // Sort filter files alphabetically + asort($allINIs); + + // Include each filter def file + foreach ($allINIs as $filename) + { + $information = array(); + $parameters = array(); + + $this->parseInterfaceINI($filename, $information, $parameters); + + if (!array_key_exists('03.filters', $this->gui_list)) + { + $this->gui_list['03.filters'] = array('parameters' => array()); + } + + if (!array_key_exists('parameters', $this->gui_list['03.filters'])) + { + $this->gui_list['03.filters']['parameters'] = array(); + } + + if (!is_array($parameters)) + { + $parameters = array(); + } + + $this->gui_list['03.filters']['parameters'] = array_merge($this->gui_list['03.filters']['parameters'], $parameters); + } + } + + return $this->gui_list; + } + + /** + * Parses the installer INI files and returns an array of installers and their data + * + * @param boolean $forDisplay If true only returns the information relevant for displaying the GUI + * + * @return array + */ + public function getInstallerList($forDisplay = false) + { + // Try to serve cached data first + if (!empty($this->installer_list) && is_array($this->installer_list)) + { + if (count($this->installer_list) > 0) + { + return $this->installer_list; + } + } + + // Find absolute path to normal and plugins directories + $path_list = array( + Platform::getInstance()->get_installer_images_path() + ); + + // Initialize the array where we store our data + $this->installer_list = array(); + + // Loop for the paths where engines can be found + foreach ($path_list as $path) + { + if (!@is_dir($path)) + { + continue; + } + + if (!@is_readable($path)) + { + continue; + } + + $di = new \DirectoryIterator($path); + + /** @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') + { + continue; + } + + $data = ParseIni::parse_ini_file($file->getRealPath(), true); + + if ($forDisplay) + { + $innerData = reset($data); + + if (array_key_exists('listinoptions', $innerData)) + { + if ($innerData['listinoptions'] == 0) + { + continue; + } + } + } + + foreach ($data as $key => $values) + { + $this->installer_list[$key] = array(); + + foreach ($values as $key2 => $value) + { + $this->installer_list[$key][$key2] = $value; + } + } + } + } + + return $this->installer_list; + } + + /** + * Returns the JSON representation of the GUI definition and the associated values + * + * @return string + */ + public function getJsonGuiDefinition() + { + // Initialize the array which will be converted to JSON representation + $json_array = array( + 'engines' => array(), + 'installers' => array(), + 'gui' => array() + ); + + // Get a reference to the configuration + $configuration = Factory::getConfiguration(); + + // Get data for all engines + $engine_types = array( + 'archiver', + 'dump', + 'scan', + 'writer', + 'postproc', + ); + + foreach ($engine_types as $type) + { + $engines = $this->getEnginesList($type); + + $tempArray = array(); + $engineTitles = array(); + + foreach ($engines as $engine_name => $engine_data) + { + // Translate information + foreach ($engine_data['information'] as $key => $value) + { + switch ($key) + { + case 'title': + case 'description': + $value = Platform::getInstance()->translate($value); + break; + } + + $tempArray[$engine_name]['information'][$key] = $value; + + if ($key == 'title') + { + $engineTitles[$engine_name] = $value; + } + } + + // Process parameters + $parameters = array(); + + foreach ($engine_data['parameters'] as $param_key => $param) + { + $param['default'] = $configuration->get($param_key, $param['default'], false); + + foreach ($param as $option_key => $option_value) + { + // Translate title, description, enumkeys + switch ($option_key) + { + case 'title': + case 'description': + case 'labelempty': + case 'labelnotempty': + $param[$option_key] = Platform::getInstance()->translate($option_value); + break; + + case 'enumkeys': + $enumkeys = explode('|', $option_value); + $new_keys = array(); + foreach ($enumkeys as $old_key) + { + $new_keys[] = Platform::getInstance()->translate($old_key); + } + $param[$option_key] = implode('|', $new_keys); + break; + + default: + } + } + + $parameters[$param_key] = $param; + } + + // Add processed parameters + $tempArray[$engine_name]['parameters'] = $parameters; + } + + asort($engineTitles); + + foreach ($engineTitles as $engineName => $title) + { + $json_array['engines'][$type][$engineName] = $tempArray[$engineName]; + } + } + + // Get data for GUI elements + $json_array['gui'] = array(); + $groupdefs = $this->getGUIGroups(); + + foreach ($groupdefs as $group_ini => $definition) + { + $group_name = ''; + + if (isset($definition['information']) && isset($definition['information']['description'])) + { + $group_name = Platform::getInstance()->translate($definition['information']['description']); + } + + // Skip no-name groups + if (empty($group_name)) + { + continue; + } + + $parameters = array(); + + foreach ($definition['parameters'] as $param_key => $param) + { + $param['default'] = $configuration->get($param_key, $param['default'], false); + + foreach ($param as $option_key => $option_value) + { + // Translate title, description, enumkeys + switch ($option_key) + { + case 'title': + case 'description': + $param[$option_key] = Platform::getInstance()->translate($option_value); + break; + + case 'enumkeys': + $enumkeys = explode('|', $option_value); + $new_keys = array(); + foreach ($enumkeys as $old_key) + { + $new_keys[] = Platform::getInstance()->translate($old_key); + } + $param[$option_key] = implode('|', $new_keys); + break; + + default: + } + } + $parameters[$param_key] = $param; + } + $json_array['gui'][$group_name] = $parameters; + } + + // Get data for the installers + $json_array['installers'] = $this->getInstallerList(true); + + uasort($json_array['installers'], function($a, $b){ + if ($a['name'] == $b['name']) + { + return 0; + } + + return ($a['name'] < $b['name']) ? -1 : 1; + }); + + $json = json_encode($json_array); + + return $json; + } + + /** + * Parses an engine INI file returning two arrays, one with the general information + * of that engine and one with its configuration variables' definitions + * + * @param string $inifile Absolute path to engine INI file + * @param array $information [out] The engine information hash array + * @param array $parameters [out] The parameters hash array + * + * @return bool True if the file was loaded + */ + public function parseEngineINI($inifile, &$information, &$parameters) + { + if (!file_exists($inifile)) + { + return false; + } + + $information = array( + 'title' => '', + 'description' => '' + ); + + $parameters = array(); + + $inidata = ParseIni::parse_ini_file($inifile, true); + + foreach ($inidata as $section => $data) + { + if (is_array($data)) + { + if ($section == '_information') + { + // Parse information + foreach ($data as $key => $value) + { + $information[$key] = $value; + } + } + elseif (substr($section, 0, 1) != '_') + { + // Parse parameters + $newparam = array( + 'title' => '', + 'description' => '', + 'type' => 'string', + 'default' => '' + ); + + foreach ($data as $key => $value) + { + $newparam[$key] = $value; + } + $parameters[$section] = $newparam; + } + } + } + + return true; + } + + /** + * Parses a graphical interface INI file returning two arrays, one with the general + * information of that configuration section and one with its configuration variables' + * definitions. + * + * @param string $inifile Absolute path to engine INI file + * @param array $information [out] The GUI information hash array + * @param array $parameters [out] The parameters hash array + * + * @return bool True if the file was loaded + */ + public function parseInterfaceINI($inifile, &$information, &$parameters) + { + if (!file_exists($inifile)) + { + return false; + } + + $information = array( + 'description' => '' + ); + + $parameters = array(); + $inidata = ParseIni::parse_ini_file($inifile, true); + + foreach ($inidata as $section => $data) + { + if (is_array($data)) + { + if ($section == '_group') + { + // Parse information + foreach ($data as $key => $value) + { + $information[$key] = $value; + } + + continue; + } + + if (substr($section, 0, 1) != '_') + { + // Parse parameters + $newparam = array( + 'title' => '', + 'description' => '', + 'type' => 'string', + 'default' => '', + 'protected' => 0, + ); + + foreach ($data as $key => $value) + { + $newparam[$key] = $value; + } + + $parameters[$section] = $newparam; + } + } + } + + return true; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FactoryStorage.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FactoryStorage.php new file mode 100644 index 00000000..846918f7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FactoryStorage.php @@ -0,0 +1,289 @@ +setStorageEngine($storageEngine); + } + + /** + * Sets the storage engine which will be used + * + * @param string $engine The storage engine (currently only db or file can be specified) + */ + public function setStorageEngine($engine = null) + { + if (empty($engine)) + { + $config = Factory::getConfiguration(); + $usedb = $config->get('akeeba.core.usedbstorage', 0); + $engine = $usedb ? 'db' : 'file'; + } + + $this->storageEngine = $engine; + } + + /** + * Returns the name of the storage engine + * + * @return string + */ + public function getStorageEngine() + { + return $this->storageEngine; + } + + /** + * Returns the fully qualified path to the storage file + * + * @param string $tag + * + * @return string + */ + public function get_storage_filename($tag = null) + { + static $basepath = null; + + if ($this->storageEngine == 'db') + { + return empty($tag) ? 'storage' : $tag; + } + else + { + if (is_null($basepath)) + { + $registry = Factory::getConfiguration(); + $basepath = $registry->get('akeeba.basic.output_directory') . DIRECTORY_SEPARATOR; + } + + if (empty($tag)) + { + $tag = 'storage'; + } + + return $basepath . 'akeeba_' . $tag; + } + } + + /** + * Resets the storage. This method removes all stored values. + * @param null $tag + * + * @return bool True on success + */ + public function reset($tag = null) + { + switch ($this->storageEngine) + { + case 'file': + $filename = $this->get_storage_filename($tag); + + if (!is_file($filename) && !is_link($filename)) + { + return false; + } + + return @unlink($this->get_storage_filename($tag)); + + break; + + case 'db': + $dbtag = $this->get_storage_filename($tag); + $db = Factory::getDatabase(); + $sql = $db->getQuery(true) + ->delete($db->qn('#__ak_storage')) + ->where($db->qn('tag') . ' = ' . $db->q($dbtag)); + $db->setQuery($sql); + + try + { + $result = $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + return ($result !== false); + + break; + } + + return false; + } + + public function set($value, $tag = null) + { + $storage_filename = $this->get_storage_filename($tag); + + switch ($this->storageEngine) + { + case 'file': + // Remove old file (if exists) + if (file_exists($storage_filename)) + { + @unlink($storage_filename); + } + + // Open the new file + $fp = @fopen($storage_filename, 'wb'); + + if ($fp === false) + { + return false; + } + + // Add a header + fwrite($fp, $this->encode($value)); + fclose($fp); + + return true; + + break; + + case 'db': + $db = Factory::getDatabase(); + + // Delete any old records + $sql = $db->getQuery(true) + ->delete($db->qn('#__ak_storage')) + ->where($db->qn('tag') . ' = ' . $db->q($storage_filename)); + $db->setQuery($sql); + + try + { + $result = $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + // Add the new record + $sql = $db->getQuery(true) + ->insert($db->qn('#__ak_storage')) + ->columns(array( + $db->qn('tag'), + $db->qn('data'), + ))->values($db->q($storage_filename) . ',' . $db->q($this->encode($value))); + + $db->setQuery($sql); + + try + { + $result = $db->query(); + } + catch (\Exception $exc) + { + return false; + } + + return ($result !== false); + + break; + } + } + + public function &get($tag = null) + { + $storage_filename = $this->get_storage_filename($tag); + + $ret = false; + + switch ($this->storageEngine) + { + case 'file': + $data = @file_get_contents($storage_filename); + + if ($data === false) + { + return $ret; + } + + break; + + case 'db': + $db = Factory::getDatabase(); + $sql = $db->getQuery(true) + ->select($db->qn('data')) + ->from($db->qn('#__ak_storage')) + ->where($db->qn('tag') . ' = ' . $db->q($storage_filename)); + $db->setQuery($sql); + + try + { + $data = $db->loadResult(); + + if (empty($data)) + { + return $ret; + } + } + catch (\Exception $e) + { + return $ret; + } + + break; + } + + $ret = $this->decode($data); + unset($data); + + return $ret; + } + + public function encode(&$data) + { + // Should I base64-encode? + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + return base64_encode($data); + } + elseif (function_exists('convert_uuencode') && function_exists('convert_uudecode')) + { + return convert_uuencode($data); + } + else + { + return $data; + } + } + + public function decode(&$data) + { + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + return base64_decode($data); + } + elseif (function_exists('convert_uuencode') && function_exists('convert_uudecode')) + { + return convert_uudecode($data); + } + else + { + return $data; + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileLister.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileLister.php new file mode 100644 index 00000000..92827b21 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileLister.php @@ -0,0 +1,154 @@ +get('engine.archiver.common.dereference_symlinks'); + + while ((($file = @readdir($handle)) !== false)) + { + if (($file != '.') && ($file != '..')) + { + // # Fix 2.4.b1: Do not add DS if we are on the site's root and it's an empty string + // # Fix 2.4.b2: Do not add DS is the last character _is_ DS + $ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR; + $dir = "$folder/$file"; + $isDir = @is_dir($dir); + $isLink = @is_link($dir); + + //if (!$isDir || ($isDir && $isLink && !$dereferencesymlinks) ) { + if ( !$isDir) + { + if ($fullpath) + { + $data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir; + } + else + { + $data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file; + } + if ($data) + { + $arr[] = $data; + } + } + } + } + @closedir($handle); + + return $arr; + } + + public function &getFolders($folder, $fullpath = false) + { + // Initialize variables + $arr = array(); + $false = false; + + if ( !is_dir($folder) && !is_dir($folder . '/')) + { + return $false; + } + + $handle = @opendir($folder); + if ($handle === false) + + { + $handle = @opendir($folder . '/'); + } + + // If directory is not accessible, just return FALSE + if ($handle === false) + { + return $false; + } + + $registry = Factory::getConfiguration(); + $dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks'); + + while ((($file = @readdir($handle)) !== false)) + { + if (($file != '.') && ($file != '..')) + { + $dir = "$folder/$file"; + $isDir = @is_dir($dir); + $isLink = @is_link($dir); + + if ($isDir) + { + //if(!$dereferencesymlinks && $isLink) continue; + if ($fullpath) + { + $data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir; + } + else + { + $data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file; + } + + if ($data) + { + $arr[] = $data; + } + } + } + } + @closedir($handle); + + return $arr; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileSystem.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileSystem.php new file mode 100644 index 00000000..8c8ee125 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/FileSystem.php @@ -0,0 +1,230 @@ +isWindows = (DIRECTORY_SEPARATOR == '\\'); + } + + /** + * Makes a Windows path more UNIX-like, by turning backslashes to forward slashes. + * It takes into account UNC paths, e.g. \\myserver\some\folder becomes + * \\myserver/some/folder. + * + * This function will also fix paths with multiple slashes, e.g. convert /var//www////html to /var/www/html + * + * @param string $p_path The path to transform + * + * @return string + */ + public function TranslateWinPath($p_path) + { + $is_unc = false; + + if ($this->isWindows) + { + // Is this a UNC path? + $is_unc = (substr($p_path, 0, 2) == '\\\\') || (substr($p_path, 0, 2) == '//'); + + // Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) + { + $p_path = strtr($p_path, '\\', '/'); + } + } + + // Remove multiple slashes + $p_path = str_replace('///', '/', $p_path); + $p_path = str_replace('//', '/', $p_path); + + // Fix UNC paths + if ($is_unc) + { + $p_path = '//' . ltrim($p_path, '/'); + } + + return $p_path; + } + + /** + * Removes trailing slash or backslash from a pathname + * + * @param string $path The path to treat + * + * @return string The path without the trailing slash/backslash + */ + public function TrimTrailingSlash($path) + { + $newpath = $path; + + if (substr($path, strlen($path) - 1, 1) == '\\') + { + $newpath = substr($path, 0, strlen($path) - 1); + } + + if (substr($path, strlen($path) - 1, 1) == '/') + { + $newpath = substr($path, 0, strlen($path) - 1); + } + + return $newpath; + } + + /** + * Returns an array with the archive name variables and their values. This is used to replace variables in archive + * and directory names, etc. + * + * If there is a non-empty configuration value called volatile.core.archivenamevars with a serialised array it will + * be unserialised and used. Otherwise the name variables will be calculated on-the-fly. + * + * IMPORTANT: These variables do NOT include paths such as [SITEROOT] + * + * @return array + */ + public function get_archive_name_variables() + { + $variables = array(); + + $registry = Factory::getConfiguration(); + $serialized = $registry->get('volatile.core.archivenamevars', null); + + if (!empty($serialized)) + { + $variables = @unserialize($serialized); + } + + if (empty($variables) || !is_array($variables)) + { + $host = Platform::getInstance()->get_host(); + $version = defined('AKEEBA_VERSION') ? AKEEBA_VERSION : 'svn'; + $version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : $version; + $platformVars = Platform::getInstance()->getPlatformVersion(); + + $siteName = Platform::getInstance()->get_site_name(); + $siteName = htmlentities(utf8_decode($siteName)); + $siteName = preg_replace( + array('/ß/', '/&(..)lig;/', '/&([aouAOU])uml;/', '/&(.)[^;]*;/'), + array('ss', "$1", "$1" . 'e', "$1"), + $siteName); + $siteName = trim(strtolower($siteName)); + $siteName = preg_replace(array('/\s+/', '/[^A-Za-z0-9\-]/'), array('-', ''), $siteName); + + if (strlen($siteName) > 50) + { + $siteName = substr($siteName, 0, 50); + } + + /** + * Time components. Expressed in whatever timezone the Platform decides to use. + */ + // Raw timezone, e.g. "EEST" + $rawTz = Platform::getInstance()->get_local_timestamp("T"); + // Filename-safe timezone, e.g. "eest". Note the lowercase letters. + $fsSafeTZ = strtolower(str_replace(array(' ', '/', ':'), array('_', '_', '_'), $rawTz)); + + $variables = array( + '[DATE]' => Platform::getInstance()->get_local_timestamp("Ymd"), + '[YEAR]' => Platform::getInstance()->get_local_timestamp("Y"), + '[MONTH]' => Platform::getInstance()->get_local_timestamp("m"), + '[DAY]' => Platform::getInstance()->get_local_timestamp("d"), + '[TIME]' => Platform::getInstance()->get_local_timestamp("His"), + '[TIME_TZ]' => Platform::getInstance()->get_local_timestamp("His") . $fsSafeTZ, + '[WEEK]' => Platform::getInstance()->get_local_timestamp("W"), + '[WEEKDAY]' => Platform::getInstance()->get_local_timestamp("l"), + '[TZ]' => $fsSafeTZ, + '[TZ_RAW]' => $rawTz, + '[GMT_OFFSET]' => Platform::getInstance()->get_local_timestamp("O"), + '[HOST]' => empty($host) ? 'unknown_host' : $host, + '[RANDOM]' => md5(microtime()), + '[VERSION]' => $version, + '[PLATFORM_NAME]' => $platformVars['name'], + '[PLATFORM_VERSION]' => $platformVars['version'], + '[SITENAME]' => $siteName, + ); + } + + return $variables; + } + + /** + * Expands the archive name variables in $source. For example "[DATE]-foobar" would be expanded to something + * like "141101-foobar". IMPORTANT: These variables do NOT include paths. + * + * @param string $source The input string, possibly containing variables in the form of [VARIABLE] + * + * @return string The expanded string + */ + public function replace_archive_name_variables($source) + { + $tagReplacements = $this->get_archive_name_variables(); + + return str_replace(array_keys($tagReplacements), array_values($tagReplacements), $source); + } + + /** + * Expand the platform-specific stock directories variables in the input string. For example "[SITEROOT]/foobar" + * would be expanded to something like "/var/www/html/mysite/foobar" + * + * @param string $folder The input string to expand + * @param bool $translate_win_dirs Should I translate Windows path separators to UNIX path separators? (default: false) + * @param bool $trim_trailing_slash Should I remove the trailing slash (default: false) + * + * @return string The expanded string + */ + function translateStockDirs($folder, $translate_win_dirs = false, $trim_trailing_slash = false) + { + static $stock_dirs; + + if (empty($stock_dirs)) + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + } + + $temp = $folder; + + foreach ($stock_dirs as $find => $replace) + { + $temp = str_replace($find, $replace, $temp); + } + + if ($translate_win_dirs) + { + $temp = $this->TranslateWinPath($temp); + } + + if ($trim_trailing_slash) + { + $temp = $this->TrimTrailingSlash($temp); + } + + return $temp; + } +} + diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ListingParser.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ListingParser.php new file mode 100644 index 00000000..f9ca65c7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ListingParser.php @@ -0,0 +1,388 @@ +parseUnixListing($list, $quick); + + if (empty($res)) + { + $res = $this->parseMSDOSListing($list, $quick); + } + + return $res; + } + + /** + * Parse a UNIX-style directory listing. This is the format produced by ls -la on *NIX systems. + * + * You get a hash array with entries. Each entry has the following keys: + * name: the file / folder name. + * type: file, dir or link. + * target: link target (when type == link). + * user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner". + * group: owner group, numeric or text. IIS FTP fakes this with the literal string "group". + * size: size in bytes; note that some Linux servers report non-zero sizes for directories. + * date: file creation date, most likely blatantly wrong; see below + * perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755 + * + * @param string $list The raw listing + * @param bool $quick True to only include name, type, size and link target for each file. + * + * @return array + */ + protected function parseUnixListing($list, $quick = false) + { + $ret = array(); + + $list = str_replace(array("\r\n", "\r", "\n\n"), array("\n", "\n", "\n"), $list); + $list = explode("\n", $list); + $list = array_map('rtrim', $list); + + foreach ($list as $v) + { + $vInfo = preg_split("/[\s]+/", $v, 9); + + if (count($vInfo) != 9) + { + continue; + } + + $entry = array( + 'name' => '', + 'type' => 'file', + 'target' => '', + 'user' => '0', + 'group' => '0', + 'size' => '0', + 'date' => '0', + 'perms' => '0', + ); + + if ($quick) + { + $entry = array( + 'name' => '', + 'type' => 'file', + 'size' => '0', + 'target' => '', + ); + } + + // ===== Parse permissions ===== + $permString = $vInfo[0]; + $permStringLen = strlen($permString); + $typeBit = '-'; + $userPerms = 'r--'; + $groupPerms = 'r--'; + $otherPerms = 'r--'; + + if ($permStringLen) + { + $typeBit = substr($permString, 0, 1); + } + + switch ($typeBit) + { + case "d": + $entry['type'] = 'dir'; + break; + + case "l": + $entry['type'] = 'link'; + break; + } + + // ===== Parse size ===== + $entry['size'] = $vInfo[4]; + + if (!$quick) + { + if ($permStringLen >= 4) + { + $userPerms = substr($permString, 1, 3); + } + + if ($permStringLen >= 7) + { + $groupPerms = substr($permString, 4, 3); + } + + if ($permStringLen >= 10) + { + $otherPerms = substr($permString, 7, 3); + } + + $bitPart = 0; + $permsPart = ''; + + list($thisPerms, $thisBit) = $this->textPermsDecode($userPerms); + $bitPart += 4 * $thisBit; // SetUID + $permsPart .= $thisPerms; + + list($thisPerms, $thisBit) = $this->textPermsDecode($groupPerms); + $bitPart += 2 * $thisBit; // SetGID + $permsPart .= $thisPerms; + + list($thisPerms, $thisBit) = $this->textPermsDecode($otherPerms); + $bitPart += $thisBit; // Sticky (restricted deletion) + $permsPart .= $thisPerms; + + $entry['perms'] = octdec($bitPart . $permsPart); + + // ===== Parse ownership ===== + $entry['user'] = $vInfo[2]; + $entry['group'] = $vInfo[3]; + + // ===== Parse date ===== + $dateString = $vInfo[6] . ' ' . $vInfo[5] . ' ' . $vInfo[7]; + $x = date_create($dateString); + $entry['date'] = ($x === false) ? 0 : $x->getTimestamp(); + } + + // ===== Parse name ===== + $name = $vInfo[8]; + + // Ubuntu (possibly others?) tacks a start when either suid/sgid bits is set + if (substr($name, -1) == '*') + { + $name = substr($name, 0, -1); + } + + // Link target parsing + if (strpos($name, '->') !== false) + { + list($name, $target) = explode('->', $name); + + $entry['target'] = trim($target); + } + + $entry['name'] = trim($name); + + // ===== Return the entry ===== + $ret[] = $entry; + } + + return $ret; + } + + /** + * Parse am MS-DOS-style directory listing. This is the format produced by dir on MS-DOS and Windows systems. + * + * You get a hash array with entries. Each entry has the following keys: + * name: the file / folder name. + * type: file, dir or link. + * target: link target (when type == link). + * user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner". + * group: owner group, numeric or text. IIS FTP fakes this with the literal string "group". + * size: size in bytes; note that some Linux servers report non-zero sizes for directories. + * date: file creation date, most likely blatantly wrong; see below + * perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755 + * + * @param string $list The raw listing + * @param bool $quick True to only include name, type, size and link target for each file. + * + * @return array + */ + protected function parseMSDOSListing($list, $quick = false) + { + $ret = array(); + + $list = str_replace(array("\r\n", "\r", "\n\n"), array("\n", "\n", "\n"), $list); + $list = explode("\n", $list); + $list = array_map('rtrim', $list); + + foreach ($list as $v) + { + $vInfo = preg_split("/[\s]+/", $v, 5); + + if (count($vInfo) < 4) + { + continue; + } + + $entry = array( + 'name' => '', + 'type' => 'file', + 'target' => '', + 'user' => '0', + 'group' => '0', + 'size' => '0', + 'date' => '0', + 'perms' => '0', + ); + + if ($quick) + { + $entry = array( + 'name' => '', + 'type' => 'file', + 'size' => '0', + 'target' => '', + ); + } + + // The first two fields are date and time + $dateString = $vInfo[0] . ' ' . $vInfo[1]; + + // If position 2 is AM/PM append it and remove it from the list + if (in_array(strtoupper($vInfo[2]), array('AM', 'PM'))) + { + $dateString .= ' ' . $vInfo[2]; + + // This trick is required to remove the element and fix the indices for the rest of the parsing to work. + unset ($vInfo[2]); + $vInfo = array_merge($vInfo); + } + + if (!$quick) + { + $x = date_create($dateString); + $entry['date'] = ($x === false) ? 0 : $x->getTimestamp(); + } + + // The third field is either a special type indicator or the file size + switch (strtoupper($vInfo[2])) + { + // Regular directory + case '': + $entry['type'] = 'dir'; + break; + + // Junction (like a directory symlink, pre-Win7) + case '': + // File symlink + case '': + // Directory symlink + case '': + $entry['type'] = 'link'; + break; + + default: + $entry['size'] = (int) $vInfo[2]; + break; + } + + // And finally the file name. If it's a link it's in the format 'name [target]' + preg_match('/(.*)[\s]+\[(.*)\]/', $vInfo[3], $matches); + + if (empty($matches)) + { + $entry['name'] = $vInfo[3]; + } + else + { + $entry['type'] = 'link'; + $entry['name'] = $matches[1]; + $entry['target'] = $matches[2]; + } + + // ===== Return the entry ===== + $ret[] = $entry; + } + + return $ret; + } + + /** + * Decode a textual permissions representation for a user, group or others to a pair of octal digits (permissions + * and flags). For example "r--" is converted to [4, 0], "r-x" to [5, 0], "r-t" to [5, 1] + * + * @param string $perms The textual permissions representation for a user, group or others + * + * @return array Two octal digits for permissions and flags (suid/sgid/sticky bit) + */ + private function textPermsDecode($perms) + { + $permBit = 0; + $flagBits = 0; + + if (strpos($perms, 'r')) + { + $permBit += 4; + } + + if (strpos($perms, 'w')) + { + $permBit += 2; + } + + /** + * Both s and t denote flag set and imply the execute permissions is also granted. For user/groups it's + * SetUID/SetGID respectively, for others it's the "sticky" bit (restricted deletion). Since only one of x, s + * and t can be present at one time we use an if/elseif block. I don't use a switch because a. I am not 100% + * sure that all servers will report the text permissions in rwx order and b. I am not sure that switch and + * substr are faster than strpos (and too lazy to benchmark; sorry). + */ + if (strpos($perms, 'x')) + { + $permBit += 1; + } + elseif (strpos($perms, 't')) + { + $flagBits += 1; + } + elseif (strpos($perms, 's')) + { + $flagBits += 1; + } + + return array($permBit, $flagBits); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Log/LogInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Log/LogInterface.php new file mode 100644 index 00000000..1c8dd071 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Log/LogInterface.php @@ -0,0 +1,82 @@ +initialiseWithProfileParameters(); + } + + /** + * When shutting down this class always close any open log files. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Clears the logfile + * + * @param string $tag Backup origin + */ + public function reset($tag = null) + { + // Pause logging + $this->pause(); + + // Get the file names for the default log and the tagged log + $currentLogName = $this->logName; + $this->logName = $this->getLogFilename($tag); + $defaultLog = $this->getLogFilename(null); + + // Close the file if it's open + if ($currentLogName == $this->logName) + { + $this->close(); + } + + // Remove the log file if it exists + @unlink($this->logName); + + // Reset the log file + $fp = @fopen($this->logName, 'w'); + + if ($fp !== false) + { + @fclose($fp); + } + + // Delete the default log file if it exists + if (!empty($tag) && @file_exists($defaultLog)) + { + @unlink($defaultLog); + } + + // Set the current log tag + $this->currentTag = $tag; + + // Unpause logging + $this->unpause(); + } + + /** + * Writes a line to the log, if the log level is high enough + * + * @param string $level The log level + * @param string $message The message to write to the log + * @param array $context The logging context. For PSR-3 compatibility but not used in text file logs. + * + * @return void + */ + public function log($level, $message = '', array $context = array()) + { + // If we are told to not log anything we can't continue + if ($this->configuredLoglevel == 0) + { + return; + } + + // Open the log if it's closed + if (is_null($this->fp)) + { + $this->open($this->currentTag); + } + + // If the log could not be opened we can't continue + if (is_null($this->fp)) + { + return; + } + + // If the logging is paused we can't continue + if ($this->paused) + { + return; + } + + // Get the log level as an integer (compatibility with our minimum log level configuration parameter) + switch ($level) + { + case LogLevel::EMERGENCY: + case LogLevel::ALERT: + case LogLevel::CRITICAL: + case LogLevel::ERROR: + $intLevel = 1; + break; + + case LogLevel::WARNING: + case LogLevel::NOTICE: + $intLevel = 2; + break; + + case LogLevel::INFO: + $intLevel = 3; + break; + + case LogLevel::DEBUG: + $intLevel = 4; + break; + + default: + throw new InvalidArgumentException("Unknown log level $level", 500); + break; + } + + // If the minimum log level is lower than what we're trying to log we cannot continue + if ($this->configuredLoglevel < $intLevel) + { + return; + } + + // Replace the site's root with in the log file + if (!defined('AKEEBADEBUG')) + { + $message = str_replace($this->site_root_untranslated, "", $message); + $message = str_replace($this->site_root, "", $message); + } + + // Replace new lines + $message = str_replace("\r\n", "\n", $message); + $message = str_replace("\r", "\n", $message); + $message = str_replace("\n", ' \n ', $message); + + switch ($level) + { + case LogLevel::EMERGENCY: + case LogLevel::ALERT: + case LogLevel::CRITICAL: + case LogLevel::ERROR: + $string = "ERROR |"; + break; + + case LogLevel::WARNING: + case LogLevel::NOTICE: + $string = "WARNING |"; + break; + + case LogLevel::INFO: + $string = "INFO |"; + break; + + default: + $string = "DEBUG |"; + break; + } + + $string .= @strftime("%y%m%d %H:%M:%S") . "|$message\r\n"; + + @fwrite($this->fp, $string); + } + + /** + * Calculates the absolute path to the log file + * + * @param string $tag The backup run's tag + * + * @return string The absolute path to the log file + */ + public function getLogFilename($tag = null) + { + if (empty($tag)) + { + $fileName = 'akeeba.log'; + } + else + { + $fileName = "akeeba.$tag.log"; + } + + // Get output directory + $registry = Factory::getConfiguration(); + $outputDirectory = $registry->get('akeeba.basic.output_directory'); + + // Get the log file name + return Factory::getFilesystemTools()->TranslateWinPath($outputDirectory . DIRECTORY_SEPARATOR . $fileName); + } + + /** + * Close the currently active log and set the current tag to null. + * + * @return void + */ + public function close() + { + // The log file changed. Close the old log. + if (is_resource($this->fp)) + { + @fclose($this->fp); + } + + $this->fp = null; + $this->currentTag = null; + } + + /** + * Open a new log instance with the specified tag. If another log is already open it is closed before switching to + * the new log tag. If the tag is null use the default log defined in the logging system. + * + * @param string|null $tag The log to open + * + * @return void + */ + public function open($tag = null) + { + // If the log is already open do nothing + if (is_resource($this->fp) && ($tag == $this->currentTag)) + { + return; + } + + // If another log is open, close it + if (is_resource($this->fp)) + { + $this->close(); + } + + // Re-initialise site root and minimum log level since the active profile might have changed in the meantime + $this->initialiseWithProfileParameters(); + + // Set the current tag + $this->currentTag = $tag; + + // Get the log filename + $this->logName = $this->getLogFilename($tag); + + // Touch the file + @touch($this->logName); + + // Open the log file + $this->fp = @fopen($this->logName, 'ab'); + + // If we couldn't open the file set the file pointer to null + if ($this->fp === false) + { + $this->fp = null; + } + } + + /** + * Temporarily pause log output. The log() method MUST respect this. + * + * @return void + */ + public function pause() + { + $this->paused = true; + } + + /** + * Resume the previously paused log output. The log() method MUST respect this. + * + * @return void + */ + public function unpause() + { + $this->paused = false; + } + + /** + * Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the + * specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the + * engine to find out the age of stalled backups which may have crashed. + * + * @param string|null $tag The log tag for which the last timestamp is returned + * + * @return int|null The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp. + */ + public function getLastTimestamp($tag = null) + { + $fileName = $this->getLogFilename($tag); + $timestamp = @filemtime($fileName); + + if ($timestamp === false) + { + return null; + } + + return $timestamp; + } + + /** + * 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); + } + + /** + * Initialise the logger properties with parameters from the backup profile and the platform + * + * @return void + */ + protected function initialiseWithProfileParameters() + { + // Get the site's translated and untranslated root + $this->site_root_untranslated = Platform::getInstance()->get_site_root(); + $this->site_root = Factory::getFilesystemTools()->TranslateWinPath($this->site_root_untranslated); + + // Load the registry and fetch log level + $registry = Factory::getConfiguration(); + $this->configuredLoglevel = $registry->get('akeeba.basic.log_level'); + $this->configuredLoglevel = $this->configuredLoglevel * 1; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ParseIni.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ParseIni.php new file mode 100644 index 00000000..1d99645f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/ParseIni.php @@ -0,0 +1,206 @@ +get_platform_configuration_option('push_preference', '0'); + $apiKey = Platform::getInstance()->get_platform_configuration_option('push_apikey', ''); + + // No API key? No push messages are enabled, so no point continuing really... + if (empty($apiKey)) + { + $pushPreference = 0; + } + + // We use a switch in case we add support for more push APIs in the future. The push_preference platform + // option will tell us which service to use. In that case we'll have to refactor this class, but the public + // API will remain the same. + switch ($pushPreference) + { + default: + case 0: + $this->enabled = false; + break; + + case 1: + $keys = explode(',', $apiKey); + $keys = array_map('trim', $keys); + + foreach ($keys as $key) + { + try + { + $connector = new Connector($key); + $connector->getDevices(); + $this->connectors[] = $connector; + } + catch (\Exception $e) + { + Factory::getLog()->warning("Push messages cannot be sent with API key $key. Error received when trying to establish PushBullet connection: " . $e->getMessage()); + } + } + + if (empty($this->connectors)) + { + Factory::getLog()->warning('No push messages can be sent: none of the provided API keys is usable. Push messages have been deactivated.'); + + $this->enabled = false; + } + + break; + } + } + + /** + * Sends a push message to all connected devices. The intent is to provide the user with an information message, + * e.g. notify them about the progress of the backup. + * + * @param string $subject The subject of the message, shown in the lock screen. Keep it short. + * @param string $details Long(er) description of what the message is about. Plain text (no HTML). + * + * @return void + */ + public function message($subject, $details = null) + { + if (!$this->enabled) + { + return; + } + + foreach ($this->connectors as $connector) + { + try + { + $connector->pushNote('', $subject, $details); + } + catch (\Exception $e) + { + Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message:' . $e->getMessage()); + $this->enabled = false; + } + } + } + + /** + * Sends a push message, containing a URL/URI, to all connected devices. The URL will be rendered as something + * clickable on most devices. + * + * @param string $url The URL/URI + * @param string $subject The subject of the message, shown in the lock screen. Keep it short. + * @param string $details Long(er) description of what the message is about. Plain text (no HTML). + * + * @return void + */ + public function link($url, $subject, $details = null) + { + if (!$this->enabled) + { + return; + } + + foreach ($this->connectors as $connector) + { + try + { + $connector->pushLink('', $subject, $url, $details); + } + catch (\Exception $e) + { + Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message with a link:' . $e->getMessage()); + $this->enabled = false; + } + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Pushbullet/ApiException.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Pushbullet/ApiException.php new file mode 100644 index 00000000..75bf1a73 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Pushbullet/ApiException.php @@ -0,0 +1,20 @@ +_apiKey = $apiKey; + + if (!function_exists('curl_init')) + { + throw new ApiException('cURL library is not loaded.'); + } + } + + /** + * Push a note. + * + * @param string $recipient Recipient. Can be device_iden, email or channel #tagname. + * @param string $title The note's title. + * @param string $body The note's message. + * + * @return object Response. + * @throws ApiException + */ + public function pushNote($recipient, $title, $body = null) + { + $data = array(); + + Connector::_parseRecipient($recipient, $data); + $data['type'] = 'note'; + $data['title'] = $title; + $data['body'] = $body; + + return $this->_curlRequest(self::URL_PUSHES, 'POST', $data); + } + + /** + * Push a link. + * + * @param string $recipient Recipient. Can be device_iden, email or channel #tagname. + * @param string $title The link's title. + * @param string $url The URL to open. + * @param string $body A message associated with the link. + * + * @return object Response. + * @throws ApiException + */ + public function pushLink($recipient, $title, $url, $body = null) + { + $data = array(); + + Connector::_parseRecipient($recipient, $data); + $data['type'] = 'link'; + $data['title'] = $title; + $data['url'] = $url; + $data['body'] = $body; + + return $this->_curlRequest(self::URL_PUSHES, 'POST', $data); + } + + /** + * Push a checklist. + * + * @param string $recipient Recipient. Can be device_iden, email or channel #tagname. + * @param string $title The list's title. + * @param string[] $items The list items. + * + * @return object Response. + * @throws ApiException + */ + public function pushList($recipient, $title, array $items) + { + $data = array(); + + Connector::_parseRecipient($recipient, $data); + $data['type'] = 'list'; + $data['title'] = $title; + $data['items'] = $items; + + return $this->_curlRequest(self::URL_PUSHES, 'POST', $data); + } + + /** + * Push a file. + * + * @param string $recipient Recipient. Can be device_iden, email or channel #tagname. + * @param string $filePath The path of the file to push. + * @param string $mimeType The MIME type of the file. If null, we'll try to guess it. + * @param string $title The title of the push notification. + * @param string $body The body of the push notification. + * @param string $altFileName Alternative file name to use instead of the original one. + * For example, you might want to push 'someFile.tmp' as 'image.jpg'. + * + * @return object Response. + * @throws ApiException + */ + public function pushFile($recipient, $filePath, $mimeType = null, $title = null, $body = null, $altFileName = null) + { + $data = array(); + + $fullFilePath = realpath($filePath); + + if (!is_readable($fullFilePath)) + { + throw new ApiException('File: File does not exist or is unreadable.'); + } + + if (filesize($fullFilePath) > 25 * 1024 * 1024) + { + throw new ApiException('File: File size exceeds 25 MB.'); + } + + $data['file_name'] = $altFileName === null ? basename($fullFilePath) : $altFileName; + + // Try to guess the MIME type if the argument is NULL + $data['file_type'] = $mimeType === null ? mime_content_type($fullFilePath) : $mimeType; + + // Request authorization to upload the file + $response = $this->_curlRequest(self::URL_UPLOAD_REQUEST, 'GET', $data); + $data['file_url'] = $response->file_url; + + if (version_compare(PHP_VERSION, '5.5.0', '>=')) + { + $response->data->file = new \CURLFile($fullFilePath); + } + else + { + $response->data->file = '@' . $fullFilePath; + } + + // Upload the file + $this->_curlRequest($response->upload_url, 'POST', $response->data, false, false); + + Connector::_parseRecipient($recipient, $data); + $data['type'] = 'file'; + $data['title'] = $title; + $data['body'] = $body; + + return $this->_curlRequest(self::URL_PUSHES, 'POST', $data); + } + + /** + * Get push history. + * + * @param int $modifiedAfter Request pushes modified after this UNIX timestamp. + * @param string $cursor Request the next page via its cursor from a previous response. See the API + * documentation (https://docs.pushbullet.com/http/) for a detailed description. + * @param int $limit Maximum number of objects on each page. + * + * @return object Response. + * @throws ApiException + */ + public function getPushHistory($modifiedAfter = 0, $cursor = null, $limit = null) + { + $data = array(); + $data['modified_after'] = $modifiedAfter; + + if ($cursor !== null) + { + $data['cursor'] = $cursor; + } + + if ($limit !== null) + { + $data['limit'] = $limit; + } + + return $this->_curlRequest(self::URL_PUSHES, 'GET', $data); + } + + /** + * Dismiss a push. + * + * @param string $pushIden push_iden of the push notification. + * + * @return object Response. + * @throws ApiException + */ + public function dismissPush($pushIden) + { + return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'POST', array('dismissed' => true)); + } + + /** + * Delete a push. + * + * @param string $pushIden push_iden of the push notification. + * + * @return object Response. + * @throws ApiException + */ + public function deletePush($pushIden) + { + return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'DELETE'); + } + + /** + * Get a list of available devices. + * + * @param int $modifiedAfter Request devices modified after this UNIX timestamp. + * @param string $cursor Request the next page via its cursor from a previous response. See the API + * documentation (https://docs.pushbullet.com/http/) for a detailed description. + * @param int $limit Maximum number of objects on each page. + * + * @return object Response. + * @throws ApiException + */ + public function getDevices($modifiedAfter = 0, $cursor = null, $limit = null) + { + $data = array(); + $data['modified_after'] = $modifiedAfter; + + if ($cursor !== null) + { + $data['cursor'] = $cursor; + } + + if ($limit !== null) + { + $data['limit'] = $limit; + } + + return $this->_curlRequest(self::URL_DEVICES, 'GET', $data); + } + + /** + * Get information about the current user. + * + * @return object Response. + * @throws ApiException + */ + public function getUserInformation() + { + return $this->_curlRequest(self::URL_USERS . '/me', 'GET'); + } + + /** + * Update preferences for the current user. + * + * @param array $preferences Preferences. + * + * @return object Response. + * @throws ApiException + */ + public function updateUserPreferences($preferences) + { + return $this->_curlRequest(self::URL_USERS . '/me', 'POST', array('preferences' => $preferences)); + } + + /** + * Add a callback function that will be invoked right before executing each cURL request. + * + * @param callable $callback The callback function. + */ + public function addCurlCallback(callable $callback) + { + $this->_curlCallback = $callback; + } + + + /** + * Parse recipient. + * + * @param string $recipient Recipient string. + * @param array $data Data array to populate with the correct recipient parameter. + */ + private static function _parseRecipient($recipient, array &$data) + { + if (!empty($recipient)) + { + if (filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false) + { + $data['email'] = $recipient; + } + else + { + if (substr($recipient, 0, 1) == "#") + { + $data['channel_tag'] = substr($recipient, 1); + } + else + { + $data['device_iden'] = $recipient; + } + } + } + } + + /** + * Send a request to a remote server using cURL. + * + * @param string $url URL to send the request to. + * @param string $method HTTP method. + * @param array $data Query data. + * @param bool $sendAsJSON Send the request as JSON. + * @param bool $auth Use the API key to authenticate + * + * @return object Response. + * @throws ApiException + */ + private function _curlRequest($url, $method, $data = null, $sendAsJSON = true, $auth = true) + { + $curl = curl_init(); + + if ($method == 'GET' && $data !== null) + { + $url .= '?' . http_build_query($data); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + if ($auth) + { + curl_setopt($curl, CURLOPT_USERPWD, $this->_apiKey); + } + + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + + if ($method == 'POST' && $data !== null) + { + if ($sendAsJSON) + { + $data = json_encode($data); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data) + )); + } + + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); + } + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER, false); + + @curl_setopt($curl, CURLOPT_CAINFO, AKEEBA_CACERT_PEM); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); + + if ($this->_curlCallback !== null) + { + $curlCallback = $this->_curlCallback; + $curlCallback($curl); + } + + $response = curl_exec($curl); + + if ($response === false) + { + $curlError = curl_error($curl); + curl_close($curl); + throw new ApiException('cURL Error: ' . $curlError); + } + + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($httpCode >= 400) + { + curl_close($curl); + $responseParsed = json_decode($response); + throw new ApiException('HTTP Error ' . $httpCode . + ' (' . $responseParsed->error->type . '): ' . $responseParsed->error->message); + } + + curl_close($curl); + + return json_decode($response); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/RandomValue.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/RandomValue.php new file mode 100644 index 00000000..a0907655 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/RandomValue.php @@ -0,0 +1,252 @@ += 0 || IS_WIN)) + { + $strong = false; + $randBytes = openssl_random_pseudo_bytes($bytes, $strong); + + if ($strong) + { + return $randBytes; + } + } + } + + if (function_exists('extension_loaded') && function_exists('mcrypt_create_iv')) + { + if (extension_loaded('mcrypt')) + { + return mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM); + } + } + + return $this->genRandomBytes($bytes); + } + + /** + * Generates a random string with the specified length. WARNING: You get to specify the number of + * random characters in the string, not the number of random bytes. The character pool is 64 characters + * (6 bits) long. The entropy of your string is 6 * $characters bits. This means that a random string + * of 32 characters has an entropy of 192 bits whereas a random sequence of 32 bytes returned by generate() + * has an entropy of 8 * 32 = 256 bits. + * + * @param int $characters + * + * @return string + */ + public function generateString($characters = 32) + { + $sourceString = str_split('abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789', 1); + $ret = ''; + + $bytes = ceil($characters / 4) * 3; + $randBytes = $this->generate($bytes); + + for ($i = 0; $i < $bytes; $i += 3) + { + $subBytes = substr($randBytes, $i, 3); + $subBytes = str_split($subBytes, 1); + $subBytes = ord($subBytes[0]) * 65536 + ord($subBytes[1]) * 256 + ord($subBytes[2]); + $subBytes = $subBytes & bindec('00000000111111111111111111111111'); + + $b = array(); + $b[0] = $subBytes >> 18; + $b[1] = ($subBytes >> 12) & bindec('111111'); + $b[2] = ($subBytes >> 6) & bindec('111111'); + $b[3] = $subBytes & bindec('111111'); + + $ret .= $sourceString[$b[0]] . $sourceString[$b[1]] . $sourceString[$b[2]] . $sourceString[$b[3]]; + } + + return substr($ret, 0, $characters); + } + + /** + * Generate random bytes. Adapted from Joomla! 3.2. + * + * @param integer $length Length of the random data to generate + * + * @return string Random binary data + */ + protected function genRandomBytes($length = 32) + { + $length = (int) $length; + $sslStr = ''; + + /* + * Collect any entropy available in the system along with a number + * of time measurements of operating system randomness. + */ + $bitsPerRound = 2; + $maxTimeMicro = 400; + $shaHashLength = 20; + $randomStr = ''; + $total = $length; + + // Check if we can use /dev/urandom. + $urandom = false; + $handle = null; + + // This is PHP 5.3.3 and up + if (function_exists('stream_set_read_buffer') && @is_readable('/dev/urandom')) + { + $handle = @fopen('/dev/urandom', 'rb'); + + if ($handle) + { + $urandom = true; + } + } + + while ($length > strlen($randomStr)) + { + $bytes = ($total > $shaHashLength)? $shaHashLength : $total; + $total -= $bytes; + + /* + * Collect any entropy available from the PHP system and filesystem. + * If we have ssl data that isn't strong, we use it once. + */ + $entropy = rand() . uniqid(mt_rand(), true) . $sslStr; + $entropy .= implode('', @fstat(fopen(__FILE__, 'r'))); + $entropy .= memory_get_usage(); + $sslStr = ''; + + if ($urandom) + { + stream_set_read_buffer($handle, 0); + $entropy .= @fread($handle, $bytes); + } + else + { + /* + * There is no external source of entropy so we repeat calls + * to mt_rand until we are assured there's real randomness in + * the result. + * + * Measure the time that the operations will take on average. + */ + $samples = 3; + $duration = 0; + + for ($pass = 0; $pass < $samples; ++$pass) + { + $microStart = microtime(true) * 1000000; + $hash = sha1(mt_rand(), true); + + for ($count = 0; $count < 50; ++$count) + { + $hash = sha1($hash, true); + } + + $microEnd = microtime(true) * 1000000; + $entropy .= $microStart . $microEnd; + + if ($microStart >= $microEnd) + { + $microEnd += 1000000; + } + + $duration += $microEnd - $microStart; + } + + $duration = $duration / $samples; + + /* + * Based on the average time, determine the total rounds so that + * the total running time is bounded to a reasonable number. + */ + $rounds = (int) (($maxTimeMicro / $duration) * 50); + + /* + * Take additional measurements. On average we can expect + * at least $bitsPerRound bits of entropy from each measurement. + */ + $iter = $bytes * (int) ceil(8 / $bitsPerRound); + + for ($pass = 0; $pass < $iter; ++$pass) + { + $microStart = microtime(true); + $hash = sha1(mt_rand(), true); + + for ($count = 0; $count < $rounds; ++$count) + { + $hash = sha1($hash, true); + } + + $entropy .= $microStart . microtime(true); + } + } + + $randomStr .= sha1($entropy, true); + } + + if ($urandom) + { + @fclose($handle); + } + + return substr($randomStr, 0, $length); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/SecureSettings.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/SecureSettings.php new file mode 100644 index 00000000..a3e17c8b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/SecureSettings.php @@ -0,0 +1,239 @@ +keyFilename = $filename; + } + + /** + * Gets the configured server key, automatically loading the server key storage file + * if required. + * + * @return string + */ + public function getKey() + { + if (defined('AKEEBA_SERVERKEY')) + { + return base64_decode(AKEEBA_SERVERKEY); + } + + $filename = dirname(__FILE__) . '/../' . $this->keyFilename; + + if (file_exists($filename)) + { + include_once $filename; + } + + if (defined('AKEEBA_SERVERKEY')) + { + return base64_decode(AKEEBA_SERVERKEY); + } + + return ''; + } + + /** + * Do the server options allow us to use settings encryption? + * + * @return bool + */ + public function supportsEncryption() + { + // Do we have the encrypt.php plugin? + if (!class_exists('\\Akeeba\\Engine\\Util\\Encrypt', true)) + { + return false; + } + + // Did the user intentionally disable settings encryption? + $useEncryption = Platform::getInstance()->get_platform_configuration_option('useencryption', -1); + + if ($useEncryption == 0) + { + return false; + } + + // Do we have base64_encode/_decode required for encryption? + if (!function_exists('base64_encode') || !function_exists('base64_decode')) + { + return false; + } + + // Pre-requisites met. We can encrypt and decrypt! + return true; + } + + /** + * Gets the preferred encryption mode. Currently, if mcrypt is installed and activated we will + * use AES128. + * + * @return string + */ + public function preferredEncryption() + { + $aes = new Encrypt(); + $adapter = $aes->getAdapter(); + + if (!$adapter->isSupported()) + { + return 'CTR128'; + } + + return 'AES128'; + } + + /** + * Encrypts the settings using the automatically detected preferred algorithm + * + * @param $settingsINI string The raw settings INI string + * + * @return string The encrypted data to store in the database + */ + public function encryptSettings($settingsINI, $key = null) + { + // Do we really support encryption? + if (!$this->supportsEncryption()) + { + return $settingsINI; + } + + // Does any of the preferred encryption engines exist? + $encryption = $this->preferredEncryption(); + + if (empty($encryption)) + { + return $settingsINI; + } + + // Do we have a non-empty key to begin with? + if (empty($key)) + { + $key = $this->getKey(); + } + + if (empty($key)) + { + return $settingsINI; + } + + if ($encryption == 'AES128') + { + $encrypted = Factory::getEncryption()->AESEncryptCBC($settingsINI, $key); + + if (empty($encrypted)) + { + $encryption = 'CTR128'; + } + else + { + // Note: CBC returns the encrypted data as a binary string and requires Base 64 encoding + $settingsINI = '###AES128###' . base64_encode($encrypted); + } + } + + if ($encryption == 'CTR128') + { + $encrypted = Factory::getEncryption()->AESEncryptCtr($settingsINI, $key, 128); + + if (!empty($encrypted)) + { + // Note: CTR returns the encrypted data readily encoded in Base 64 + $settingsINI = '###CTR128###' . $encrypted; + } + } + + return $settingsINI; + } + + /** + * Decrypts the encrypted settings and returns the plaintext INI string + * + * @param string $encrypted The encrypted data + * + * @return string The decrypted data + */ + public function decryptSettings($encrypted, $key = null) + { + if (substr($encrypted, 0, 12) == '###AES128###') + { + $mode = 'AES128'; + } + elseif (substr($encrypted, 0, 12) == '###CTR128###') + { + $mode = 'CTR128'; + } + else + { + return $encrypted; + } + + if (empty($key)) + { + $key = $this->getKey(); + } + + if (empty($key)) + { + return ''; + } + + $encrypted = substr($encrypted, 12); + + switch ($mode) + { + default: + case 'AES128': + $encrypted = base64_decode($encrypted); + $decrypted = rtrim(Factory::getEncryption()->AESDecryptCBC($encrypted, $key), "\0"); + break; + + case 'CTR128': + $decrypted = Factory::getEncryption()->AESDecryptCtr($encrypted, $key, 128); + break; + } + + if (empty($decrypted)) + { + $decrypted = ''; + } + + return $decrypted; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Statistics.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Statistics.php new file mode 100644 index 00000000..adaea0fb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Statistics.php @@ -0,0 +1,243 @@ +multipart_lock = false; + } + + /** + * Updates the multipart status of the current backup attempt's statistics record + * + * @param int $multipart The new multipart status + */ + public function updateMultipart($multipart) + { + if ($this->multipart_lock) + { + return; + } + + Factory::getLog()->log(LogLevel::DEBUG, 'Updating multipart status to ' . $multipart); + + // Cache this change and commit to db only after the backup is done, or failed + $registry = Factory::getConfiguration(); + $registry->set('volatile.statistics.multipart', $multipart); + } + + /** + * Sets or updates the statistics record of the current backup attempt + * + * @param array $data + */ + public function setStatistics($data) + { + $ret = Platform::getInstance()->set_or_update_statistics($this->statistics_id, $data, $this); + if ($ret !== false) + { + if (!is_null($ret)) + { + $this->statistics_id = $ret; + } + $this->cached_data = array_merge($this->cached_data, $data); + $result = true; + } + elseif ($ret === false) + { + $result = false; + } + + return $result; + } + + /** + * Returns the statistics record ID (used in DB backup classes) + * @return int + */ + public function getId() + { + return $this->statistics_id; + } + + /** + * Returns a copy of the cached data + * @return array + */ + public function getRecord() + { + return $this->cached_data; + } + + + /** + * Returns all the filenames of the backup archives for the specified stat record, + * or null if the backup type is wrong or the file doesn't exist. It takes into + * account the multipart nature of Split Backup Archives. + * + * @param array $stat The backup statistics record + * @param bool $skipNonComplete Skips over backups with no files produced + * + * @return array|null The filenames or null if it's not applicable + */ + public static function get_all_filenames($stat, $skipNonComplete = true) + { + // Shortcut for database entries marked as having no files + if ($stat['filesexist'] == 0) + { + return array(); + } + + // Initialize + $base_directory = @dirname($stat['absolute_path']); + $base_filename = $stat['archivename']; + $filenames = array($base_filename); + + if (empty($base_filename)) + { + // This is a backup with a writer which doesn't store files on the server + return null; + } + + // Calculate all the filenames for this backup + if ($stat['multipart'] > 1) + { + // Find the base filename and extension + $dotpos = strrpos($base_filename, '.'); + $extension = substr($base_filename, $dotpos); + $basefile = substr($base_filename, 0, $dotpos); + + // Calculate the multiple names + $multipart = $stat['multipart']; + for ($i = 1; $i < $multipart; $i++) + { + // Note: For $multipart = 10, it will produce i.e. .z01 through .z10 + // This is intentional. If the backup aborts and multipart=1, we + // might be stuck with a .z01 file instead of a .zip. So do not + // change the less than or equal with a straight less than. + $filenames[] = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i); + } + } + + // Check if the files exist, otherwise attempt to provide relocated filename + $ret = array(); + + $ds = DIRECTORY_SEPARATOR; + // $test_file is the first file which must have been created + $test_file = count($filenames) == 1 ? $filenames[0] : $filenames[1]; + if ( + (!@file_exists($base_directory . $ds . $test_file)) || + (!is_dir($base_directory)) + ) + { + // The test file wasn't detected. Use the configured output directory. + $registry = Factory::getConfiguration(); + $base_directory = $registry->get('akeeba.basic.output_directory'); + } + + foreach ($filenames as $filename) + { + // Turn relative path to absolute + $filename = $base_directory . $ds . $filename; + + // Return the new filename IF IT EXISTS! + if (!@file_exists($filename)) + { + $filename = ''; + } + + // Do not return filename for invalid backups + if (!empty($filename)) + { + $ret[] = $filename; + } + } + + // Edge case: still running backups, we have to brute force the scan + // of existing files (multipart may be lying) + if ($stat['status'] == 'run') + { + $base_filename = $stat['archivename']; + $dotpos = strrpos($base_filename, '.'); + $extension = substr($base_filename, $dotpos); + $basefile = substr($base_filename, 0, $dotpos); + + $registry = Factory::getConfiguration(); + $dirs = array( + @dirname($stat['absolute_path']), + $registry->get('akeeba.basic.output_directory') + ); + + // Look for base file + foreach ($dirs as $dir) + { + if (@file_exists($dir . $ds . $base_filename)) + { + $ret[] = $dir . $ds . $base_filename; + break; + } + } + + // Look for added files + $found = true; + $i = 0; + while ($found) + { + $i++; + $found = false; + $part_file_name = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i); + foreach ($dirs as $dir) + { + if (@file_exists($dir . $ds . $part_file_name)) + { + $ret[] = $dir . $ds . $part_file_name; + $found = true; + break; + } + } + } + } + + if ((count($ret) == 0) && $skipNonComplete) + { + $ret = null; + } + + if (!empty($ret) && is_array($ret)) + { + $ret = array_unique($ret); + } + + return $ret; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/TemporaryFiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/TemporaryFiles.php new file mode 100644 index 00000000..e8d29528 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/TemporaryFiles.php @@ -0,0 +1,230 @@ +get('akeeba.basic.output_directory'), 'ak'); + + // Register it and return its absolute path + $tempName = basename($tempFile); + + return Factory::getTempFiles()->registerTempFile($tempName); + } + + /** + * Registers a temporary file with the Akeeba Engine, storing the list of temporary files + * in another temporary flat database file. + * + * @param string $fileName The path of the file, relative to the temporary directory + * + * @return string The absolute path to the temporary file, for use in file operations + */ + public function registerTempFile($fileName) + { + $configuration = Factory::getConfiguration(); + $tempFiles = $configuration->get('volatile.tempfiles', false); + if ($tempFiles === false) + { + $tempFiles = array(); + } + else + { + $tempFiles = @unserialize($tempFiles); + + if ($tempFiles === false) + { + $tempFiles = array(); + } + } + + if (!in_array($fileName, $tempFiles)) + { + $tempFiles[] = $fileName; + $configuration->set('volatile.tempfiles', serialize($tempFiles)); + } + + return Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory') . '/' . $fileName); + } + + /** + * Unregister and delete a temporary file + * + * @param string $fileName The filename to unregister and delete + * @param bool $removePrefix The prefix to remove + * + * @return bool True on success + */ + public function unregisterAndDeleteTempFile($fileName, $removePrefix = false) + { + $configuration = Factory::getConfiguration(); + + if ($removePrefix) + { + $fileName = str_replace(Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory')), '', $fileName); + + if ((substr($fileName, 0, 1) == '/') || (substr($fileName, 0, 1) == '\\')) + { + $fileName = substr($fileName, 1); + } + + if ((substr($fileName, -1) == '/') || (substr($fileName, -1) == '\\')) + { + $fileName = substr($fileName, 0, -1); + } + } + + // Make sure this file is registered + $configuration = Factory::getConfiguration(); + + $serialised = $configuration->get('volatile.tempfiles', false); + $tempFiles = array(); + + if ($serialised !== false) + { + $tempFiles = @unserialize($serialised); + } + + if (!is_array($tempFiles)) + { + return false; + } + + if (!in_array($fileName, $tempFiles)) + { + return false; + } + + $file = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName; + Factory::getLog()->log(LogLevel::DEBUG, "-- Removing temporary file $fileName"); + $platform = strtoupper(PHP_OS); + + // TODO What exactly are we doing here? chown won't work on Windows. Huh...? + if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN')) + { + // On Windows we have to chown() the file first to make it owned by Nobody + Factory::getLog()->log(LogLevel::DEBUG, "-- Windows hack: chowning $fileName"); + @chown($file, 600); + } + + $result = @$this->nullifyAndDelete($file); + + // Make sure the file is removed before unregistering it + if (!@file_exists($file)) + { + $aPos = array_search($fileName, $tempFiles); + + if ($aPos !== false) + { + unset($tempFiles[$aPos]); + + $configuration->set('volatile.tempfiles', serialize($tempFiles)); + } + } + + return $result; + } + + + /** + * Deletes all temporary files + * + * @return void + */ + public function deleteTempFiles() + { + $configuration = Factory::getConfiguration(); + + $serialised = $configuration->get('volatile.tempfiles', false); + $tempFiles = array(); + + if ($serialised !== false) + { + $tempFiles = @unserialize($serialised); + } + + if (!is_array($tempFiles)) + { + $tempFiles = array(); + } + + $fileName = null; + + if (!empty($tempFiles)) + { + foreach ($tempFiles as $fileName) + { + Factory::getLog()->log(LogLevel::DEBUG, "-- Removing temporary file $fileName"); + $file = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName; + $platform = strtoupper(PHP_OS); + + // TODO Like above, this shouldn't even work on Windows. I wonder why it's necessary. + if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN')) + { + // On Windows we have to chwon() the file first to make it owned by Nobody + @chown($file, 600); + } + + $ret = @$this->nullifyAndDelete($file); + } + } + + $tempFiles = array(); + $configuration->set('volatile.tempfiles', serialize($tempFiles)); + } + + /** + * Nullify the contents of the file and try to delete it as well + * + * @param string $filename The absolute path to the file to delete + * + * @return bool True of the deletion is successful + */ + public function nullifyAndDelete($filename) + { + // Try to nullify (method #1) + $fp = @fopen($filename, 'w'); + + if (is_resource($fp)) + { + @fclose($fp); + } + else + { + // Try to nullify (method #2) + @file_put_contents($filename, ''); + } + + // Unlink + return @unlink($filename); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/Ftp.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/Ftp.php new file mode 100644 index 00000000..259e0f50 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/Ftp.php @@ -0,0 +1,624 @@ +host = $options['host']; + } + + if (isset($options['port'])) + { + $this->port = (int)$options['port']; + } + + if (isset($options['username'])) + { + $this->username = $options['username']; + } + + if (isset($options['password'])) + { + $this->password = $options['password']; + } + + if (isset($options['directory'])) + { + $this->directory = '/' . ltrim(trim($options['directory']), '/'); + } + + if (isset($options['ssl'])) + { + $this->ssl = $options['ssl']; + } + + if (isset($options['passive'])) + { + $this->passive = $options['passive']; + } + + if (isset($options['timeout'])) + { + $this->timeout = max(1, (int) $options['timeout']); + } + + $this->connect(); + } + + /** + * Save all parameters on serialization except the connection resource + * + * @return array + */ + public function __sleep() + { + return array('host', 'port', 'username', 'password', 'directory', 'ssl', 'passive', 'timeout'); + } + + /** + * Reconnect to the server on unserialize + * + * @return void + */ + public function __wakeup() + { + $this->connect(); + } + + /** + * Connect to the FTP server + * + * @throws \RuntimeException + */ + public function connect() + { + // Try to connect to the server + if ($this->ssl) + { + if (function_exists('ftp_ssl_connect')) + { + $this->connection = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $this->connection = false; + throw new \RuntimeException('ftp_ssl_connect not available on this server', 500); + } + } + else + { + $this->connection = @ftp_connect($this->host, $this->port, $this->timeout); + } + + if ($this->connection === false) + { + throw new \RuntimeException(sprintf('Cannot connect to FTP server [host:port] = %s:%s', $this->host, $this->port), 500); + } + + // Attempt to authenticate + if (!@ftp_login($this->connection, $this->username, $this->password)) + { + @ftp_close($this->connection); + $this->connection = null; + + throw new \RuntimeException(sprintf('Cannot log in to FTP server [username:password] = %s:%s', $this->username, $this->password), 500); + } + + // Attempt to change to the initial directory + if (!@ftp_chdir($this->connection, $this->directory)) + { + @ftp_close($this->connection); + $this->connection = null; + + throw new \RuntimeException(sprintf('Cannot change to initial FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $this->directory), 500); + } + + // Apply the passive mode preference + @ftp_pasv($this->connection, $this->passive); + } + + /** + * Is this transfer method blocked by a server firewall? + * + * @param array $params Any additional parameters you might need to pass + * + * @return boolean True if the firewall blocks connections to a known host + */ + public static function isFirewalled(array $params = array()) + { + try + { + $connector = new static(array( + 'host' => 'test.rebex.net', + 'port' => 21, + 'username' => 'demo', + 'password' => 'password', + 'directory' => '', + 'ssl' => isset($params['ssl']) ? $params['ssl'] : false, + 'passive' => true, + 'timeout' => 5, + )); + + $data = $connector->read('readme.txt'); + + if (empty($data)) + { + return true; + } + } + catch (\Exception $e) + { + return true; + } + + return false; + } + + /** + * Public destructor, closes any open FTP connections + */ + public function __destruct() + { + if (!is_null($this->connection)) + { + @ftp_close($this->connection); + } + } + + /** + * Write the contents into the file + * + * @param string $fileName The full path to the file + * @param string $contents The contents to write to the file + * + * @return boolean True on success + */ + public function write($fileName, $contents) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); + fwrite($handle, $contents); + rewind($handle); + + $ret = @ftp_fput($this->connection, $fileName, $handle, FTP_BINARY); + + fclose($handle); + + return $ret; + } + + /** + * Uploads a local file to the remote storage + * + * @param string $localFilename The full path to the local file + * @param string $remoteFilename The full path to the remote file + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function upload($localFilename, $remoteFilename, $useExceptions = true) + { + $handle = @fopen($localFilename, 'rb'); + + if ($handle === false) + { + if ($useExceptions) + { + throw new \RuntimeException("Unreadable local file $localFilename"); + } + + return false; + } + + $ret = @ftp_fput($this->connection, $remoteFilename, $handle, FTP_BINARY); + + @fclose($handle); + + return $ret; + } + + /** + * Read the contents of a remote file into a string + * + * @param string $fileName The full path to the remote file + * + * @return string The contents of the remote file + */ + public function read($fileName) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); + + $result = @ftp_fget($this->connection, $handle, $fileName, FTP_BINARY); + + if ($result === false) + { + fclose($handle); + throw new \RuntimeException("Can not download remote file $fileName"); + } + + rewind($handle); + + $ret = ''; + + while (!feof($handle)) + { + $ret .= fread($handle, 131072); + } + + fclose($handle); + + return $ret; + } + + /** + * Download a remote file into a local file + * + * @param string $remoteFilename The remote file path to download from + * @param string $localFilename The local file path to download to + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function download($remoteFilename, $localFilename, $useExceptions = true) + { + $ret = @ftp_get($this->connection, $localFilename, $remoteFilename, FTP_BINARY); + + if (!$ret && $useExceptions) + { + throw new \RuntimeException("Cannot download remote file $remoteFilename through FTP."); + } + + return $ret; + } + + /** + * Delete a file (remove it from the disk) + * + * @param string $fileName The full path to the file + * + * @return boolean True on success + */ + public function delete($fileName) + { + return @ftp_delete($this->connection, $fileName); + } + + /** + * Create a copy of the file. Actually, we have to read it in memory and upload it again. + * + * @param string $from The full path of the file to copy from + * @param string $to The full path of the file that will hold the copy + * + * @return boolean True on success + */ + public function copy($from, $to) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); + + $ret = @ftp_fget($this->connection, $handle, $from, FTP_BINARY); + + if ($ret !== false) + { + rewind($handle); + $ret = @ftp_fput($this->connection, $to, $handle, FTP_BINARY); + } + + fclose($handle); + + return $ret; + } + + /** + * Move or rename a file + * + * @param string $from The full path of the file to move + * @param string $to The full path of the target file + * + * @return boolean True on success + */ + public function move($from, $to) + { + return @ftp_rename($this->connection, $from, $to); + } + + /** + * Change the permissions of a file + * + * @param string $fileName The full path of the file whose permissions will change + * @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!) + * + * @return boolean True on success + */ + public function chmod($fileName, $permissions) + { + if (@ftp_chmod($this->connection, $permissions, $fileName) !== false) + { + return true; + } + + $permissionsOctal = decoct((int) $permissions); + + if (@ftp_site($this->connection, "CHMOD $permissionsOctal $fileName") !== false) + { + return true; + } + + return false; + } + + /** + * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all + * intermediate directories if they do not already exist. + * + * @param string $dirName The full path of the directory to create + * @param integer $permissions The permissions of the created directory + * + * @return boolean True on success + */ + public function mkdir($dirName, $permissions = 0755) + { + $targetDir = rtrim($dirName, '/'); + + $directories = explode('/', $targetDir); + + $remoteDir = ''; + + foreach ($directories as $dir) + { + if (!$dir) + { + continue; + } + + $remoteDir .= '/' . $dir; + + // Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine + if ($this->isDir($remoteDir)) + { + continue; + } + + $ret = @ftp_mkdir($this->connection, $remoteDir); + + if ($ret === false) + { + return $ret; + } + } + + $this->chmod($dirName, $permissions); + + return true; + } + + /** + * Checks if the given directory exists + * + * @param string $path The full path of the remote directory to check + * + * @return boolean True if the directory exists + */ + public function isDir($path) + { + $cur_dir = ftp_pwd($this->connection); + + if (@ftp_chdir($this->connection, $path ) ) + { + // If it is a directory, then change the directory back to the original directory + ftp_chdir($this->connection, $cur_dir); + + return true; + } + else + { + return false; + } + } + + /** + * Get the current working directory + * + * @return string + */ + public function cwd() + { + return ftp_pwd($this->connection); + } + + /** + * Returns the absolute remote path from a path relative to the initial directory configured when creating the + * transfer object. + * + * @param string $fileName The relative path of a file or directory + * + * @return string The absolute path for use by the transfer object + */ + public function getPath($fileName) + { + $fileName = str_replace('\\', '/', $fileName); + + if (strpos($fileName, $this->directory) === 0) + { + return $fileName; + } + + $fileName = trim($fileName, '/'); + $fileName = rtrim($this->directory, '/') . '/' . $fileName; + + return $fileName; + } + + /** + * Lists the subdirectories inside an FTP directory + * + * @param null|string $dir The directory to scan. Skip to use the current directory. + * + * @return array|bool A list of folders, or false if we could not get a listing + * + * @throws \RuntimeException When the server is incompatible with our FTP folder scanner + */ + public function listFolders($dir = null) + { + if (!@ftp_chdir($this->connection, $dir)) + { + throw new \RuntimeException(sprintf('Cannot change to FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500); + } + + $list = @ftp_rawlist($this->connection, '.'); + + if ($list === false) + { + throw new \RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser."); + } + + $folders = array(); + + foreach ($list as $v) + { + $vInfo = preg_split("/[\s]+/", $v, 9); + + if ($vInfo[0] !== "total") + { + $perms = $vInfo[0]; + + if (substr($perms,0,1) == 'd') + { + $folders[] = $vInfo[8]; + } + } + } + + asort($folders); + + return $folders; + } + + /** + * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP + * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents, + * fopen etc. + * + * @param string $path + * + * @return string + */ + public function getWrapperStringFor($path) + { + $passwordEncoded = urlencode($this->password); + $hostname = $this->host . ($this->port ? ":{$this->port}" : ''); + $protocol = $this->ssl ? "ftps" : "ftp"; + + return "{$protocol}://{$this->username}:{$passwordEncoded}@{$hostname}{$path}"; + } + + /** + * Return the raw server listing for the requested folder. + * + * @param string $folder The path name to list + * + * @return string + */ + public function getRawList($folder) + { + return ftp_rawlist($this->connection, $folder); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/FtpCurl.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/FtpCurl.php new file mode 100644 index 00000000..53ab232d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/FtpCurl.php @@ -0,0 +1,772 @@ +skipPassiveIP = $options['passive_fix'] ? true : false; + } + + if (isset($options['verbose'])) + { + $this->verbose = $options['verbose'] ? true : false; + } + } + + /** + * Save all parameters on serialization except the connection resource + * + * @return array + */ + public function __sleep() + { + return array( + 'host', + 'port', + 'username', + 'password', + 'directory', + 'ssl', + 'passive', + 'timeout', + 'skipPassiveIP', + 'verbose', + ); + } + + /** + * Returns a cURL resource handler for the remote FTP server + * + * @param string $remoteFile Optional. The remote file / folder on the FTP server you'll be manipulating with cURL. + * + * @return resource + */ + protected function getCurlHandle($remoteFile = '') + { + /** + * Get the FTP URI + * + * VERY IMPORTANT! WE NEED THE DOUBLE SLASH AFTER THE HOST NAME since we are giving an absolute path. + * @see https://technicalsanctuary.wordpress.com/2012/11/01/curl-curl-9-server-denied-you-to-change-to-the-given-directory/ + */ + + $ftpUri = 'ftp://' . $this->host . '/'; + + // Relative path? Append the initial directory. + if (substr($remoteFile, 0, 1) != '/') + { + $ftpUri .= $this->directory; + } + + // Add a remote file if necessary. The filename must be URL encoded since we're creating a URI. + if (!empty($remoteFile)) + { + $suffix = ''; + + if (substr($remoteFile, -7, 6) == ';type=') + { + $suffix = substr($remoteFile, -7); + $remoteFile = substr($remoteFile, 0, -7); + } + + $dirname = dirname($remoteFile); + + // Windows messing up dirname('/'). KILL ME. + if ($dirname == '\\') + { + $dirname = ''; + } + + $dirname = trim($dirname, '/'); + $basename = basename($remoteFile); + + if ((substr($remoteFile, -1) == '/') && !empty($basename)) + { + $suffix = '/' . $suffix; + } + + $ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix; + } + + // Colons in usernames must be URL escaped + $username = str_replace(':', '%3A', $this->username); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $ftpUri); + curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $this->password); + curl_setopt($ch, CURLOPT_PORT, $this->port); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + + // Should I enable Implict SSL? + if ($this->ssl) + { + curl_setopt($ch, CURLOPT_FTP_SSL, CURLFTPSSL_ALL); + curl_setopt($ch, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT); + + // Most FTPS servers use self-signed certificates. That's the only way to connect to them :( + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + + // Should I ignore the server-supplied passive mode IP address? + if ($this->passive && $this->skipPassiveIP) + { + curl_setopt($ch, CURLOPT_FTP_SKIP_PASV_IP, 1); + } + + // Should I enable active mode? + if (!$this->passive) + { + /** + * cURL always uses passive mode for FTP transfers. Setting the CURLOPT_FTPPORT flag enables the FTP PORT + * command which makes the connection active. Setting it to '-' lets the library use your system's default + * IP address. + * + * @see https://curl.haxx.se/libcurl/c/CURLOPT_FTPPORT.html + */ + curl_setopt($ch, CURLOPT_FTPPORT, '-'); + } + + // Should I enable verbose output? Useful for debugging. + if ($this->verbose) + { + curl_setopt($ch, CURLOPT_VERBOSE, 1); + } + + // Automatically create missing directories + curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1); + + return $ch; + } + + /** + * Test the connection to the FTP server and whether the initial directory is correct. This is done by attempting to + * list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check + * if we can upload files to that remote folder. + * + * @throws \RuntimeException + */ + public function connect() + { + $ch = $this->getCurlHandle($this->directory . '/'); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + curl_exec($ch); + + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException("cURL Error $errNo connecting to remote FTP server: $error", 500); + } + } + + /** + * Write the contents into the file + * + * @param string $fileName The full path to the file + * @param string $contents The contents to write to the file + * + * @return boolean True on success + */ + public function write($fileName, $contents) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+'); + fwrite($handle, $contents); + + // Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle + try + { + $this->uploadFromHandle($fileName, $handle); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Uploads a local file to the remote storage + * + * @param string $localFilename The full path to the local file + * @param string $remoteFilename The full path to the remote file + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function upload($localFilename, $remoteFilename, $useExceptions = true) + { + $fp = @fopen($localFilename, 'rb'); + + if ($fp === false) + { + throw new \RuntimeException("Unreadable local file $localFilename"); + } + + // Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle + try + { + $this->uploadFromHandle($remoteFilename, $fp); + } + catch (\RuntimeException $e) + { + if ($useExceptions) + { + throw $e; + } + + return false; + } + + return true; + } + + /** + * Read the contents of a remote file into a string + * + * @param string $fileName The full path to the remote file + * + * @return string The contents of the remote file + */ + public function read($fileName) + { + try + { + return $this->downloadToString($fileName); + } + catch (\RuntimeException $e) + { + throw new \RuntimeException("Can not download remote file $fileName", 500, $e); + } + } + + /** + * Download a remote file into a local file + * + * @param string $remoteFilename The remote file path to download from + * @param string $localFilename The local file path to download to + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function download($remoteFilename, $localFilename, $useExceptions = true) + { + $fp = @fopen($localFilename, 'wb'); + + if ($fp === false) + { + return false; + } + + // Note: don't manually close the file pointer, it's closed automatically by downloadToHandle + try + { + $this->downloadToHandle($remoteFilename, $fp); + } + catch (\RuntimeException $e) + { + if ($useExceptions) + { + throw $e; + } + + return false; + } + + return true; + } + + /** + * Delete a file (remove it from the disk) + * + * @param string $fileName The full path to the file + * + * @return boolean True on success + */ + public function delete($fileName) + { + $commands = array( + 'DELE /' . $this->getPath($fileName), + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Create a copy of the file. Actually, we have to read it in memory and upload it again. + * + * @param string $from The full path of the file to copy from + * @param string $to The full path of the file that will hold the copy + * + * @return boolean True on success + */ + public function copy($from, $to) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); + + try + { + $this->downloadToHandle($from, $handle, false); + $this->uploadFromHandle($to, $handle); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Move or rename a file + * + * @param string $from The full path of the file to move + * @param string $to The full path of the target file + * + * @return boolean True on success + */ + public function move($from, $to) + { + $from = $this->getPath($from); + $to = $this->getPath($to); + + $commands = array( + 'RNFR /' . $from, + 'RNTO /' . $to, + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Change the permissions of a file + * + * @param string $fileName The full path of the file whose permissions will change + * @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!) + * + * @return boolean True on success + */ + public function chmod($fileName, $permissions) + { + // Make sure permissions are in an octal string representation + if (!is_string($permissions)) + { + $permissions = decoct($permissions); + } + + $commands = array( + 'SITE CHMOD ' . $permissions . ' /' . $this->getPath($fileName), + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all + * intermediate directories if they do not already exist. + * + * @param string $dirName The full path of the directory to create + * @param integer $permissions The permissions of the created directory + * + * @return boolean True on success + */ + public function mkdir($dirName, $permissions = 0755) + { + $targetDir = rtrim($dirName, '/'); + + $directories = explode('/', $targetDir); + + $remoteDir = ''; + + foreach ($directories as $dir) + { + if (!$dir) + { + continue; + } + + $remoteDir .= '/' . $dir; + + // Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine + if ($this->isDir($remoteDir)) + { + continue; + } + + $commands = array( + 'MKD ' . $remoteDir, + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + } + + $this->chmod($dirName, $permissions); + + return true; + } + + /** + * Checks if the given directory exists + * + * @param string $path The full path of the remote directory to check + * + * @return boolean True if the directory exists + */ + public function isDir($path) + { + $ch = $this->getCurlHandle($path . '/'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + curl_exec($ch); + + $errNo = curl_errno($ch); + curl_close($ch); + + if ($errNo) + { + return false; + } + + return true; + } + + /** + * Get the current working directory. NOT IMPLEMENTED. + * + * @return string + */ + public function cwd() + { + return ''; + } + + /** + * Lists the subdirectories inside an FTP directory + * + * @param null|string $dir The directory to scan. Skip to use the current directory. + * + * @return array|bool A list of folders, or false if we could not get a listing + * + * @throws \RuntimeException When the server is incompatible with our FTP folder scanner + */ + public function listFolders($dir = null) + { + if (empty($dir)) + { + $dir = $this->directory; + } + + $dir = rtrim($dir, '/'); + + $ch = $this->getCurlHandle($dir . '/'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $list = curl_exec($ch); + + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500); + } + + if (empty($list)) + { + throw new \RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser."); + } + + $folders = array(); + + // Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters + $list = explode("\n", $list); + $list = array_map('rtrim', $list); + + foreach ($list as $v) + { + $vInfo = preg_split("/[\s]+/", $v, 9); + + if ($vInfo[0] !== "total") + { + $perms = $vInfo[0]; + + if (substr($perms, 0, 1) == 'd') + { + $folders[] = $vInfo[8]; + } + } + } + + asort($folders); + + return $folders; + } + + /** + * Is the verbose debug option set? + * + * @return boolean + */ + public function isVerbose() + { + return $this->verbose; + } + + /** + * Set the verbose debug option + * + * @param boolean $verbose + * + * @return void + */ + public function setVerbose($verbose) + { + $this->verbose = $verbose; + } + + /** + * Uploads a file using file contents provided through a file handle + * + * @param string $remoteFilename Remote file to write contents to + * @param resource $fp File or stream handler of the source data to upload + * + * @return void + * + * @throws \RuntimeException + */ + protected function uploadFromHandle($remoteFilename, $fp) + { + // We need the file size. We can do that by getting the file position at EOF + fseek($fp, 0, SEEK_END); + $filesize = ftell($fp); + rewind($fp); + + /** + * The ;type=i suffix forces Binary file transfer mode + * + * @see https://curl.haxx.se/mail/archive-2008-05/0089.html + */ + $ch = $this->getCurlHandle($remoteFilename . ';type=i'); + curl_setopt($ch, CURLOPT_UPLOAD, 1); + curl_setopt($ch, CURLOPT_INFILE, $fp); + curl_setopt($ch, CURLOPT_INFILESIZE, $filesize); + + curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + fclose($fp); + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + } + + /** + * Downloads a remote file to the provided file handle + * + * @param string $remoteFilename Filename on the remote server + * @param resource $fp File handle where the downloaded content will be written to + * @param bool $close Optional. Should I close the file handle when I'm done? (Default: true) + * + * @return void + * + * @throws \RuntimeException + */ + protected function downloadToHandle($remoteFilename, $fp, $close = true) + { + /** + * The ;type=i suffix forces Binary file transfer mode + * + * @see https://curl.haxx.se/mail/archive-2008-05/0089.html + */ + $ch = $this->getCurlHandle($remoteFilename . ';type=i'); + + curl_setopt($ch, CURLOPT_FILE, $fp); + + curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($close) + { + fclose($fp); + } + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + } + + /** + * Downloads a remote file and returns it as a string + * + * @param string $remoteFilename Filename on the remote server + * + * @return string + * + * @throws \RuntimeException + */ + protected function downloadToString($remoteFilename) + { + /** + * The ;type=i suffix forces Binary file transfer mode + * + * @see https://curl.haxx.se/mail/archive-2008-05/0089.html + */ + $ch = $this->getCurlHandle($remoteFilename . ';type=i'); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + + $ret = curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + + return $ret; + } + + /** + * Executes arbitrary FTP commands + * + * @param array $commands An array with the FTP commands to be executed + * + * @return string The output of the executed commands + * + * @throws \RuntimeException + */ + protected function executeServerCommands($commands) + { + $ch = $this->getCurlHandle($this->directory . '/'); + + curl_setopt($ch, CURLOPT_QUOTE, $commands); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $listing = curl_exec($ch); + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException($error, $errNo); + } + + return $listing; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/RemoteResourceInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/RemoteResourceInterface.php new file mode 100644 index 00000000..fca2ddd9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/RemoteResourceInterface.php @@ -0,0 +1,42 @@ +host = $options['host']; + } + + if (isset($options['port'])) + { + $this->port = (int)$options['port']; + } + + if (isset($options['username'])) + { + $this->username = $options['username']; + } + + if (isset($options['password'])) + { + $this->password = $options['password']; + } + + if (isset($options['directory'])) + { + $this->directory = '/' . ltrim(trim($options['directory']), '/'); + } + + if (isset($options['privateKey'])) + { + $this->privateKey = $options['privateKey']; + } + + if (isset($options['publicKey'])) + { + $this->publicKey = $options['publicKey']; + } + + $this->connect(); + } + + /** + * Save all parameters on serialization except the connection resource + * + * @return array + */ + public function __sleep() + { + return array('host', 'port', 'username', 'password', 'directory', 'privateKey', 'publicKey'); + } + + /** + * Reconnect to the server on unserialize + * + * @return void + */ + public function __wakeup() + { + $this->connect(); + } + + + public function __destruct() + { + if (is_resource($this->connection)) + { + @ssh2_exec($this->connection, 'exit;'); + $this->connection = null; + $this->sftpHandle = null; + } + } + + /** + * Connect to the FTP server + * + * @throws \RuntimeException + */ + public function connect() + { + // Try to connect to the SSH server + if (!function_exists('ssh2_connect')) + { + throw new \RuntimeException('Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', 500); + } + + $this->connection = ssh2_connect($this->host, $this->port); + + if ($this->connection === false) + { + $this->connection = null; + + throw new \RuntimeException(sprintf('Cannot connect to SFTP server [host:port] = %s:%s', $this->host, $this->port), 500); + } + + // Attempt to authenticate + if (!empty($this->publicKey) && !empty($this->privateKey)) + { + if (!@ssh2_auth_pubkey_file($this->connection,$this->username, $this->publicKey, $this->privateKey, $this->password)) + { + $this->connection = null; + + throw new \RuntimeException(sprintf('Cannot log in to SFTP server using key files [username:private_key_file:public_key_file:password] = %s:%s:%s:%s', $this->username, $this->privateKey, $this->publicKey, $this->password), 500); + } + } + else + { + if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) + { + $this->connection = null; + + throw new \RuntimeException(sprintf('Cannot log in to SFTP server [username:password] = %s:%s', $this->username, $this->password), 500); + } + } + + // Get an SFTP handle + $this->sftpHandle = ssh2_sftp($this->connection); + + if ($this->sftpHandle === false) + { + throw new \RuntimeException('Cannot start an SFTP session with the server', 500); + } + } + + /** + * Is this transfer method blocked by a server firewall? + * + * @param array $params Any additional parameters you might need to pass + * + * @return boolean True if the firewall blocks connections to a known host + */ + public static function isFirewalled(array $params = array()) + { + try + { + $connector = new static(array( + 'host' => 'test.rebex.net', + 'port' => 22, + 'username' => 'demo', + 'password' => 'password', + 'directory' => '', + )); + + $data = $connector->read('readme.txt'); + + if (empty($data)) + { + return true; + } + } + catch (\Exception $e) + { + return true; + } + + return false; + } + + /** + * Write the contents into the file + * + * @param string $fileName The full path to the file + * @param string $contents The contents to write to the file + * + * @return boolean True on success + */ + public function write($fileName, $contents) + { + $fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'w'); + + if ($fp === false) + { + return false; + } + + $ret = @fwrite($fp, $contents); + + @fclose($fp); + + return $ret; + } + + /** + * Uploads a local file to the remote storage + * + * @param string $localFilename The full path to the local file + * @param string $remoteFilename The full path to the remote file + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function upload($localFilename, $remoteFilename, $useExceptions = true) + { + $fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'w'); + + if ($fp === false) + { + if ($useExceptions) + { + throw new \RuntimeException("Could not open remote SFTP file $remoteFilename for writing"); + } + + return false; + } + + $localFp = @fopen($localFilename, 'rb'); + + if ($localFp === false) + { + fclose($fp); + + if ($useExceptions) + { + throw new \RuntimeException("Could not open local file $localFilename for reading"); + } + + return false; + } + + while (!feof($localFp)) + { + $data = fread($localFp, 131072); + $ret = @fwrite($fp, $data); + + if ($ret < strlen($data)) + { + fclose($fp); + fclose($localFp); + + if ($useExceptions) + { + throw new \RuntimeException("An error occurred while copying file $localFilename to $remoteFilename"); + } + + return false; + } + } + + @fclose($fp); + @fclose($localFp); + + return true; + } + + /** + * Read the contents of a remote file into a string + * + * @param string $fileName The full path to the remote file + * + * @return string The contents of the remote file + */ + public function read($fileName) + { + $fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'r'); + + if ($fp === false) + { + throw new \RuntimeException("Can not download remote file $fileName"); + } + + $ret = ''; + + while (!feof($fp)) + { + $ret .= fread($fp, 131072); + } + + @fclose($fp); + + return $ret; + } + + /** + * Download a remote file into a local file + * + * @param string $remoteFilename The remote file path to download from + * @param string $localFilename The local file path to download to + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function download($remoteFilename, $localFilename, $useExceptions = true) + { + $fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'r'); + + if ($fp === false) + { + throw new \RuntimeException("Could not open remote SFTP file $remoteFilename for reading"); + } + + $localFp = @fopen($localFilename, 'w'); + + if ($localFp === false) + { + fclose($fp); + + throw new \RuntimeException("Could not open local file $localFilename for writing"); + } + + while (!feof($fp)) + { + $chunk = fread($fp, 131072); + + if ($chunk === false) + { + fclose($fp); + fclose($localFp); + + throw new \RuntimeException("An error occurred while copying file $remoteFilename to $localFilename"); + } + + fwrite($localFp, $chunk); + } + + @fclose($fp); + @fclose($localFp); + + return true; + } + + /** + * Delete a file (remove it from the disk) + * + * @param string $fileName The full path to the file + * + * @return boolean True on success + */ + public function delete($fileName) + { + try + { + $ret = @ssh2_sftp_unlink($this->sftpHandle, $fileName); + } + catch (\Exception $e) + { + $ret = false; + } + + return $ret; + } + + /** + * Create a copy of the file. Actually, we have to read it in memory and upload it again. + * + * @param string $from The full path of the file to copy from + * @param string $to The full path of the file that will hold the copy + * + * @return boolean True on success + */ + public function copy($from, $to) + { + $contents = @file_get_contents($from); + + return $this->write($to, $contents); + } + + /** + * Move or rename a file. Actually, we have to read it, upload it again and then delete the original. + * + * @param string $from The full path of the file to move + * @param string $to The full path of the target file + * + * @return boolean True on success + */ + public function move($from, $to) + { + $ret = $this->copy($from, $to); + + if ($ret) + { + $ret = $this->delete($from); + } + + return $ret; + } + + /** + * Change the permissions of a file + * + * @param string $fileName The full path of the file whose permissions will change + * @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!) + * + * @return boolean True on success + */ + public function chmod($fileName, $permissions) + { + // Prefer the SFTP way, if available + if (function_exists('ssh2_sftp_chmod')) + { + return @ssh2_sftp_chmod($this->sftpHandle, $fileName, $permissions); + } + // Otherwise fall back to the (likely to fail) raw command mode + else + { + $cmd = 'chmod ' . decoct($permissions) . ' ' . escapeshellarg($fileName); + return @ssh2_exec($this->connection, $cmd); + } + } + + /** + * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all + * intermediate directories if they do not already exist. + * + * @param string $dirName The full path of the directory to create + * @param integer $permissions The permissions of the created directory + * + * @return boolean True on success + */ + public function mkdir($dirName, $permissions = 0755) + { + $targetDir = rtrim($dirName, '/'); + + $ret = @ssh2_sftp_mkdir($this->sftpHandle, $targetDir, $permissions, true); + + return $ret; + } + + /** + * Checks if the given directory exists + * + * @param string $path The full path of the remote directory to check + * + * @return boolean True if the directory exists + */ + public function isDir($path) + { + return @ssh2_sftp_stat($this->sftpHandle, $path); + } + + /** + * Get the current working directory + * + * @return string + */ + public function cwd() + { + return ssh2_sftp_realpath($this->sftpHandle, "."); + } + + /** + * Returns the absolute remote path from a path relative to the initial directory configured when creating the + * transfer object. + * + * @param string $fileName The relative path of a file or directory + * + * @return string The absolute path for use by the transfer object + */ + public function getPath($fileName) + { + $fileName = str_replace('\\', '/', $fileName); + $fileName = rtrim($this->directory, '/') . '/' . $fileName; + + return $fileName; + } + + /** + * Lists the subdirectories inside an SFTP directory + * + * @param null|string $dir The directory to scan. Skip to use the current directory. + * + * @return array|bool A list of folders, or false if we could not get a listing + * + * @throws \RuntimeException When the server is incompatible with our SFTP folder scanner + */ + public function listFolders($dir = null) + { + if (empty($dir)) + { + $dir = $this->directory; + } + + // Get a raw directory listing (hoping it's a UNIX server!) + $list = array(); + $dir = ltrim($dir, '/'); + + try + { + $di = new \DirectoryIterator("ssh2.sftp://" . $this->sftpHandle . "/$dir"); + } + catch (\Exception $e) + { + throw new \RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500); + } + + if (!$di->valid()) + { + throw new \RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500); + } + + /** @var \DirectoryIterator $entry */ + foreach ($di as $entry) + { + if ($entry->isDot()) + { + continue; + } + + if (!$entry->isDir()) + { + continue; + } + + $list[] = $entry->getFilename(); + } + + unset($di); + + if (!empty($list)) + { + asort($list); + } + + return $list; + } + + /** + * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP + * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents, + * fopen etc. + * + * @param string $path + * + * @return string + */ + public function getWrapperStringFor($path) + { + return "ssh2.sftp://{$this->sftpHandle}{$path}"; + } + + /** + * Return the raw server listing for the requested folder. + * + * @param string $folder The path name to list + * + * @return string + */ + public function getRawList($folder) + { + // First try the command for Linxu servers + $res = $this->ssh2cmd('ls -l ' . escapeshellarg($folder)); + + // If an error occurred let's try the command for Windows servers + if (empty($res)) + { + $res = $this->ssh2cmd('CMD /C ' . escapeshellarg($folder)); + } + + return $res; + } + + private function ssh2cmd($command) + { + $stream = ssh2_exec($this->connection, $command); + stream_set_blocking($stream, true); + $res = @stream_get_contents($stream); + @fclose($stream); + + return $res; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/SftpCurl.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/SftpCurl.php new file mode 100644 index 00000000..761261a8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/SftpCurl.php @@ -0,0 +1,866 @@ +host = $options['host']; + } + + if (isset($options['port'])) + { + $this->port = (int) $options['port']; + } + + if (isset($options['username'])) + { + $this->username = $options['username']; + } + + if (isset($options['password'])) + { + $this->password = $options['password']; + } + + if (isset($options['directory'])) + { + $this->directory = '/' . ltrim(trim($options['directory']), '/'); + } + + if (isset($options['privateKey'])) + { + $this->privateKey = $options['privateKey']; + } + + if (isset($options['publicKey'])) + { + $this->publicKey = $options['publicKey']; + } + + if (isset($options['timeout'])) + { + $this->timeout = max(1, (int) $options['timeout']); + } + + if (isset($options['passive_fix'])) + { + $this->skipPassiveIP = $options['passive_fix'] ? true : false; + } + + if (isset($options['verbose'])) + { + $this->verbose = $options['verbose'] ? true : false; + } + } + + /** + * Save all parameters on serialization except the connection resource + * + * @return array + */ + public function __sleep() + { + return array( + 'host', + 'port', + 'username', + 'password', + 'directory', + 'privateKey', + 'publicKey', + 'timeout', + 'skipPassiveIP', + 'verbose', + ); + } + + /** + * Returns a cURL resource handler for the remote SFTP server + * + * @param string $remoteFile Optional. The remote file / folder on the SFTP server you'll be manipulating with cURL. + * + * @return resource + */ + protected function getCurlHandle($remoteFile = '') + { + // Remember, the username has to be URL encoded as it's part of a URI! + $authentication = urlencode($this->username); + + // We will only use username and password authentication if there are no certificates configured. + if (empty($this->publicKey)) + { + // Remember, both the username and password have to be URL encoded as they're part of a URI! + $password = urlencode($this->password); + $authentication .= ':' . $password; + } + + $ftpUri = 'sftp://' . $authentication . '@' . $this->host; + + if (!empty($this->port)) + { + $ftpUri .= ':' . (int) $this->port; + } + + // Relative path? Append the initial directory. + if (substr($remoteFile, 0, 1) != '/') + { + $ftpUri .= $this->directory; + } + + // Add a remote file if necessary. The filename must be URL encoded since we're creating a URI. + if (!empty($remoteFile)) + { + $suffix = ''; + + $dirname = dirname($remoteFile); + + // Windows messing up dirname('/'). KILL ME. + if ($dirname == '\\') + { + $dirname = ''; + } + + $dirname = trim($dirname, '/'); + $basename = basename($remoteFile); + + if ((substr($remoteFile, -1) == '/') && !empty($basename)) + { + $suffix = '/' . $suffix; + } + + $ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $ftpUri); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + + // Do I have to use certificate authentication? + if (!empty($this->publicKey)) + { + // We always need to provide a public key file + curl_setopt($ch, CURLOPT_SSH_PUBLIC_KEYFILE, $this->publicKey); + + // Since SSH certificates are self-signed we cannot have cURL verify their signatures against a CA. + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, 0); + + /** + * This is optional because newer versions of cURL can extract the private key file from a combined + * certificate file. + */ + if (!empty($this->privateKey)) + { + curl_setopt($ch, CURLOPT_SSH_PRIVATE_KEYFILE, $this->privateKey); + } + + /** + * In case of encrypted (a.k.a. password protected) private key files you need to also specify the + * certificate decryption key in the password field. However, if libcurl is compiled against the GnuTLS + * library (instead of OpenSSL) this will NOT work because of bugs / missing features in GnuTLS. It's the + * same problem you get when libssh is compiled against GnuTLS. The solution to that is having an + * unencrypted private key file. + */ + if (!empty($this->password)) + { + curl_setopt($ch, CURLOPT_KEYPASSWD, $this->password); + } + } + + // Should I enable verbose output? Useful for debugging. + if ($this->verbose) + { + curl_setopt($ch, CURLOPT_VERBOSE, 1); + } + + // Automatically create missing directories + curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1); + + return $ch; + } + + /** + * Test the connection to the SFTP server and whether the initial directory is correct. This is done by attempting to + * list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check + * if we can upload files to that remote folder. + * + * @throws \RuntimeException + */ + public function connect() + { + $ch = $this->getCurlHandle($this->directory . '/'); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $listing = curl_exec($ch); + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException("cURL Error $errNo connecting to remote SFTP server: $error", 500); + } + } + + /** + * Write the contents into the file + * + * @param string $fileName The full path to the file + * @param string $contents The contents to write to the file + * + * @return boolean True on success + */ + public function write($fileName, $contents) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+'); + fwrite($handle, $contents); + + // Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle + try + { + $this->uploadFromHandle($fileName, $handle); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Uploads a local file to the remote storage + * + * @param string $localFilename The full path to the local file + * @param string $remoteFilename The full path to the remote file + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function upload($localFilename, $remoteFilename, $useExceptions = true) + { + $fp = @fopen($localFilename, 'rb'); + + if ($fp === false) + { + throw new \RuntimeException("Unreadable local file $localFilename"); + } + + // Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle + try + { + $this->uploadFromHandle($remoteFilename, $fp); + } + catch (\RuntimeException $e) + { + if ($useExceptions) + { + throw $e; + } + + return false; + } + + return true; + } + + /** + * Read the contents of a remote file into a string + * + * @param string $fileName The full path to the remote file + * + * @return string The contents of the remote file + */ + public function read($fileName) + { + try + { + return $this->downloadToString($fileName); + } + catch (\RuntimeException $e) + { + throw new \RuntimeException("Can not download remote file $fileName", 500, $e); + } + } + + /** + * Download a remote file into a local file + * + * @param string $remoteFilename The remote file path to download from + * @param string $localFilename The local file path to download to + * @param bool $useExceptions Throw an exception instead of returning "false" on connection error. + * + * @return boolean True on success + */ + public function download($remoteFilename, $localFilename, $useExceptions = true) + { + $fp = @fopen($localFilename, 'wb'); + + if ($fp === false) + { + return false; + } + + // Note: don't manually close the file pointer, it's closed automatically by downloadToHandle + try + { + $this->downloadToHandle($remoteFilename, $fp); + } + catch (\RuntimeException $e) + { + if ($useExceptions) + { + throw $e; + } + + return false; + } + + return true; + } + + /** + * Delete a file (remove it from the disk) + * + * @param string $fileName The full path to the file + * + * @return boolean True on success + */ + public function delete($fileName) + { + $commands = array( + 'rm ' . $this->getPath($fileName), + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Create a copy of the file. Actually, we have to read it in memory and upload it again. + * + * @param string $from The full path of the file to copy from + * @param string $to The full path of the file that will hold the copy + * + * @return boolean True on success + */ + public function copy($from, $to) + { + // Make sure the buffer:// wrapper is loaded + class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); + + $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); + + try + { + $this->downloadToHandle($from, $handle, false); + $this->uploadFromHandle($to, $handle); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Move or rename a file + * + * @param string $from The full path of the file to move + * @param string $to The full path of the target file + * + * @return boolean True on success + */ + public function move($from, $to) + { + $from = $this->getPath($from); + $to = $this->getPath($to); + + $commands = array( + 'rename ' . $from . ' ' . $to, + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Change the permissions of a file + * + * @param string $fileName The full path of the file whose permissions will change + * @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!) + * + * @return boolean True on success + */ + public function chmod($fileName, $permissions) + { + // Make sure permissions are in an octal string representation + if (!is_string($permissions)) + { + $permissions = decoct($permissions); + } + + $commands = array( + 'chmod ' . $permissions . ' ' . $this->getPath($fileName), + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + + return true; + } + + /** + * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all + * intermediate directories if they do not already exist. + * + * @param string $dirName The full path of the directory to create + * @param integer $permissions The permissions of the created directory + * + * @return boolean True on success + */ + public function mkdir($dirName, $permissions = 0755) + { + $targetDir = rtrim($dirName, '/'); + + $directories = explode('/', $targetDir); + + $remoteDir = ''; + + foreach ($directories as $dir) + { + if (!$dir) + { + continue; + } + + $remoteDir .= '/' . $dir; + + // Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine + if ($this->isDir($remoteDir)) + { + continue; + } + + $commands = array( + 'mkdir ' . $remoteDir, + ); + + try + { + $this->executeServerCommands($commands); + } + catch (\RuntimeException $e) + { + return false; + } + } + + $this->chmod($dirName, $permissions); + + return true; + } + + /** + * Checks if the given directory exists + * + * @param string $path The full path of the remote directory to check + * + * @return boolean True if the directory exists + */ + public function isDir($path) + { + $ch = $this->getCurlHandle($path . '/'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $list = curl_exec($ch); + + $errNo = curl_errno($ch); + curl_close($ch); + + if ($errNo) + { + return false; + } + + return true; + } + + /** + * Get the current working directory. NOT IMPLEMENTED. + * + * @return string + */ + public function cwd() + { + return ''; + } + + /** + * Returns the absolute remote path from a path relative to the initial directory configured when creating the + * transfer object. + * + * @param string $fileName The relative path of a file or directory + * + * @return string The absolute path for use by the transfer object + */ + public function getPath($fileName) + { + $fileName = str_replace('\\', '/', $fileName); + + if (strpos($fileName, $this->directory) === 0) + { + return $fileName; + } + + $fileName = trim($fileName, '/'); + $fileName = rtrim($this->directory, '/') . '/' . $fileName; + + return $fileName; + } + + /** + * Lists the subdirectories inside an SFTP directory + * + * @param null|string $dir The directory to scan. Skip to use the current directory. + * + * @return array|bool A list of folders, or false if we could not get a listing + * + * @throws \RuntimeException When the server is incompatible with our SFTP folder scanner + */ + public function listFolders($dir = null) + { + if (empty($dir)) + { + $dir = $this->directory; + } + + $dir = rtrim($dir, '/'); + + $ch = $this->getCurlHandle($dir . '/'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $list = curl_exec($ch); + + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500); + } + + if (empty($list)) + { + throw new \RuntimeException("Sorry, your SFTP server doesn't support our SFTP directory browser."); + } + + $folders = array(); + + // Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters + $list = explode("\n", $list); + $list = array_map('rtrim', $list); + + foreach ($list as $v) + { + $vInfo = preg_split("/[\s]+/", $v, 9); + + if ($vInfo[0] !== "total") + { + $perms = $vInfo[0]; + + if (substr($perms, 0, 1) == 'd') + { + $folders[] = $vInfo[8]; + } + } + } + + asort($folders); + + return $folders; + } + + /** + * Is the verbose debug option set? + * + * @return boolean + */ + public function isVerbose() + { + return $this->verbose; + } + + /** + * Set the verbose debug option + * + * @param boolean $verbose + * + * @return void + */ + public function setVerbose($verbose) + { + $this->verbose = $verbose; + } + + /** + * Uploads a file using file contents provided through a file handle + * + * @param string $remoteFilename + * @param resource $fp + * + * @return void + * + * @throws \RuntimeException + */ + protected function uploadFromHandle($remoteFilename, $fp) + { + // We need the file size. We can do that by getting the file position at EOF + fseek($fp, 0, SEEK_END); + $filesize = ftell($fp); + rewind($fp); + + $ch = $this->getCurlHandle($remoteFilename); + curl_setopt($ch, CURLOPT_UPLOAD, 1); + curl_setopt($ch, CURLOPT_INFILE, $fp); + curl_setopt($ch, CURLOPT_INFILESIZE, $filesize); + + curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + fclose($fp); + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + } + + /** + * Downloads a remote file to the provided file handle + * + * @param string $remoteFilename Filename on the remote server + * @param resource $fp File handle where the downloaded content will be written to + * @param bool $close Optional. Should I close the file handle when I'm done? (Default: true) + * + * @return void + * + * @throws \RuntimeException + */ + protected function downloadToHandle($remoteFilename, $fp, $close = true) + { + $ch = $this->getCurlHandle($remoteFilename); + + curl_setopt($ch, CURLOPT_FILE, $fp); + + curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($close) + { + fclose($fp); + } + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + } + + /** + * Downloads a remote file and returns it as a string + * + * @param string $remoteFilename Filename on the remote server + * + * @return string + * + * @throws \RuntimeException + */ + protected function downloadToString($remoteFilename) + { + $ch = $this->getCurlHandle($remoteFilename); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + + $ret = curl_exec($ch); + + $error_no = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($error_no) + { + throw new \RuntimeException($error, $error_no); + } + + return $ret; + } + + /** + * Executes arbitrary SFTP commands + * + * @param array $commands An array with the SFTP commands to be executed + * + * @return string The output of the executed commands + * + * @throws \RuntimeException + */ + protected function executeServerCommands($commands) + { + $ch = $this->getCurlHandle($this->directory . '/'); + + curl_setopt($ch, CURLOPT_QUOTE, $commands); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $listing = curl_exec($ch); + $errNo = curl_errno($ch); + $error = curl_error($ch); + curl_close($ch); + + if ($errNo) + { + throw new \RuntimeException($error, $errNo); + } + + return $listing; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/TransferInterface.php b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/TransferInterface.php new file mode 100644 index 00000000..8b9ce366 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupEngine/Util/Transfer/TransferInterface.php @@ -0,0 +1,168 @@ + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/02.advanced.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/02.advanced.ini new file mode 100644 index 00000000..d122c5dd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/02.advanced.ini @@ -0,0 +1,63 @@ +; 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 +title = COM_AKEEBA_CONFIG_DUMPENGINE_TITLE +description = COM_AKEEBA_CONFIG_DUMPENGINE_DESCRIPTION +protected = 0 + +; 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 + +[akeeba.advanced.integritycheck] +default=0 +type=bool +title=COM_AKEEBA_ENGINE_TEXTEXTRACT_LBL +description=COM_AKEEBA_ENGINE_TEXTEXTRACT_DESC + +; Embedded installer +[akeeba.advanced.embedded_installer] +default = "angie" +type = "installer" +title = COM_AKEEBA_CONFIG_INSTALLER_TITLE +description = COM_AKEEBA_CONFIG_INSTALLER_DESCRIPTION +protected = 0 + +[engine.installer.angie.key] +default="" +type=password +title=COM_AKEEBA_CONFIG_ANGIE_KEY_TITLE +description=COM_AKEEBA_CONFIG_ANGIE_KEY_DESCRIPTION +protected="0" diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/01.basic.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/01.basic.ini new file mode 100644 index 00000000..711b0778 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/01.basic.ini @@ -0,0 +1,62 @@ +; Akeeba core engine configuration values +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: 01.basic.ini 930 2011-09-19 12:54:04Z nikosdion $ + +; ====================================================================== +; 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_ERROR|COM_AKEEBA_CONFIG_LOGLEVEL_WARNING|COM_AKEEBA_CONFIG_LOGLEVEL_INFO|COM_AKEEBA_CONFIG_LOGLEVEL_DEBUG" +enumvalues="0|1|2|3|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|COM_AKEEBA_CONFIG_BACKUPTYPE_FILEONLY|COM_AKEEBA_CONFIG_BACKUPTYPE_ALLDB|COM_AKEEBA_CONFIG_BACKUPTYPE_INCFILE|COM_AKEEBA_CONFIG_BACKUPTYPE_INCFULL" +enumvalues="full|dbonly|fileonly|alldb|incfile|incfull" +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 + +[akeeba.core.usedbstorage] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_USEDBSTORAGE_TITLE +description=COM_AKEEBA_CONFIG_USEDBSTORAGE_DESCRIPTION diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.advanced.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.advanced.ini new file mode 100644 index 00000000..bb32ec75 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.advanced.ini @@ -0,0 +1,79 @@ +; Akeeba core engine configuration values +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: 02.advanced.ini 738 2011-06-15 13:11:38Z nikosdion $ + +; ====================================================================== +; Advanced configuration +; ====================================================================== + +[_group] +description=COM_AKEEBA_CONFIG_ADVANCED + +; Database dump engine +[akeeba.advanced.dump_engine] +default=native +type=engine +subtype=dump +title=COM_AKEEBA_CONFIG_DUMPENGINE_TITLE +description=COM_AKEEBA_CONFIG_DUMPENGINE_DESCRIPTION +protected=0 + +; File scanner engine +[akeeba.advanced.scan_engine] +default=smart +type=engine +subtype=scan +title=COM_AKEEBA_CONFIG_SCANENGINE_TITLE +description=COM_AKEEBA_CONFIG_SCANENGINE_DESCRIPTION +protected=0 + +; 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=engine +subtype=postproc +title=COM_AKEEBA_CONFIG_PROCENGINE_TITLE +description=COM_AKEEBA_CONFIG_PROCENGINE_DESCRIPTION +protected=0 + +[akeeba.advanced.uploadkickstart] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_UPLOADKICKSTART_TITLE +description=COM_AKEEBA_CONFIG_UPLOADKICKSTART_DESCRIPTION + +[akeeba.advanced.integritycheck] +default=0 +type=bool +title=COM_AKEEBA_ENGINE_TEXTEXTRACT_LBL +description=COM_AKEEBA_ENGINE_TEXTEXTRACT_DESC + +; Embedded installer +[akeeba.advanced.embedded_installer] +default="angie" +type=installer +title=COM_AKEEBA_CONFIG_INSTALLER_TITLE +description=COM_AKEEBA_CONFIG_INSTALLER_DESCRIPTION +protected=0 + +[engine.installer.angie.key] +default="" +type=password +title=COM_AKEEBA_CONFIG_ANGIE_KEY_TITLE +description=COM_AKEEBA_CONFIG_ANGIE_KEY_DESCRIPTION +protected="0" + +; Virtual folder name for external files +[akeeba.advanced.virtual_folder] +default=external_files +type=string +title=COM_AKEEBA_CONFIG_VIRTUALFOLDER_TITLE +description=COM_AKEEBA_CONFIG_VIRTUALFOLDER_DESCRIPTION diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.platform.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.platform.ini new file mode 100644 index 00000000..9279f7f8 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/02.platform.ini @@ -0,0 +1,81 @@ +; Akeeba core engine configuration values +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + +; ====================================================================== +; Platform configuration +; ====================================================================== + +[_group] +description=COM_AKEEBA_CONFIG_PLATFORM + +; Should we override the site's root? +[akeeba.platform.override_root] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_PLATFORM_OVERRIDEROOT_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_OVERRIDEROOT_DESCRIPTION + +; Which site's root should I use, then? +[akeeba.platform.newroot] +default="" +type=browsedir +title=COM_AKEEBA_CONFIG_PLATFORM_NEWROOT_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_NEWROOT_DESCRIPTION + +; Should we override the site's database? +[akeeba.platform.override_db] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_PLATFORM_OVERRIDEDB_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_OVERRIDEDB_DESCRIPTION + +; Database driver +[akeeba.platform.dbdriver] +default=mysqli +type=enum +enumkeys="MySQL|MySQLi" +enumvalues="mysql|mysqli" +title=COM_AKEEBA_CONFIG_PLATFORM_DBDRIVER_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBDRIVER_DESCRIPTION + +; Database hostname +[akeeba.platform.dbhost] +default="" +type=string +title=COM_AKEEBA_CONFIG_PLATFORM_DBHOST_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBHOST_DESCRIPTION + +; Database port +[akeeba.platform.dbport] +default="" +type=string +title=COM_AKEEBA_CONFIG_PLATFORM_DBPORT_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBPORT_DESCRIPTION + +; Database username +[akeeba.platform.dbusername] +default="" +type=string +title=COM_AKEEBA_CONFIG_PLATFORM_DBUSERNAME_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBUSERNAME_DESCRIPTION + +; Database password +[akeeba.platform.dbpassword] +default="" +type=password +title=COM_AKEEBA_CONFIG_PLATFORM_DBPASSWORD_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBPASSWORD_DESCRIPTION + +; Database name +[akeeba.platform.dbname] +default="" +type=string +title=COM_AKEEBA_CONFIG_PLATFORM_DBDATABASE_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBDATABASE_DESCRIPTION + +; Database prefix +[akeeba.platform.dbprefix] +default="" +type=string +title=COM_AKEEBA_CONFIG_PLATFORM_DBPREFIX_TITLE +description=COM_AKEEBA_CONFIG_PLATFORM_DBPREFIX_DESCRIPTION diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/03.filters.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/03.filters.ini new file mode 100644 index 00000000..da1e5595 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/03.filters.ini @@ -0,0 +1,6 @@ +; Optional filter definitions +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: 03.filters.ini 738 2011-06-15 13:11:38Z nikosdion $ + +[_group] +description=COM_AKEEBA_CONFIG_HEADER_OPTIONALFILTERS diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/04.quota.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/04.quota.ini new file mode 100644 index 00000000..f3213c8b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/04.quota.ini @@ -0,0 +1,97 @@ +; Akeeba core engine configuration values +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: 04.quota.ini 789 2011-07-21 11:23:36Z nikosdion $ + +[_group] +description=COM_AKEEBA_CONFIG_HEADER_QUOTA + +; ====================================================================== +; Quota management +; ====================================================================== + +; Remote quotas toggle +[akeeba.quota.remote] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_REMOTEQUOTA_ENABLE_TITLE +description=COM_AKEEBA_CONFIG_REMOTEQUOTA_ENABLE_DESCRIPTION + +; Maximum backup age quotas +[akeeba.quota.maxage.enable] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_MAXAGEQUOTA_ENABLE_TITLE +description=COM_AKEEBA_CONFIG_MAXAGEQUOTA_ENABLE_DESCRIPTION + +; How many days of backups to keep +[akeeba.quota.maxage.maxdays] +default=31 +type=integer +min=1 +max=365 +shortcuts="31|60|182|365" +scale=1 +uom=days +title=COM_AKEEBA_CONFIG_MAXAGEQUOTA_MAXDAYS_TITLE +description=COM_AKEEBA_CONFIG_MAXAGEQUOTA_MAXDAYS_DESCRIPTION + +; Force keep the backups of Xth day of each month +[akeeba.quota.maxage.keepday] +default=1 +type=integer +min=0 +max=31 +shortcuts="0|1|15" +scale=1 +uom=day +title=COM_AKEEBA_CONFIG_MAXAGEQUOTA_KEEPDAY_TITLE +description=COM_AKEEBA_CONFIG_MAXAGEQUOTA_KEEPDAY_DESCRIPTION + +; 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 +title=COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_TITLE +description=COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_DESCRIPTION diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/05.tuning.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/05.tuning.ini new file mode 100644 index 00000000..52c5174e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro/05.tuning.ini @@ -0,0 +1,115 @@ +; Akeeba core engine configuration values +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd +; Version $Id: 05.tuning.ini 738 2011-06-15 13:11:38Z nikosdion $ + +[_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=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SBBLF_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SBBLF_DESC + +[akeeba.tuning.nobreak.afterlargefile] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SBALF_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SBALF_DESC + +; +[akeeba.tuning.nobreak.proactive] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SBPA_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SBPA_DESC + +[akeeba.tuning.nobreak.domains] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SBBD_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SBBD_DESC + +[akeeba.tuning.nobreak.finalization] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SBPP_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SBPP_DESC + +[akeeba.tuning.settimelimit] +default=0 +type=bool +title=COM_AKEEBA_CONFIG_ADVANCED_SETTIMELIMIT_LABEL +description=COM_AKEEBA_CONFIG_ADVANCED_SETTIMELIMIT_DESC diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Driver/Joomla.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Driver/Joomla.php new file mode 100644 index 00000000..753147d5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Driver/Joomla.php @@ -0,0 +1,189 @@ +connect(); + + $options['connection'] = $db->getConnection(); + + switch ($db->name) + { + case 'mysql': + // So, Joomla! 4's "mysql" is, actually, "pdomysql". + $driver = 'mysql'; + + if (version_compare(JVERSION, '3.99999.99999', 'gt')) + { + $driver = 'pdomysql'; + } + break; + + case 'mysqli': + $driver = 'mysqli'; + break; + + case 'sqlsrv': + case 'mssql': + $driver = 'sqlsrv'; + break; + + case 'sqlazure': + $driver = 'sqlsrv'; + break; + + case 'postgresql': + $driver = 'postgresql'; + break; + + case 'pdomysql': + $driver = 'pdomysql'; + break; + + default: + $driver = ''; + + return; // Brace yourself, this engine is going down crashing in flames. + break; + } + + $driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver); + } + else + { + $driver = Platform::getInstance()->get_default_database_driver(false); + } + + $this->dbo = new $driver($options); + + // Propagate errors + $this->propagateFromObject($this->dbo); + } + + public function close() + { + if (method_exists($this->dbo, 'close')) + { + $this->dbo->close(); + } + elseif (method_exists($this->dbo, 'disconnect')) + { + $this->dbo->disconnect(); + } + } + + public function open() + { + if (method_exists($this->dbo, 'open')) + { + $this->dbo->open(); + } + elseif (method_exists($this->dbo, 'connect')) + { + $this->dbo->connect(); + } + } + + /** + * Magic method to proxy all calls to the loaded database driver object + */ + public function __call($name, array $arguments) + { + if (is_null($this->dbo)) + { + throw new \Exception('Akeeba Engine database driver is not loaded'); + } + + if (method_exists($this->dbo, $name) || in_array($name, array('q', 'nq', 'qn'))) + { + // Call_user_func_array is ~3 times slower than direct method calls. + // (thank you, Nooku Framework, for the tip!) + switch (count($arguments)) + { + case 0 : + $result = $this->dbo->$name(); + break; + case 1 : + $result = $this->dbo->$name($arguments[0]); + break; + case 2: + $result = $this->dbo->$name($arguments[0], $arguments[1]); + break; + case 3: + $result = $this->dbo->$name($arguments[0], $arguments[1], $arguments[2]); + break; + case 4: + $result = $this->dbo->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + break; + case 5: + $result = $this->dbo->$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($this->dbo, $name), $arguments); + } + return $result; + } + else + { + throw new \Exception('Method ' . $name . ' not found in Akeeba Platform'); + } + } + + public function __get($name) + { + if (isset($this->dbo->$name) || property_exists($this->dbo, $name)) + { + return $this->dbo->$name; + } + else + { + $this->dbo->$name = null; + user_error('Database driver does not support property ' . $name); + } + } + + public function __set($name, $value) + { + if (isset($this->dbo->name) || property_exists($this->dbo, $name)) + { + $this->dbo->$name = $value; + } + else + { + $this->dbo->$name = null; + user_error('Database driver not support property ' . $name); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Cvsfolders.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Cvsfolders.php new file mode 100644 index 00000000..ae43d074 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Cvsfolders.php @@ -0,0 +1,57 @@ +object = 'dir'; + $this->subtype = 'all'; + $this->method = 'regex'; + $this->filter_name = 'Cvsfolders'; + + if (empty($this->filter_name)) + { + $this->filter_name = strtolower(basename(__FILE__, '.php')); + } + + parent::__construct(); + + // 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]'; + } + + $this->filter_data[$root] = array( + '#/\.git$#', + '#^\.git$#', + '#/\.svn$#', + '#^\.svn$#' + ); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefiles.php new file mode 100644 index 00000000..1bacb7bc --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefiles.php @@ -0,0 +1,54 @@ +object = 'file'; + $this->subtype = 'all'; + $this->method = 'direct'; + $this->filter_name = 'Excludefiles'; + + // 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]'; + } + + // We take advantage of the filter class magic to inject our custom filters + $this->filter_data[$root] = array( + 'kickstart.php', + 'error_log', + 'administrator/error_log' + ); + + parent::__construct(); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefolders.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefolders.php new file mode 100644 index 00000000..6d1114aa --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludefolders.php @@ -0,0 +1,52 @@ +object = 'dir'; + $this->subtype = 'all'; + $this->method = 'direct'; + $this->filter_name = 'Excludefolders'; + + // 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]'; + } + + // We take advantage of the filter class magic to inject our custom filters + $this->filter_data[$root] = array( + 'awstats', + 'cgi-bin', + ); + + parent::__construct(); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludetabledata.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludetabledata.php new file mode 100644 index 00000000..ad8d3860 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Excludetabledata.php @@ -0,0 +1,41 @@ +object = 'dbobject'; + $this->subtype = 'content'; + $this->method = 'direct'; + $this->filter_name = 'Excludetabledata'; + + // We take advantage of the filter class magic to inject our custom filters + $this->filter_data['[SITEDB]'] = array( + '#__session', // Sessions table + '#__guardxt_runs' // Guard XT's run log (bloated to the bone) + ); + + parent::__construct(); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipdirs.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipdirs.php new file mode 100644 index 00000000..c2d0d76e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipdirs.php @@ -0,0 +1,118 @@ +object = 'dir'; + $this->subtype = 'children'; + $this->method = 'direct'; + $this->filter_name = 'Joomlaskipdirs'; + + // We take advantage of the filter class magic to inject our custom filters + $configuration = Factory::getConfiguration(); + $container = Container::getInstance('com_akeeba'); + $jreg = $container->platform->getConfig(); + + $tmpdir = $jreg->get('tmp_path'); + $logsdir = $jreg->get('log_path'); + + // Get the site's root + if ($configuration->get('akeeba.platform.override_root', 0)) + { + $root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]'); + } + else + { + $root = '[SITEROOT]'; + } + + $this->filter_data[$root] = [ + // Output & temp directory of the component + $this->treatDirectory($configuration->get('akeeba.basic.output_directory')), + + // Joomla! temporary directory + $this->treatDirectory($tmpdir), + + // Joomla! logs directory + $this->treatDirectory($logsdir), + + // default temp directory + $this->treatDirectory(JPATH_SITE . '/tmp'), + 'tmp', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/tmp'), + + // Joomla! front- and back-end cache, as reported by Joomla! + $this->treatDirectory(JPATH_CACHE), + $this->treatDirectory(JPATH_ADMINISTRATOR . '/cache'), + $this->treatDirectory(JPATH_ROOT . '/cache'), + // cache directories fallback + 'cache', + 'administrator/cache', + // Joomla! front- and back-end cache, as calculated by us (redundancy, for funky server setups) + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/cache'), + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/cache'), + + // This is not needed except on sites running SVN or beta releases + $this->treatDirectory(JPATH_ROOT . '/installation'), + // ...and the fallbacks + 'installation', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/installation'), + + // Default backup output (many people change it, forget to remove old backup archives and they end up backing up old backups) + $this->treatDirectory(JPATH_ADMINISTRATOR . '/components/com_akeeba/backup'), + 'administrator/components/com_akeeba/backup', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/components/com_akeeba/backup'), + + // MyBlog's cache + $this->treatDirectory(JPATH_SITE . '/components/libraries/cmslib/cache'), + // ...and fallbacks + 'components/libraries/cmslib/cache', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/components/libraries/cmslib/cache'), + + // Used by Plesk to store its logs. It's in the public root, owned by root and read-only. Yipee! + $this->treatDirectory(JPATH_ROOT . '/logs'), + 'logs', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/logs'), + + // Some developers hardcode this path for their log files. I guess they never heard of Joomla!'s Global Configuration? + $this->treatDirectory(JPATH_ROOT . '/log'), + 'log', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/log'), + + // Joomla! 3.6 is loads of fun. It changed the logs folder location. + $this->treatDirectory(JPATH_ADMINISTRATOR . '/logs'), + 'administrator/logs', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/logs'), + + // Also in case a Joomla! 3.6 site admin cocks up, let's try a singular folder name. + $this->treatDirectory(JPATH_ADMINISTRATOR . '/log'), + 'administrator/log', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/log'), + ]; + + parent::__construct(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipfiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipfiles.php new file mode 100644 index 00000000..c92b36d4 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Joomlaskipfiles.php @@ -0,0 +1,118 @@ +object = 'dir'; + $this->subtype = 'content'; + $this->method = 'direct'; + $this->filter_name = 'Joomlaskipfiles'; + + // We take advantage of the filter class magic to inject our custom filters + $configuration = Factory::getConfiguration(); + $container = Container::getInstance('com_akeeba'); + $jreg = $container->platform->getConfig(); + + $tmpdir = $jreg->get('tmp_path'); + $logsdir = $jreg->get('log_path'); + + // Get the site's root + if ($configuration->get('akeeba.platform.override_root', 0)) + { + $root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]'); + } + else + { + $root = '[SITEROOT]'; + } + + $this->filter_data[$root] = [ + // Output & temp directory of the component + $this->treatDirectory($configuration->get('akeeba.basic.output_directory')), + + // Joomla! temporary directory + $this->treatDirectory($tmpdir), + + // Joomla! logs directory + $this->treatDirectory($logsdir), + + // default temp directory + $this->treatDirectory(JPATH_SITE . '/tmp'), + 'tmp', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/tmp'), + + // Joomla! front- and back-end cache, as reported by Joomla! + $this->treatDirectory(JPATH_CACHE), + $this->treatDirectory(JPATH_ADMINISTRATOR . '/cache'), + $this->treatDirectory(JPATH_ROOT . '/cache'), + // cache directories fallback + 'cache', + 'administrator/cache', + // Joomla! front- and back-end cache, as calculated by us (redundancy, for funky server setups) + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/cache'), + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/cache'), + + // This is not needed except on sites running SVN or beta releases + $this->treatDirectory(JPATH_ROOT . '/installation'), + // ...and the fallbacks + 'installation', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/installation'), + + // Default backup output (many people change it, forget to remove old backup archives and they end up backing up old backups) + $this->treatDirectory(JPATH_ADMINISTRATOR . '/components/com_akeeba/backup'), + 'administrator/components/com_akeeba/backup', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/components/com_akeeba/backup'), + + // MyBlog's cache + $this->treatDirectory(JPATH_SITE . '/components/libraries/cmslib/cache'), + // ...and fallbacks + 'components/libraries/cmslib/cache', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/components/libraries/cmslib/cache'), + + // Used by Plesk to store its logs. It's in the public root, owned by root and read-only. Yipee! + $this->treatDirectory(JPATH_ROOT . '/logs'), + 'logs', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/logs'), + + // Some developers hardcode this path for their log files. I guess they never heard of Joomla!'s Global Configuration? + $this->treatDirectory(JPATH_ROOT . '/log'), + 'log', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/log'), + + // Joomla! 3.6 is loads of fun. It changed the logs folder location. + $this->treatDirectory(JPATH_ADMINISTRATOR . '/logs'), + 'administrator/logs', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/logs'), + + // Also in case a Joomla! 3.6 site admin cocks up, let's try a singular folder name. + $this->treatDirectory(JPATH_ADMINISTRATOR . '/log'), + 'administrator/log', + $this->treatDirectory(Platform::getInstance()->get_site_root() . '/administrator/log'), + ]; + + parent::__construct(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Libraries.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Libraries.php new file mode 100644 index 00000000..aa918303 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Libraries.php @@ -0,0 +1,84 @@ +object = 'dir'; + $this->subtype = 'inclusion'; + $this->method = 'direct'; + $this->filter_name = 'Libraries'; + + // FIXME This filter doesn't work very well on many live hosts. Disabled for now. + parent::__construct(); + + return; + + + if (empty($this->filter_name)) + { + $this->filter_name = strtolower(basename(__FILE__, '.php')); + } + + // Get the saved library path and compare it to the default + $jlibdir = Platform::getInstance()->get_platform_configuration_option('jlibrariesdir', ''); + if (empty($jlibdir)) + { + if (defined('JPATH_LIBRARIES')) + { + $jlibdir = JPATH_LIBRARIES; + } + elseif (defined('JPATH_PLATFORM')) + { + $jlibdir = JPATH_PLATFORM; + } + else + { + $jlibdir = false; + } + } + + if ($jlibdir !== false) + { + $jlibdir = Factory::getFilesystemTools()->TranslateWinPath($jlibdir); + $defaultLibraries = Factory::getFilesystemTools()->TranslateWinPath(JPATH_SITE . '/libraries'); + + if ($defaultLibraries != $jlibdir) + { + // The path differs, add it here + $this->filter_data['JPATH_LIBRARIES'] = $jlibdir; + } + } + else + { + $this->filter_data = array(); + } + parent::__construct(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Sitedb.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Sitedb.php new file mode 100644 index 00000000..34ce929c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Sitedb.php @@ -0,0 +1,107 @@ +object = 'db'; + $this->subtype = 'inclusion'; + $this->method = 'direct'; + $this->filter_name = 'Sitedb'; + + // Add a new record for the core Joomla! database + // Get core database options + $configuration = Factory::getConfiguration(); + + if ($configuration->get('akeeba.platform.override_db', 0)) + { + $options = array( + 'port' => $configuration->get('akeeba.platform.dbport', ''), + 'host' => $configuration->get('akeeba.platform.dbhost', ''), + 'user' => $configuration->get('akeeba.platform.dbusername', ''), + 'password' => $configuration->get('akeeba.platform.dbpassword', ''), + 'database' => $configuration->get('akeeba.platform.dbname', ''), + 'prefix' => $configuration->get('akeeba.platform.dbprefix', ''), + ); + $driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($configuration->get('akeeba.platform.dbdriver', 'mysqli')); + } + else + { + $options = Platform::getInstance()->get_platform_database_options(); + $driver = Platform::getInstance()->get_default_database_driver(true); + } + + + $host = $options['host']; + $port = array_key_exists('port', $options) ? $options['port'] : null; + + if (empty($port)) + { + $port = null; + } + + $socket = null; + $targetSlot = substr(strstr($host, ":"), 1); + + if ( !empty($targetSlot)) + { + // Get the port number or socket name + if (is_numeric($targetSlot) && is_null($port)) + { + $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'; + } + } + + // This is the format of the database inclusion filters + $entry = array( + 'host' => $host, + 'port' => is_null($socket) ? (is_null($port) ? '' : $port) : $socket, + 'username' => $options['user'], + 'password' => $options['password'], + 'database' => $options['database'], + 'prefix' => $options['prefix'], + 'dumpFile' => 'site.sql', + 'driver' => $driver + ); + + // We take advantage of the filter class magic to inject our custom filters + $configuration = Factory::getConfiguration(); + + $this->filter_data['[SITEDB]'] = $entry; + + parent::__construct(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Siteroot.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Siteroot.php new file mode 100644 index 00000000..9695c496 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Siteroot.php @@ -0,0 +1,55 @@ +object = 'dir'; + $this->subtype = 'inclusion'; + $this->method = 'direct'; + $this->filter_name = 'Siteroot'; + + // Directory inclusion format: + // array(real_directory, add_path) + $add_path = null; // A null add_path means that we dump this dir's contents in the archive's root + + // We take advantage of the filter class magic to inject our custom filters + $configuration = Factory::getConfiguration(); + + if ($configuration->get('akeeba.platform.override_root', 0)) + { + $root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]'); + } + else + { + $root = '[SITEROOT]'; + } + + $this->filter_data[] = array( + $root, + $add_path + ); + + parent::__construct(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackFinder.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackFinder.php new file mode 100644 index 00000000..67632b25 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackFinder.php @@ -0,0 +1,55 @@ +object = 'dbobject'; + $this->subtype = 'content'; + $this->method = 'api'; + } + + protected function is_excluded_by_api($test, $root) + { + static $finderTables = array( + '#__finder_links', '#__finder_links_terms0', '#__finder_links_terms1', + '#__finder_links_terms2', '#__finder_links_terms3', '#__finder_links_terms4', + '#__finder_links_terms5', '#__finder_links_terms6', '#__finder_links_terms7', + '#__finder_links_terms8', '#__finder_links_terms9', '#__finder_links_termsa', + '#__finder_links_termsb', '#__finder_links_termsc', '#__finder_links_termsd', + '#__finder_links_termse', '#__finder_links_termsf', '#__finder_taxonomy', + '#__finder_taxonomy_map', '#__finder_terms' + ); + + // Not the site's database? Include the tables + if($root != '[SITEDB]') return false; + + // Is it one of the blacklisted tables? + if(in_array($test, $finderTables)) return true; + + // No match? Just include the file! + return false; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackMyjoomla.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackMyjoomla.php new file mode 100644 index 00000000..d6130ea5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/StackMyjoomla.php @@ -0,0 +1,53 @@ +object = 'dbobject'; + $this->subtype = 'content'; + $this->method = 'api'; + + parent::__construct(); + } + + protected function is_excluded_by_api($test, $root) + { + static $myjoomlaTables = array( + 'bf_core_hashes', + 'bf_files', + 'bf_files_last', + 'bf_folders', + 'bf_folders_to_scan' + ); + + // Is it one of the blacklisted tables? + if(in_array($test, $myjoomlaTables)) + { + return true; + } + + // No match? Just include the file! + return false; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/finder.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/finder.ini new file mode 100644 index 00000000..18467c51 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/finder.ini @@ -0,0 +1,13 @@ +; "Skip Finder tables' data" optional filter +; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + +; ====================================================================== +; Advanced configuration +; ====================================================================== + +[core.filters.finder.enabled] +default=1 +type=bool +title=COM_AKEEBA_CONFIG_OPTIONALFILTERS_FINDER_ENABLED_TITLE +description=COM_AKEEBA_CONFIG_OPTIONALFILTERS_FINDER_ENABLED_DESCRIPTION +bold=1 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/myjoomla.ini b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/myjoomla.ini new file mode 100644 index 00000000..c94734a5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Stack/myjoomla.ini @@ -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.myjoomla.enabled] +default=1 +type=bool +title=COM_AKEEBA_CONFIG_OPTIONALFILTERS_MYJOOMLA_ENABLED_TITLE +description=COM_AKEEBA_CONFIG_OPTIONALFILTERS_MYJOOMLA_ENABLED_DESCRIPTION +bold=1 diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Systemcachefiles.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Systemcachefiles.php new file mode 100644 index 00000000..a9a8d0aa --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Systemcachefiles.php @@ -0,0 +1,58 @@ +object = 'file'; + $this->subtype = 'all'; + $this->method = 'regex'; + $this->filter_name = 'Systemcachefiles'; + + if (empty($this->filter_name)) + { + $this->filter_name = strtolower(basename(__FILE__, '.php')); + } + + parent::__construct(); + + // 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]'; + } + + $this->filter_data[$root] = array( + '#/Thumbs\.db$#', + '#^Thumbs\.db$#', + '#/\.DS_Store$#i', + '#^\.DS_Store$#i', + '#^core\.[\d]{1,10}$#i', + ); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/FakeRestorationObserver.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/FakeRestorationObserver.php new file mode 100644 index 00000000..c96da1c9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/FakeRestorationObserver.php @@ -0,0 +1,37 @@ +type == 'startfile' ) + { + $this->filesProcessed++; + $this->compressedTotal += $message->content->compressed; + $this->uncompressedTotal += $message->content->uncompressed; + } + } + + public function __toString() + { + return __CLASS__; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/TestExtract.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/TestExtract.php new file mode 100644 index 00000000..8cb4db94 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Finalization/TestExtract.php @@ -0,0 +1,203 @@ +get('akeeba.advanced.integritycheck', 0); + + if (!$enabled) + { + return true; + } + + // Make sure the "Process each part immediately" option is not enabled + $postProcImmediately = $config->get('engine.postproc.common.after_part', 0, false); + $postProcEngine = $config->get('akeeba.advanced.postproc_engine', 'none'); + + if ($postProcImmediately && ($postProcEngine != 'none')) + { + $parent->setWarning(JText::_('COM_AKEEBA_ENGINE_TEXTEXTRACT_ERR_PROCESSIMMEDIATELY')); + + return true; + } + + // Make sure an archiver engine producing backup archives of JPA, JPS or ZIP files is in use + $archiver = Factory::getArchiverEngine(); + $extension = $archiver->getExtension(); + $extension = strtoupper($extension); + $extension = ltrim($extension, '.'); + + if (!in_array($extension, array('JPA', 'JPS', 'ZIP'))) + { + $parent->setWarning(JText::_('COM_AKEEBA_ENGINE_TEXTEXTRACT_ERR_INVALIDARCHIVERTYPE')); + + return true; + } + + // Set the KICKSTART constant to prevent Akeeba Restore from taking over the page output + if (!defined('KICKSTART')) + { + define('KICKSTART', 1); + } + + // Try to load Akeeba Restore + try + { + $this->loadAkeebaRestore(); + } + catch (\RuntimeException $e) + { + $parent->setError($e->getMessage()); + + return true; + } + + // Set up the Akeeba Restore engine, either from a serialised factory or from scratch + $factory = $config->get('volatile.finalization.testextract.factory', null, false); + + if (!is_null($factory) && is_string($factory)) + { + \AKFactory::unserialize($factory); + } + else + { + $this->setUpAkeebaRestore(); + } + + $parent->relayStep('Archive integrity check'); + $parent->relaySubstep('Testing if archive can be extracted'); + + \AKFactory::set('kickstart.enabled', true); + + /** @var \AKAbstractUnarchiver $engine */ + $engine = \AKFactory::getUnarchiver(); + $observer = new FakeRestorationObserver(); + $engine->attach($observer); + + $engine->tick(); + $ret = $engine->getStatusArray(); + + // Did an error occur? + if ($ret['Error'] != '') + { + $parent->setError(JText::sprintf('COM_AKEEBA_ENGINE_TEXTEXTRACT_ERR_INTEGRITYCHECKFAILED', $ret['Error'])); + + return true; + } + + // Did we finish successfully? + if (!$ret['HasRun']) + { + Factory::getLog()->log(LogLevel::INFO, __CLASS__ . ": The archive's integrity has been validated"); + $config->set('volatile.finalization.testextract.factory', null, false); + + return true; + } + + // Step finished and we need one more step to proceed. + $factory = \AKFactory::serialize(); + $config->set('volatile.finalization.testextract.factory', $factory, false); + + return false; + } + + /** + * Try to load the Akeeba Restore engine + */ + private function loadAkeebaRestore() + { + $path = __DIR__ . '/../../../restore.php'; + + if (!file_exists($path) || !include_once($path)) + { + throw new \RuntimeException(JText::_('COM_AKEEBA_ENGINE_TEXTEXTRACT_ERR_ENGINENOTFOUND'), 500); + } + } + + /** + * Set up the Akeeba Restore engine for the current archive + */ + private function setUpAkeebaRestore() + { + $config = Factory::getConfiguration(); + + $maxTime = Factory::getTimer()->getTimeLeft(); + $maxTime = floor($maxTime); + $maxTime = max(2, $maxTime); + + $statistics = Factory::getStatistics(); + $stat = $statistics->getRecord(); + $backup_parts = Factory::getStatistics()->get_all_filenames($stat, false); + $filePath = array_shift($backup_parts); + + $specialDirs = Platform::getInstance()->get_stock_directories(); + $tmpPath = $specialDirs['[SITETMP]']; + + $archiver = Factory::getArchiverEngine(); + $extension = $archiver->getExtension(); + $extension = strtoupper($extension); + $extension = ltrim($extension, '.'); + + $ksOptions = array( + 'kickstart.tuning.max_exec_time' => $maxTime, + 'kickstart.tuning.run_time_bias' => $config->get('akeeba.tuning.run_time_bias', 75), + 'kickstart.tuning.min_exec_time' => '0', + 'kickstart.procengine' => 'direct', + 'kickstart.setup.sourcefile' => $filePath, + 'kickstart.setup.destdir' => $tmpPath, + 'kickstart.setup.restoreperms' => '0', + 'kickstart.setup.filetype' => $extension, + 'kickstart.setup.dryrun' => '1', + 'kickstart.jps.password' => $config->get('engine.archiver.jps.key', '', false) + ); + + \AKFactory::nuke(); + + foreach ($ksOptions as $k => $v) + { + \AKFactory::set($k, $v); + } + + \AKFactory::set('kickstart.enabled', true); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Platform.php b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Platform.php new file mode 100644 index 00000000..010a5c53 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/BackupPlatform/Joomla3x/Platform.php @@ -0,0 +1,1243 @@ +configOverrides = $configOverrides; + + $this->container = Container::getInstance('com_akeeba'); + } + + + /** + * Loads the current configuration off the database table + * + * @param int $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) + { + // Load the configuration + parent::load_configuration($profile_id); + + // If there is no embedded installer or the wrong embedded installer is selected, fix it automatically + $config = Factory::getConfiguration(); + $embedded_installer = $config->get('akeeba.advanced.embedded_installer', null); + + if (empty($embedded_installer) || ($embedded_installer == 'angie-joomla')) + { + $protectedKeys = $config->getProtectedKeys(); + $config->setProtectedKeys(array()); + $config->set('akeeba.advanced.embedded_installer', 'angie'); + $config->setProtectedKeys($protectedKeys); + } + } + + /** + * Saves the current configuration to the database table + * + * @param int $profile_id The profile where to save the configuration to, defaults to current profile + * + * @return bool True if everything was saved properly + */ + public function save_configuration($profile_id = null) + { + // If there is no embedded installer or the wrong embedded installer is selected, fix it automatically + $config = Factory::getConfiguration(); + $embedded_installer = $config->get('akeeba.advanced.embedded_installer', null); + + if (empty($embedded_installer) || ($embedded_installer == 'angie-joomla')) + { + $protectedKeys = $config->getProtectedKeys(); + $config->setProtectedKeys(array()); + $config->set('akeeba.advanced.embedded_installer', 'angie'); + $config->setProtectedKeys($protectedKeys); + } + + // Save the configuration + return parent::save_configuration($profile_id); + } + + + /** + * Performs heuristics to determine if this platform object is the ideal + * candidate for the environment Akeeba Engine is running in. + * + * @return bool + */ + public function isThisPlatform() + { + // Make sure _JEXEC is defined + if ( !defined('_JEXEC')) + { + return false; + } + + // We need JVERSION to be defined + if ( !defined('JVERSION')) + { + return false; + } + + // Check if JFactory exists + if ( !class_exists('JFactory')) + { + return false; + } + + // Check if JApplication exists + $appExists = class_exists('JApplication'); + $appExists = $appExists || class_exists('JCli'); + $appExists = $appExists || class_exists('JApplicationCli'); + $appExists = $appExists || class_exists('AkeebaCliBase'); + + if ( !$appExists) + { + return false; + } + + return true; + } + + /** + * Returns an associative array of stock platform directories + * + * @return array + */ + public function get_stock_directories() + { + static $stock_directories = array(); + + if (empty($stock_directories)) + { + $jreg = $this->container->platform->getConfig(); + $tmpdir = $jreg->get('tmp_path'); + $stock_directories['[SITEROOT]'] = $this->get_site_root(); + $stock_directories['[ROOTPARENT]'] = @realpath($this->get_site_root() . '/..'); + $stock_directories['[SITETMP]'] = $tmpdir; + $stock_directories['[DEFAULT_OUTPUT]'] = $this->get_site_root() . '/administrator/components/com_akeeba/backup'; + } + + return $stock_directories; + } + + /** + * Returns the absolute path to the site's root + * + * @return string + */ + public function get_site_root() + { + static $root = null; + + if (empty($root) || is_null($root)) + { + $root = JPATH_ROOT; + + if (empty($root) || ($root == DIRECTORY_SEPARATOR) || ($root == '/')) + { + // Try to get the current root in a different way + if (function_exists('getcwd')) + { + $root = getcwd(); + } + + if ($this->container->platform->isBackend()) + { + if (empty($root)) + { + $root = '../'; + } + else + { + $adminPos = strpos($root, 'administrator'); + if ($adminPos !== false) + { + $root = substr($root, 0, $adminPos); + } + else + { + $root = '../'; + } + + // Degenerate case where $root = 'administrator' + // without a leading slash before entering this + // if-block + if (empty($root)) + { + $root = '../'; + } + } + } + else + { + if (empty($root) || ($root == DIRECTORY_SEPARATOR) || ($root == '/')) + { + $root = './'; + } + } + } + } + + return $root; + } + + /** + * Returns the absolute path to the installer images directory + * + * @return string + */ + public function get_installer_images_path() + { + return JPATH_ADMINISTRATOR . '/components/com_akeeba/Master/Installers'; + } + + /** + * Returns the active profile number + * + * @return int + */ + public function get_active_profile() + { + // Automated testing override + if (!is_null(self::$profile_id) && (self::$profile_id > 0)) + { + return self::$profile_id; + } + // Constant override + elseif (defined('AKEEBA_PROFILE')) + { + return AKEEBA_PROFILE; + } + // Use the session. If it's a CLI app always default to profile #1 (unless explicitly set otherwise) + else + { + $defaultProfile = $this->container->platform->isCli() ? 1 : null; + + return $this->container->platform->getSessionVar('profile', $defaultProfile, 'akeeba'); + } + } + + /** + * Returns the selected profile's name. If no ID is specified, the current + * profile's name is returned. + * + * @return string + */ + public function get_profile_name($id = null) + { + if (empty($id)) + { + $id = $this->get_active_profile(); + } + $id = (int)$id; + + $db = Factory::getDatabase($this->get_platform_database_options()); + $sql = $db->getQuery(true) + ->select($db->qn('description')) + ->from($db->qn('#__ak_profiles')) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $db->setQuery($sql); + + return $db->loadResult(); + } + + /** + * Returns the backup origin + * + * @return string Backup origin: backend|frontend + */ + public function get_backup_origin() + { + if (defined('AKEEBA_BACKUP_ORIGIN')) + { + return AKEEBA_BACKUP_ORIGIN; + } + + if ($this->container->platform->isBackend()) + { + return 'backend'; + } + + if ($this->container->platform->isFrontend()) + { + return 'frontend'; + } + + return 'cli'; + } + + /** + * Returns a MySQL-formatted timestamp out of the current date + * + * @param string $date [optional] The timestamp to use. Omit to use current timestamp. + * + * @return string + */ + public function get_timestamp_database($date = 'now') + { + \JLoader::import('joomla.utilities.date'); + $date = new Date($date); + + if (method_exists($date, 'toSql')) + { + return $date->toSql(); + } + + if (method_exists($date, 'toMySQL')) + { + return $date->toMySQL(); + } + + + return '0000-00-00 00:00:00'; + } + + /** + * 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) + { + \JLoader::import('joomla.utilities.date'); + \JLoader::import('joomla.environment.request'); + + // Do I have a forced timezone? + $tz = $this->get_platform_configuration_option('forced_backup_timezone', 'AKEEBA/DEFAULT'); + + // No forced timezone set? Use the default Joomla! behavior. + if (empty($tz) || ($tz == 'AKEEBA/DEFAULT')) + { + $tz = $this->getJoomlaTimezone(); + } + + $utcTimeZone = new DateTimeZone('UTC'); + $dateNow = new Date('now', $utcTimeZone); + $timezone = new DateTimeZone($tz); + $dateNow->setTimezone($timezone); + + return $dateNow->format($format, true); + } + + + /** + * Returns the current host name + * + * @return string + */ + public function get_host() + { + if ($this->container->platform->isCli()) + { + \JLoader::import('joomla.environment.uri'); + \JLoader::import('joomla.uri.uri'); + + $url = Platform::getInstance()->get_platform_configuration_option('siteurl', ''); + $oURI = new \JUri($url); + } + else + { + // Running under the web server + $oURI = \JUri::getInstance(); + } + + return $oURI->getHost(); + } + + public function get_site_name() + { + $jconfig = $this->container->platform->getConfig(); + return $jconfig->get('sitename', ''); + } + + /** + * Gets the best matching database driver class, according to CMS settings + * + * @param bool $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 a platform driver. + * + * @return string + */ + public function get_default_database_driver($use_platform = true) + { + $jconfig = $this->container->platform->getConfig(); + $driver = $jconfig->get('dbtype'); + $driver = strtolower($driver); + + $hasPdo = class_exists('\PDO'); + $hasMySQL = function_exists('mysql_connect'); + $hasMySQLi = function_exists('mysqli_connect'); + + // Prime with a default return value, favoring PDO MySQL if available + $defaultDriver = '\\Akeeba\\Engine\\Driver\\Pdomysql'; + + if (!$hasPdo) + { + // Second best choice is MySQLi + $defaultDriver = '\\Akeeba\\Engine\\Driver\\Mysqli'; + + // Third best choice is MySQL + if (!$hasMySQLi && $hasMySQL) + { + $defaultDriver = '\\Akeeba\\Engine\\Driver\\Mysql'; + } + } + + // Let's see what driver Joomla! uses... + if ($use_platform) + { + $hasNookuContent = file_exists(JPATH_ROOT . '/plugins/system/nooku.php'); + + switch ($driver) + { + // MySQL or MySQLi drivers are known to be working; use their + // Akeeba Engine extended version, Akeeba\Engine\Driver\Joomla + case 'mysql': + // So, Joomla! 4's "mysql" is, actually, "pdomysql". Therefore I can use our own wrapper driver + if (version_compare(JVERSION, '3.99999.99999', 'gt')) + { + return '\\Akeeba\\Engine\\Driver\\Joomla'; + } + + // The piece of crap called FaLang is lying about the database driver + if (!$hasMySQL) + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + + if ($hasNookuContent) + { + return '\\Akeeba\\Engine\\Driver\\Mysql'; + } + + return '\\Akeeba\\Engine\\Driver\\Joomla'; + + break; + + case 'mysqli': + if ($hasNookuContent) + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + + return '\\Akeeba\\Engine\\Driver\\Joomla'; + + break; + + // Any other case, use our platform-specific driver + default: + return '\\Akeeba\\Engine\\Driver\\Joomla'; + + break; + } + } + + // Is this a subcase of mysqli or mysql drivers? + if (substr($driver, 0, 8) == 'pdomysql') + { + return '\\Akeeba\\Engine\\Driver\\Pdomysql'; + } + elseif (substr($driver, 0, 6) == 'mysqli') + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + elseif (substr($driver, 0, 5) == 'mysql') + { + // The piece of crap called FaLang is lying about the database driver + if (!$hasMySQL) + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + + return '\\Akeeba\\Engine\\Driver\\Mysql'; + } + elseif (substr($driver, 0, 6) == 'sqlsrv') + { + return '\\Akeeba\\Engine\\Driver\\Sqlsrv'; + } + elseif (substr($driver, 0, 8) == 'sqlazure') + { + return '\\Akeeba\\Engine\\Driver\\Sqlazure'; + } + elseif (substr($driver, 0, 10) == 'postgresql') + { + return '\\Akeeba\\Engine\\Driver\\Postgresql'; + } + + // Sometimes we get driver names in the form of foomysql instead of mysqlfoo. Let's look for that too. + if (substr($driver, -8) == 'pdomysql') + { + return '\\Akeeba\\Engine\\Driver\\Pdomysql'; + } + elseif (substr($driver, -6) == 'mysqli') + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + elseif (substr($driver, -5) == 'mysql') + { + /** + * Apparently there are some folks of dubious intelligence out there writing custom database drivers without + * understanding or caring about the differences between mysql and mysqli drivers in PHP. They don't play + * nice but I have my way to work around their ignorance, FORCING mysqli when they erroneously report mysql + * on servers which no longer support this ancient, obsolete database connector. Of course the proper way + * to address this would be having these folks fix their broken software but I think I'm asking for too + * much. They know who they are, fa la la... + */ + if (!$hasMySQL) + { + return '\\Akeeba\\Engine\\Driver\\Mysqli'; + } + + return '\\Akeeba\\Engine\\Driver\\Mysql'; + } + elseif (substr($driver, -6) == 'sqlsrv') + { + return '\\Akeeba\\Engine\\Driver\\Sqlsrv'; + } + elseif (substr($driver, -8) == 'sqlazure') + { + return '\\Akeeba\\Engine\\Driver\\Sqlazure'; + } + elseif (substr($driver, -10) == 'postgresql') + { + return '\\Akeeba\\Engine\\Driver\\Postgresql'; + } + + // I give up! You'd better be usign a MySQL db server. + return $defaultDriver; + } + + /** + * Returns a set of options to connect to the default database of the current CMS + * + * @return array + */ + public function get_platform_database_options() + { + static $options; + + if (empty($options)) + { + $conf = $this->container->platform->getConfig(); + $options = array( + 'host' => $conf->get('host'), + 'user' => $conf->get('user'), + 'password' => $conf->get('password'), + 'database' => $conf->get('db'), + 'prefix' => $conf->get('dbprefix') + ); + } + + return $options; + } + + /** + * Provides a platform-specific translation function + * + * @param string $key The translation key + * + * @return string + */ + public function translate($key) + { + return \JText::_($key); + } + + /** + * Populates global constants holding the Akeeba version + */ + public function load_version_defines() + { + $basePath = JPATH_ADMINISTRATOR . '/components/com_akeeba'; + + if (file_exists($basePath . '/version.php')) + { + require_once($basePath . '/version.php'); + } + + if ( !defined('AKEEBA_VERSION')) + { + define("AKEEBA_VERSION", "dev"); + } + if ( !defined('AKEEBA_PRO')) + { + define('AKEEBA_PRO', false); + } + if ( !defined('AKEEBA_DATE')) + { + \JLoader::import('joomla.utilities.date'); + $date = new Date(); + define("AKEEBA_DATE", $date->format('Y-m-d')); + } + } + + /** + * Returns the platform name and version + * + * @param string $platform_name Name of the platform, e.g. Joomla! + * @param string $version Full version of the platform + */ + public function getPlatformVersion() + { + $v = new \JVersion(); + + return array( + 'name' => 'Joomla!', + 'version' => $v->getShortVersion() + ); + } + + /** + * Logs platform-specific directories with LogLevel::INFO log level + */ + public function log_platform_special_directories() + { + $ret = array(); + + Factory::getLog()->log(LogLevel::INFO, "JPATH_BASE :" . JPATH_BASE); + Factory::getLog()->log(LogLevel::INFO, "JPATH_SITE :" . JPATH_SITE); + Factory::getLog()->log(LogLevel::INFO, "JPATH_ROOT :" . JPATH_ROOT); + Factory::getLog()->log(LogLevel::INFO, "JPATH_CACHE :" . JPATH_CACHE); + Factory::getLog()->log(LogLevel::INFO, "Computed root :" . $this->get_site_root()); + + // If the release is older than 3 months, issue a warning + if (defined('AKEEBA_DATE')) + { + $releaseDate = new Date(AKEEBA_DATE); + + if (time() - $releaseDate->toUnix() > 7776000) + { + if ( !isset($ret['warnings'])) + { + $ret['warnings'] = array(); + $ret['warnings'] = array_merge($ret['warnings'], array( + 'Your version of Akeeba Backup is more than 90 days old and most likely already out of date. Please check if a newer version is published and install it.' + )); + } + } + + } + + // Detect UNC paths and warn the user + if (DIRECTORY_SEPARATOR == '\\') + { + if ((substr(JPATH_ROOT, 0, 2) == '\\\\') || (substr(JPATH_ROOT, 0, 2) == '//')) + { + if ( !isset($ret['warnings'])) + { + $ret['warnings'] = array(); + } + + $ret['warnings'] = array_merge($ret['warnings'], array( + 'Your site\'s root is using a UNC path (e.g. \\SERVER\path\to\root). PHP has known bugs which may', + 'prevent it from working properly on a site like this. Please take a look at', + 'https://bugs.php.net/bug.php?id=40163 and https://bugs.php.net/bug.php?id=52376. As a result your', + 'backup may fail.' + )); + } + } + + if (empty($ret)) + { + $ret = null; + } + + return $ret; + } + + /** + * Loads a platform-specific software configuration option + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get_platform_configuration_option($key, $default) + { + $value = $this->container->params->get($key, $default); + + // Some configuration options may have to be decrypted + switch ($key) + { + case 'frontend_secret_word': + $secureSettings = Factory::getSecureSettings(); + $value = $secureSettings->decryptSettings($value); + break; + } + + return $value; + } + + /** + * Returns a list of emails to the Super Administrators + * + * @return array + */ + public function get_administrator_emails() + { + $options = $this->get_platform_database_options(); + $db = Factory::getDatabase($options); + + // Load the root asset node and read the rules + $query = $db->getQuery(true) + ->select($db->qn('rules')) + ->from('#__assets') + ->where($db->qn('name') . ' = ' . $db->q('root.1')); + $db->setQuery($query); + $jsonRules = $db->loadResult(); + + $rules = json_decode($jsonRules, true); + $adminGroups = array(); + $mails = array(); + + if (array_key_exists('core.admin', $rules)) + { + $rawGroups = $rules['core.admin']; + + if ( !empty($rawGroups)) + { + foreach ($rawGroups as $group => $allowed) + { + if ($allowed) + { + $adminGroups[] = $db->q($group); + } + } + } + } + + if (empty($adminGroups)) + { + return $mails; + } + + $adminGroups = implode(',', $adminGroups); + + $query = $db->getQuery(true) + ->select(array( + $db->qn('u') . '.' . $db->qn('name'), + $db->qn('u') . '.' . $db->qn('email'), + )) + ->from($db->qn('#__users') . ' AS ' . $db->qn('u')) + ->join( + 'INNER', $db->qn('#__user_usergroup_map') . ' AS ' . $db->qn('m') . ' ON (' . + $db->qn('m') . '.' . $db->qn('user_id') . ' = ' . $db->qn('u') . '.' . $db->qn('id') . ')' + ) + ->where($db->qn('m') . '.' . $db->qn('group_id') . ' IN (' . $adminGroups . ')'); + $db->setQuery($query); + $superAdmins = $db->loadAssocList(); + + if ( !empty($superAdmins)) + { + foreach ($superAdmins as $admin) + { + $mails[] = $admin['email']; + } + } + + return $mails; + } + + /** + * Sends a very simple email using the platform's mailer facility + * + * @param string $to The recipient's email address + * @param string $subject The subject of the email + * @param string $body The body of the email + * @param string $attachFile The file to attach (null to not attach any files) + * + * @return boolean + */ + public function send_email($to, $subject, $body, $attachFile = null) + { + Factory::getLog()->log(LogLevel::DEBUG, "-- Fetching mailer object"); + + /** @var \JMail $mailer */ + try + { + $mailer = Platform::getInstance()->getMailer(); + } + catch (\Exception $e) + { + $mailer = null; + } + + if ( !is_object($mailer)) + { + Factory::getLog()->log(LogLevel::WARNING, "Could not send email to $to - Joomla! cannot send e-mails. Please check your From EMail and From Name fields in Global Configuration."); + + return false; + } + + Factory::getLog()->log(LogLevel::DEBUG, "-- Creating email message"); + + try + { + $recipient = array($to); + + $mailer->addRecipient($recipient); + $mailer->setSubject($subject); + $mailer->setBody($body); + } + catch (\Exception $e) + { + Factory::getLog()->log(LogLevel::WARNING, "Could not send email to $to - Problem setting up the email. Joomla! reports error: " . $e->getMessage()); + + return false; + } + + try + { + if ( !empty($attachFile)) + { + Factory::getLog()->log(LogLevel::WARNING, "-- Attaching $attachFile"); + + if ( !file_exists($attachFile) || !(is_file($attachFile) || is_link($attachFile))) + { + Factory::getLog()->log(LogLevel::WARNING, "The file does not exist, or it's not a file; no email sent"); + + return false; + } + + if ( !is_readable($attachFile)) + { + Factory::getLog()->log(LogLevel::WARNING, "The file is not readable; no email sent"); + + return false; + } + + $filesize = @filesize($attachFile); + + if ($filesize) + { + // Check that we have AT LEAST 2.5 times free RAM as the filesize (that's how much we'll need) + if ( !function_exists('ini_get')) + { + // Assume 8Mb of PHP memory limit (worst case scenario) + $totalRAM = 8388608; + } + else + { + $totalRAM = ini_get('memory_limit'); + if (strstr($totalRAM, 'M')) + { + $totalRAM = (int)$totalRAM * 1048576; + } + elseif (strstr($totalRAM, 'K')) + { + $totalRAM = (int)$totalRAM * 1024; + } + elseif (strstr($totalRAM, 'G')) + { + $totalRAM = (int)$totalRAM * 1073741824; + } + else + { + $totalRAM = (int)$totalRAM; + } + if ($totalRAM <= 0) + { + // No memory limit? Cool! Assume 1Gb of available RAM (which is absurdely abundant as of March 2011...) + $totalRAM = 1086373952; + } + } + if ( !function_exists('memory_get_usage')) + { + $usedRAM = 8388608; + } + else + { + $usedRAM = memory_get_usage(); + } + + $availableRAM = $totalRAM - $usedRAM; + + if ($availableRAM < 2.5 * $filesize) + { + Factory::getLog()->log(LogLevel::WARNING, "The file is too big to be sent by email. Please use a smaller Part Size for Split Archives setting."); + Factory::getLog()->log(LogLevel::DEBUG, "Memory limit $totalRAM bytes -- Used memory $usedRAM bytes -- File size $filesize -- Attachment requires approx. " . (2.5 * $filesize) . " bytes"); + + return false; + } + } + else + { + Factory::getLog()->log(LogLevel::WARNING, "Your server fails to report the file size of $attachFile. If the backup crashes, please use a smaller Part Size for Split Archives setting"); + } + + $mailer->addAttachment($attachFile); + } + } + catch (\Exception $e) + { + Factory::getLog()->log(LogLevel::WARNING, "Could not send email to $to - Problem attaching file. Joomla! reports error: " . $e->getMessage()); + + return false; + } + + Factory::getLog()->log(LogLevel::DEBUG, "-- Sending message"); + + try + { + $result = $mailer->Send(); + } + catch (\Exception $e) + { + $result = $e; + } + + if ($result instanceof \Exception) + { + Factory::getLog()->log(LogLevel::WARNING, "Could not email $to:"); + Factory::getLog()->log(LogLevel::WARNING, $result->getMessage()); + $ret = $result->getMessage(); + unset($result); + unset($mailer); + + return $ret; + } + + Factory::getLog()->log(LogLevel::DEBUG, "-- Email sent"); + + return true; + } + + /** + * Deletes a file from the local server using direct file access or FTP + * + * @param string $file + * + * @return bool + */ + public function unlink($file) + { + if (function_exists('jimport')) + { + \JLoader::import('joomla.filesystem.file'); + $result = \JFile::delete($file); + if ( !$result) + { + $result = @unlink($file); + } + } + else + { + $result = parent::unlink($file); + } + + return $result; + } + + /** + * Moves a file around within the local server using direct file access or FTP + * + * @param string $from + * @param string $to + * + * @return bool + */ + public function move($from, $to) + { + if (function_exists('jimport')) + { + \JLoader::import('joomla.filesystem.file'); + $result = \JFile::move($from, $to); + // JFile failed. Let's try rename() + if ( !$result) + { + $result = @rename($from, $to); + } + // Rename failed, too. Let's try copy/delete + if ( !$result) + { + // Try copying with JFile. If it fails, use copy(). + $result = \JFile::copy($from, $to); + if ( !$result) + { + $result = @copy($from, $to); + } + + // If the copy succeeded, try deleting the original with JFile. If it fails, use unlink(). + if ($result) + { + $result = $this->unlink($from); + } + } + } + else + { + $result = parent::move($from, $to); + } + + return $result; + } + + /** + * Registers Akeeba Engine's core classes with JLoader + * + * @param string $path_prefix The path prefix to look in + */ + protected function register_akeeba_engine_classes($path_prefix) + { + global $Akeeba_Class_Map; + \JLoader::import('joomla.filesystem.folder'); + foreach ($Akeeba_Class_Map as $class_prefix => $path_suffix) + { + // Bail out if there is such directory, so as not to have Joomla! throw errors + if ( !@is_dir($path_prefix . '/' . $path_suffix)) + { + continue; + } + + $file_list = \JFolder::files($path_prefix . '/' . $path_suffix, '.*\.php'); + if (is_array($file_list) && !empty($file_list)) + { + foreach ($file_list as $file) + { + $class_suffix = ucfirst(basename($file, '.php')); + \JLoader::register($class_prefix . $class_suffix, $path_prefix . '/' . $path_suffix . '/' . $file); + } + } + } + } + + /** + * Joomla!-specific function to get an instance of the mailer class + * + * @return \JMail + */ + public function &getMailer() + { + $mailer = \JFactory::getMailer(); + if ( !is_object($mailer)) + { + Factory::getLog()->log(LogLevel::WARNING, "Fetching Joomla!'s mailer was impossible; imminent crash!"); + } + else + { + $emailMethod = $mailer->Mailer; + Factory::getLog()->log(LogLevel::DEBUG, "-- Joomla!'s mailer is using $emailMethod mail method."); + } + + return $mailer; + } + + /** + * 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) + { + if ($this->container->platform->isCli()) + { + $this->flashVariables[$name] = $value; + + return; + } + + $this->container->platform->setSessionVar($name, $value, 'akeebabackup'); + } + + /** + * 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) + { + if ($this->container->platform->isCli()) + { + $ret = $default; + + if (isset($this->flashVariables[$name])) + { + $ret = $this->flashVariables[$name]; + unset($this->flashVariables[$name]); + } + + return $ret; + } + + $ret = $this->container->platform->getSessionVar($name, $default, 'akeebabackup'); + $this->container->platform->setSessionVar($name, null, 'akeebabackup'); + + return $ret; + } + + /** + * Perform an immediate redirection to the defined URL + * + * @param string $url The URL to redirect to + * + * @return void + */ + public function redirect($url) + { + $this->container->platform->redirect($url); + } + + public function apply_quirk_definitions() + { + Factory::getConfigurationChecks()->addConfigurationCheckDefinition('013', 'critical', 'COM_AKEEBA_CPANEL_WARNING_Q013', array('\\Akeeba\\Engine\\Platform\\Joomla3x', 'quirk_013')); + } + + public static function quirk_013() + { + $stock_dirs = Platform::getInstance()->get_stock_directories(); + $default_out = @realpath($stock_dirs['[DEFAULT_OUTPUT]']); + + $registry = Factory::getConfiguration(); + $outdir = $registry->get('akeeba.basic.output_directory'); + + foreach ($stock_dirs as $macro => $replacement) + { + $outdir = str_replace($macro, $replacement, $outdir); + } + + $outdir_real = @realpath($outdir); + + // If the output folder is the default one (or any subdir), we are safe + if (strpos($outdir_real, $default_out) !== false) + { + return false; + } + + $component_path = @realpath(JPATH_ROOT.'/administrator/components/com_akeeba'); + + if (strpos($outdir_real, $component_path) !== false) + { + return true; + } + + return false; + } + + /** + * Get the applicable timezone in the same way Joomla! calculates it: if there is a logged in + * user with a specific timezone set, use it. Otherwise use the Server Timezone defined in the + * site's Global Configuration. If nothing is set there, use GMT instead. + * + * @return string + */ + private function getJoomlaTimezone() + { + // Out ultimate default is the server timezone set up in the Global Configuration + $jregistry = $this->container->platform->getConfig(); + $tz = $jregistry->get('offset', 'GMT'); + + // If this is a CLI script, tough luck, we can't use a different TZ + if ($this->container->platform->isCli()) + { + return $tz; + } + + // If it's a guest user they can't have a special TZ set, return. + $user = $this->container->platform->getUser(); + + if ($user->guest) + { + return $tz; + } + + $tz = $user->getParam('timezone', $tz); + + return $tz; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/CHANGELOG.php b/deployed/akeeba/administrator/components/com_akeeba/CHANGELOG.php new file mode 100644 index 00000000..e9cd02b6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/CHANGELOG.php @@ -0,0 +1,375 @@ + +Akeeba Backup 6.1.1 +================================================================================ ++ Added support for new Amazon S3 regions: Canada, Mumbai, Seoul, Osaka-Local, Ningxia, London, Paris ++ OneDrive for Business support ++ OVH cloud storage support ++ OpenStack Swift support +# [LOW] DirectFTP and Upload to FTP create folders with 0744 instead of 0755 permissions + +Akeeba Backup 6.1.0 +================================================================================ ++ Support for Amazon S3 OneZone-IA (single zone, infrequent access) storage class ++ Revamped Site Transfer Wizard, with more options to improve compatibility with more servers +# [HIGH] Links pointing outside open_basedir restrictions cause a PHP Fatal error, halting the backup +# [MEDIUM] Installer (ANGIE) language files not included in the backup +# [MEDIUM] Default backup file permissions should be 0644, not 0755 +# [MEDIUM] Fixed folder scanning when a file is inaccessible due to open_basedir restrictions +# [LOW] The View Log link displayed after backup is broken when the backup completes in a single page load +# [LOW] The Site Transfer Wizard uses the wrong color labels +# [LOW] The Site Transfer Wizard interface works erratically + +Akeeba Backup 6.0.1 +================================================================================ ++ Warn the user if either FOF or FEF is not installed ++ While importing backup archives, they are now sorted alphabetically +# [HIGH] Site Transfer Wizard: browser-specific BUTTON element behavior prevents using this feature in Firefox +# [LOW] Inadvertent space between domain and path in the Schedule page + +Akeeba Backup 6.0.0 +================================================================================ +# [LOW] Fixed warning message during the installation due to missing folders + +Akeeba Backup 6.0.0.b1 +================================================================================ ++ Rewritten interface using our brand new Akeeba Frontend Framework ++ Preliminary Joomla! 4.0 support +# [MEDIUM] Fixed connection issue to the database when certain hostnames are used (named pipes) +# [LOW] The kicktemp folder and the kickstart.transfer.php file were not removed after Site Transfer Wizard had finished +# [LOW] The Transfer Wizard may select a chunk size which is too big for the target server +# [LOW] Using the Site Transfer Wizard may result in a "wordpress" folder being left behind on some servers with a broken FTP server (technically a bug with your FTP server; we had to remove a minor feature to work around it) +# [LOW] Add-on ANGIE language files would not be included in the backup archive +# [LOW] Leftover empty file after the end of the Configuration Wizard run + +Akeeba Backup 5.6.4 +================================================================================ +# [LOW] Add-on ANGIE language files would not be included in the backup archive + +Akeeba Backup 5.6.3 +================================================================================ +! Some JavaScript files had a zero size in the 5.6.2 package + +Akeeba Backup 5.6.1 & 5.6.2 +================================================================================ +! Missing language strings (fixed in 5.6.2) ++ Display info about exceptions and PHP 7+ fatal errors in the backend ++ BackBlaze B2 integration (Pro only) +~ PHP 7.2 compatibility: renaming Object class to BaseObject +~ Update Dropbox API with the new "_v2" endpoints where applicable +~ JSON API stepBackup: now enforcing the check for the MANDATORY parameter "backupid" +# [MEDIUM] Backing up to JPS with a static salt using the JSON API would always fail +# [MEDIUM] "Apply to all" filter button does not work due to a Javascript issue +# [LOW] Core version: some post-processing options were displayed even though the code to make them work was not present. +# [LOW] The ANGIE Password option in Configuration and Backup Now pages didn't show up in the Core release +# [LOW] Calendar fields did not display the month due to a Joomla! 3.7+ CSS-related bug +# [LOW] Programmatically triggering mouse events could fail in some browsers, e.g. newer versions of Firefox +# [LOW] WebDav post-processing: Fixed issue with hosts without a valid certificate file and SSL connections +# [LOW] Backup On Update was available on Pro version only. Now it's available in the Core version, too. + +Akeeba Backup 5.6.0 +================================================================================ ++ Added warning if HHVM instead of PHP is used ++ Added variables for timezone information: [TIME_TZ], [TZ], [TZ_RAW], [GMT_OFFSET] ++ Backup Timezone: force all date / time variables to be expressed in a fixed timezone during backup (gh-626) ++ Clicking on "Reload update information" will fix Joomla! erroneously reporting an update is available when you have the latest version installed ++ Display the Secret Word unencrypted in the component's Options page (required for you to easily set up third party services) +~ Updates now use HTTPS URLs for both the XML update stream and the package download itself +# [LOW] Save & New button in the Configuration page didn't work correctly +# [LOW] Editing RegEx filters creates a new entry, doesn't edit in place (gh-623) +# [LOW] Repeated messages in the JavaScript console after editing a filter in Tabular or RegEx views (gh-625) + +Akeeba Backup 5.5.2 +================================================================================ +! [SECURITY] Settings encryption key was neither cryptographically random or big enough. Now changed to 64 crypto-safe random bytes. +! [SECURITY] Secret Word for front-end and JSON backups is now stored encrypted in the database (as long as settings encryption in the application's Options is NOT disabled and your server supports encryption). +! [SECURITY] Improved internal authentication in restore.php makes brute forcing the restoration takeover a few dozen orders of magnitude harder. +- [SECURITY ADVICE] ANGIE will no longer lock its session to prevent information leaks. Please always use the ANGIE Password feature. +~ [SECURITY] akeeba-altbackup.php: verify SSL certificates by default. Use --no-verify command line option to revert to the old behavior. +~ Work around broken MijoShop plugin causing an error in Joomla's backend when the System - Backup on Update plugin is enabled. +# [HIGH] Editing two or more Multiple Databases definitions consecutively would overwrite all of them with the settings of the last definition saved +# [HIGH] Disabling decryption can lead to loss of current settings +# [LOW] "_QQ_" shown during restoration instead of double quotes +# [LOW] Removed MB label from count quota +# [LOW] ANGIE: restoring sites served by a server cluster could result in "random" errors due to session handling being tied to the server IP + +Akeeba Backup 5.5.1 +================================================================================ +! The System - Backup on Update plugin may cause an error accessing the backend on some sites + +Akeeba Backup 5.5.0 +================================================================================ ++ Prevent simultaneous use of ANGIE (restoration script) from two or more people / browsers ++ Workaround for Joomla! bug "Sometimes files are not copied on update" ++ Alphabetical sorting of engines and installation scripts in the Configuration page ++ Backup on Update: Show the status in the backend status bar (footer), with the ability to quickly toggle it off ++ Support for Google Storage native JSON API +~ ANGIE for Joomla (restoration): Use alternate method to read the site's configuration, preventing PHP errors from getting in the way +# [HIGH] Cannot change database prefix on restoration if the backup was taken with No Dependency Tracking enabled +# [MEDIUM] Double slashes in the WebDAV path cause 0 byte uploads on some servers +# [MEDIUM] Version information not loaded correctly (thanks Joe F.!) +# [LOW] Notice thrown converting memory_limit to bytes under PHP 7.1 +# [LOW] Workaround for Joomla! bug 16147 (https://github.com/joomla/joomla-cms/issues/16147) - Cannot access component after installation when cache is enabled + +Akeeba Backup 5.4.0 +================================================================================ +# [HIGH] Resuming after error was broken when using the file storage for temporary files (default) +# [MEDIUM] Errors when reading the backup engine's state were not reported, causing the backup to seem like it runs forever +# [MEDIUM] Database storage wouldn't report the lack of stored engine state, potentially causing forever stuck backups +# [MEDIUM] Blank page if you delete all backup profiles from the database (a default backup profile could not be created in this case) +# [MEDIUM] Errors always result in the resume pane being shown, no matter what your settings are +# [LOW] The Warnings pane was always displayed following resuming from a backup error +# [LOW] The How to Restore modal would get in the way unless you chose to not be reminded again + +Akeeba Backup 5.4.0.b1 +================================================================================ +! Yet another BIG Joomla! 3.7.0 bug throws an exception when using the CLI backup under some circumstances. +! Yet another BIG Joomla! 3.7.0 bug kills the front-end and remote backup when you are using the System - Page Cache plugin. +- Removing the automatic update CLI script. Joomla! 3.7.0 can no longer execute extension installation under a CLI application. +# [HIGH] PHP Fatal Error if the row batch size leads to an amount of data that exceeds free PHP memory +# [HIGH] Large database records can cause an infinitely growing runaway backup or, if you're lucky, a backup crash due to exceeding PHP time limits. +# [MEDIUM] Database hostname localhost:3306 leads to connection error under some circumstances. Note that this hostname is wrong: you should use localhost without a port instead! +# [MEDIUM] Logic error leads to leftover temporary database dump files in the backup output directory under some circumstances +# [LOW] The wrong message is shown by ANGIE when performing an integrated restoration through Akeeba Backup for Joomla! +# [LOW] Wouldn't show up in the Joomla! 3.7 new backend menu type selection dialog +# [LOW] FTP/SFTP over cURL uploads would fail if the remote directory couldn't be created + +Akeeba Backup 5.3.4 +================================================================================ +# [MEDIUM] Integrated restoration leads to an error message about the file extension being wrong + +Akeeba Backup 5.3.3 +================================================================================ +! The workaround to Joomla! 3.7's date bugs could cause a blank / error page (with an error about double timezone) under some circumstances. +! Joomla! 3.7.0 broke backwards compatibility again, making CLI scripts fail. +! Joomla! 3.7.0 broke the JDate package, effectively ignoring timezones, causing grave errors in date / time calculations and display +~ Workaround for badly configured servers which print out notices before we have the chance to set the error reporting +~ Control Panel: Display the backup date/time in the user's timezone +~ Default backup description in the component backend includes the date / time in the user's local timezone instead of GMT +~ Date and time shown in Site Transfer Wizard is in the user's local timezone instead of GMT +# [LOW] Joomla! 3.7 added a fixed width to specific button classes in the toolbar, breaking the page layout + +Akeeba Backup 5.3.2 +================================================================================ +! Joomla! 3.7.0 broke backwards compatibility again, making CLI scripts fail. +! Joomla! 3.7.0 broke the JDate package, effectively ignoring timezones, causing grave errors in date / time calculations and display +~ Workaround for badly configured servers which print out notices before we have the chance to set the error reporting +~ Control Panel: Display the backup date/time in the user's timezone +~ Default backup description in the component backend includes the date / time in the user's local timezone instead of GMT +~ Date and time shown in Site Transfer Wizard is in the user's local timezone instead of GMT +# [LOW] Joomla! 3.7 added a fixed width to specific button classes in the toolbar, breaking the page layout + +Akeeba Backup 5.3.1 +================================================================================ +# [HIGH] Integrated restoration: clicking Run the Installer does nothing; you can still run the installer manually +# [HIGH] Archive integrity and restoration of JPS archives fails (the archive is valid, it's the extraction script that's the problem). +# [HIGH] The "Replace main .htaccess with default" feature, enabled by default, causes the restoration to fail. +# [LOW] Restoration was not removing password protection from the administrator folder even if requested to + +Akeeba Backup 5.3.0 +================================================================================ +! SECURITY: Workaround for MySQL security issue CVE-2016-5483 (https://blog.tarq.io/cve-2016-5483-backdooring-mysqldump-backups/) affecting SQL-only backups. This is a security issue in MySQL itself, not our backup engine. Full site backups / restoration were NOT affected. ++ You can use multiple PushBullet tokens to notify multiple accounts +# [MEDIUM] gh-619 "Invalid upload ID specified" when trying to re-upload backup to remote storage from the Manage Backups page +# [HIGH] PHP's memory_limit given in bytes (e.g. 134217728 instead of 128M) prevent the backup from running +# [LOW] How To Restore modal in Manage Backups appearing halfway outside the page if your computer is a bit too fast + +Akeeba Backup 5.3.0.b1 +================================================================================ ++ Add support for Canada (Montreal) Amazon S3 region ++ Add support for EU (London) Amazon S3 region ++ Add support for Joomla! 3.7's new administrator menu manager ++ Support for JPS format v2.0 with improved password security ++ Hide action icons based on the user's permissions +~ Permissions are now more reasonably assigned to different views +~ Now using the Reverse Engineering database dump engine when a Native database dump engine is not available (PostgreSQL, Microsoft SQL Server, SQLite) +# [MEDIUM] The web.config file introduced in Akeeba Backup 3.3 was removed from the backup output directory +# [MEDIUM] Infinite recursion if the current profile doesn't exist +# [MEDIUM] Views defined against fully qualified tables (i.e. including the database name) could not be restored on a database with a different name +# [LOW] The unit of measurement was not displayed in the Configuration page + +Akeeba Backup 5.2.5 +================================================================================ +# [HIGH] Trailing slashes in the Directory name would cause donwloading a backup back from Dropbox to fail ++ Alternative FTP post-processing engine and DirectFTP engine using cURL providing better compatibility with misconfigured and broken FTP servers ++ Alternative SFTP post-processing engine and DirectSFTP engine using cURL providing compatibility with a wide range of servers +~ Anticipate and report database errors in more places while backing up MySQL databases +~ Do not show the JPS password field in the Restoration page when not restoring JPS archives +# [HIGH] Site Transfer Wizard does not work with single part backup archives +# [HIGH] Outdated, end-of-life PHP 5.4.4 in old Debian distributions has a MAJOR bug resulting in file data not being backed up (zero length files). We've rewritten our code to work around the bug in this OLD, OUTDATED, END-OF-LIFE AND INSECURE version of PHP. PLEASE UPGRADE YOUR SERVERS. OLD PHP VERSIONS ARE **DANGEROUS**!!! +# [MEDIUM] Dumping VIEW definers in newer MySQL versions can cause restoration issues when restoring to a new host +# [MEDIUM] Dropbox: error while fetching the archive back from the server +# [MEDIUM] Error restoring procedures, functions or triggers originally defined with inline MySQL comments +# [LOW] Folders not added to archive when both their subdirectories and all their files are filtered. +# [LOW] ALICE would display many false positives in the old backups detection step +# [LOW] The quickicon plugin was sometimes not sure which Akeeba Backup version is installed on your site +# [LOW] The "No Installer" option was accidentally removed + +Akeeba Backup 5.2.4 +================================================================================ ++ ALICE: Added check about old backups being included in the backup after changing your backup output directory ++ JSON API: export and import a profile's configuration +# [HIGH] Changes in Joomla 3.6.3 and 3.6.4 regarding Two Factor Authentication setup handling could lead to disabling TFA when restoring a site +# [HIGH] Javascript error when using on sites with the sequence "src" in their domain name +# [HIGH] Site Transfer Wizard fails on sites with too much memory or very fast connection speeds to the target site +# [MEDIUM] In several instances there was a typo declaring 1Mb = 1048756 bytes instead of the correct 1048576 bytes (1024 tiems 1024). This threw off some size calculations which, in extreme cases, could lead to backup failure. +# [MEDIUM] Obsolete records quota was applied to all backup records, not just the ones from the currently active backup profile +# [MEDIUM] Obsolete records quota did not delete the associated log file when removing an obsolete backup record +# [MEDIUM] The backup quickicon plugin would always deactivate itself upon first use +# [MEDIUM] Infinite loop creating part size in rare cases where the space left in the part is one byte or less +# [LOW] Fixed ordering in Manage Backups page +# [LOW] Fixed removing One Click backup flag + +Akeeba Backup 5.2.3 +================================================================================ ++ ANGIE: Prevent direct web access to the installation/sql directory +~ PHP 5.6.3 (and possibly other old 5.6 versions) are buggy. We rearranged the order of some code to work around these PHP bugs. + +Akeeba Backup 5.2.2 +================================================================================ +! The ZIP archiver was not working properly + +Akeeba Backup 5.2.1 +================================================================================ +! PHP 5.4 compatibility (now working around a PHP bug which has been fixed years ago in PHP 5.5 and later) + +Akeeba Backup 5.2.0 +================================================================================ +! mcrypt is deprecated in PHP 7.1. Replacing it with OpenSSL. +! Missing files from the backup on some servers, especially in CLI mode ++ Added warning if CloudFlare Rocket Loader is enabled on the site ++ Added warning if database updates are stuck due to table corruption ++ ALICE raw output now is always in English ++ Support the newer Microsoft Azure API version 2015-04-05 ++ Support uploading files larger than 64Mb to Microsoft Azure ++ You can now choose whether to display GMT or local time in the Manage Backups page ++ Sort the log files from newest to oldest in the View Log page (based on the backup ID) ++ View Log after successful backup now takes you to this backup's log file, not the generic View Log page +# [MEDIUM] Cannot download archives from S3 to browser when using the Amazon S3 v4 API +# [MEDIUM] gh-601 CloudFlare Rocket Loader is broken and kills the Javascript on your site, causing Akeeba Backup to fail +# [LOW] Front-end URL without a view and secrety key should return a plain text 403 error, not a Joomla 404 error page. +# [LOW] Reverse Engineering database dump engine must be available in Core version, required for backing up PostgreSQL and MS SQL Server +# [LOW] Editing a profile's Configuration would always reset the One-click backup icon checkbox +# [LOW] Archive integrity check refused to run when you are using the "No post-processing" option but the "Process each part immediately" option was previously selected in a different post-processing engine +# [LOW] Total archive size would be doubled when you are using the "Process each part immediately" option but not the "Delete archive after processing" option (or when the post-processing engine is "No post-processing") +# [LOW] Warnings from the database dump engine were not propagated to the interface + +Akeeba Backup 5.1.4 +================================================================================ +# [MEDIUM] Updated "Backup on Update" plugin to be compatible with Joomla 3.6.1 and later + +Akeeba Backup 5.1.3 +================================================================================ ++ Automatically handle unsupported database storage engines when restoring MySQL databases ++ Help buttons everywhere. No more excuses for not reading the fine manual. +# [HIGH] Failure to upload to newly created Amazon S3 buckets +# [MEDIUM] Import from S3 didn't work with API v4-only regions (Frankfurt, S??o Paulo) +# [LOW] The [WEEKDAY] variable in archive name templates returned the weekday number (e.g 1) instead of text (e.g. Sunday) +# [LOW] Deleting the currently active profile would cause a white page / internal server error +# [LOW] Chrome and other misbehaving browsers autofill the database username/password, leading to restoration failure if you're not paying very close attention. We are now working around these browsers. +# [LOW] WebDAV prost-processing: Fixed handling of URLs containing spaces + +Akeeba Backup 5.1.2 +================================================================================ +- Removed Dropbox API v1 integration. The v1 API is going to be discontinued by Dropbox, see https://blogs.dropbox.com/developers/2016/06/api-v1-deprecated/ Please use the Dropbox API v2 integration instead. ++ Restoration: a warning is displayed if the database table name prefix contains uppercase characters +~ Changing the #__akeeba_common table schema +~ Workaround for sites with upper- or mixed-case prefixes / table names on MySQL servers running on case-insensitive filesystems and lower_case_table_names = 1 (default on Windows) +~ The backup engine will now warn you if you are using a really old PHP version +~ You will get a warning if you try to backup sites with upper- or mixed-case prefixes as this can cause major restoration issues. +# [HIGH] Conservative time settings (minimum execution time greated than the maximum execution time) cause the backup to fail +# [MEDIUM] Akeeba Backup's control panel would not be displayed if you didn't have the Configure privilege (the correct privilege is, in fact, Access Administrator Interface) +# [MEDIUM] Restoration: The PDOMySQL driver would always crash with an error +# [LOW] Restoration: Database error information does not contain the actual error message generated by the database +# [LOW] Integrated Restoration: The last response timer jumped between values + +Akeeba Backup 5.1.1 +================================================================================ ++ Added optional filter to automatically exclude MyJoomla database tables +~ Removed dependency on jQuery timers +~ Taking into account Joomla! 3.6's changes for the log directory location +~ Better information about outdated PHP versions +~ Warn about broken eAccelerator +~ Work around broken hosts with invalid / no default server timezone in CLI scripts +# [HIGH] gh-592 Misleading 403 Access forbidden error when your Akeeba Backup tables are missing or corrupt. +# [LOW] Seldom fatal error when installing or updating (most likely caused by bad opcode optimization by PHP itself) +# [LOW] PHP warning in the File and Directories Exclusion page when the root is inaccessible / does not exist. + +Akeeba Backup 5.1.0 +================================================================================ ++ Big, detailed error page when you try to use an ancient PHP version which is not supported. ++ Do not automatically display the backup log if it's too big ++ Halt the backup if the encrypted settings cannot be decrypted when using the native CLI backup script ++ Better display of tooltips: you now have to click on the label to make them static ++ Enable the View Log button on failed, pending and remote backups +~ Automatically exclude core dump files +# [HIGH] PHP's INI parsing "undocumented features" (read: bugs) cause parsing errors when certain characters are contained in a password. +# [LOW] Database dump could fail on servers where the get_magic_quotes_runtime function is not available + +Akeeba Backup 5.1.0.b2 +================================================================================ +# [LOW] The CHANGELOG shown in the back-end is out of date (thanks @brianteeman) +# [LOW] The integrated restoration would appear to be in an infinite loop if an HTTP error occurred +# [HIGH] CLI scripts in old versions of Joomla were not working +# [HIGH] CLI scripts do not respect configuration overrides (bug in engine's initialization) +# [HIGH] Backups taken with JSON API would always be full site backups, ignoring your preferences + +Akeeba Backup 5.1.0.b1 +================================================================================ +! Restoration was broken +! Integrated restoration was broken ++ Integrated restoration in Akeeba Backup Core +~ If environment information collection does not exist, do not fail (apparently the file to this feature is auto-deleted by some subpar hosts) +~ Chrome and Safari autofill the JPS key field in integrated restoration, usually with the WRONG password (e.g. your login password) +~ Less confusing buttons in integrated restoration ++ ANGIE for Joomla!: Option to set up Force SSL in Site Setup step +# [HIGH] Remote JSON API backups always using profile #1 despite reporting otherwise +# [LOW] The "How do I restore" modal appeared always until you configured the backup profile, no matter your preferences + +Akeeba Backup 5.0.4 +================================================================================ +# [HIGH] FaLang erroneously makes Joomla! report that the active database driver is mysql instead of mysqli, causing the backup to fail on PHP 7. Now we try to detect and work around this badly written extension. +# [HIGH] Remote JSON API backups always using profile #1 +# [MEDIUM] Obsolete .blade.php files from 5.0.0-5.0.2 were not being removed +# [LOW] Junk akeeba_AKEEBA_BACKUP_ORIGIN and akeeba.AKEEBA_BACKUP_ORIGIN files would be created by legacy front-end backup +# [LOW] Backup download confirmation message had escaped \n instead of newlines + +Akeeba Backup 5.0.3 +================================================================================ +! Blade templates would not work on servers where the Toeknizer extension is disabled / not installed +# [HIGH] The backup on update plugin wouldn't let you update Joomla! +# [MEDIUM] The "Upload Kickstart" feature would fail +# [MEDIUM] Site Transfer Wizard: could not set Passive Mode +# [LOW] Testing the connection in Multiple Databases Definitions would not show you a success message +# [LOW] If saving the file and directories filters failed you would not receive an error message, it would just hang + +Akeeba Backup 5.0.2 +================================================================================ +! Multipart backups are broken +# [HIGH] Remote JSON API backups would result in an error +# [HIGH] Front-end (remote) backups would always result in an error +# [MEDIUM] Sometimes the back-end wouldn't load complaining that a class is already loaded + +Akeeba Backup 5.0.1 +================================================================================ +# [HIGH] The update sites are sometimes not refreshed when upgrading directly from Akeeba Backup 4.6 to 5.0 +# [HIGH] The Quick Icon plugin does not work and disables itself +# [MEDIUM] Profile copy wasn't working +# [MEDIUM] The update sites in the XML manifest were wrong + +Akeeba Backup 5.0.0 +================================================================================ ++ Automatic detection and working around of temporary data load failure ++ Improved detection and removal of duplicate update sites ++ Direct download link to Akeeba Kickstart in the Manage Backups page ++ Working around PHP opcode cache issues occurring right before the restoration starts if the old restoration.php configuration file was not removed ++ Schedule Automatic backups button is shown after the Configuration Wizard completes ++ Schedule Automatic backups button in the Configuration page's toolbar ++ Download log button from ALICE page +~ Remove obsolete FOF 2.x update site if it exists +~ Chrome won't let developers restore the values of password fields it ERRONEOUSLY auto-fills with random passwords. We worked around Google's atrocious bugs with extreme prejudice. You're welcome. +# [HIGH] Joomla! "Conservative" cache bug: you could not enter the Download ID when prompted +# [HIGH] Joomla! "Conservative" cache bug: you could not apply the proposed Secret Word when prompted +# [HIGH] Joomla! "Conservative" cache bug: component Options (e.g. Download ID, Secret Word, front-end backup feature) would be forgotten on the next page load +# [HIGH] Joomla! "Conservative" cache bug: the "How do I restore" popup can never be made to not display +# [MEDIUM] Fixed Rackspace CloudFiles when using a region different then London +# [LOW] Missing language strings in ALICE diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Backup.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Backup.php new file mode 100644 index 00000000..4fde0575 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Backup.php @@ -0,0 +1,137 @@ +setPredefinedTaskList([ + 'main', + 'ajax' + ]); + } + + /** + * Default task; shows the initial page where the user selects a profile and enters description and comment + */ + protected function onBeforeMain() + { + // Did the user ask to switch the active profile? + $newProfile = $this->input->get('profileid', -10, 'int'); + $autostart = $this->input->get('autostart', 0, 'int'); + + if (is_numeric($newProfile) && ($newProfile > 0)) + { + /** + * We have to remove CSRF protection due to the way the Joomla administrator menu manager works. Menu item + * options are passed as URL parameters. However, we cannot pass dynamic parameters (like the token). This + * means that a user can create a menu item with a specific backup profile ID. Normally this would cause a + * 403 which is frustrating to the user because they might want to give their client the option to run a + * backup with a specific profile AND let them enter a description and comment. Therefore we have to remove + * the CSRF protection. + * + * NB! We do understand the potential risk involved. Between Joomla's AMATEURISH implementation of custom + * administrator menus and user demands for features we have to (have these very vocal users and everyone + * else) assume that (actually really small) risk. + */ + // $this->csrfProtection(); + + $this->container->platform->setSessionVar('profile', $newProfile, 'akeeba'); + + /** + * DO NOT REMOVE! + * + * The Model will only try to load the configuration after nuking the factory. This causes Profile 1 to be + * loaded first. Then it figures out it needs to load a different profile and it does – but the protected keys + * are NOT replaced, meaning that certain configuration parameters are not replaced. Most notably, the chain. + * This causes backups to behave weirdly. So, DON'T REMOVE THIS UNLESS WE REFACTOR THE MODEL. + */ + Platform::getInstance()->load_configuration($newProfile); + } + + // Deactivate the menus + \JFactory::getApplication()->input->set('hidemainmenu', 1); + + /** @var \Akeeba\Backup\Admin\Model\Backup $model */ + $model = $this->getModel(); + + // Push data to the model + $model->setState('profile', $this->input->get('profileid', -10, 'int')); + $model->setState('description', $this->input->get('description', '', 'string', 2)); + $model->setState('comment', $this->input->get('comment', '', 'html', 2)); + $model->setState('ajax', $this->input->get('ajax', '', 'cmd')); + $model->setState('autostart', $autostart); + $model->setState('jpskey', $this->input->get('jpskey', '', 'raw', 2)); + $model->setState('angiekey', $this->input->get('angiekey', '', 'raw', 2)); + $model->setState('returnurl', $this->input->get('returnurl', '', 'raw', 2)); + $model->setState('backupid', $this->input->get('backupid', null, 'cmd')); + } + + /** + * This task handles the AJAX requests + */ + public function ajax() + { + /** @var \Akeeba\Backup\Admin\Model\Backup $model */ + $model = $this->getModel(); + + // Push all necessary information to the model's state + $model->setState('profile', $this->input->get('profileid', -10, 'int')); + $model->setState('ajax', $this->input->get('ajax', '', 'cmd')); + $model->setState('description', $this->input->get('description', '', 'string')); + $model->setState('comment', $this->input->get('comment', '', 'html', 2)); + $model->setState('jpskey', $this->input->get('jpskey', '', 'raw', 2)); + $model->setState('angiekey', $this->input->get('angiekey', '', 'raw', 2)); + $model->setState('backupid', $this->input->get('backupid', null, 'cmd')); + $model->setState('tag', $this->input->get('tag', 'backend', 'cmd')); + $model->setState('errorMessage', $this->input->getString('errorMessage', '')); + + // System Restore Point backup state variables (obsolete) + $model->setState('type', strtolower($this->input->get('type', '', 'cmd'))); + $model->setState('name', strtolower($this->input->get('name', '', 'cmd'))); + $model->setState('group', strtolower($this->input->get('group', '', 'cmd'))); + $model->setState('customdirs', $this->input->get('customdirs', array(), 'array', 2)); + $model->setState('customfiles', $this->input->get('customfiles', array(), 'array', 2)); + $model->setState('extraprefixes', $this->input->get('extraprefixes', array(), 'array', 2)); + $model->setState('customtables', $this->input->get('customtables', array(), 'array', 2)); + $model->setState('skiptables', $this->input->get('skiptables', array(), 'array', 2)); + $model->setState('langfiles', $this->input->get('langfiles', array(), 'array', 2)); + $model->setState('xmlname', $this->input->getString('xmlname', '')); + + // Set up the tag + define('AKEEBA_BACKUP_ORIGIN', $this->input->get('tag', 'backend', 'cmd')); + + // Run the backup step + $ret_array = $model->runBackup(); + + // We use this nasty trick to avoid broken 3PD plugins from barfing all over our output + @ob_end_clean(); + header('Content-type: text/plain'); + header('Connection: close'); + echo '###' . json_encode($ret_array) . '###'; + flush(); + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Browser.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Browser.php new file mode 100644 index 00000000..ebb6d023 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Browser.php @@ -0,0 +1,31 @@ +input->get('folder', '', 'string'); + $processfolder = $this->input->get('processfolder', 0, 'int'); + + /** @var \Akeeba\Backup\Admin\Model\Browser $model */ + $model = $this->getModel(); + $model->setState('folder', $folder); + $model->setState('processfolder', $processfolder); + $model->makeListing(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Configuration.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Configuration.php new file mode 100644 index 00000000..06d54d5e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Configuration.php @@ -0,0 +1,245 @@ +csrfProtection(); + + // Get the var array from the request + $data = $this->input->get('var', array(), 'array', 4); + $data['akeeba.flag.confwiz'] = 1; + + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('engineconfig', $data); + $model->saveEngineConfig(); + + // Finally, save the profile description if it has changed + $profileid = Platform::getInstance()->get_active_profile(); + + // Get profile name + /** @var Profiles $profileRecord */ + $profileRecord = $this->container->factory->model('Profiles')->tmpInstance(); + $profileRecord->findOrFail($profileid); + $oldProfileName = $profileRecord->description; + $oldQuickIcon = $profileRecord->quickicon; + + $profileName = $this->input->getString('profilename', null); + $profileName = trim($profileName); + + $quickIconValue = $this->input->getCmd('quickicon', ''); + $quickIcon = (int) !empty($quickIconValue); + + $mustSaveProfile = !empty($profileName) && ($profileName != $oldProfileName); + $mustSaveProfile = $mustSaveProfile || ($quickIcon != $oldQuickIcon); + + if ($mustSaveProfile) + { + $profileRecord->save([ + 'description' => $profileName, + 'quickicon' => $quickIcon + ]); + } + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Configuration', JText::_('COM_AKEEBA_CONFIG_SAVE_OK')); + } + + /** + * Handle the save task which saves the configuration settings and returns to the Control Panel page + */ + public function save() + { + $this->apply(); + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba', JText::_('COM_AKEEBA_CONFIG_SAVE_OK')); + } + + /** + * Handle the save & new task which saves settings, creates a new backup profile, activates it and proceed to the + * configuration page once more. + */ + public function savenew() + { + // Save the current profile + $this->apply(); + + // Create a new profile + $profileid = Platform::getInstance()->get_active_profile(); + + /** @var Profiles $profile */ + $profile = $this->container->factory->model('Profiles')->tmpInstance(); + $profile + // Load and clone the record we just saved + ->findOrFail($profileid) + ->getClone() + ; + // Must unset ID before save. The ID cannot be bound with bind()/save(), hence the need to do it the hard way. + $profile->id = null; + $profile + ->save([ + 'description' => JText::_('COM_AKEEBA_CONFIG_SAVENEW_DEFAULT_PROFILE_NAME') + ]) + ; + + // Activate and edit the new profile + $returnUrl = base64_encode($this->redirect); + $token = $this->container->platform->getToken(true); + $url = JUri::base() . 'index.php?option=com_akeeba&task=SwitchProfile&profileid=' . $profile->getId() . + '&returnurl=' . $returnUrl . '&' . $token . '=1'; + $this->setRedirect($url); + } + + /** + * Handle the cancel task which doesn't save anything and returns to the Control Panel page + */ + public function cancel() + { + // CSRF prevention + $this->csrfProtection(); + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba'); + } + + /** + * Tests the validity of the FTP connection details + */ + public function testftp() + { + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('host', $this->input->get('host', '', 'raw', 2)); + $model->setState('port', $this->input->get('port', 21, 'int')); + $model->setState('user', $this->input->get('user', '', 'raw', 2)); + $model->setState('pass', $this->input->get('pass', '', 'raw', 2)); + $model->setState('initdir', $this->input->get('initdir', '', 'raw', 2)); + $model->setState('usessl', $this->input->get('usessl', '', 'raw', 2) == 'true'); + $model->setState('passive', $this->input->get('passive', '', 'raw', 2) == 'true'); + + try + { + $model->testFTP(); + $testResult = true; + } + catch (\RuntimeException $e) + { + $testResult = $e->getMessage(); + } + + @ob_end_clean(); + echo '###' . json_encode($testResult) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } + + /** + * Tests the validity of the SFTP connection details + */ + public function testsftp() + { + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('host', $this->input->get('host', '', 'raw', 2)); + $model->setState('port', $this->input->get('port', 21, 'int')); + $model->setState('user', $this->input->get('user', '', 'raw', 2)); + $model->setState('pass', $this->input->get('pass', '', 'raw', 2)); + $model->setState('privkey', $this->input->get('privkey', '', 'raw', 2)); + $model->setState('pubkey', $this->input->get('pubkey', '', 'raw', 2)); + $model->setState('initdir', $this->input->get('initdir', '', 'raw', 2)); + + try + { + $model->testSFTP(); + $testResult = true; + } + catch (\RuntimeException $e) + { + $testResult = $e->getMessage(); + } + + @ob_end_clean(); + echo '###' . json_encode($testResult) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } + + /** + * Opens an OAuth window for the selected data processing engine + */ + public function dpeoauthopen() + { + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('engine', $this->input->get('engine', '', 'raw')); + $model->setState('params', $this->input->get('params', array(), 'array', 2)); + + @ob_end_clean(); + $model->dpeOuthOpen(); + flush(); + + $this->container->platform->closeApplication(); + } + + /** + * Runs a custom API call against the selected data processing engine and returns the JSON encoded result + */ + public function dpecustomapi() + { + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('engine', $this->input->get('engine', '', 'raw', 2)); + $model->setState('method', $this->input->get('method', '', 'raw', 2)); + $model->setState('params', $this->input->get('params', array(), 'array', 2)); + + @ob_end_clean(); + echo '###' . json_encode($model->dpeCustomAPICall()) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } + + /** + * Runs a custom API call against the selected data processing engine and returns the raw result + */ + public function dpecustomapiraw() + { + /** @var \Akeeba\Backup\Admin\Model\Configuration $model */ + $model = $this->getModel(); + $model->setState('engine', $this->input->get('engine', '', 'raw', 2)); + $model->setState('method', $this->input->get('method', '', 'raw', 2)); + $model->setState('params', $this->input->get('params', array(), 'array', 2)); + + @ob_end_clean(); + echo $model->dpeCustomAPICall(); + flush(); + + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/ConfigurationWizard.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/ConfigurationWizard.php new file mode 100644 index 00000000..4f650d9b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/ConfigurationWizard.php @@ -0,0 +1,50 @@ +setPredefinedTaskList(['main', 'ajax']); + } + + /** + * Handles AJAX request by proxying the call to the Model, which does all the work, and returning the JSON encoded + * result back to the browser. + */ + public function ajax() + { + /** @var \Akeeba\Backup\Admin\Model\ConfigurationWizard $model */ + $model = $this->getModel(); + $model->setState('act', $this->input->get('act', '', 'cmd')); + $ret = $model->runAjax(); + + @ob_end_clean(); + echo '###' . json_encode($ret) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/ControlPanel.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/ControlPanel.php new file mode 100644 index 00000000..25094745 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/ControlPanel.php @@ -0,0 +1,250 @@ +setPredefinedTaskList([ + 'main', 'SwitchProfile', 'UpdateInfo', 'applydlid', 'resetSecretWord', 'reloadUpdateInformation', 'forceUpdateDb' + ]); + } + + protected function onBeforeMain() + { + /** @var \Akeeba\Backup\Admin\Model\ControlPanel $model */ + $model = $this->getModel(); + + $engineConfig = Factory::getConfiguration(); + + // Invalidate stale backups + $params = $this->container->params; + + Factory::resetState(array( + 'global' => true, + 'log' => false, + 'maxrun' => $params->get('failure_timeout', 180) + )); + + // Just in case the reset() loaded a stale configuration... + Platform::getInstance()->load_configuration(); + Platform::getInstance()->apply_quirk_definitions(); + + // Let's make sure the temporary and output directories are set correctly and writable... + /** @var ConfigurationWizard $wizmodel */ + $wizmodel = $this->container->factory->model('ConfigurationWizard')->tmpInstance(); + $wizmodel->autofixDirectories(); + + // Check if we need to toggle the settings encryption feature + $model->checkSettingsEncryption(); + + // Run the automatic update site refresh + /** @var Updates $updateModel */ + $updateModel = $this->container->factory->model('Updates')->tmpInstance(); + $updateModel->refreshUpdateSite(); + } + + public function SwitchProfile() + { + // CSRF prevention + $this->csrfProtection(); + + $newProfile = $this->input->get('profileid', -10, 'int'); + + if (!is_numeric($newProfile) || ($newProfile <= 0)) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba', JText::_('COM_AKEEBA_CPANEL_PROFILE_SWITCH_ERROR'), 'error'); + + return; + } + + $this->container->platform->setSessionVar('profile', $newProfile, 'akeeba'); + $url = ''; + $returnurl = $this->input->get('returnurl', '', 'base64'); + + if (!empty($returnurl)) + { + $url = base64_decode($returnurl); + } + + if (empty($url)) + { + $url = JUri::base() . 'index.php?option=com_akeeba'; + } + + $this->setRedirect($url, JText::_('COM_AKEEBA_CPANEL_PROFILE_SWITCH_OK')); + } + + public function UpdateInfo() + { + /** @var Updates $updateModel */ + $updateModel = $this->container->factory->model('Updates')->tmpInstance(); + $infoArray = $updateModel->getUpdates(); + $updateInfo = (object)$infoArray; + + $result = ''; + + if ($updateInfo->hasUpdate) + { + $strings = array( + 'header' => JText::sprintf('COM_AKEEBA_CPANEL_MSG_UPDATEFOUND', $updateInfo->version), + 'button' => JText::sprintf('COM_AKEEBA_CPANEL_MSG_UPDATENOW', $updateInfo->version), + 'infourl' => $updateInfo->infoURL, + 'infolbl' => JText::_('COM_AKEEBA_CPANEL_MSG_MOREINFO'), + ); + + $result = << +

+ + {$strings['header']} +

+

+ + {$strings['button']} + + + {$strings['infolbl']} + +

+ +HTML; + } + + echo '###' . $result . '###'; + + // Cut the execution short + $this->container->platform->closeApplication(); + } + + /** + * Applies the Download ID when the user is prompted about it in the Control Panel + */ + public function applydlid() + { + // CSRF prevention + $this->csrfProtection(); + + $msg = JText::_('COM_AKEEBA_CPANEL_ERR_INVALIDDOWNLOADID'); + $msgType = 'error'; + $dlid = $this->input->getString('dlid', ''); + + // If the Download ID seems legit let's apply it + if (preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $dlid)) + { + $msg = null; + $msgType = null; + + $this->container->params->set('update_dlid', $dlid); + $this->container->params->save(); + } + + // Redirect back to the control panel + $url = ''; + $returnurl = $this->input->get('returnurl', '', 'base64'); + + if (!empty($returnurl)) + { + $url = base64_decode($returnurl); + } + + if (empty($url)) + { + $url = JUri::base() . 'index.php?option=com_akeeba'; + } + + $this->setRedirect($url, $msg, $msgType); + } + + /** + * Reset the Secret Word for front-end and remote backup + * + * @return void + */ + public function resetSecretWord() + { + // CSRF prevention + $this->csrfProtection(); + + $newSecret = $this->container->platform->getSessionVar('newSecretWord', null, 'akeeba.cpanel'); + + if (empty($newSecret)) + { + $random = new \Akeeba\Engine\Util\RandomValue(); + $newSecret = $random->generateString(32); + $this->container->platform->setSessionVar('newSecretWord', $newSecret, 'akeeba.cpanel'); + } + + $this->container->params->set('frontend_secret_word', $newSecret); + $this->container->params->save(); + + $msg = JText::sprintf('COM_AKEEBA_CPANEL_MSG_FESECRETWORD_RESET', $newSecret); + + $url = 'index.php?option=com_akeeba'; + $this->setRedirect($url, $msg); + } + + public function reloadUpdateInformation() + { + $msg = null; + + /** @var Updates $model */ + $model = $this->container->factory->model('Updates')->tmpInstance(); + $model->getUpdates(true); + + $msg = JText::_('COM_AKEEBA_COMMON_UPDATE_INFORMATION_RELOADED'); + $url = 'index.php?option=com_akeeba'; + + $this->setRedirect($url, $msg); + } + + /** + * Resets the "updatedb" flag and forces the database updates + */ + public function forceUpdateDb() + { + // Reset the flag so the updates could take place + $this->container->params->set('updatedb', null); + $this->container->params->save(); + + /** @var \Akeeba\Backup\Admin\Model\ControlPanel $model */ + $model = $this->getModel(); + + try + { + $model->checkAndFixDatabase(); + } + catch (\RuntimeException $e) + { + // This should never happen, since we reset the flag before execute the update, but you never know + } + + $this->setRedirect('index.php?option=com_akeeba'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/DatabaseFilters.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/DatabaseFilters.php new file mode 100644 index 00000000..c907c6e2 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/DatabaseFilters.php @@ -0,0 +1,77 @@ +setPredefinedTaskList(['main', 'ajax']); + } + + /** + * Handles the "main" task, which displays a folder and file list + * + */ + public function main() + { + $task = $this->input->get('task', 'normal', 'cmd'); + + /** @var \Akeeba\Backup\Admin\Model\DatabaseFilters $model */ + $model = $this->getModel(); + $model->setState('browse_task', $task); + + $this->display(false, false); + } + + /** + * AJAX proxy + */ + public function ajax() + { + // Parse the JSON data and reset the action query param to the resulting array + $action_json = $this->input->get('action', '', 'none', 2); + $action = json_decode($action_json, $this->decodeJsonAsArray); + + /** @var \Akeeba\Backup\Admin\Model\DatabaseFilters $model */ + $model = $this->getModel(); + + $model->setState('action', $action); + + $ret = $model->doAjax(); + + @ob_end_clean(); + echo '###' . json_encode($ret) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/FTPBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/FTPBrowser.php new file mode 100644 index 00000000..3e967e4b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/FTPBrowser.php @@ -0,0 +1,49 @@ +getModel(); + + // Grab the data and push them to the model + $model->host = $this->input->get('host', '', 'string'); + $model->port = $this->input->get('port', 21, 'int'); + $model->passive = $this->input->get('passive', 1, 'int'); + $model->ssl = $this->input->get('ssl', 0, 'int'); + $model->username = $this->input->get('username', '', 'none', 2); + $model->password = $this->input->get('password', '', 'none', 2); + $model->directory = $this->input->get('directory', '', 'none', 2); + + if (empty($model->port)) + { + $model->port = $model->ssl ? 990 : 21; + } + + $ret = $model->doBrowse(); + + @ob_end_clean(); + echo '###' . json_encode($ret) . '###'; + flush(); + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/FileFilters.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/FileFilters.php new file mode 100644 index 00000000..fb045633 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/FileFilters.php @@ -0,0 +1,41 @@ + 'FileFilters', + 'viewName' => 'FileFilters' + ], $config); + + parent::__construct($container, $config); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Log.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Log.php new file mode 100644 index 00000000..672310c7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Log.php @@ -0,0 +1,127 @@ +onCustomACLBeforeExecute($task); + + $profile_id = $this->input->getInt('profileid', null); + + if (!empty($profile_id) && is_numeric($profile_id) && ($profile_id > 0)) + { + $this->container->platform->setSessionVar('profile', $profile_id, 'akeeba'); + } + } + + /** + * Display the log page + * + * @return void + */ + public function onBeforeDefault() + { + $tag = $this->input->get('tag', null, 'cmd'); + $latest = $this->input->get('latest', false, 'int'); + + if (empty($tag)) + { + $tag = null; + } + + /** @var LogModel $model */ + $model = $this->getModel(); + + if ($latest) + { + $logFiles = $model->getLogFiles(); + $tag = array_shift($logFiles); + } + + $model->setState('tag', $tag); + + Platform::getInstance()->load_configuration(Platform::getInstance()->get_active_profile()); + } + + /** + * Renders the contents of the log, used inside the IFRAME of the log page + * + * @return void + */ + public function iframe() + { + $tag = $this->input->get('tag', null, 'cmd'); + + if (empty($tag)) + { + $tag = null; + } + + /** @var LogModel $model */ + $model = $this->getModel(); + $model->setState('tag', $tag); + + Platform::getInstance()->load_configuration(Platform::getInstance()->get_active_profile()); + + $this->display(); + } + + /** + * Download the log file as a text file + * + * @return void + */ + public function download() + { + Platform::getInstance()->load_configuration(Platform::getInstance()->get_active_profile()); + + $tag = $this->input->get('tag', null, 'cmd'); + + if (empty($tag)) + { + $tag = null; + } + + $asAttachment = $this->input->getBool('attachment', true); + + @ob_end_clean(); // In case some braindead plugin spits its own HTML + header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 + header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past + header("Content-Description: File Transfer"); + header('Content-Type: text/plain'); + + if ($asAttachment) + { + header('Content-Disposition: attachment; filename="Akeeba Backup Debug Log.txt"'); + } + + /** @var LogModel $model */ + $model = $this->getModel(); + $model->setState('tag', $tag); + $model->echoRawLog(); + + flush(); + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Manage.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Manage.php new file mode 100644 index 00000000..f03473c7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Manage.php @@ -0,0 +1,385 @@ +getIDsFromRequest(); + $id = count($ids) ? array_pop($ids) : -1; + + $part = $this->input->get('part', -1, 'int'); + + if ($id <= 0) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error'); + + return; + } + + $stat = Platform::getInstance()->get_statistics($id); + $allFilenames = Factory::getStatistics()->get_all_filenames($stat); + + $filename = null; + + // Check single part files + if ((count($allFilenames) == 1) && ($part == -1)) + { + $filename = array_shift($allFilenames); + } + elseif ((count($allFilenames) > 0) && (count($allFilenames) > $part) && ($part >= 0)) + { + $filename = $allFilenames[ $part ]; + } + + if (is_null($filename) || empty($filename) || !@file_exists($filename)) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDDOWNLOAD'), 'error'); + + return; + } + + // For a certain unmentionable browser -- Thank you, Nooku, for the tip + if (function_exists('ini_get') && function_exists('ini_set')) + { + if (ini_get('zlib.output_compression')) + { + ini_set('zlib.output_compression', 'Off'); + } + } + + // Remove php's time limit + if (function_exists('ini_get') && function_exists('set_time_limit')) + { + if (!ini_get('safe_mode')) + { + @set_time_limit(0); + } + } + + $basename = @basename($filename); + $filesize = @filesize($filename); + $extension = strtolower(str_replace(".", "", strrchr($filename, "."))); + + while (@ob_end_clean()) + { + ; + } + @clearstatcache(); + // Send MIME headers + header('MIME-Version: 1.0'); + header('Content-Disposition: attachment; filename="' . $basename . '"'); + header('Content-Transfer-Encoding: binary'); + header('Accept-Ranges: bytes'); + + switch ($extension) + { + case 'zip': + // ZIP MIME type + header('Content-Type: application/zip'); + break; + + default: + // Generic binary data MIME type + header('Content-Type: application/octet-stream'); + break; + } + + // Notify of filesize, if this info is available + if ($filesize > 0) + { + header('Content-Length: ' . @filesize($filename)); + } + + // Disable caching + header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + header("Expires: 0"); + header('Pragma: no-cache'); + + flush(); + + if (!$filesize) + { + // If the filesize is not reported, hope that readfile works + @readfile($filename); + + $this->container->platform->closeApplication(0); + } + + // If the filesize is reported, use 1M chunks for echoing the data to the browser + $blocksize = 1048576; //1M chunks + $handle = @fopen($filename, "r"); + + // Now we need to loop through the file and echo out chunks of file data + if ($handle !== false) + { + while (!@feof($handle)) + { + echo @fread($handle, $blocksize); + @ob_flush(); + flush(); + } + } + + if ($handle !== false) + { + @fclose($handle); + } + + $this->container->platform->closeApplication(0); + } + + /** + * Deletes one or more backup statistics records and their associated backup files + */ + public function remove() + { + // CSRF prevention + $this->csrfProtection(); + + $ids = $this->getIDsFromRequest(); + + if (empty($ids)) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error'); + + return; + } + + foreach ($ids as $id) + { + try + { + $msg = JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'); + $result = false; + + if ($id > 0) + { + /** @var Statistics $model */ + $model = $this->getModel(); + $model->setState('id', $id); + $result = $model->delete(); + } + + } + catch (\RuntimeException $e) + { + $result = false; + $msg = $e->getMessage(); + } + + if (!$result) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', $msg, 'error'); + + return; + } + } + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_MSG_DELETED')); + } + + /** + * Deletes backup files associated to one or several backup statistics records + */ + public function deletefiles() + { + // CSRF prevention + $this->csrfProtection(); + + $ids = $this->getIDsFromRequest(); + + if (empty($ids)) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error'); + + return; + } + + foreach ($ids as $id) + { + try + { + $msg = JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'); + $result = false; + + if ($id > 0) + { + /** @var Statistics $model */ + $model = $this->getModel(); + $model->setState('id', $id); + $result = $model->deleteFile(); + } + } + catch (\RuntimeException $e) + { + $result = false; + $msg = $e->getMessage(); + } + + if (!$result) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', $msg, 'error'); + + return; + } + } + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_MSG_DELETEDFILE')); + } + + public function showcomment() + { + $ids = $this->getIDsFromRequest(); + + if (empty($ids)) + { + $ids = [0]; + } + + $id = array_pop($ids); + + if ($id <= 0) + { + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error'); + } + + /** @var Statistics $model */ + $model = $this->getModel(); + $model->setState('id', $id); + + $this->layout = 'comment'; + $this->display(false); + } + + /** + * Save the comments back to a backup record + */ + public function save() + { + // CSRF prevention + $this->csrfProtection(); + + $id = $this->input->get('id', 0, 'int'); + $description = $this->input->get('description', '', 'string'); + $comment = $this->input->get('comment', null, 'string', 4); + + $statistic = Platform::getInstance()->get_statistics($id); + $statistic['description'] = $description; + $statistic['comment'] = $comment; + + $dummy = null; + $result = Platform::getInstance()->set_or_update_statistics($id, $statistic, $dummy); + + $message = JText::_('COM_AKEEBA_BUADMIN_LOG_SAVEDOK'); + $type = 'message'; + + if ($result === false) + { + $message = JText::_('COM_AKEEBA_BUADMIN_LOG_SAVEERROR'); + $type = 'error'; + } + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage', $message, $type); + } + + public function restore() + { + // CSRF prevention + $this->csrfProtection(); + + $ids = $this->getIDsFromRequest(); + + if (empty($ids)) + { + $ids = [0]; + } + + $id = array_pop($ids); + + $url = JUri::base() . 'index.php?option=com_akeeba&view=Restore&id=' . $id; + $this->setRedirect($url); + } + + public function cancel() + { + // CSRF prevention + $this->csrfProtection(); + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage'); + } + + public function hidemodal() + { + /** @var Statistics $model */ + $model = $this->getModel(); + $model->hideRestorationInstructionsModal(); + + $this->setRedirect(JUri::base() . 'index.php?option=com_akeeba&view=Manage'); + } + + /** + * Gets the list of IDs from the request data + * + * @return array + */ + protected function getIDsFromRequest() + { + // Get the ID or list of IDs from the request or the configuration + $cid = $this->input->get('cid', array(), 'array'); + $id = $this->input->getInt('id', 0); + + $ids = array(); + + if (is_array($cid) && !empty($cid)) + { + $ids = $cid; + } + elseif (!empty($id)) + { + $ids = array($id); + } + + return $ids; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/CustomACL.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/CustomACL.php new file mode 100644 index 00000000..bf581e92 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/CustomACL.php @@ -0,0 +1,74 @@ +akeebaBackupACLCheck($this->view, $this->task); + } + + /** + * Checks if the currently logged in user has the required ACL privileges to access the current view. If not, a + * RuntimeException is thrown. + * + * @return void + */ + protected function akeebaBackupACLCheck($view, $task) + { + // Akeeba Backup-specific ACL checks. All views not listed here are limited by the akeeba.configure privilege. + $viewACLMap = [ + 'ControlPanel' => 'core.manage', + 'Backup' => 'akeeba.backup', + 'Manage' => 'core.manage', + 'Manage.download' => 'akeeba.download', + 'Manage.remove' => 'akeeba.download', + 'Manage.deletefiles' => 'akeeba.download', + 'Manage.showcomment' => 'akeeba.backup', + 'Manage.save' => 'akeeba.download', + 'Manage.restore' => 'akeeba.configure', + 'Manage.cancel' => 'akeeba.backup', + 'Upload' => 'akeeba.backup', + 'RemoteFiles' => 'akeeba.download', + 'Transfer' => 'akeeba.download', + ]; + + // Default + $privilege = 'akeeba.configure'; + + // Just the view was found + if (array_key_exists($view, $viewACLMap)) + { + $privilege = $viewACLMap[$view]; + } + + // The view AND task was found + if (array_key_exists($view . '.' . $task, $viewACLMap)) + { + $privilege = $viewACLMap[$view . '.' . $task]; + } + + // If an empty privilege is defined do not perform any ACL checks + if (empty($privilege)) + { + return; + } + + if (!$this->container->platform->authorise($privilege, 'com_akeeba')) + { + throw new RuntimeException(JText::_('JERROR_ALERTNOAUTHOR'), 403); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/PredefinedTaskList.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/PredefinedTaskList.php new file mode 100644 index 00000000..462f8982 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Mixin/PredefinedTaskList.php @@ -0,0 +1,71 @@ +predefinedTaskList)) + { + $task = reset($this->predefinedTaskList); + } + + return parent::execute($task); + } + + /** + * Sets the predefined task list and registers the first task in the list as the Controller's default task + * + * @param array $taskList The task list to register + */ + public function setPredefinedTaskList(array $taskList) + { + // First, unregister all known tasks which are not in the taskList + $allTasks = $this->getTasks(); + + foreach ($allTasks as $task) + { + if (in_array($task, $taskList)) + { + continue; + } + + $this->unregisterTask($task); + } + + // Set the predefined task list + $this->predefinedTaskList = $taskList; + + // Set the default task + $this->registerDefaultTask(reset($this->predefinedTaskList)); + + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Profile.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Profile.php new file mode 100644 index 00000000..a565ae37 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Profile.php @@ -0,0 +1,16 @@ +csrfProtection(); + + if (!$this->container->platform->authorise('akeeba.configure', 'com_akeeba')) + { + throw new RuntimeException(JText::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var \Akeeba\Backup\Admin\Model\Profiles $model */ + $model = $this->getModel(); + + // Get some data from the request + $file = $this->input->files->get('importfile', array(), 'array'); + + if (!isset($file['name'])) + { + $this->setRedirect('index.php?option=com_akeeba&view=Profiles', JText::_('MSG_UPLOAD_INVALID_REQUEST'), 'error'); + + return; + } + + // Load the file data + $data = @file_get_contents($file['tmp_name']); + @unlink($file['tmp_name']); + + // JSON decode + $data = json_decode($data, true); + + // Import + $message = JText::_('COM_AKEEBA_PROFILES_MSG_IMPORT_COMPLETE'); + $messageType = null; + + try + { + $model->reset()->import($data); + } + catch (RuntimeException $e) + { + $message = $e->getMessage(); + $messageType = 'error'; + } + + // Redirect back to the main page + $this->setRedirect('index.php?option=com_akeeba&view=Profiles', $message, $messageType); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Restore.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Restore.php new file mode 100644 index 00000000..ea976c29 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Restore.php @@ -0,0 +1,127 @@ +setPredefinedTaskList(['main', 'start', 'ajax']); + } + + /** + * Main task, displays the main page + */ + public function main() + { + /** @var RestoreModel $model */ + $model = $this->getModel(); + + $message = $model->validateRequest(); + + if ($message !== true) + { + $this->setRedirect('index.php?option=com_akeeba&view=Manage', $message, 'error'); + $this->redirect(); + + return; + } + + $model->setState('restorationstep', 0); + + $this->display(false, false); + } + + /** + * Start the restoration + */ + public function start() + { + $this->csrfProtection(); + + /** @var RestoreModel $model */ + $model = $this->getModel(); + + $model->setState('restorationstep', 1); + $message = $model->validateRequest(); + + if ($message !== true) + { + $this->setRedirect('index.php?option=com_akeeba&view=Manage', $message, 'error'); + $this->redirect(); + + return; + } + + $model->setState('jps_key', $this->input->get('jps_key', '', 'cmd')); + $model->setState('procengine', $this->input->get('procengine', 'direct', 'cmd')); + $model->setState('ftp_host', $this->input->get('ftp_host', '', 'none', 2)); + $model->setState('ftp_port', $this->input->get('ftp_port', 21, 'int')); + $model->setState('ftp_user', $this->input->get('ftp_user', '', 'none', 2)); + $model->setState('ftp_pass', $this->input->get('ftp_pass', '', 'none', 2)); + $model->setState('ftp_root', $this->input->get('ftp_root', '', 'none', 2)); + $model->setState('tmp_path', $this->input->get('tmp_path', '', 'none', 2)); + $model->setState('ftp_ssl', $this->input->get('usessl', 'false', 'cmd') == 'true'); + $model->setState('ftp_pasv', $this->input->get('passive', 'true', 'cmd') == 'true'); + + $status = $model->createRestorationINI(); + + if ($status === false) + { + $this->setRedirect('index.php?option=com_akeeba&view=Manage', JText::_('COM_AKEEBA_RESTORE_ERROR_CANT_WRITE'), 'error'); + $this->redirect(); + + return; + } + + $this->display(false, false); + } + + /** + * Perform a step through AJAX + */ + public function ajax() + { + /** @var RestoreModel $model */ + $model = $this->getModel(); + + $ajax = $this->input->get('ajax', '', 'cmd'); + $model->setState('ajax', $ajax); + + $ret = $model->doAjax(); + + @ob_end_clean(); + echo '###' . json_encode($ret) . '###'; + flush(); + + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/SFTPBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/SFTPBrowser.php new file mode 100644 index 00000000..d6ba93fe --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/SFTPBrowser.php @@ -0,0 +1,49 @@ +getModel(); + + // Grab the data and push them to the model + $model->host = $this->input->get('host', '', 'string'); + $model->port = $this->input->get('port', 21, 'int'); + $model->username = $this->input->get('username', '', 'none', 2); + $model->password = $this->input->get('password', '', 'none', 2); + $model->privkey = $this->input->get('privkey', '', 'none', 2); + $model->pubkey = $this->input->get('pubkey', '', 'none', 2); + $model->directory = $this->input->get('directory', '', 'none', 2); + + if (empty($model->port)) + { + $model->port = 22; + } + + $ret = $model->doBrowse(); + + @ob_end_clean(); + echo '###' . json_encode($ret) . '###'; + flush(); + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Controller/Schedule.php b/deployed/akeeba/administrator/components/com_akeeba/Controller/Schedule.php new file mode 100644 index 00000000..181de1dc --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Controller/Schedule.php @@ -0,0 +1,22 @@ +setPredefinedTaskList(['wizard', 'checkUrl', 'applyConnection', 'initialiseUpload', 'upload', 'reset']); + } + + /** + * Reset the wizard + * + * @return void + */ + public function reset() + { + $this->container->platform->setSessionVar('transfer', null, 'akeeba'); + $this->container->platform->setSessionVar('transfer.url', null, 'akeeba'); + $this->container->platform->setSessionVar('transfer.url_status', null, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpsupport', null, 'akeeba'); + + /** @var \Akeeba\Backup\Admin\Model\Transfer $model */ + $model = $this->getModel(); + $model->resetUpload(); + + $this->setRedirect('index.php?option=com_akeeba&view=Transfer'); + } + + /** + * Cleans and checks the validity of the new site's URL + * + * @return void + */ + public function checkUrl() + { + $url = $this->input->get('url', '', 'raw'); + + /** @var \Akeeba\Backup\Admin\Model\Transfer $model */ + $model = $this->getModel(); + $model->savestate(true); + $result = $model->checkAndCleanUrl($url); + + $this->container->platform->setSessionVar('transfer.url', $result['url'], 'akeeba'); + $this->container->platform->setSessionVar('transfer.url_status', $result['status'], 'akeeba'); + + @ob_end_clean(); + echo '###' . json_encode($result) . '###'; + $this->container->platform->closeApplication(); + } + + /** + * Applies the FTP/SFTP connection information and makes some preliminary validation + * + * @return void + */ + public function applyConnection() + { + $result = (object) [ + 'status' => true, + 'message' => '', + 'ignorable' => false, + ]; + + // Get the parameters from the request + $transferOption = $this->input->getCmd('method', 'ftp'); + $force = $this->input->getInt('force', 0); + $ftpHost = $this->input->get('host', '', 'raw', 2); + $ftpPort = $this->input->getInt('port', null); + $ftpUsername = $this->input->get('username', '', 'raw', 2); + $ftpPassword = $this->input->get('password', '', 'raw', 2); + $ftpPubKey = $this->input->get('public', '', 'raw', 2); + $ftpPrivateKey = $this->input->get('private', '', 'raw', 2); + $ftpPassive = $this->input->getInt('passive', 1); + $ftpPassiveFix = $this->input->getInt('passive_fix', 1); + $ftpDirectory = $this->input->get('directory', '', 'raw', 2); + $chunkMode = $this->input->getCmd('chunkMode', 'chunked'); + $chunkSize = $this->input->getInt('chunkSize', '5242880'); + + // Fix the port if it's missing + if (empty($ftpPort)) + { + switch ($transferOption) + { + case 'ftp': + case 'ftpcurl': + $ftpPort = 21; + break; + + case 'ftps': + case 'ftpscurl': + $ftpPort = 990; + break; + + case 'sftp': + case 'sftpcurl': + $ftpPort = 22; + break; + } + } + + // Store everything in the session + $this->container->platform->setSessionVar('transfer.transferOption', $transferOption, 'akeeba'); + $this->container->platform->setSessionVar('transfer.force', $force, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpHost', $ftpHost, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPort', $ftpPort, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpUsername', $ftpUsername, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPassword', $ftpPassword, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPubKey', $ftpPubKey, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPrivateKey', $ftpPrivateKey, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpDirectory', $ftpDirectory, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPassive', $ftpPassive ? 1 : 0, 'akeeba'); + $this->container->platform->setSessionVar('transfer.ftpPassiveFix', $ftpPassiveFix ? 1 : 0, 'akeeba'); + $this->container->platform->setSessionVar('transfer.chunkMode', $chunkMode, 'akeeba'); + $this->container->platform->setSessionVar('transfer.uploadLimit', $chunkSize, 'akeeba'); + + /** @var \Akeeba\Backup\Admin\Model\Transfer $model */ + $model = $this->getModel(); + + try + { + $config = $model->getFtpConfig(); + $model->testConnection($config); + } + catch (TransferIgnorableError $e) + { + $result = (object) [ + 'status' => false, + 'ignorable' => true, + 'message' => $e->getMessage(), + ]; + } + catch (Exception $e) + { + $result = (object) [ + 'status' => false, + 'message' => $e->getMessage(), + 'ignorable' => false, + ]; + } + + @ob_end_clean(); + echo '###' . json_encode($result) . '###'; + $this->container->platform->closeApplication(); + } + + /** + * Initialise the upload: sends Kickstart and our add-on script to the remote server + * + * @return void + */ + public function initialiseUpload() + { + $result = (object)[ + 'status' => true, + 'message' => '', + ]; + + /** @var \Akeeba\Backup\Admin\Model\Transfer $model */ + $model = $this->getModel(); + + try + { + $config = $model->getFtpConfig(); + $model->initialiseUpload($config); + } + catch (Exception $e) + { + $result = (object)[ + 'status' => false, + 'message' => $e->getMessage(), + ]; + } + + @ob_end_clean(); + echo '###' . json_encode($result) . '###'; + $this->container->platform->closeApplication(); + } + + /** + * Perform an upload step. Pass start=1 to reset the upload and start over. + * + * @return void + */ + public function upload() + { + /** @var \Akeeba\Backup\Admin\Model\Transfer $model */ + $model = $this->getModel(); + + if ($this->input->getBool('start', false)) + { + $model->resetUpload(); + } + + try + { + $config = $model->getFtpConfig(); + $uploadResult = $model->uploadChunk($config); + } + catch (Exception $e) + { + $uploadResult = (object)[ + 'status' => false, + 'message' => $e->getMessage(), + 'totalSize' => 0, + 'doneSize' => 0, + 'done' => false + ]; + } + + $result = (object)$uploadResult; + + @ob_end_clean(); + echo '###' . json_encode($result) . '###'; + $this->container->platform->closeApplication(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/Dispatcher.php b/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/Dispatcher.php new file mode 100644 index 00000000..9f24711a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/Dispatcher.php @@ -0,0 +1,278 @@ +viewNameAliases = [ + 'buadmin' => 'Manage', + 'buadmins' => 'Manage', + 'config' => 'Configuration', + 'configs' => 'Configuration', + 'confwiz' => 'ConfigurationWizard', + 'confwizs' => 'ConfigurationWizard', + 'confwizes' => 'ConfigurationWizard', + 'cpanel' => 'ControlPanel', + 'cpanels' => 'ControlPanel', + 'dbef' => 'DatabaseFilters', + 'dbefs' => 'DatabaseFilters', + 'eff' => 'IncludeFolders', + 'effs' => 'IncludeFolders', + 'fsfilter' => 'FileFilters', + 'fsfilters' => 'FileFilters', + 'ftpbrowser' => 'FTPBrowser', + 'ftpbrowsers' => 'FTPBrowser', + 'sftpbrowser' => 'SFTPBrowser', + 'sftpbrowsers' => 'SFTPBrowser', + 'multidb' => 'MultipleDatabases', + 'multidbs' => 'MultipleDatabases', + 'regexdbfilter' => 'RegExDatabaseFilters', + 'regexdbfilters' => 'RegExDatabaseFilters', + 'regexfsfilter' => 'RegExFileFilters', + 'regexfsfilters' => 'RegExFileFilters', + 'remotefile' => 'RemoteFiles', + 'remotefiles' => 'RemoteFiles', + 's3import' => 'S3Import', + 's3imports' => 'S3Import', + ]; + + } + + /** + * Executes before dispatching the request to the appropriate controller + */ + public function onBeforeDispatch() + { + $this->onBeforeDispatchViewAliases(); + + // Load the FOF language + $lang = $this->container->platform->getLanguage(); + $lang->load('lib_fof30', JPATH_ADMINISTRATOR, 'en-GB', true, true); + $lang->load('lib_fof30', JPATH_ADMINISTRATOR, null, true, false); + + // Necessary for routing the Alice view + $this->container->inflector->addWord('Alice', 'Alices'); + + // Does the user have adequate permissions to access our component? + if (!$this->container->platform->authorise('core.manage', 'com_akeeba')) + { + throw new \RuntimeException(\JText::_('JERROR_ALERTNOAUTHOR'), 404); + } + + // FEF Renderer options. Used to load the common CSS file. + $this->container->renderer->setOptions([ + 'custom_css' => 'media://com_akeeba/css/akeebaui.min.css' + ]); + + // Load Akeeba Engine + $this->loadAkeebaEngine(); + + // Load the Akeeba Engine configuration + try + { + $this->loadAkeebaEngineConfiguration(); + } + catch (\Exception $e) + { + // Maybe the tables are not installed? + /** @var ControlPanel $cPanelModel */ + $cPanelModel = $this->container->factory->model('ControlPanel')->tmpInstance(); + + try + { + $cPanelModel->checkAndFixDatabase(); + } + catch (\RuntimeException $e) + { + // The update is stuck. We will display a warning in the Control Panel + } + + $msg = \JText::_('COM_AKEEBA_CONTROLPANEL_MSG_REBUILTTABLES'); + $this->container->platform->redirect('index.php', 307, $msg, 'warning'); + } + + // Prevents the "SQLSTATE[HY000]: General error: 2014" due to resource sharing with Akeeba Engine + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // !!!!! WARNING: ALWAYS GO THROUGH JFactory; DO NOT GO THROUGH $this->container->db !!!!! + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + $jDbo = JFactory::getDbo(); + + if ($jDbo->name == 'pdomysql') + { + @JFactory::getDbo()->disconnect(); + } + + // Load the utils helper library + Platform::getInstance()->load_version_defines(); + Platform::getInstance()->apply_quirk_definitions(); + + // Make sure the front-end backup Secret Word is stored encrypted + $params = $this->container->params; + SecretWord::enforceEncryption($params, 'frontend_secret_word'); + + // Make sure we have a version loaded + @include_once($this->container->backEndPath . '/version.php'); + + if (!defined('AKEEBA_VERSION')) + { + define('AKEEBA_VERSION', 'dev'); + define('AKEEBA_DATE', date('Y-m-d')); + } + + // Create a media file versioning tag + $this->container->mediaVersion = md5(AKEEBA_VERSION . AKEEBA_DATE); + + // Perform certain functionality only in HTML tasks + $format = $this->input->getCmd('format', 'html'); + + if ($format == 'html') + { + // Load common Javascript files. NOTE: CSS and anything style-related is loaded by the FEF Renderer class. + $this->loadCommonJavascript(); + + // Perform common maintenance tasks + $this->autoMaintenance(); + } + + // Set the linkbar style to Classic (Bootstrap tabs). The sidebar takes too much space and requires adding + // manual HTML to render it... + $this->container->renderer->setOption('linkbar_style', 'classic'); + } + + /** + * Loads the Javascript files which are common across many views of the component. + * + * @return void + */ + private function loadCommonJavascript() + { + \JHtml::_('jquery.framework'); + + $mediaVersion = $this->container->mediaVersion; + + // Do not mode: everything depends on UserInterfaceCommon + $this->container->template->addJS('media://com_akeeba/js/UserInterfaceCommon.min.js', false, false, $mediaVersion); + // Do not move: System depends on Modal + $this->container->template->addJS('media://com_akeeba/js/Modal.min.js', false, false, $mediaVersion); + // Do not move: System depends on Ajax + $this->container->template->addJS('media://com_akeeba/js/Ajax.min.js', false, false, $mediaVersion); + // Do not move: System depends on Ajax + $this->container->template->addJS('media://com_akeeba/js/System.min.js', false, false, $mediaVersion); + // Do not move: Tooltip depends on System + $this->container->template->addJS('media://com_akeeba/js/Tooltip.min.js', false, false, $mediaVersion); + // Always add last (it's the least important) + $this->container->template->addJS('media://com_akeeba/js/piecon.min.js', false, false, $mediaVersion); + } + + /** + * Perform common maintenance tasks + * + * @return void + */ + private function autoMaintenance() + { + /** @var \Akeeba\Backup\Admin\Model\ControlPanel $model */ + $model = $this->container->factory->model('ControlPanel')->tmpInstance(); + + // Update the db structure if necessary (once per session at most) + $lastVersion = $this->container->platform->getSessionVar('magicParamsUpdateVersion', null, 'com_akeeba'); + + if ($lastVersion != AKEEBA_VERSION) + { + try + { + $model->checkAndFixDatabase(); + $this->container->platform->setSessionVar('magicParamsUpdateVersion', AKEEBA_VERSION, 'com_akeeba'); + } + catch (\RuntimeException $e) + { + // The update is stuck. We will display a warning in the Control Panel + } + } + + // Update magic parameters if necessary + $model->updateMagicParameters(); + } + + public function onAfterDispatch() + { + // See the after_render.php file for an explanation. TL;DR: CloudFlare Rocket Loader is a broken pile of crap. + if ($this->input->get('format', 'html') != 'html') + { + return; + } + + if (!function_exists('akeebaBackupOnAfterRenderToFixBrokenCloudFlareRocketLoader')) + { + require_once __DIR__ . '/after_render.php'; + } + + JFactory::getApplication()->registerEvent('onAfterRender', 'akeebaBackupOnAfterRenderToFixBrokenCloudFlareRocketLoader'); + } + + public function loadAkeebaEngine() + { + // Necessary defines for Akeeba Engine + if (!defined('AKEEBAENGINE')) + { + define('AKEEBAENGINE', 1); + define('AKEEBAROOT', $this->container->backEndPath . '/BackupEngine'); + define('ALICEROOT', $this->container->backEndPath . '/AliceEngine'); + } + + // Make sure we have a profile set throughout the component's lifetime + $profile_id = $this->container->platform->getSessionVar('profile', null, 'akeeba'); + + if (is_null($profile_id)) + { + $this->container->platform->setSessionVar('profile', 1, 'akeeba'); + } + + // Load Akeeba Engine + $basePath = $this->container->backEndPath; + require_once $basePath . '/BackupEngine/Factory.php'; + + // Load ALICE (Pro version only) + if (@file_exists($basePath . '/AliceEngine/factory.php')) + { + require_once $basePath . '/AliceEngine/factory.php'; + } + } + + public function loadAkeebaEngineConfiguration() + { + Platform::addPlatform('joomla3x', $this->container->backEndPath . '/BackupPlatform/Joomla3x'); + $akeebaEngineConfig = Factory::getConfiguration(); + Platform::getInstance()->load_configuration(); + unset($akeebaEngineConfig); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/after_render.php b/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/after_render.php new file mode 100644 index 00000000..2d2241e5 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Dispatcher/after_render.php @@ -0,0 +1,74 @@ +getBody(); + + // Find the section + $from = stripos($buffer, ' section + $head = substr($buffer, $from, $to - $from); + + // Replace 'setBody($buffer); +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Helper/SecretWord.php b/deployed/akeeba/administrator/components/com_akeeba/Helper/SecretWord.php new file mode 100644 index 00000000..1fd30631 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Helper/SecretWord.php @@ -0,0 +1,102 @@ +get('useencryption', -1) == 0) + { + return; + } + + // If encryption is not supported on this server we can't encrypt the Secret Word + if (!Factory::getSecureSettings()->supportsEncryption()) + { + return; + } + + // Get the raw version of frontend_secret_word and check if it has a valid encryption signature + $raw = $params->get($settingsKey, ''); + $signature = substr($raw, 0, 12); + $validSignatures = array('###AES128###', '###CTR128###'); + + // If the setting is already encrypted I have nothing to do here + if (in_array($signature, $validSignatures)) + { + return; + } + + // The setting was NOT encrypted. I need to encrypt it. + $secureSettings = Factory::getSecureSettings(); + $encrypted = $secureSettings->encryptSettings($raw); + + // Finally, I need to save it back to the database + $params->set($settingsKey, $encrypted); + $params->save(); + } + + /** + * Forcibly store the Secret Word settings $settingsKey unencrypted in the database. This is meant to be called when + * the user disables settings encryption. Since the encryption key will be deleted we need to decrypt the Secret + * Word at the same time as the Engine settings. Otherwise we will never be able to access it again. + * + * @param Params $params The component parameters object + * @param string $settingsKey The key of the Secret Word parameter + * @param string|null $encryptionKey (Optional) The AES key with which to decrypt the parameter + * + * @return void + * + * @since 5.5.2 + */ + public static function enforceDecrypted(Params $params, $settingsKey, $encryptionKey = null) + { + // Get the raw version of frontend_secret_word and check if it has a valid encryption signature + $raw = $params->get($settingsKey, ''); + $signature = substr($raw, 0, 12); + $validSignatures = array('###AES128###', '###CTR128###'); + + // If the setting is not already encrypted I have nothing to decrypt + if (!in_array($signature, $validSignatures)) + { + return; + } + + // The setting was encrypted. I need to decrypt it. + $secureSettings = Factory::getSecureSettings(); + $encrypted = $secureSettings->decryptSettings($raw, $encryptionKey); + + // Finally, I need to save it back to the database + $params->set($settingsKey, $encrypted); + $params->save(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Helper/Status.php b/deployed/akeeba/administrator/components/com_akeeba/Helper/Status.php new file mode 100644 index 00000000..388ee414 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Helper/Status.php @@ -0,0 +1,259 @@ +getFolderStatus(); + $this->outputWritable = $this->status['output']; + $this->tempWritable = $this->status['temporary']; + $this->status = Factory::getConfigurationChecks()->getShortStatus(); + $this->warnings = Factory::getConfigurationChecks()->getDetailedStatus(); + } + + /** + * Returns the HTML for the backup status cell + * + * @return string HTML + */ + public function getStatusCell() + { + $status = Factory::getConfigurationChecks()->getShortStatus(); + $quirks = Factory::getConfigurationChecks()->getDetailedStatus(); + + if ($status && empty($quirks)) + { + $html = '

' . JText::_('COM_AKEEBA_CPANEL_LBL_STATUS_OK') . '

'; + } + elseif ($status && !empty($quirks)) + { + $html = '

' . JText::_('COM_AKEEBA_CPANEL_LBL_STATUS_WARNING') . '

'; + } + else + { + $html = '

' . JText::_('COM_AKEEBA_CPANEL_LBL_STATUS_ERROR') . '

'; + } + + return $html; + } + + /** + * Returns HTML for the warnings (status details) + * + * @param bool $onlyErrors Should I only return errors? If false (default) errors AND warnings are returned. + * + * @return string HTML + */ + public function getQuirksCell($onlyErrors = false) + { + $html = '

' . JText::_('COM_AKEEBA_CPANEL_WARNING_QNONE') . '

'; + $quirks = Factory::getConfigurationChecks()->getDetailedStatus(); + + if (!empty($quirks)) + { + $html = "
    \n"; + + foreach ($quirks as $quirk) + { + $html .= $this->renderWarnings($quirk, $onlyErrors); + } + + $html .= "
\n"; + } + + return $html; + } + + /** + * Returns a boolean value, indicating if warnings have been detected. + * + * @return bool True if there is at least one detected warnings + */ + public function hasQuirks() + { + $quirks = Factory::getConfigurationChecks()->getDetailedStatus(); + + return !empty($quirks); + } + + /** + * Gets the HTML for a single line of the warnings area. + * + * @param array $quirk A quirk definition array + * @param bool $onlyErrors Should I only return errors? If false (default) errors AND warnings are returned. + * + * @return string HTML + */ + private function renderWarnings($quirk, $onlyErrors = false) + { + if ($onlyErrors && ($quirk['severity'] != 'critical')) + { + return ''; + } + + $quirk['severity'] = $quirk['severity'] == 'critical' ? 'high' : $quirk['severity']; + + return '
  • ' . $quirk['description'] . '' . "
  • \n"; + + } + + /** + * Returns the details of the latest backup as HTML + * + * @return string HTML + */ + public function getLatestBackupDetails() + { + $db = Container::getInstance('com_akeeba')->db; + $query = $db->getQuery(true) + ->select('MAX(' . $db->qn('id') . ')') + ->from($db->qn('#__ak_stats')) + ->where($db->qn('origin') . ' != ' . $db->q('restorepoint')); + $db->setQuery($query); + $id = $db->loadResult(); + + $backup_types = Factory::getEngineParamsProvider()->loadScripting(); + + if (empty($id)) + { + return '

    ' . JText::_('COM_AKEEBA_BACKUP_STATUS_NONE') . '

    '; + } + + $record = Platform::getInstance()->get_statistics($id); + + switch ($record['status']) + { + case 'run': + $status = JText::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_PENDING'); + $statusClass = "akeeba-label--warning"; + break; + + case 'fail': + $status = JText::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_FAIL'); + $statusClass = "akeeba-label--failure"; + break; + + case 'complete': + $status = JText::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_OK'); + $statusClass = "akeeba-label--success"; + break; + + default: + $status = ''; + $statusClass = ''; + } + + switch ($record['origin']) + { + case 'frontend': + $origin = JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_FRONTEND'); + break; + + case 'backend': + $origin = JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_BACKEND'); + break; + + case 'cli': + $origin = JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_CLI'); + break; + + default: + $origin = '–'; + break; + } + + $type = ''; + + if (array_key_exists($record['type'], $backup_types['scripts'])) + { + $type = Platform::getInstance()->translate($backup_types['scripts'][$record['type']]['text']); + } + + $container = Container::getInstance('com_akeeba'); + $startTime = new Date($record['backupstart'], 'UTC'); + $tz = new \DateTimeZone($container->platform->getUser()->getParam('timezone', $container->platform->getConfig()->get('offset', 'UTC'))); + $startTime->setTimezone($tz); + + $html = ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '
    ' . JText::_('COM_AKEEBA_BUADMIN_LABEL_START') . '' . $startTime->format(JText::_('DATE_FORMAT_LC2'), true) . '
    ' . JText::_('COM_AKEEBA_BUADMIN_LABEL_DESCRIPTION') . '' . $record['description'] . '
    ' . JText::_('COM_AKEEBA_BUADMIN_LABEL_STATUS') . '' . $status . '
    ' . JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN') . '' . $origin . '
    ' . JText::_('COM_AKEEBA_BUADMIN_LABEL_TYPE') . '' . $type . '
    '; + + return $html; + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Helper/Utils.php b/deployed/akeeba/administrator/components/com_akeeba/Helper/Utils.php new file mode 100644 index 00000000..fc9f38cb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Helper/Utils.php @@ -0,0 +1,82 @@ + $dir) + { + // find first non-matching dir + if ($dir === $to[ $depth ]) + { + // ignore this directory + array_shift($relPath); + + continue; + } + + // Get number of remaining dirs to $from + $remaining = count($from) - $depth; + + if ($remaining > 1) + { + // add traversals up to first matching dir + $padLength = (count($relPath) + $remaining - 1) * -1; + $relPath = array_pad($relPath, $padLength, '..'); + + break; + } + + $relPath[0] = './' . $relPath[0]; + } + + return implode('/', $relPath); + } + + /** + * Escapes a string for use with Javascript + * + * @param string $string The string to escape + * @param string $extras The characters to escape + * + * @return string + */ + static function escapeJS($string, $extras = '') + { + // Make sure we escape single quotes, slashes and brackets + if (empty($extras)) + { + $extras = "'\\[]"; + } + + return addcslashes($string, $extras); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Cli/Base.php b/deployed/akeeba/administrator/components/com_akeeba/Master/Cli/Base.php new file mode 100644 index 00000000..e9dbb6dd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Cli/Base.php @@ -0,0 +1,835 @@ + $v) + { + $query .= " $k"; + if ($v != "") + { + $query .= "=$v"; + } + } + } + $query = ltrim($query); + $argv = explode(' ', $query); + $argc = count($argv); + + $_SERVER['argv'] = $argv; + } + + $this->input = new JInputCLI(); + + // Create the registry with a default namespace of config + $this->config = new JRegistry; + + // Load the configuration object. + $this->loadConfiguration($this->fetchConfigurationData()); + + // Set the execution datetime and timestamp; + $this->config->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->config->set('execution.timestamp', time()); + + // Set the current directory. + $this->config->set('cwd', getcwd()); + + // Create a new JFilterInput + if (class_exists('JFilterInput')) + { + $this->filter = JFilterInput::getInstance(); + } + + // Parse the POSIX options + $this->parseOptions(); + } + + /** + * Returns a reference to the global AkeebaCliBase object, only creating it if it + * doesn't already exist. + * + * This method must be invoked as: $cli = AkeebaCliBase::getInstance(); + * + * @param string $name The name of the AkeebaCliBase class to instantiate. + * + * @return AkeebaCliBase A AkeebaCliBase object + */ + public static function &getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(self::$instance)) + { + if (class_exists($name) && (is_subclass_of($name, 'AkeebaCliBase'))) + { + self::$instance = new $name; + } + else + { + self::$instance = new AkeebaCliBase; + } + } + + return self::$instance; + } + + /** + * Execute the application. + * + * @return void + */ + public function execute() + { + $this->close(); + } + + /** + * Exit the application. + * + * @param integer $code Exit code. + * + * @return void + */ + public function close($code = 0) + { + exit($code); + } + + /** + * Load an object or array into the application configuration object. + * + * @param mixed $data Either an array or object to be loaded into the configuration object. + * + * @return void + */ + public function loadConfiguration($data) + { + // Load the data into the configuration object. + if (is_array($data)) + { + $this->config->loadArray($data); + } + elseif (is_object($data)) + { + $this->config->loadObject($data); + } + } + + /** + * Write a string to standard output. + * + * @param string $text The text to display. + * @param boolean $nl True to append a new line at the end of the output string. + * + * @return void + */ + public function out($text = '', $nl = true) + { + fwrite(STDOUT, $text . ($nl ? "\n" : null)); + } + + /** + * Get a value from standard input. + * + * @return string The input string from standard input. + */ + public function in() + { + return rtrim(fread(STDIN, 8192), "\n"); + } + + /** + * Method to load a PHP configuration class file based on convention and return the instantiated data object. You + * will extend this method in child classes to provide configuration data from whatever data source is relevant + * for your specific application. + * + * @return mixed Either an array or object to be loaded into the configuration object. + */ + protected function fetchConfigurationData() + { + // Set the configuration file name. + $file = JPATH_BASE . '/configuration.php'; + + // Import the configuration file. + if (!is_file($file)) + { + return false; + } + include_once $file; + + // Instantiate the configuration object. + if (!class_exists('JConfig')) + { + return false; + } + $config = new JConfig; + + return $config; + } + + /** + * Returns a fancy formatted time lapse code + * + * @param int $referencedate Timestamp of the reference date/time + * @param int $timepointer Timestamp of the current date/time + * @param string $measureby Time unit. One of s, m, h, d, or y. + * @param bool $autotext Add "ago" / "from now" suffix? + * + * @return string + */ + protected function timeago($referencedate = 0, $timepointer = '', $measureby = '', $autotext = true) + { + if ($timepointer == '') + { + $timepointer = time(); + } + + // Raw time difference + $Raw = $timepointer - $referencedate; + $Clean = abs($Raw); + + $calcNum = array( + array('s', 60), + array('m', 60 * 60), + array('h', 60 * 60 * 60), + array('d', 60 * 60 * 60 * 24), + array('y', 60 * 60 * 60 * 24 * 365) + ); + + $calc = array( + 's' => array(1, 'second'), + 'm' => array(60, 'minute'), + 'h' => array(60 * 60, 'hour'), + 'd' => array(60 * 60 * 24, 'day'), + 'y' => array(60 * 60 * 24 * 365, 'year') + ); + + if ($measureby == '') + { + $usemeasure = 's'; + + for ($i = 0; $i < count($calcNum); $i++) + { + if ($Clean <= $calcNum[$i][1]) + { + $usemeasure = $calcNum[$i][0]; + $i = count($calcNum); + } + } + } + else + { + $usemeasure = $measureby; + } + + $datedifference = floor($Clean / $calc[$usemeasure][0]); + + if ($autotext == true && ($timepointer == time())) + { + if ($Raw < 0) + { + $prospect = ' from now'; + } + else + { + $prospect = ' ago'; + } + } + else + { + $prospect = ''; + } + + if ($referencedate != 0) + { + if ($datedifference == 1) + { + return $datedifference . ' ' . $calc[$usemeasure][1] . ' ' . $prospect; + } + else + { + return $datedifference . ' ' . $calc[$usemeasure][1] . 's ' . $prospect; + } + } + else + { + return 'No input time referenced.'; + } + } + + /** + * Formats a number of bytes in human readable format + * + * @param int $size The size in bytes to format, e.g. 8254862 + * + * @return string The human-readable representation of the byte size, e.g. "7.87 Mb" + */ + protected function formatByteSize($size) + { + $unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB'); + return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i]; + } + + /** + * Returns the current memory usage, formatted + * + * @return string + */ + protected function memUsage() + { + if (function_exists('memory_get_usage')) + { + $size = memory_get_usage(); + return $this->formatByteSize($size); + } + else + { + return "(unknown)"; + } + } + + /** + * Returns the peak memory usage, formatted + * + * @return string + */ + protected function peakMemUsage() + { + if (function_exists('memory_get_peak_usage')) + { + $size = memory_get_peak_usage(); + return $this->formatByteSize($size); + } + else + { + return "(unknown)"; + } + } + + /** + * Parses POSIX command line options and sets the self::$cliOptions associative array. Each array item contains + * a single dimensional array of values. Arguments without a dash are silently ignored. + * + * This works much better than JInputCli since it allows you to use all POSIX-valid ways of defining CLI parameters. + * + * @return void + */ + protected function parseOptions() + { + global $argc, $argv; + + // Workaround for PHP-CGI + if (!isset($argc) && !isset($argv)) + { + $query = ""; + + if (!empty($_GET)) + { + foreach ($_GET as $k => $v) + { + $query .= " $k"; + + if ($v != "") + { + $query .= "=$v"; + } + } + } + + $query = ltrim($query); + $argv = explode(' ', $query); + $argc = count($argv); + } + + $currentName = ""; + $options = array(); + + for ($i = 1; $i < $argc; $i++) + { + $argument = $argv[ $i ]; + + $value = $argument; + + if (strpos($argument, "-") === 0) + { + $argument = ltrim($argument, '-'); + + $name = $argument; + $value = null; + + if (strstr($argument, '=')) + { + list($name, $value) = explode('=', $argument, 2); + } + + $currentName = $name; + + if (!isset($options[ $currentName ]) || ($options[ $currentName ] == null)) + { + $options[ $currentName ] = array(); + } + } + + if ((!is_null($value)) && (!is_null($currentName))) + { + $key = null; + + if (strstr($value, '=')) + { + $parts = explode('=', $value, 2); + $key = $parts[0]; + $value = $parts[1]; + } + + $values = $options[ $currentName ]; + + if (is_null($values)) + { + $values = array(); + } + + if (is_null($key)) + { + array_push($values, $value); + } + else + { + $values[ $key ] = $value; + } + + $options[ $currentName ] = $values; + } + } + + self::$cliOptions = $options; + } + + /** + * Returns the value of a command line option + * + * @param string $key The full name of the option, e.g. "foobar" + * @param mixed $default The default value to return + * @param string $type Joomla! filter type, e.g. cmd, int, bool and so on. + * + * @return mixed The value of the option + */ + protected function getOption($key, $default = null, $type = 'raw') + { + // If the key doesn't exist set it to the default value + if (!array_key_exists($key, self::$cliOptions)) + { + self::$cliOptions[$key] = is_array($default) ? $default : array($default); + } + + $type = strtolower($type); + + if ($type == 'array') + { + return self::$cliOptions[$key]; + } + + $value = null; + + if (!empty(self::$cliOptions[$key])) + { + $value = self::$cliOptions[$key][0]; + } + + return $this->filterVariable($value, $type); + } + + protected function filterVariable($var, $type = 'cmd') + { + // If JFilterInput exists we shall use it + if (is_object($this->filter)) + { + return $this->filter->clean($var, $type); + } + + // Otherwise we'll have to do it THE REALLY HARD WAY, using reflection to call JRequest::_cleanVar. + $reflector = new ReflectionClass('JRequest'); + $refMethod = $reflector->getMethod('_cleanVar'); + $refMethod->setAccessible(true); + return $refMethod->invoke(null, $var, 0, $type); + } +} + +/** + * @param Throwable $ex The Exception / Error being handled + */ +function akeeba_exception_handler($ex) +{ + echo "\n\n"; + echo "********** ERROR! **********\n\n"; + echo $ex->getMessage(); + echo "\n\nTechnical information:\n\n"; + echo "Code: " . $ex->getCode() . "\n"; + echo "File: " . $ex->getFile() . "\n"; + echo "Line: " . $ex->getLine() . "\n"; + echo "\nStack Trace:\n\n" . $ex->getTraceAsString(); + die("\n\n"); +} + +/** + * Timeout handler + * + * This function is registered as a shutdown script. If a catchable timeout occurs it will detect it and print a helpful + * error message instead of just dying cold. + * + * @return void + */ +function akeeba_timeout_handler() +{ + $connection_status = connection_status(); + + if ($connection_status == 0) + { + // Normal script termination, do not report an error. + return; + } + + echo "\n\n"; + echo "********** ERROR! **********\n\n"; + + if ($connection_status == 1) + { + echo <<< END +The process was aborted on user's request. + +This usually means that you pressed CTRL-C to terminate the script (if you're +running it from a terminal / SSH session), or that your host's CRON daemon +aborted the execution of this script. + +If you are running this script through a CRON job and saw this message, please +contact your host and request an increase in the timeout limit for CRON jobs. +Moreover you need to ask them to increase the max_execution_time in the +php.ini file or, even better, set it to 0. +END; + } + else + { + echo <<< END +This script has timed out. As a result, the process has FAILED to complete. + +Your host applies a maximum execution time for CRON jobs which is too low for +this script to work properly. Please contact your host and request an increase +in the timeout limit for CRON jobs. Moreover you need to ask them to increase +the max_execution_time in the php.ini file or, even better, set it to 0. +END; + + + if (!function_exists('php_ini_loaded_file')) + { + echo "\n\n"; + + return; + } + + $ini_location = php_ini_loaded_file(); + + echo << +Order deny,allow +Deny from all + + + + Require all denied + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie-joomla.jpa b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie-joomla.jpa new file mode 100644 index 00000000..1f2aacd3 Binary files /dev/null and b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie-joomla.jpa differ diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.ini b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.ini new file mode 100644 index 00000000..6cbd8dfd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.ini @@ -0,0 +1,10 @@ +[angie] +name="ANGIE for Joomla! Sites" +package="angie.jpa,angie-joomla.jpa" +language="language-angie.jpa,language-joomla.jpa" +installerroot="installation" +sqlroot="installation/sql" +databasesini=1 +readme=1 +extrainfo=1 +password=1 diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.jpa b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.jpa new file mode 100644 index 00000000..a9f5aa18 Binary files /dev/null and b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/angie.jpa differ diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/index.html b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.transfer.php b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.transfer.php new file mode 100644 index 00000000..af4582d6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.transfer.php @@ -0,0 +1,268 @@ + disk_free_space(dirname(__FILE__)), + 'phpVersion' => PHP_VERSION, + 'phpSAPI' => PHP_SAPI, + 'phpOS' => PHP_OS, + 'osVersion' => php_uname('s'), + 'server' => $server, + 'canWrite' => $this->canWriteToFiles(), + 'canWriteTemp' => $this->canWriteToFiles('kicktemp'), + 'maxExecTime' => $maxExecTime, + 'memLimit' => $this->memoryToBytes($memLimit), + 'maxPost' => $this->memoryToBytes($maxPostSize), + 'maxUpload' => $this->memoryToBytes($uploadMaxSize), + 'baseDir' => $baseDir, + 'disabledFuncs' => $disabled, + ); + + return $infoArray; + } + + public function uploadFile($params) + { + // Get the parameters describing the upload + $file = isset($_GET['file']) ? $_GET['file'] : ''; + $directory = isset($_GET['directory']) ? $_GET['directory'] : ''; + $frag = isset($_GET['frag']) ? $_GET['frag'] : 0; + $fragSize = isset($_GET['fragSize']) ? $_GET['fragSize'] : 1048576; + $data = isset($_POST['data']) ? $_POST['data'] : ''; + $dataFile = isset($_GET['dataFile']) ? $_GET['dataFile'] : ''; + + // We need a file + if (empty($file)) + { + return array( + 'status' => false, + 'message' => 'You have not specified a file' + ); + } + + // Let's make sure the remote end is not trying to do something nasty + $file = basename($file); + $pos = strrpos($file, '.'); + + if ($pos === false) + { + return array( + 'status' => false, + 'message' => 'Invalid file name specified' + ); + } + + $extension = substr($file, $pos + 1); + + if (empty($extension)) + { + return array( + 'status' => false, + 'message' => 'Invalid file name specified' + ); + } + + if (!preg_match('(jpa|zip|jps|j[\d]{2,}|z[\d]{2,})', $extension)) + { + return array( + 'status' => false, + 'message' => 'Invalid file name specified' + ); + } + + // We only allow very specific directories + $directory = trim($directory, '/'); + + if (!in_array($directory, array('', 'kicktemp'))) + { + return array( + 'status' => false, + // Yes, the message is intentionally vague + 'message' => 'Invalid file name specified' + ); + } + + // If a data file was given, read it to memory + if (empty($data) && !empty($dataFile)) + { + // Do not remove the basename(). It makes sure we won't try to read a file outside our directory. + $data = @file_get_contents(__DIR__ . '/' . basename($dataFile)); + } + + // We need some data to write, yes? + if (empty($data)) + { + return array( + 'status' => false, + 'message' => 'No data specified' + ); + } + + if (!empty($directory)) + { + $directory = '/' . $directory; + } + + $filename = __DIR__ . $directory . '/' . $file; + + // Open the file for writing or append + $mode = ($frag == 0) ? 'w' : 'a'; + $fp = @fopen($filename, $mode); + + if ($fp === false) + { + $modeHuman = ($mode == 'w') ? 'write' : 'append'; + + return array( + 'status' => false, + 'message' => "Cannot open $file for $modeHuman" + ); + } + + // Seek to the correct offset + $offset = $frag * $fragSize; + @fseek($fp, $offset); + + // Write to the file + $written = @fwrite($fp, $data); + + @fclose($fp); + + if (!$written || ($written != strlen($data))) + { + return array( + 'status' => false, + 'message' => "Cannot write to $file" + ); + } + + return array( + 'status' => true, + 'message' => '' + ); + } + + /** + * Can I write to arbitrary files in the Kickstart directory? + * + * @return bool + */ + private function canWriteToFiles($directory = '') + { + // Try to create a temporary file + $directory = dirname(__FILE__) . '/' . $directory; + $directory = rtrim($directory, '/'); + + $testFilename = tempnam($directory, 'kst'); + + // Failed completely? + if ($testFilename === false) + { + return false; + } + + // File created in another directory? + if (dirname($testFilename) != $directory) + { + @unlink($testFilename); + + return false; + } + + @unlink($testFilename); + + return true; + } + + /** + * 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 + */ + private function memoryToBytes($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; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.txt b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.txt new file mode 100644 index 00000000..32d83e2c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/kickstart.txt @@ -0,0 +1,12805 @@ +. + */ + +define('KICKSTART', 1); +define('VERSION', 'rev23305F8'); +define('KICKSTARTPRO', '0'); +// Uncomment the following line to enable Kickstart's debug mode +//define('KSDEBUG', 1); + +// Used during development +if (!defined('KSDEBUG') && isset($_SERVER) && isset($_SERVER['HTTP_HOST']) && (strpos($_SERVER['HTTP_HOST'], 'local.web') !== false)) +{ + define('KSDEBUG', 1); +} + +define('KSWINDOWS', substr(PHP_OS, 0, 3) == 'WIN'); + +if (!defined('KSROOTDIR')) +{ + define('KSROOTDIR', dirname(__FILE__)); +} + +if (defined('KSDEBUG')) +{ + ini_set('error_log', KSROOTDIR . '/kickstart_error_log'); + if (file_exists(KSROOTDIR . '/kickstart_error_log')) + { + @unlink(KSROOTDIR . '/kickstart_error_log'); + } + error_reporting(E_ALL | E_STRICT); +} +else +{ + @error_reporting(E_NONE); +} + +// ========================================================================================== +// IIS missing REQUEST_URI workaround +// ========================================================================================== + +/* + * Based REQUEST_URI for IIS Servers 1.0 by NeoSmart Technologies + * The proper method to solve IIS problems is to take a look at this: + * http://neosmart.net/dl.php?id=7 + */ + +//This file should be located in the same directory as php.exe or php5isapi.dll + +if (!isset($_SERVER['REQUEST_URI'])) +{ + if (isset($_SERVER['HTTP_REQUEST_URI'])) + { + $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_REQUEST_URI']; + //Good to go! + } + else + { + //Someone didn't follow the instructions! + if (isset($_SERVER['SCRIPT_NAME'])) + { + $_SERVER['HTTP_REQUEST_URI'] = $_SERVER['SCRIPT_NAME']; + } + else + { + $_SERVER['HTTP_REQUEST_URI'] = $_SERVER['PHP_SELF']; + } + if ($_SERVER['QUERY_STRING']) + { + $_SERVER['HTTP_REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING']; + } + //WARNING: This is a workaround! + //For guaranteed compatibility, HTTP_REQUEST_URI *MUST* be defined! + //See product documentation for instructions! + $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_REQUEST_URI']; + } +} + +// Define the cacert.pem location, if it exists +$cacertpem = KSROOTDIR . '/cacert.pem'; +if (is_file($cacertpem)) +{ + if (is_readable($cacertpem)) + { + define('AKEEBA_CACERT_PEM', $cacertpem); + } +} +unset($cacertpem); + +// Loads other PHP files containing extra Kickstart features +$dh = @opendir(KSROOTDIR); +if ($dh === false) +{ + return; +} +while ($filename = readdir($dh)) +{ + if (in_array($filename, array('.', '..'))) + { + continue; + } + if (!is_file($filename)) + { + continue; + } + if (substr($filename, 0, 10) != 'kickstart.') + { + continue; + } + if (substr($filename, -4) != '.php') + { + continue; + } + if ($filename == 'kickstart.php') + { + continue; + } + if (function_exists('opcache_invalidate')) + { + opcache_invalidate($filename); + } + if (function_exists('apc_compile_file')) + { + apc_compile_file($filename); + } + if (function_exists('wincache_refresh_if_changed')) + { + wincache_refresh_if_changed(array($filename)); + } + if (function_exists('xcache_asm')) + { + xcache_asm($filename); + } + include_once $filename; +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +define('_AKEEBA_RESTORATION', 1); +defined('DS') or define('DS', DIRECTORY_SEPARATOR); + +// Unarchiver run states +define('AK_STATE_NOFILE', 0); // File header not read yet +define('AK_STATE_HEADER', 1); // File header read; ready to process data +define('AK_STATE_DATA', 2); // Processing file data +define('AK_STATE_DATAREAD', 3); // Finished processing file data; ready to post-process +define('AK_STATE_POSTPROC', 4); // Post-processing +define('AK_STATE_DONE', 5); // Done with post-processing + +/* Windows system detection */ +if (!defined('_AKEEBA_IS_WINDOWS')) +{ + if (function_exists('php_uname')) + { + define('_AKEEBA_IS_WINDOWS', stristr(php_uname(), 'windows')); + } + else + { + define('_AKEEBA_IS_WINDOWS', DIRECTORY_SEPARATOR == '\\'); + } +} + +// Get the file's root +if (!defined('KSROOTDIR')) +{ + define('KSROOTDIR', dirname(__FILE__)); +} +if (!defined('KSLANGDIR')) +{ + define('KSLANGDIR', KSROOTDIR); +} + +// Make sure the locale is correct for basename() to work +if (function_exists('setlocale')) +{ + @setlocale(LC_ALL, 'en_US.UTF8'); +} + +// fnmatch not available on non-POSIX systems +// Thanks to soywiz@php.net for this usefull alternative function [http://gr2.php.net/fnmatch] +if (!function_exists('fnmatch')) +{ + function fnmatch($pattern, $string) + { + return @preg_match( + '/^' . strtr(addcslashes($pattern, '/\\.+^$(){}=!<>|'), + array('*' => '.*', '?' => '.?')) . '$/i', $string + ); + } +} + +// Unicode-safe binary data length function +if (!function_exists('akstringlen')) +{ + if (function_exists('mb_strlen')) + { + function akstringlen($string) + { + return mb_strlen($string, '8bit'); + } + } + else + { + function akstringlen($string) + { + return strlen($string); + } + } +} + +if (!function_exists('aksubstr')) +{ + if (function_exists('mb_strlen')) + { + function aksubstr($string, $start, $length = null) + { + return mb_substr($string, $start, $length, '8bit'); + } + } + else + { + function aksubstr($string, $start, $length = null) + { + return substr($string, $start, $length); + } + } +} + +/** + * Gets a query parameter from GET or POST data + * + * @param $key + * @param $default + */ +function getQueryParam($key, $default = null) +{ + $value = $default; + + if (array_key_exists($key, $_REQUEST)) + { + $value = $_REQUEST[$key]; + } + + if (get_magic_quotes_gpc() && !is_null($value)) + { + $value = stripslashes($value); + } + + return $value; +} + +// Debugging function +function debugMsg($msg) +{ + if (!defined('KSDEBUG')) + { + return; + } + + $fp = fopen('debug.txt', 'at'); + + fwrite($fp, $msg . "\n"); + fclose($fp); + + // Echo to stdout if KSDEBUGCLI is defined + if (defined('KSDEBUGCLI')) + { + echo $msg . "\n"; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * 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 AKAbstractObject +{ + /** @var array The queue size of the $_errors array. Set to 0 for infinite size. */ + protected $_errors_queue_size = 0; + /** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */ + protected $_warnings_queue_size = 0; + /** @var array An array of errors */ + private $_errors = array(); + /** @var array An array of warnings */ + private $_warnings = array(); + + /** + * Public constructor, makes sure we are instantiated only by the factory class + */ + public function __construct() + { + /* + // Assisted Singleton pattern + if(function_exists('debug_backtrace')) + { + $caller=debug_backtrace(); + if( + ($caller[1]['class'] != 'AKFactory') && + ($caller[2]['class'] != 'AKFactory') && + ($caller[3]['class'] != 'AKFactory') && + ($caller[4]['class'] != 'AKFactory') + ) { + var_dump(debug_backtrace()); + trigger_error("You can't create direct descendants of ".__CLASS__, E_USER_ERROR); + } + } + */ + } + + /** + * 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); + } + + /** + * 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 + */ + private function getItemFromArray($array, $i = null) + { + // Find the item + if ($i === null) + { + // Default, return the last item + $item = end($array); + } + else if (!array_key_exists($i, $array)) + { + // If $i has been specified but does not exist, return false + return false; + } + else + { + $item = $array[$i]; + } + + return $item; + } + + /** + * Return all errors, if any + * + * @return array Array of error messages + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Resets all error messages + */ + 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; + } + + /** + * Resets all warning messages + */ + public function resetWarnings() + { + $this->_warnings = array(); + } + + /** + * Propagates errors and warnings to a foreign object. The foreign object SHOULD + * implement the setError() and/or setWarning() methods but DOESN'T HAVE TO be of + * AKAbstractObject type. For example, this can even be used to propagate to a + * JObject instance in Joomla!. Propagated items will be removed from ourselves. + * + * @param object $object The object to propagate errors and warnings to. + */ + 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 object $object The object to propagate errors and warnings from + */ + public function propagateFromObject(&$object) + { + if (method_exists($object, 'getErrors')) + { + $errors = $object->getErrors(); + if (!empty($errors)) + { + foreach ($errors as $error) + { + $this->setError($error); + } + } + 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); + } + } + if (method_exists($object, 'resetWarnings')) + { + $object->resetWarnings(); + } + } + } + + /** + * Add an error message + * + * @param string $error Error message + */ + public function setError($error) + { + if ($this->_errors_queue_size > 0) + { + if (count($this->_errors) >= $this->_errors_queue_size) + { + array_shift($this->_errors); + } + } + + $this->_errors[] = $error; + } + + /** + * Add an error message + * + * @param string $error Error message + */ + public function setWarning($warning) + { + if ($this->_warnings_queue_size > 0) + { + if (count($this->_warnings) >= $this->_warnings_queue_size) + { + array_shift($this->_warnings); + } + } + + $this->_warnings[] = $warning; + } + + /** + * 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. + */ + 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. + */ + protected function setWarningsQueueSize($newSize = 0) + { + $this->_warnings_queue_size = (int) $newSize; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The superclass of all Akeeba Kickstart 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 AKAbstractPart extends AKAbstractObject +{ + /** + * 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 array An array of observers */ + protected $observers = array(); + /** @var int Last reported warnings's position in array */ + private $warnings_pointer = -1; + + /** + * 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. + * + * @return array A Response Array + */ + final public function tick() + { + // 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; + } + + // Send a Return Table back to the caller + $out = $this->_makeReturnTable(); + + return $out; + } + + /** + * 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. + */ + final public function getState() + { + if ($this->getError()) + { + return "error"; + } + + if (!($this->isPrepared)) + { + return "init"; + } + + if (!($this->isFinished) && !($this->isRunning) && !($this->hasRun) && ($this->isPrepared)) + { + return "prepared"; + } + + if (!($this->isFinished) && $this->isRunning && !($this->hasRun)) + { + return "running"; + } + + if (!($this->isFinished) && !($this->isRunning) && $this->hasRun) + { + return "postrun"; + } + + if ($this->isFinished) + { + return "finished"; + } + } + + /** + * Runs the preparation for this part. Should set _isPrepared + * to true + */ + abstract protected function _prepare(); + + /** + * 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. + */ + abstract protected function _run(); + + /** + * Runs the finalisation process for this part. Should set + * _isFinished to true. + */ + abstract protected function _finalize(); + + /** + * Constructs a Response Array based on the engine part's state. + * + * @return array The Response Array for the current state + */ + final 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; + } + + /** + * Returns a copy of the class's status array + * + * @return 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. + */ + final public function setup($parametersArray) + { + if ($this->isPrepared) + { + $this->setState('error', "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']; + } + } + } + + /** + * 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 + */ + protected function setState($state = 'init', $errorMessage = 'Invalid setState argument') + { + switch ($state) + { + case 'init': + $this->isPrepared = false; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'prepared': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'running': + $this->isPrepared = true; + $this->isRunning = true; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'postrun': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = true; + break; + + case 'finished': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = true; + $this->hasRun = false; + break; + + case 'error': + default: + $this->setError($errorMessage); + break; + } + } + + final public function getDomain() + { + return $this->active_domain; + } + + final public function getStep() + { + return $this->active_step; + } + + final public function getSubstep() + { + return $this->active_substep; + } + + /** + * Attaches an observer object + * + * @param AKAbstractPartObserver $obs + */ + function attach(AKAbstractPartObserver $obs) + { + $this->observers["$obs"] = $obs; + } + + /** + * Detaches an observer object + * + * @param AKAbstractPartObserver $obs + */ + function detach(AKAbstractPartObserver $obs) + { + delete($this->observers["$obs"]); + } + + /** + * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately, + * in fear of timing out. + */ + protected function setBreakFlag() + { + AKFactory::set('volatile.breakflag', true); + } + + final protected function setDomain($new_domain) + { + $this->active_domain = $new_domain; + } + + final protected function setStep($new_step) + { + $this->active_step = $new_step; + } + + final protected function setSubstep($new_substep) + { + $this->active_substep = $new_substep; + } + + /** + * Notifies observers each time something interesting happened to the part + * + * @param mixed $message The event object + */ + protected function notify($message) + { + foreach ($this->observers as $obs) + { + $obs->update($this, $message); + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The base class of unarchiver classes + */ +abstract class AKAbstractUnarchiver extends AKAbstractPart +{ + /** @var array List of the names of all archive parts */ + public $archiveList = array(); + /** @var int The total size of all archive parts */ + public $totalSize = array(); + /** @var array Which files to rename */ + public $renameFiles = array(); + /** @var array Which directories to rename */ + public $renameDirs = array(); + /** @var array Which files to skip */ + public $skipFiles = array(); + /** @var string Archive filename */ + protected $filename = null; + /** @var integer Current archive part number */ + protected $currentPartNumber = -1; + /** @var integer The offset inside the current part */ + protected $currentPartOffset = 0; + /** @var bool Should I restore permissions? */ + protected $flagRestorePermissions = false; + /** @var AKAbstractPostproc Post processing class */ + protected $postProcEngine = null; + /** @var string Absolute path to prepend to extracted files */ + protected $addPath = ''; + /** @var string Absolute path to remove from extracted files */ + protected $removePath = ''; + /** @var integer Chunk size for processing */ + protected $chunkSize = 524288; + + /** @var resource File pointer to the current archive part file */ + protected $fp = null; + + /** @var int Run state when processing the current archive file */ + protected $runState = null; + + /** @var stdClass File header data, as read by the readFileHeader() method */ + protected $fileHeader = null; + + /** @var int How much of the uncompressed data we've read so far */ + protected $dataReadLength = 0; + + /** @var array Unwriteable files in these directories are always ignored and do not cause errors when not extracted */ + protected $ignoreDirectories = array(); + + /** + * Public constructor + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Wakeup function, called whenever the class is unserialized + */ + public function __wakeup() + { + if ($this->currentPartNumber >= 0) + { + $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); + if ((is_resource($this->fp)) && ($this->currentPartOffset > 0)) + { + @fseek($this->fp, $this->currentPartOffset); + } + } + } + + /** + * Sleep function, called whenever the class is serialized + */ + public function shutdown() + { + if (is_resource($this->fp)) + { + $this->currentPartOffset = @ftell($this->fp); + @fclose($this->fp); + } + } + + /** + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory + */ + public function isIgnoredDirectory($shortFilename) + { + // return false; + + if (substr($shortFilename, -1) == '/') + { + $check = rtrim($shortFilename, '/'); + } + else + { + $check = dirname($shortFilename); + } + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Implements the abstract _prepare() method + */ + final protected function _prepare() + { + parent::__construct(); + + if (count($this->_parametersArray) > 0) + { + foreach ($this->_parametersArray as $key => $value) + { + switch ($key) + { + // Archive's absolute filename + case 'filename': + $this->filename = $value; + + // Sanity check + if (!empty($value)) + { + $value = strtolower($value); + + if (strlen($value) > 6) + { + if ( + (substr($value, 0, 7) == 'http://') + || (substr($value, 0, 8) == 'https://') + || (substr($value, 0, 6) == 'ftp://') + || (substr($value, 0, 7) == 'ssh2://') + || (substr($value, 0, 6) == 'ssl://') + ) + { + $this->setState('error', 'Invalid archive location'); + } + } + } + + + break; + + // Should I restore permissions? + case 'restore_permissions': + $this->flagRestorePermissions = $value; + break; + + // Should I use FTP? + case 'post_proc': + $this->postProcEngine = AKFactory::getpostProc($value); + break; + + // Path to add in the beginning + case 'add_path': + $this->addPath = $value; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + if (!empty($this->addPath)) + { + $this->addPath .= '/'; + } + break; + + // Path to remove from the beginning + case 'remove_path': + $this->removePath = $value; + $this->removePath = str_replace('\\', '/', $this->removePath); + $this->removePath = rtrim($this->removePath, '/'); + if (!empty($this->removePath)) + { + $this->removePath .= '/'; + } + break; + + // Which files to rename (hash array) + case 'rename_files': + $this->renameFiles = $value; + break; + + // Which files to rename (hash array) + case 'rename_dirs': + $this->renameDirs = $value; + break; + + // Which files to skip (indexed array) + case 'skip_files': + $this->skipFiles = $value; + break; + + // Which directories to ignore when we can't write files in them (indexed array) + case 'ignoredirectories': + $this->ignoreDirectories = $value; + break; + } + } + } + + $this->scanArchives(); + + $this->readArchiveHeader(); + $errMessage = $this->getError(); + if (!empty($errMessage)) + { + $this->setState('error', $errMessage); + } + else + { + $this->runState = AK_STATE_NOFILE; + $this->setState('prepared'); + } + } + + /** + * Scans for archive parts + */ + private function scanArchives() + { + if (defined('KSDEBUG')) + { + @unlink('debug.txt'); + } + debugMsg('Preparing to scan archives'); + + $privateArchiveList = array(); + + // Get the components of the archive filename + $dirname = dirname($this->filename); + $base_extension = $this->getBaseExtension(); + $basename = basename($this->filename, $base_extension); + $this->totalSize = 0; + + // Scan for multiple parts until we don't find any more of them + $count = 0; + $found = true; + $this->archiveList = array(); + while ($found) + { + ++$count; + $extension = substr($base_extension, 0, 2) . sprintf('%02d', $count); + $filename = $dirname . DIRECTORY_SEPARATOR . $basename . $extension; + $found = file_exists($filename); + if ($found) + { + debugMsg('- Found archive ' . $filename); + // Add yet another part, with a numeric-appended filename + $this->archiveList[] = $filename; + + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + else + { + debugMsg('- Found archive ' . $this->filename); + // Add the last part, with the regular extension + $this->archiveList[] = $this->filename; + + $filename = $this->filename; + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + } + debugMsg('Total archive parts: ' . $count); + + $this->currentPartNumber = -1; + $this->currentPartOffset = 0; + $this->runState = AK_STATE_NOFILE; + + // Send start of file notification + $message = new stdClass; + $message->type = 'totalsize'; + $message->content = new stdClass; + $message->content->totalsize = $this->totalSize; + $message->content->filelist = $privateArchiveList; + $this->notify($message); + } + + /** + * Returns the base extension of the file, e.g. '.jpa' + * + * @return string + */ + private function getBaseExtension() + { + static $baseextension; + + if (empty($baseextension)) + { + $basename = basename($this->filename); + $lastdot = strrpos($basename, '.'); + $baseextension = substr($basename, $lastdot); + } + + return $baseextension; + } + + /** + * Concrete classes are supposed to use this method in order to read the archive's header and + * prepare themselves to the point of being ready to extract the first file. + */ + protected abstract function readArchiveHeader(); + + protected function _run() + { + if ($this->getState() == 'postrun') + { + return; + } + + $this->setState('running'); + + $timer = AKFactory::getTimer(); + + $status = true; + while ($status && ($timer->getTimeLeft() > 0)) + { + switch ($this->runState) + { + case AK_STATE_NOFILE: + debugMsg(__CLASS__ . '::_run() - Reading file header'); + $status = $this->readFileHeader(); + if ($status) + { + // Send start of file notification + $message = new stdClass; + $message->type = 'startfile'; + $message->content = new stdClass; + $message->content->realfile = $this->fileHeader->file; + $message->content->file = $this->fileHeader->file; + $message->content->uncompressed = $this->fileHeader->uncompressed; + + if (array_key_exists('realfile', get_object_vars($this->fileHeader))) + { + $message->content->realfile = $this->fileHeader->realFile; + } + + if (array_key_exists('compressed', get_object_vars($this->fileHeader))) + { + $message->content->compressed = $this->fileHeader->compressed; + } + else + { + $message->content->compressed = 0; + } + + debugMsg(__CLASS__ . '::_run() - Preparing to extract ' . $message->content->realfile); + + $this->notify($message); + } + else + { + debugMsg(__CLASS__ . '::_run() - Could not read file header'); + } + break; + + case AK_STATE_HEADER: + case AK_STATE_DATA: + debugMsg(__CLASS__ . '::_run() - Processing file data'); + $status = $this->processFileData(); + break; + + case AK_STATE_DATAREAD: + case AK_STATE_POSTPROC: + debugMsg(__CLASS__ . '::_run() - Calling post-processing class'); + $this->postProcEngine->timestamp = $this->fileHeader->timestamp; + $status = $this->postProcEngine->process(); + $this->propagateFromObject($this->postProcEngine); + $this->runState = AK_STATE_DONE; + break; + + case AK_STATE_DONE: + default: + if ($status) + { + debugMsg(__CLASS__ . '::_run() - Finished extracting file'); + // Send end of file notification + $message = new stdClass; + $message->type = 'endfile'; + $message->content = new stdClass; + if (array_key_exists('realfile', get_object_vars($this->fileHeader))) + { + $message->content->realfile = $this->fileHeader->realFile; + } + else + { + $message->content->realfile = $this->fileHeader->file; + } + $message->content->file = $this->fileHeader->file; + if (array_key_exists('compressed', get_object_vars($this->fileHeader))) + { + $message->content->compressed = $this->fileHeader->compressed; + } + else + { + $message->content->compressed = 0; + } + $message->content->uncompressed = $this->fileHeader->uncompressed; + $this->notify($message); + } + $this->runState = AK_STATE_NOFILE; + continue; + } + } + + $error = $this->getError(); + if (!$status && ($this->runState == AK_STATE_NOFILE) && empty($error)) + { + debugMsg(__CLASS__ . '::_run() - Just finished'); + // We just finished + $this->setState('postrun'); + } + elseif (!empty($error)) + { + debugMsg(__CLASS__ . '::_run() - Halted with an error:'); + debugMsg($error); + $this->setState('error', $error); + } + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected abstract function readFileHeader(); + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected abstract function processFileData(); + + protected function _finalize() + { + // Nothing to do + $this->setState('finished'); + } + + /** + * Opens the next part file for reading + */ + protected function nextFile() + { + debugMsg('Current part is ' . $this->currentPartNumber . '; opening the next part'); + ++$this->currentPartNumber; + + if ($this->currentPartNumber > (count($this->archiveList) - 1)) + { + $this->setState('postrun'); + + return false; + } + else + { + if (is_resource($this->fp)) + { + @fclose($this->fp); + } + debugMsg('Opening file ' . $this->archiveList[$this->currentPartNumber]); + $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); + if ($this->fp === false) + { + debugMsg('Could not open file - crash imminent'); + $this->setError(AKText::sprintf('ERR_COULD_NOT_OPEN_ARCHIVE_PART', $this->archiveList[$this->currentPartNumber])); + } + fseek($this->fp, 0); + $this->currentPartOffset = 0; + + return true; + } + } + + /** + * Returns true if we have reached the end of file + * + * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of + * the archive set + * + * @return bool True if we have reached End Of File + */ + protected function isEOF($local = false) + { + $eof = @feof($this->fp); + + if (!$eof) + { + // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why + // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report + // true. Incredible! :( + $position = @ftell($this->fp); + $filesize = @filesize($this->archiveList[$this->currentPartNumber]); + if ($filesize <= 0) + { + // 2Gb or more files on a 32 bit version of PHP tend to get screwed up. Meh. + $eof = false; + } + elseif ($position >= $filesize) + { + $eof = true; + } + } + + if ($local) + { + return $eof; + } + else + { + return $eof && ($this->currentPartNumber >= (count($this->archiveList) - 1)); + } + } + + /** + * Tries to make a directory user-writable so that we can write a file to it + * + * @param $path string A path to a file + */ + protected function setCorrectPermissions($path) + { + static $rootDir = null; + + if (is_null($rootDir)) + { + $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir', ''), '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + if ($directory != $rootDir) + { + // Is this an unwritable directory? + if (!is_writeable($directory)) + { + $this->postProcEngine->chmod($directory, 0755); + } + } + $this->postProcEngine->chmod($path, 0644); + } + + /** + * Reads data from the archive and notifies the observer with the 'reading' message + * + * @param $fp + * @param $length + */ + protected function fread($fp, $length = null) + { + if (is_numeric($length)) + { + if ($length > 0) + { + $data = fread($fp, $length); + } + else + { + $data = fread($fp, PHP_INT_MAX); + } + } + else + { + $data = fread($fp, PHP_INT_MAX); + } + if ($data === false) + { + $data = ''; + } + + // Send start of file notification + $message = new stdClass; + $message->type = 'reading'; + $message->content = new stdClass; + $message->content->length = strlen($data); + $this->notify($message); + + return $data; + } + + /** + * Removes the configured $removePath from the path $path + * + * @param string $path The path to reduce + * + * @return string The reduced path + */ + protected function removePath($path) + { + if (empty($this->removePath)) + { + return $path; + } + + if (strpos($path, $this->removePath) === 0) + { + $path = substr($path, strlen($this->removePath)); + $path = ltrim($path, '/\\'); + } + + return $path; + } + + /** + * Am I supposed to skip the extraction of the current file? This depends on + * + * @return bool + */ + protected function mustSkip() + { + static $isDryRun = null; + + // List of files (and patterns) to extract + static $extractList = null; + + // Internal cache of the last file we checked and whether it must be skipped + static $lastFileName = ''; + static $mustSkip = false; + + // Make sure the dry run flag is, indeed, populated + if (is_null($isDryRun)) + { + $isDryRun = AKFactory::get('kickstart.setup.dryrun', '0'); + } + + // If it's a Kickstart dry run we have to skip the extraction of the file + if ($isDryRun) + { + return true; + } + + // Make sure I have a list of files and patterns to extract + if (is_null($extractList)) + { + $extractList = $this->getExtractList(); + } + + // No list of files to extract is given; we must extract everything. + if (empty($extractList)) + { + return false; + } + + // I am asked about the same file again. Return the cached result. + if ($this->fileHeader->file == $lastFileName) + { + return $mustSkip; + } + + // Does the current file match the extract patterns or not? + $lastFileName = $this->fileHeader->file; + $lastFileName = (strpos($lastFileName, $this->addPath) === 0) ? substr($lastFileName, strlen(rtrim($this->addPath, "\\/")) + 1) : $lastFileName; + $mustSkip = !$this->matchesGlobPatterns($lastFileName, $extractList); + + return $mustSkip; + } + + /** + * Get the list of files / folders to extract. The list can contain filenames or glob patterns. + * + * @return array + */ + private function getExtractList() + { + $rawList = AKFactory::get('kickstart.setup.extract_list', ''); + + // Sometimes I could get an array, e.g. from CLI + if (is_array($rawList)) + { + $rawList = implode("\n", $rawList); + } + + // Remove any whitespace + $rawList = trim($rawList); + + if (empty($rawList)) + { + return array(); + } + + // Convert commas to newlines so we can support both ways to express lists + $rawList = str_replace(",", "\n", $rawList); + $rawList = trim($rawList); + + // Convert the list to an array and clean it + $list = explode("\n", $rawList); + $list = array_map('trim', $list); + + return array_unique($list); + } + + /** + * Tests whether the item $item matches the list of shell patterns $list. + * + * @param string $item The file name to test + * @param array $list The list of glob patterns to match + * + * @return bool + */ + private function matchesGlobPatterns($item, array $list) + { + if (empty($list)) + { + return true; + } + + foreach ($list as $pattern) + { + if (fnmatch($pattern, $item)) + { + return true; + } + } + + return false; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * File post processor engines base class + */ +abstract class AKAbstractPostproc extends AKAbstractObject +{ + /** @var int The UNIX timestamp of the file's desired modification date */ + public $timestamp = 0; + /** @var string The current (real) file path we'll have to process */ + protected $filename = null; + /** @var int The requested permissions */ + protected $perms = 0755; + /** @var string The temporary file path we gave to the unarchiver engine */ + protected $tempFilename = null; + /** @var string The temporary directory where the data will be stored */ + protected $tempDir = ''; + + /** + * Processes the current file, e.g. moves it from temp to final location by FTP + */ + abstract public function process(); + + /** + * The unarchiver tells us the path to the filename it wants to extract and we give it + * a different path instead. + * + * @param string $filename The path to the real file + * @param int $perms The permissions we need the file to have + * + * @return string The path to the temporary file + */ + abstract public function processFilename($filename, $perms = 0755); + + /** + * Recursively creates a directory if it doesn't exist + * + * @param string $dirName The directory to create + * @param int $perms The permissions to give to that directory + */ + abstract public function createDirRecursive($dirName, $perms); + + abstract public function chmod($file, $perms); + + abstract public function unlink($file); + + abstract public function rmdir($directory); + + abstract public function rename($from, $to); + + /** + * Returns the configured temporary directory + * + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify) + * + * @author Nicholas + * + */ +abstract class AKAbstractPartObserver +{ + abstract public function update($object, $message); +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Direct file writer + */ +class AKPostprocDirect extends AKAbstractPostproc +{ + public function process() + { + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if ($restorePerms) + { + @chmod($this->filename, $this->perms); + } + else + { + if (@is_file($this->filename)) + { + @chmod($this->filename, 0644); + } + else + { + @chmod($this->filename, 0755); + } + } + if ($this->timestamp > 0) + { + @touch($this->filename, $this->timestamp); + } + + return true; + } + + public function processFilename($filename, $perms = 0755) + { + $this->perms = $perms; + $this->filename = $filename; + + return $filename; + } + + public function createDirRecursive($dirName, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + if (@mkdir($dirName, 0755, true)) + { + @chmod($dirName, 0755); + + return true; + } + + $root = AKFactory::get('kickstart.setup.destdir'); + $root = rtrim(str_replace('\\', '/', $root), '/'); + $dir = rtrim(str_replace('\\', '/', $dirName), '/'); + if (strpos($dir, $root) === 0) + { + $dir = ltrim(substr($dir, strlen($root)), '/'); + $root .= '/'; + } + else + { + $root = ''; + } + + if (empty($dir)) + { + return true; + } + + $dirArray = explode('/', $dir); + $path = ''; + foreach ($dirArray as $dir) + { + $path .= $dir . '/'; + $ret = is_dir($root . $path) ? true : @mkdir($root . $path); + if (!$ret) + { + // Is this a file instead of a directory? + if (is_file($root . $path)) + { + @unlink($root . $path); + $ret = @mkdir($root . $path); + } + if (!$ret) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $path)); + + return false; + } + } + // Try to set new directory permissions to 0755 + @chmod($root . $path, $perms); + } + + return true; + } + + public function chmod($file, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + return @chmod($file, $perms); + } + + public function unlink($file) + { + return @unlink($file); + } + + public function rmdir($directory) + { + return @rmdir($directory); + } + + public function rename($from, $to) + { + return @rename($from, $to); + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + /** @var resource The FTP handle */ + private $handle = null; + + public function __construct() + { + parent::__construct(); + + $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); + $this->passive = AKFactory::get('kickstart.ftp.passive', true); + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 21); + if (trim($this->port) == '') + { + $this->port = 21; + } + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + public function connect() + { + // Connect to server, using SSL if so required + if ($this->useSSL) + { + $this->handle = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $this->handle = @ftp_connect($this->host, $this->port); + } + if ($this->handle === false) + { + $this->setError(AKText::_('WRONG_FTP_HOST')); + + return false; + } + + // Login + if (!@ftp_login($this->handle, $this->user, $this->pass)) + { + $this->setError(AKText::_('WRONG_FTP_USER')); + @ftp_close($this->handle); + + return false; + } + + // Change to initial directory + if (!@ftp_chdir($this->handle, $this->dir)) + { + $this->setError(AKText::_('WRONG_FTP_PATH1')); + @ftp_close($this->handle); + + return false; + } + + // Enable passive mode if the user requested it + if ($this->passive) + { + @ftp_pasv($this->handle, true); + } + else + { + @ftp_pasv($this->handle, false); + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + fclose($tempHandle); + + return true; + } + + private function isDirWritable($dir) + { + $fp = @fopen($dir . '/kickstart.dat', 'wb'); + if ($fp === false) + { + return false; + } + else + { + @fclose($fp); + unlink($dir . '/kickstart.dat'); + + return true; + } + } + + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if (empty($dirName)) + { + $dirName = ''; + } // 'cause the substr() above may return FALSE. + + $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir); + foreach ($alldirs as $curdir) + { + $check = $previousDir . '/' . $curdir; + if (!$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + @ftp_delete($this->handle, $check); + + if (@ftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($removePath . $check); + if (@ftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if (!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + $trustMeIKnowWhatImDoing = + 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($check, $trustMeIKnowWhatImDoing); + + return true; + } + } + } + @ftp_chmod($this->handle, $perms, $check); + } + $previousDir = $check; + } + + return true; + } + + private function is_dir($dir) + { + return @ftp_chdir($this->handle, $dir); + } + + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = @error_reporting(E_NONE); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + if (!empty($basePath)) + { + $basePath .= '/'; + } + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + if (is_dir($oldPath . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + function __wakeup() + { + $this->connect(); + } + + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $removePath = ltrim($removePath, "/"); + $remotePath = ltrim($remotePath, "/"); + $left = substr($remotePath, 0, strlen($removePath)); + if ($left == $removePath) + { + $remotePath = substr($remotePath, strlen($removePath)); + } + } + + $absoluteFSPath = dirname($this->filename); + $relativeFTPPath = trim($remotePath, '/'); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if ($ret === false) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if ($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } + } + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if ($restorePerms) + { + @ftp_chmod($this->_handle, $this->perms, $remoteName); + } + else + { + @ftp_chmod($this->_handle, 0644, $remoteName); + } + + return true; + } + + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + + public function unlink($file) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + return @ftp_delete($this->handle, $check); + } + + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + public function close() + { + @ftp_close($this->handle); + } + + public function chmod($file, $perms) + { + return @ftp_chmod($this->handle, $perms, $file); + } + + public function rmdir($directory) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($directory, 0, strlen($removePath)); + if ($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + return @ftp_rmdir($this->handle, $check); + } + + public function rename($from, $to) + { + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($from, 0, strlen($removePath)); + if ($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } + } + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + + if (!empty($removePath)) + { + $left = substr($to, 0, strlen($removePath)); + if ($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } + } + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $result = @ftp_rename($this->handle, $from, $to); + if ($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } + } + +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocSFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + + /** @var resource SFTP resource handle */ + private $handle = null; + + /** @var resource SSH2 connection resource handle */ + private $_connection = null; + + /** @var string Current remote directory, including the remote directory string */ + private $_currentdir; + + public function __construct() + { + parent::__construct(); + + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 22); + + if (trim($this->port) == '') + { + $this->port = 22; + } + + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('SFTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + public function connect() + { + $this->_connection = false; + + if (!function_exists('ssh2_connect')) + { + $this->setError(AKText::_('SFTP_NO_SSH2')); + + return false; + } + + $this->_connection = @ssh2_connect($this->host, $this->port); + + if (!@ssh2_auth_password($this->_connection, $this->user, $this->pass)) + { + $this->setError(AKText::_('SFTP_WRONG_USER')); + + $this->_connection = false; + + return false; + } + + $this->handle = @ssh2_sftp($this->_connection); + + // I must have an absolute directory + if (!$this->dir) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + return false; + } + + // Change to initial directory + if (!$this->sftp_chdir('/')) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + unset($this->_connection); + unset($this->handle); + + return false; + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $basePath = '/' . trim($this->dir, '/'); + + if (@fopen("ssh2.sftp://{$this->handle}$basePath/$testFilename", 'r+') === false) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + unset($this->_connection); + unset($this->handle); + + return false; + } + + return true; + } + + /** + * Changes to the requested directory in the remote server. You give only the + * path relative to the initial directory and it does all the rest by itself, + * including doing nothing if the remote directory is the one we want. + * + * @param string $dir The (realtive) remote directory + * + * @return bool True if successful, false otherwise. + */ + private function sftp_chdir($dir) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dir = str_replace('\\', '/', $dir); + + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dir = rtrim($dir, '/\\') . '/'; + + // Process the path removal + $left = substr($dir, 0, strlen($removePath)); + + if ($left == $removePath) + { + $dir = substr($dir, strlen($removePath)); + } + } + + if (empty($dir)) + { + // Because the substr() above may return FALSE. + $dir = ''; + } + + // Calculate "real" (absolute) SFTP path + $realdir = substr($this->dir, -1) == '/' ? substr($this->dir, 0, strlen($this->dir) - 1) : $this->dir; + $realdir .= '/' . $dir; + $realdir = substr($realdir, 0, 1) == '/' ? $realdir : '/' . $realdir; + + if ($this->_currentdir == $realdir) + { + // Already there, do nothing + return true; + } + + $result = @ssh2_sftp_stat($this->handle, $realdir); + + if ($result === false) + { + return false; + } + else + { + // Update the private "current remote directory" variable + $this->_currentdir = $realdir; + + return true; + } + } + + private function isDirWritable($dir) + { + if (@fopen("ssh2.sftp://{$this->handle}$dir/kickstart.dat", 'wb') === false) + { + return false; + } + else + { + @ssh2_sftp_unlink($this->handle, $dir . '/kickstart.dat'); + + return true; + } + } + + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if (empty($dirName)) + { + $dirName = ''; + } // 'cause the substr() above may return FALSE. + + $check = '/' . trim($this->dir, '/ ') . '/' . trim($dirName, '/'); + + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir, '/ '); + + foreach ($alldirs as $curdir) + { + if (!$curdir) + { + continue; + } + + $check = $previousDir . '/' . $curdir; + + if (!$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + @ssh2_sftp_unlink($this->handle, $check); + + if (@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($check); + + if (@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if (!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + $trustMeIKnowWhatImDoing = + 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($check, $trustMeIKnowWhatImDoing); + + return true; + } + } + } + + @ssh2_sftp_chmod($this->handle, $check, $perms); + } + + $previousDir = $check; + } + + return true; + } + + private function is_dir($dir) + { + return $this->sftp_chdir($dir); + } + + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = @error_reporting(E_NONE); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + + if (!empty($basePath)) + { + $basePath .= '/'; + } + + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + + if (is_dir($oldPath . '/' . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + function __wakeup() + { + $this->connect(); + } + + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $absoluteFSPath = dirname($this->filename); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + $ret = $this->sftp_chdir($absoluteFTPPath); + + if ($ret === false) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + $ret = $this->sftp_chdir($absoluteFTPPath); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + // Create the file + $ret = $this->write($this->tempFilename, $remoteName); + + // If I got a -1 it means that I wasn't able to open the file, so I have to stop here + if ($ret === -1) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $ret = $this->write($this->tempFilename, $remoteName); + } + + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if ($restorePerms) + { + $this->chmod($remoteName, $this->perms); + } + else + { + $this->chmod($remoteName, 0644); + } + + return true; + } + + private function write($local, $remote) + { + $fp = @fopen("ssh2.sftp://{$this->handle}$remote", 'w'); + $localfp = @fopen($local, 'rb'); + + if ($fp === false) + { + return -1; + } + + if ($localfp === false) + { + @fclose($fp); + + return -1; + } + + $res = true; + + while (!feof($localfp) && ($res !== false)) + { + $buffer = @fread($localfp, 65567); + $res = @fwrite($fp, $buffer); + } + + @fclose($fp); + @fclose($localfp); + + return $res; + } + + public function unlink($file) + { + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + return @ssh2_sftp_unlink($this->handle, $check); + } + + public function chmod($file, $perms) + { + return @ssh2_sftp_chmod($this->handle, $file, $perms); + } + + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + public function close() + { + unset($this->_connection); + unset($this->handle); + } + + public function rmdir($directory) + { + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + return @ssh2_sftp_rmdir($this->handle, $check); + } + + public function rename($from, $to) + { + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $result = @ssh2_sftp_rename($this->handle, $from, $to); + + if ($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } + } + +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Hybrid direct / FTP mode file writer + */ +class AKPostprocHybrid extends AKAbstractPostproc +{ + + /** @var bool Should I use the FTP layer? */ + public $useFTP = false; + + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + + /** @var bool use Passive mode? */ + public $passive = true; + + /** @var string FTP host name */ + public $host = ''; + + /** @var int FTP port */ + public $port = 21; + + /** @var string FTP user name */ + public $user = ''; + + /** @var string FTP password */ + public $pass = ''; + + /** @var string FTP initial directory */ + public $dir = ''; + + /** @var resource The FTP handle */ + private $handle = null; + + /** @var null The FTP connection handle */ + private $_handle = null; + + /** + * Public constructor. Tries to connect to the FTP server. + */ + public function __construct() + { + parent::__construct(); + + $this->useFTP = true; + $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); + $this->passive = AKFactory::get('kickstart.ftp.passive', true); + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 21); + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + if (trim($this->port) == '') + { + $this->port = 21; + } + + // If FTP is not configured, skip it altogether + if (empty($this->host) || empty($this->user) || empty($this->pass)) + { + $this->useFTP = false; + } + + // Try to connect to the FTP server + $connected = $this->connect(); + + // If the connection fails, skip FTP altogether + if (!$connected) + { + $this->useFTP = false; + } + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + /** + * Tries to connect to the FTP server + * + * @return bool + */ + public function connect() + { + if (!$this->useFTP) + { + return false; + } + + // Connect to server, using SSL if so required + if ($this->useSSL) + { + $this->handle = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $this->handle = @ftp_connect($this->host, $this->port); + } + if ($this->handle === false) + { + $this->setError(AKText::_('WRONG_FTP_HOST')); + + return false; + } + + // Login + if (!@ftp_login($this->handle, $this->user, $this->pass)) + { + $this->setError(AKText::_('WRONG_FTP_USER')); + @ftp_close($this->handle); + + return false; + } + + // Change to initial directory + if (!@ftp_chdir($this->handle, $this->dir)) + { + $this->setError(AKText::_('WRONG_FTP_PATH1')); + @ftp_close($this->handle); + + return false; + } + + // Enable passive mode if the user requested it + if ($this->passive) + { + @ftp_pasv($this->handle, true); + } + else + { + @ftp_pasv($this->handle, false); + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + + fclose($tempHandle); + + return true; + } + + /** + * Is the directory writeable? + * + * @param string $dir The directory ti check + * + * @return bool + */ + private function isDirWritable($dir) + { + $fp = @fopen($dir . '/kickstart.dat', 'wb'); + + if ($fp === false) + { + return false; + } + + @fclose($fp); + unlink($dir . '/kickstart.dat'); + + return true; + } + + /** + * Create a directory, recursively + * + * @param string $dirName The directory to create + * @param int $perms The permissions to give to the directory + * + * @return bool + */ + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + + // 'cause the substr() above may return FALSE. + if (empty($dirName)) + { + $dirName = ''; + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); + $checkFS = $removePath . trim($dirName, '/'); + + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir); + $previousDirFS = rtrim($removePath, '/\\'); + + foreach ($alldirs as $curdir) + { + $check = $previousDir . '/' . $curdir; + $checkFS = $previousDirFS . '/' . $curdir; + + if (!is_dir($checkFS) && !$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + if (!@unlink($checkFS) && $this->useFTP) + { + @ftp_delete($this->handle, $check); + } + + $createdDir = @mkdir($checkFS, 0755); + + if (!$createdDir && $this->useFTP) + { + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($checkFS); + + $createdDir = @mkdir($checkFS, 0755); + if (!$createdDir && $this->useFTP) + { + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + } + + if (!@chmod($checkFS, $perms) && $this->useFTP) + { + @ftp_chmod($this->handle, $perms, $check); + } + } + + $previousDir = $check; + $previousDirFS = $checkFS; + } + + return true; + } + + private function is_dir($dir) + { + if ($this->useFTP) + { + return @ftp_chdir($this->handle, $dir); + } + + return false; + } + + /** + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * + * @param $path string The full path to a directory or file + */ + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = error_reporting(0); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + + if (!empty($basePath)) + { + $basePath .= '/'; + } + + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + + if (is_dir($oldPath . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + /** + * Called after unserialisation, tries to reconnect to FTP + */ + function __wakeup() + { + if ($this->useFTP) + { + $this->connect(); + } + } + + function __destruct() + { + if (!$this->useFTP) + { + @ftp_close($this->handle); + } + } + + /** + * Post-process an extracted file, using FTP or direct file writes to move it + * + * @return bool + */ + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + $root = rtrim($removePath, '/\\'); + + if (!empty($removePath)) + { + $removePath = ltrim($removePath, "/"); + $remotePath = ltrim($remotePath, "/"); + $left = substr($remotePath, 0, strlen($removePath)); + + if ($left == $removePath) + { + $remotePath = substr($remotePath, strlen($removePath)); + } + } + + $absoluteFSPath = dirname($this->filename); + $relativeFTPPath = trim($remotePath, '/'); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + // Does the directory exist? + if (!is_dir($root . '/' . $absoluteFSPath)) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + + if (($ret === false) && ($this->useFTP)) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + } + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + if ($this->useFTP) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + } + + // Try copying directly + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + + if ($ret === false) + { + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + } + + if ($this->useFTP && ($ret === false)) + { + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if ($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } + } + } + + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + $perms = $restorePerms ? $this->perms : 0644; + + $ret = @chmod($root . '/' . $this->filename, $perms); + + if ($this->useFTP && ($ret === false)) + { + @ftp_chmod($this->_handle, $perms, $remoteName); + } + + return true; + } + + public function unlink($file) + { + $ret = @unlink($file); + + if (!$ret && $this->useFTP) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + $ret = @ftp_delete($this->handle, $check); + } + + return $ret; + } + + /** + * Create a temporary filename + * + * @param string $filename The original filename + * @param int $perms The file permissions + * + * @return string + */ + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + /** + * Closes the FTP connection + */ + public function close() + { + if (!$this->useFTP) + { + @ftp_close($this->handle); + } + } + + public function chmod($file, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + $ret = @chmod($file, $perms); + + if (!$ret && $this->useFTP) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + // Trim slash on the left + $file = ltrim($file, '/'); + + $ret = @ftp_chmod($this->handle, $perms, $file); + } + + return $ret; + } + + public function rmdir($directory) + { + $ret = @rmdir($directory); + + if (!$ret && $this->useFTP) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($directory, 0, strlen($removePath)); + if ($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + $ret = @ftp_rmdir($this->handle, $check); + } + + return $ret; + } + + public function rename($from, $to) + { + $ret = @rename($from, $to); + + if (!$ret && $this->useFTP) + { + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($from, 0, strlen($removePath)); + if ($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } + } + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + + if (!empty($removePath)) + { + $left = substr($to, 0, strlen($removePath)); + if ($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } + } + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $ret = @ftp_rename($this->handle, $from, $to); + } + + return $ret; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * JPA archive extraction class + */ +class AKUnarchiverJPA extends AKAbstractUnarchiver +{ + protected $archiveHeaderData = array(); + + protected function readArchiveHeader() + { + debugMsg('Preparing to read archive header'); + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + debugMsg('Opening the first part'); + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + debugMsg('Could not open the first part'); + + return false; + } + + // Read the signature + $sig = fread($this->fp, 3); + + if ($sig != 'JPA') + { + // Not a JPA file + debugMsg('Invalid archive signature'); + $this->setError(AKText::_('ERR_NOT_A_JPA_FILE')); + + return false; + } + + // Read and parse header length + $header_length_array = unpack('v', fread($this->fp, 2)); + $header_length = $header_length_array[1]; + + // Read and parse the known portion of header data (14 bytes) + $bin_data = fread($this->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->fp, $rest_length); + } + else + { + $junk = ''; + } + + // Temporary array with all the data we read + $temp = array( + 'signature' => $sig, + 'length' => $header_length, + 'major' => $header_data['major'], + 'minor' => $header_data['minor'], + 'filecount' => $header_data['count'], + 'uncompressedsize' => $header_data['uncsize'], + 'compressedsize' => $header_data['csize'], + 'unknowndata' => $junk + ); + + // Array-to-object conversion + foreach ($temp as $key => $value) + { + $this->archiveHeaderData->{$key} = $value; + } + + debugMsg('Header data:'); + debugMsg('Length : ' . $header_length); + debugMsg('Major : ' . $header_data['major']); + debugMsg('Minor : ' . $header_data['minor']); + debugMsg('File count : ' . $header_data['count']); + debugMsg('Uncompressed size : ' . $header_data['uncsize']); + debugMsg('Compressed size : ' . $header_data['csize']); + + $this->currentPartOffset = @ftell($this->fp); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + debugMsg('Archive part EOF; moving to next file'); + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + debugMsg("Reading file signature; part {$this->currentPartNumber}, offset {$this->currentPartOffset}"); + // Get and decode Entity Description Block + $signature = fread($this->fp, 3); + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Check signature + if ($signature != 'JPF') + { + if ($this->isEOF(true)) + { + // This file is finished; make sure it's the last one + $this->nextFile(); + + if (!$this->isEOF(false)) + { + debugMsg('Invalid file signature before end of archive encountered'); + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + + // We're just finished + return false; + } + else + { + $screwed = true; + + if (AKFactory::get('kickstart.setup.ignoreerrors', false)) + { + debugMsg('Invalid file block signature; launching heuristic file block signature scanner'); + $screwed = !$this->heuristicFileHeaderLocator(); + + if (!$screwed) + { + $signature = 'JPF'; + } + else + { + debugMsg('Heuristics failed. Brace yourself for the imminent crash.'); + } + } + + if ($screwed) + { + debugMsg('Invalid file block signature'); + // This is not a file block! The archive is corrupt. + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + } + } + // This a JPA Entity Block. Process the header. + + $isBannedFile = false; + + // Read length of EDB and of the Entity Path Data + $length_array = unpack('vblocksize/vpathsize', fread($this->fp, 4)); + // Read the path data + if ($length_array['pathsize'] > 0) + { + $file = fread($this->fp, $length_array['pathsize']); + } + else + { + $file = ''; + } + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($file, $this->renameFiles)) + { + $file = $this->renameFiles[$file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($file), $this->renameDirs)) + { + $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read and parse the known data portion + $bin_data = fread($this->fp, 14); + $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data); + // Read any unknown data + $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']); + + if ($restBytes > 0) + { + // Start reading the extra fields + while ($restBytes >= 4) + { + $extra_header_data = fread($this->fp, 4); + $extra_header = unpack('vsignature/vlength', $extra_header_data); + $restBytes -= 4; + $extra_header['length'] -= 4; + + switch ($extra_header['signature']) + { + case 256: + // File modified timestamp + if ($extra_header['length'] > 0) + { + $bindata = fread($this->fp, $extra_header['length']); + $restBytes -= $extra_header['length']; + $timestamps = unpack('Vmodified', substr($bindata, 0, 4)); + $filectime = $timestamps['modified']; + $this->fileHeader->timestamp = $filectime; + } + break; + + default: + // Unknown field + if ($extra_header['length'] > 0) + { + $junk = fread($this->fp, $extra_header['length']); + $restBytes -= $extra_header['length']; + } + break; + } + } + + if ($restBytes > 0) + { + $junk = fread($this->fp, $restBytes); + } + } + + $compressionType = $header_data['compression']; + + // Populate the return array + $this->fileHeader->file = $file; + $this->fileHeader->compressed = $header_data['compsize']; + $this->fileHeader->uncompressed = $header_data['uncompsize']; + + switch ($header_data['type']) + { + case 0: + $this->fileHeader->type = 'dir'; + break; + + case 1: + $this->fileHeader->type = 'file'; + break; + + case 2: + $this->fileHeader->type = 'link'; + break; + } + + switch ($compressionType) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 1: + $this->fileHeader->compression = 'gzip'; + break; + case 2: + $this->fileHeader->compression = 'bzip2'; + break; + } + + $this->fileHeader->permissions = $header_data['perms']; + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + debugMsg('Skipping file ' . $this->fileHeader->file); + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + while ($seekleft > 0) + { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->archiveList[$this->currentPartNumber]); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + if ($canSeek > $seekleft) + { + $canSeek = $seekleft; + } + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + if ($seekleft) + { + $this->nextFile(); + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + // Regular file; ask the postproc engine to process its filename + if ($restorePerms) + { + $this->fileHeader->realFile = + $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + } + elseif ($this->fileHeader->type == 'dir') + { + $dir = $this->fileHeader->file; + + // Directory; just create it + if ($restorePerms) + { + $this->postProcEngine->createDirRecursive($dir, $this->fileHeader->permissions); + } + else + { + $this->postProcEngine->createDirRecursive($dir, 0755); + } + + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + // Header is read + $this->runState = AK_STATE_HEADER; + + $this->dataReadLength = 0; + + return true; + } + + protected function heuristicFileHeaderLocator() + { + $ret = false; + $fullEOF = false; + + while (!$ret && !$fullEOF) + { + $this->currentPartOffset = @ftell($this->fp); + + if ($this->isEOF(true)) + { + $this->nextFile(); + } + + if ($this->isEOF(false)) + { + $fullEOF = true; + continue; + } + + // Read 512Kb + $chunk = fread($this->fp, 524288); + $size_read = mb_strlen($chunk, '8bit'); + //$pos = strpos($chunk, 'JPF'); + $pos = mb_strpos($chunk, 'JPF', 0, '8bit'); + + if ($pos !== false) + { + // We found it! + $this->currentPartOffset += $pos + 3; + @fseek($this->fp, $this->currentPartOffset, SEEK_SET); + $ret = true; + } + else + { + // Not yet found :( + $this->currentPartOffset = @ftell($this->fp); + } + } + + return $ret; + } + + /** + * Creates the directory this file points to + */ + protected function createDirectory() + { + if ($this->mustSkip()) + { + return true; + } + + // Do we need to create a directory? + if (empty($this->fileHeader->realFile)) + { + $this->fileHeader->realFile = $this->fileHeader->file; + } + + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = $this->flagRestorePermissions ? $this->fileHeader->permissions : 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); + + if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); + + return false; + } + else + { + return true; + } + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected function processFileData() + { + switch ($this->fileHeader->type) + { + case 'dir': + return $this->processTypeDir(); + break; + + case 'link': + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) + { + case 'none': + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + return $this->processTypeFileCompressedSimple(); + break; + + } + break; + + default: + debugMsg('Unknown file type ' . $this->fileHeader->type); + break; + } + } + + /** + * Process the file data of a directory entry + * + * @return bool + */ + private function processTypeDir() + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return bool + */ + private function processTypeLink() + { + $readBytes = 0; + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) + { + $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + } + else + { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + } + + $filename = isset($this->fileHeader->realFile) ? $this->fileHeader->realFile : $this->fileHeader->file; + + if (!$this->mustSkip()) + { + // Try to remove an existing file or directory by the same name + if (file_exists($filename)) + { + @unlink($filename); + @rmdir($filename); + } + + // Remove any trailing slash + if (substr($filename, -1) == '/') + { + $filename = substr($filename, 0, -1); + } + // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( + @symlink($data, $filename); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; // No matter if the link was created! + } + + private function processTypeFileUncompressed() + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + + if ($this->dataReadLength == 0) + { + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + } + else + { + $outfp = @fopen($this->fileHeader->realFile, 'ab'); + } + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) + { + // An error occurred + debugMsg('Could not write to output file'); + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (!$this->mustSkip() && is_resource($outfp)) + { + @fclose($outfp); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Reference to the global timer + $timer = AKFactory::getTimer(); + + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) + { + $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + $leftBytes -= $reallyReadBytes; + $this->dataReadLength += $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + } + else + { + // Nope. The archive is corrupt + debugMsg('Not enough data in file. The archive is truncated or corrupt.'); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fwrite($outfp, $data); + } + } + } + + // Close the file pointer + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) + { + $this->runState = AK_STATE_DATA; + } + else + { + // Oh! We just finished! + $this->runState = AK_STATE_DATAREAD; + $this->dataReadLength = 0; + } + + return true; + } + + private function processTypeFileCompressedSimple() + { + if (!$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) + { + // An error occurred + debugMsg('Could not write to output file'); + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Simple compressed files are processed as a whole; we can't do chunk processing + $zipData = $this->fread($this->fp, $this->fileHeader->compressed); + while (akstringlen($zipData) < $this->fileHeader->compressed) + { + // End of local file before reading all data, but have more archive parts? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Read from the next file + $this->nextFile(); + $bytes_left = $this->fileHeader->compressed - akstringlen($zipData); + $zipData .= $this->fread($this->fp, $bytes_left); + } + else + { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + if ($this->fileHeader->compression == 'gzip') + { + $unzipData = gzinflate($zipData); + } + elseif ($this->fileHeader->compression == 'bzip2') + { + $unzipData = bzdecompress($zipData); + } + unset($zipData); + + // Write to the file. + if (!$this->mustSkip() && is_resource($outfp)) + { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + unset($unzipData); + + $this->runState = AK_STATE_DATAREAD; + + return true; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * ZIP archive extraction class + * + * Since the file data portion of ZIP and JPA are similarly structured (it's empty for dirs, + * linked node name for symlinks, dumped binary data for no compressions and dumped gzipped + * binary data for gzip compression) we just have to subclass AKUnarchiverJPA and change the + * header reading bits. Reusable code ;) + */ +class AKUnarchiverZIP extends AKUnarchiverJPA +{ + var $expectDataDescriptor = false; + + protected function readArchiveHeader() + { + debugMsg('Preparing to read archive header'); + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + debugMsg('Opening the first part'); + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + debugMsg('The first part is not readable'); + + return false; + } + + // Read a possible multipart signature + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // Roll back if it's not a multipart archive + if ($headerData['sig'] == 0x04034b50) + { + debugMsg('The archive is not multipart'); + fseek($this->fp, -4, SEEK_CUR); + } + else + { + debugMsg('The archive is multipart'); + } + + $multiPartSigs = array( + 0x08074b50, // Multi-part ZIP + 0x30304b50, // Multi-part ZIP (alternate) + 0x04034b50 // Single file + ); + if (!in_array($headerData['sig'], $multiPartSigs)) + { + debugMsg('Invalid header signature ' . dechex($headerData['sig'])); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + + $this->currentPartOffset = @ftell($this->fp); + debugMsg('Current part offset after reading header: ' . $this->currentPartOffset); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + debugMsg('Opening next archive part'); + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + if ($this->expectDataDescriptor) + { + // The last file had bit 3 of the general purpose bit flag set. This means that we have a + // 12 byte data descriptor we need to skip. To make things worse, there might also be a 4 + // byte optional data descriptor header (0x08074b50). + $junk = @fread($this->fp, 4); + $junk = unpack('Vsig', $junk); + if ($junk['sig'] == 0x08074b50) + { + // Yes, there was a signature + $junk = @fread($this->fp, 12); + debugMsg('Data descriptor (w/ header) skipped at ' . (ftell($this->fp) - 12)); + } + else + { + // No, there was no signature, just read another 8 bytes + $junk = @fread($this->fp, 8); + debugMsg('Data descriptor (w/out header) skipped at ' . (ftell($this->fp) - 8)); + } + + // And check for EOF, too + if ($this->isEOF(true)) + { + debugMsg('EOF before reading header'); + + $this->nextFile(); + } + } + + // Get and decode Local File Header + $headerBinary = fread($this->fp, 30); + $headerData = + unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary); + + // Check signature + if (!($headerData['sig'] == 0x04034b50)) + { + debugMsg('Not a file signature at ' . (ftell($this->fp) - 4)); + + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) + { + debugMsg('EOCD signature at ' . (ftell($this->fp) - 4)); + // End of ZIP file detected. We'll just skip to the end of file... + while ($this->nextFile()) + { + }; + @fseek($this->fp, 0, SEEK_END); // Go to EOF + return false; + } + else + { + debugMsg('Invalid signature ' . dechex($headerData['sig']) . ' at ' . ftell($this->fp)); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // If bit 3 of the bitflag is set, expectDataDescriptor is true + $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Read the last modified data and time + $lastmodtime = $headerData['lastmodtime']; + $lastmoddate = $headerData['lastmoddate']; + + if ($lastmoddate && $lastmodtime) + { + // ----- Extract time + $v_hour = ($lastmodtime & 0xF800) >> 11; + $v_minute = ($lastmodtime & 0x07E0) >> 5; + $v_seconde = ($lastmodtime & 0x001F) * 2; + + // ----- Extract date + $v_year = (($lastmoddate & 0xFE00) >> 9) + 1980; + $v_month = ($lastmoddate & 0x01E0) >> 5; + $v_day = $lastmoddate & 0x001F; + + // ----- Get UNIX date format + $this->fileHeader->timestamp = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); + } + + $isBannedFile = false; + + $this->fileHeader->compressed = $headerData['compsize']; + $this->fileHeader->uncompressed = $headerData['uncomp']; + $nameFieldLength = $headerData['fnamelen']; + $extraFieldLength = $headerData['eflen']; + + // Read filename field + $this->fileHeader->file = fread($this->fp, $nameFieldLength); + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($this->fileHeader->file, $this->renameFiles)) + { + $this->fileHeader->file = $this->renameFiles[$this->fileHeader->file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($this->fileHeader->file), $this->renameDirs)) + { + $file = + rtrim($this->renameDirs[dirname($this->fileHeader->file)], '/') . '/' . basename($this->fileHeader->file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read extra field if present + if ($extraFieldLength > 0) + { + $extrafield = fread($this->fp, $extraFieldLength); + } + + debugMsg('*' . ftell($this->fp) . ' IS START OF ' . $this->fileHeader->file . ' (' . $this->fileHeader->compressed . ' bytes)'); + + + // Decide filetype -- Check for directories + $this->fileHeader->type = 'file'; + if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) + { + $this->fileHeader->type = 'dir'; + } + // Decide filetype -- Check for symbolic links + if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) + { + $this->fileHeader->type = 'link'; + } + + switch ($headerData['compmethod']) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 8: + $this->fileHeader->compression = 'gzip'; + break; + } + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + while ($seekleft > 0) + { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->archiveList[$this->currentPartNumber]); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + if ($canSeek > $seekleft) + { + $canSeek = $seekleft; + } + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + if ($seekleft) + { + $this->nextFile(); + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + elseif ($this->fileHeader->type == 'dir') + { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + $this->postProcEngine->createDirRecursive($dir, 0755); + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + // Header is read + $this->runState = AK_STATE_HEADER; + + return true; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * JPS archive extraction class + */ +class AKUnarchiverJPS extends AKUnarchiverJPA +{ + /** + * Header data for the archive + * + * @var array + */ + protected $archiveHeaderData = array(); + + /** + * Plaintext password from which the encryption key will be derived with PBKDF2 + * + * @var string + */ + protected $password = ''; + + /** + * Which hash algorithm should I use for key derivation with PBKDF2. + * + * @var string + */ + private $pbkdf2Algorithm = 'sha1'; + + /** + * How many iterations should I use for key derivation with PBKDF2 + * + * @var int + */ + private $pbkdf2Iterations = 1000; + + /** + * Should I use a static salt for key derivation with PBKDF2? + * + * @var bool + */ + private $pbkdf2UseStaticSalt = 0; + + /** + * Static salt for key derivation with PBKDF2 + * + * @var string + */ + private $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * How much compressed data I have read since the last file header read + * + * @var int + */ + private $compressedSizeReadSinceLastFileHeader = 0; + + public function __construct() + { + parent::__construct(); + + $this->password = AKFactory::get('kickstart.jps.password', ''); + } + + public function __wakeup() + { + parent::__wakeup(); + + // Make sure the decryption is all set up (required!) + AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); + AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); + AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); + AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); + } + + + protected function readArchiveHeader() + { + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + return false; + } + + // Read the signature + $sig = fread($this->fp, 3); + + if ($sig != 'JPS') + { + // Not a JPA file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + // Read and parse the known portion of header data (5 bytes) + $bin_data = fread($this->fp, 5); + $header_data = unpack('Cmajor/Cminor/cspanned/vextra', $bin_data); + + // Is this a v2 archive? + $versionHumanReadable = $header_data['major'] . '.' . $header_data['minor']; + $isV2Archive = version_compare($versionHumanReadable, '2.0', 'ge'); + + // Load any remaining header data + $rest_length = $header_data['extra']; + + if ($isV2Archive && $rest_length) + { + // V2 archives only have one kind of extra header + if (!$this->readKeyExpansionExtraHeader()) + { + return false; + } + } + elseif ($rest_length > 0) + { + $junk = fread($this->fp, $rest_length); + } + + // Temporary array with all the data we read + $temp = array( + 'signature' => $sig, + 'major' => $header_data['major'], + 'minor' => $header_data['minor'], + 'spanned' => $header_data['spanned'] + ); + // Array-to-object conversion + foreach ($temp as $key => $value) + { + $this->archiveHeaderData->{$key} = $value; + } + + $this->currentPartOffset = @ftell($this->fp); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + // Get and decode Entity Description Block + $signature = fread($this->fp, 3); + + // Check for end-of-archive siganture + if ($signature == 'JPE') + { + $this->setState('postrun'); + + return true; + } + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Check signature + if ($signature != 'JPF') + { + if ($this->isEOF(true)) + { + // This file is finished; make sure it's the last one + $this->nextFile(); + if (!$this->isEOF(false)) + { + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + + // We're just finished + return false; + } + else + { + fseek($this->fp, -6, SEEK_CUR); + $signature = fread($this->fp, 3); + if ($signature == 'JPE') + { + return false; + } + + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + } + + // This a JPS Entity Block. Process the header. + + $isBannedFile = false; + + // Make sure the decryption is all set up + AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); + AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); + AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); + AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); + + // Read and decrypt the header + $edbhData = fread($this->fp, 4); + $edbh = unpack('vencsize/vdecsize', $edbhData); + $bin_data = fread($this->fp, $edbh['encsize']); + + // Add the header length to the data read + $this->compressedSizeReadSinceLastFileHeader += $edbh['encsize'] + 4; + + // Decrypt and truncate + $bin_data = AKEncryptionAES::AESDecryptCBC($bin_data, $this->password); + $bin_data = substr($bin_data, 0, $edbh['decsize']); + + // Read length of EDB and of the Entity Path Data + $length_array = unpack('vpathsize', substr($bin_data, 0, 2)); + // Read the path data + $file = substr($bin_data, 2, $length_array['pathsize']); + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($file, $this->renameFiles)) + { + $file = $this->renameFiles[$file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($file), $this->renameDirs)) + { + $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read and parse the known data portion + $bin_data = substr($bin_data, 2 + $length_array['pathsize']); + $header_data = unpack('Ctype/Ccompression/Vuncompsize/Vperms/Vfilectime', $bin_data); + + $this->fileHeader->timestamp = $header_data['filectime']; + $compressionType = $header_data['compression']; + + // Populate the return array + $this->fileHeader->file = $file; + $this->fileHeader->uncompressed = $header_data['uncompsize']; + switch ($header_data['type']) + { + case 0: + $this->fileHeader->type = 'dir'; + break; + + case 1: + $this->fileHeader->type = 'file'; + break; + + case 2: + $this->fileHeader->type = 'link'; + break; + } + switch ($compressionType) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 1: + $this->fileHeader->compression = 'gzip'; + break; + case 2: + $this->fileHeader->compression = 'bzip2'; + break; + } + $this->fileHeader->permissions = $header_data['perms']; + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + $done = false; + while (!$done) + { + // Read the Data Chunk Block header + $binMiniHead = fread($this->fp, 8); + if (in_array(substr($binMiniHead, 0, 3), array('JPF', 'JPE'))) + { + // Not a Data Chunk Block header, I am done skipping the file + @fseek($this->fp, -8, SEEK_CUR); // Roll back the file pointer + $done = true; // Mark as done + continue; // Exit loop + } + else + { + // Skip forward by the amount of compressed data + $miniHead = unpack('Vencsize/Vdecsize', $binMiniHead); + @fseek($this->fp, $miniHead['encsize'], SEEK_CUR); + $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHead['encsize']; + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; + $this->compressedSizeReadSinceLastFileHeader = 0; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + // Regular file; ask the postproc engine to process its filename + if ($restorePerms) + { + $this->fileHeader->realFile = + $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + } + elseif ($this->fileHeader->type == 'dir') + { + $dir = $this->fileHeader->file; + $this->fileHeader->realFile = $dir; + + // Directory; just create it + if ($restorePerms) + { + $this->postProcEngine->createDirRecursive($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); + } + + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + + $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; + $this->compressedSizeReadSinceLastFileHeader = 0; + + // Header is read + $this->runState = AK_STATE_HEADER; + + $this->dataReadLength = 0; + + return true; + } + + /** + * Creates the directory this file points to + */ + protected function createDirectory() + { + if ($this->mustSkip()) + { + return true; + } + + // Do we need to create a directory? + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); + + if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); + + return false; + } + + return true; + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected function processFileData() + { + switch ($this->fileHeader->type) + { + case 'dir': + return $this->processTypeDir(); + break; + + case 'link': + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) + { + case 'none': + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + return $this->processTypeFileCompressedSimple(); + break; + + } + break; + } + } + + /** + * Process the file data of a directory entry + * + * @return bool + */ + private function processTypeDir() + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return bool + */ + private function processTypeLink() + { + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Read the mini header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + + if ($reallyReadBytes < 8) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Retry reading the header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + // Still not enough data? If so, the archive is corrupt or missing parts. + if ($reallyReadBytes < 8) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Read the encrypted data + $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); + $toReadBytes = $miniHeader['encsize']; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHeader['encsize']; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Read the rest of the data + $toReadBytes -= $reallyReadBytes; + $restData = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + if ($reallyReadBytes < $toReadBytes) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + $data .= $restData; + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Decrypt the data + $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); + + // Is the length of the decrypted data less than expected? + $data_length = akstringlen($data); + if ($data_length < $miniHeader['decsize']) + { + $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); + + return false; + } + + // Trim the data + $data = substr($data, 0, $miniHeader['decsize']); + + if (!$this->mustSkip()) + { + // Try to remove an existing file or directory by the same name + if (file_exists($this->fileHeader->file)) + { + @unlink($this->fileHeader->file); + @rmdir($this->fileHeader->file); + } + // Remove any trailing slash + if (substr($this->fileHeader->file, -1) == '/') + { + $this->fileHeader->file = substr($this->fileHeader->file, 0, -1); + } + // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( + @symlink($data, $this->fileHeader->file); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; // No matter if the link was created! + } + + private function processTypeFileUncompressed() + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + if ($this->dataReadLength == 0) + { + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + } + else + { + $outfp = @fopen($this->fileHeader->realFile, 'ab'); + } + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + if (!$this->mustSkip() && is_resource($outfp)) + { + @fclose($outfp); + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + $this->setError('An uncompressed file was detected; this is not supported by this archive extraction utility'); + + return false; + } + + private function processTypeFileCompressedSimple() + { + $timer = AKFactory::getTimer(); + + // Files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; + + // Loop while there's data to write and enough time to do it + while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) + { + // Read the mini header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + if ($reallyReadBytes < 8) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Retry reading the header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + // Still not enough data? If so, the archive is corrupt or missing parts. + if ($reallyReadBytes < 8) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Read the encrypted data + $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); + $toReadBytes = $miniHeader['encsize']; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + + $this->compressedSizeReadSinceLastFileHeader += $miniHeader['encsize'] + 8; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Read the rest of the data + $toReadBytes -= $reallyReadBytes; + $restData = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($restData); + if ($reallyReadBytes < $toReadBytes) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + if (akstringlen($data) == 0) + { + $data = $restData; + } + else + { + $data .= $restData; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Decrypt the data + $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); + + // Is the length of the decrypted data less than expected? + $data_length = akstringlen($data); + if ($data_length < $miniHeader['decsize']) + { + $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); + + return false; + } + + // Trim the data + $data = substr($data, 0, $miniHeader['decsize']); + + // Decompress + $data = gzinflate($data); + $unc_len = akstringlen($data); + + // Write the decrypted data + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fwrite($outfp, $data, akstringlen($data)); + } + } + + // Update the read length + $this->dataReadLength += $unc_len; + $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; + } + + // Close the file pointer + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) + { + $this->runState = AK_STATE_DATA; + } + else + { + // Oh! We just finished! + $this->runState = AK_STATE_DATAREAD; + $this->dataReadLength = 0; + } + + return true; + } + + private function readKeyExpansionExtraHeader() + { + $signature = fread($this->fp, 4); + + if ($signature != "JH\x00\x01") + { + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + $bin_data = fread($this->fp, 8); + $header_data = unpack('vlength/Calgo/Viterations/CuseStaticSalt', $bin_data); + + if ($header_data['length'] != 76) + { + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + switch ($header_data['algo']) + { + case 0: + $algorithm = 'sha1'; + break; + + case 1: + $algorithm = 'sha256'; + break; + + case 2: + $algorithm = 'sha512'; + break; + + default: + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + break; + } + + $this->pbkdf2Algorithm = $algorithm; + $this->pbkdf2Iterations = $header_data['iterations']; + $this->pbkdf2UseStaticSalt = $header_data['useStaticSalt']; + $this->pbkdf2StaticSalt = fread($this->fp, 64); + + return true; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Timer class + */ +class AKCoreTimer extends AKAbstractObject +{ + /** @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 AECoreTimer + */ + public function __construct() + { + parent::__construct(); + + // Initialize start time + $this->start_time = $this->microtime_float(); + + // Get configured max time per step and bias + $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14); + $bias = AKFactory::get('kickstart.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; + } + } + + /** + * Returns the current timestampt in decimal seconds + */ + private function microtime_float() + { + list($usec, $sec) = explode(" ", microtime()); + + return ((float) $usec + (float) $sec); + } + + /** + * 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; + } + + /** + * Enforce the minimum execution time + */ + public function enforce_min_exec_time() + { + // 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 + $minexectime = AKFactory::get('kickstart.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; + + // 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 (function_exists('usleep')) + { + usleep(1000 * $sleep_msec); + } + elseif (function_exists('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')) + { + $until_timestamp = time() + $sleep_msec / 1000; + time_sleep_until($until_timestamp); + } + elseif (function_exists('sleep')) + { + $sleep_sec = ceil($sleep_msec / 1000); + sleep($sleep_sec); + } + } + elseif ($elapsed_time > 0) + { + // No sleep required, even if user configured us to be able to do so. + } + } + + /** + * Reset the timer. It should only be used in CLI mode! + */ + public function resetTime() + { + $this->start_time = $this->microtime_float(); + } + + /** + * @param int $max_exec_time + */ + public function setMaxExecTime($max_exec_time) + { + $this->max_exec_time = $max_exec_time; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A filesystem scanner which uses opendir() + */ +class AKUtilsLister extends AKAbstractObject +{ + public function &getFiles($folder, $pattern = '*') + { + // Initialize variables + $arr = array(); + $false = false; + + if (!is_dir($folder)) + { + return $false; + } + + $handle = @opendir($folder); + // If directory is not accessible, just return FALSE + if ($handle === false) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + while (($file = @readdir($handle)) !== false) + { + if (!fnmatch($pattern, $file)) + { + continue; + } + + if (($file != '.') && ($file != '..')) + { + $ds = + ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? + '' : DIRECTORY_SEPARATOR; + $dir = $folder . $ds . $file; + $isDir = is_dir($dir); + if (!$isDir) + { + $arr[] = $dir; + } + } + } + @closedir($handle); + + return $arr; + } + + public function &getFolders($folder, $pattern = '*') + { + // Initialize variables + $arr = array(); + $false = false; + + if (!is_dir($folder)) + { + return $false; + } + + $handle = @opendir($folder); + // If directory is not accessible, just return FALSE + if ($handle === false) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + while (($file = @readdir($handle)) !== false) + { + if (!fnmatch($pattern, $file)) + { + continue; + } + + if (($file != '.') && ($file != '..')) + { + $ds = + ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? + '' : DIRECTORY_SEPARATOR; + $dir = $folder . $ds . $file; + $isDir = is_dir($dir); + if ($isDir) + { + $arr[] = $dir; + } + } + } + @closedir($handle); + + return $arr; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A simple INI-based i18n engine + */ +class AKText extends AKAbstractObject +{ + /** + * The default (en_GB) translation used when no other translation is available + * + * @var array + */ + private $default_translation = array( + 'AUTOMODEON' => 'Auto-mode enabled', + 'ERR_NOT_A_JPA_FILE' => 'The file is not a JPA archive', + 'ERR_CORRUPT_ARCHIVE' => 'The archive file is corrupt, truncated or archive parts are missing', + 'ERR_INVALID_LOGIN' => 'Invalid login', + 'COULDNT_CREATE_DIR' => 'Could not create %s folder', + 'COULDNT_WRITE_FILE' => 'Could not open %s for writing.', + 'WRONG_FTP_HOST' => 'Wrong FTP host or port', + 'WRONG_FTP_USER' => 'Wrong FTP username or password', + 'WRONG_FTP_PATH1' => 'Wrong FTP initial directory - the directory doesn\'t exist', + 'FTP_CANT_CREATE_DIR' => 'Could not create directory %s', + 'FTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', + 'SFTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', + 'FTP_COULDNT_UPLOAD' => 'Could not upload %s', + 'THINGS_HEADER' => 'Things you should know about Akeeba Kickstart', + 'THINGS_01' => 'Kickstart is not an installer. It is an archive extraction tool. The actual installer was put inside the archive file at backup time.', + 'THINGS_02' => 'Kickstart is not the only way to extract the backup archive. You can use Akeeba eXtract Wizard and upload the extracted files using FTP instead.', + 'THINGS_03' => 'Kickstart is bound by your server\'s configuration. As such, it may not work at all.', + 'THINGS_04' => 'You should download and upload your archive files using FTP in Binary transfer mode. Any other method could lead to a corrupt backup archive and restoration failure.', + 'THINGS_05' => 'Post-restoration site load errors are usually caused by .htaccess or php.ini directives. You should understand that blank pages, 404 and 500 errors can usually be worked around by editing the aforementioned files. It is not our job to mess with your configuration files, because this could be dangerous for your site.', + 'THINGS_06' => 'Kickstart overwrites files without a warning. If you are not sure that you are OK with that do not continue.', + 'THINGS_07' => 'Trying to restore to the temporary URL of a cPanel host (e.g. http://1.2.3.4/~username) will lead to restoration failure and your site will appear to be not working. This is normal and it\'s just how your server and CMS software work.', + 'THINGS_08' => 'You are supposed to read the documentation before using this software. Most issues can be avoided, or easily worked around, by understanding how this software works.', + 'THINGS_09' => 'This text does not imply that there is a problem detected. It is standard text displayed every time you launch Kickstart.', + 'CLOSE_LIGHTBOX' => 'Click here or press ESC to close this message', + 'SELECT_ARCHIVE' => 'Select a backup archive', + 'ARCHIVE_FILE' => 'Archive file:', + 'SELECT_EXTRACTION' => 'Select an extraction method', + 'WRITE_TO_FILES' => 'Write to files:', + 'WRITE_HYBRID' => 'Hybrid (use FTP only if needed)', + 'WRITE_DIRECTLY' => 'Directly', + 'WRITE_FTP' => 'Use FTP for all files', + 'WRITE_SFTP' => 'Use SFTP for all files', + 'FTP_HOST' => '(S)FTP host name:', + 'FTP_PORT' => '(S)FTP port:', + 'FTP_FTPS' => 'Use FTP over SSL (FTPS)', + 'FTP_PASSIVE' => 'Use FTP Passive Mode', + 'FTP_USER' => '(S)FTP user name:', + 'FTP_PASS' => '(S)FTP password:', + 'FTP_DIR' => '(S)FTP directory:', + 'FTP_TEMPDIR' => 'Temporary directory:', + 'FTP_CONNECTION_OK' => 'FTP Connection Established', + 'SFTP_CONNECTION_OK' => 'SFTP Connection Established', + 'FTP_CONNECTION_FAILURE' => 'The FTP Connection Failed', + 'SFTP_CONNECTION_FAILURE' => 'The SFTP Connection Failed', + 'FTP_TEMPDIR_WRITABLE' => 'The temporary directory is writable.', + 'FTP_TEMPDIR_UNWRITABLE' => 'The temporary directory is not writable. Please check the permissions.', + 'FTPBROWSER_ERROR_HOSTNAME' => "Invalid FTP host or port", + 'FTPBROWSER_ERROR_USERPASS' => "Invalid FTP username or password", + 'FTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'FTPBROWSER_ERROR_UNSUPPORTED' => "Sorry, your FTP server doesn't support our FTP directory browser.", + 'FTPBROWSER_LBL_GOPARENT' => "<up one level>", + 'FTPBROWSER_LBL_INSTRUCTIONS' => 'Click on a directory to navigate into it. Click on OK to select that directory, Cancel to abort the procedure.', + 'FTPBROWSER_LBL_ERROR' => 'An error occurred', + 'SFTP_NO_SSH2' => 'Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', + 'SFTP_NO_FTP_SUPPORT' => 'Your SSH server does not allow SFTP connections', + 'SFTP_WRONG_USER' => 'Wrong SFTP username or password', + 'SFTP_WRONG_STARTING_DIR' => 'You must supply a valid absolute path', + 'SFTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'SFTP_COULDNT_UPLOAD' => 'Could not upload %s', + 'SFTP_CANT_CREATE_DIR' => 'Could not create directory %s', + 'UI-ROOT' => '<root>', + 'CONFIG_UI_FTPBROWSER_TITLE' => 'FTP Directory Browser', + 'FTP_BROWSE' => 'Browse', + 'BTN_CHECK' => 'Check', + 'BTN_RESET' => 'Reset', + 'BTN_TESTFTPCON' => 'Test FTP connection', + 'BTN_TESTSFTPCON' => 'Test SFTP connection', + 'BTN_GOTOSTART' => 'Start over', + 'FINE_TUNE' => 'Fine tune', + 'BTN_SHOW_FINE_TUNE' => 'Show advanced options (for experts)', + 'MIN_EXEC_TIME' => 'Minimum execution time:', + 'MAX_EXEC_TIME' => 'Maximum execution time:', + 'SECONDS_PER_STEP' => 'seconds per step', + 'EXTRACT_FILES' => 'Extract files', + 'BTN_START' => 'Start', + 'EXTRACTING' => 'Extracting', + 'DO_NOT_CLOSE_EXTRACT' => 'Do not close this window while the extraction is in progress', + 'RESTACLEANUP' => 'Restoration and Clean Up', + 'BTN_RUNINSTALLER' => 'Run the Installer', + 'BTN_CLEANUP' => 'Clean Up', + 'BTN_SITEFE' => 'Visit your site\'s frontend', + 'BTN_SITEBE' => 'Visit your site\'s backend', + 'WARNINGS' => 'Extraction Warnings', + 'ERROR_OCCURED' => 'An error occurred', + 'STEALTH_MODE' => 'Stealth mode', + 'STEALTH_URL' => 'HTML file to show to web visitors', + 'ERR_NOT_A_JPS_FILE' => 'The file is not a JPA archive', + 'ERR_INVALID_JPS_PASSWORD' => 'The password you gave is wrong or the archive is corrupt', + 'JPS_PASSWORD' => 'Archive Password (for JPS files)', + 'INVALID_FILE_HEADER' => 'Invalid header in archive file, part %s, offset %s', + 'NEEDSOMEHELPKS' => 'Want some help to use this tool? Read this first:', + 'QUICKSTART' => 'Quick Start Guide', + 'CANTGETITTOWORK' => 'Can\'t get it to work? Click me!', + 'NOARCHIVESCLICKHERE' => 'No archives detected. Click here for troubleshooting instructions.', + 'POSTRESTORATIONTROUBLESHOOTING' => 'Something not working after the restoration? Click here for troubleshooting instructions.', + 'UPDATE_HEADER' => 'An updated version of Akeeba Kickstart (unknown) is available!', + 'UPDATE_NOTICE' => 'You are advised to always use the latest version of Akeeba Kickstart available. Older versions may be subject to bugs and will not be supported.', + 'UPDATE_DLNOW' => 'Download now', + 'UPDATE_MOREINFO' => 'More information', + 'IGNORE_MOST_ERRORS' => 'Ignore most errors', + 'WRONG_FTP_PATH2' => 'Wrong FTP initial directory - the directory doesn\'t correspond to your site\'s web root', + 'ARCHIVE_DIRECTORY' => 'Archive directory:', + 'RELOAD_ARCHIVES' => 'Reload', + 'CONFIG_UI_SFTPBROWSER_TITLE' => 'SFTP Directory Browser', + 'ERR_COULD_NOT_OPEN_ARCHIVE_PART' => 'Could not open archive part file %s for reading. Check that the file exists, is readable by the web server and is not in a directory made out of reach by chroot, open_basedir restrictions or any other restriction put in place by your host.', + 'RENAME_FILES' => 'Rename server configuration files', + 'RESTORE_PERMISSIONS' => 'Restore file permissions', + 'EXTRACT_LIST' => 'Files to extract', + 'EXTRACT_LIST_HELP' => 'Enter a file path such as images/cat.png or shell pattern such as images/*.png on each line. Only files matching this list will be written to disk. Leave empty to extract everything (default).', + ); + + /** + * The array holding the translation keys + * + * @var array + */ + private $strings; + + /** + * The currently detected language (ISO code) + * + * @var string + */ + private $language; + + /* + * Initializes the translation engine + * @return AKText + */ + public function __construct() + { + // Start with the default translation + $this->strings = $this->default_translation; + // Try loading the translation file in English, if it exists + $this->loadTranslation('en-GB'); + // Try loading the translation file in the browser's preferred language, if it exists + $this->getBrowserLanguage(); + if (!is_null($this->language)) + { + $this->loadTranslation(); + } + } + + private function loadTranslation($lang = null) + { + if (defined('KSLANGDIR')) + { + $dirname = KSLANGDIR; + } + else + { + $dirname = KSROOTDIR; + } + $basename = basename(__FILE__, '.php') . '.ini'; + if (empty($lang)) + { + $lang = $this->language; + } + + $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; + if (!@file_exists($translationFilename) && ($basename != 'kickstart.ini')) + { + $basename = 'kickstart.ini'; + $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; + } + if (!@file_exists($translationFilename)) + { + return; + } + $temp = self::parse_ini_file($translationFilename, false); + + if (!is_array($this->strings)) + { + $this->strings = array(); + } + if (empty($temp)) + { + $this->strings = array_merge($this->default_translation, $this->strings); + } + else + { + $this->strings = array_merge($this->strings, $temp); + } + } + + /** + * A PHP based INI file parser. + * + * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on + * the parse_ini_file page on http://gr.php.net/parse_ini_file + * + * @param string $file Filename to process + * @param bool $process_sections True to also process INI sections + * + * @return array An associative array of sections, keys and values + * @access private + */ + public static function parse_ini_file($file, $process_sections = false, $raw_data = false) + { + $process_sections = ($process_sections !== true) ? false : true; + + if (!$raw_data) + { + $ini = @file($file); + } + else + { + $ini = $file; + } + if (count($ini) == 0) + { + return array(); + } + + $sections = array(); + $values = array(); + $result = array(); + $globals = array(); + $i = 0; + if (!empty($ini)) + { + foreach ($ini as $line) + { + $line = trim($line); + $line = str_replace("\t", " ", $line); + + // Comments + if (!preg_match('/^[a-zA-Z0-9[]/', $line)) + { + continue; + } + + // Sections + if ($line{0} == '[') + { + $tmp = explode(']', $line); + $sections[] = trim(substr($tmp[0], 1)); + $i++; + continue; + } + + // Key-value pair + list($key, $value) = explode('=', $line, 2); + $key = trim($key); + $value = trim($value); + if (strstr($value, ";")) + { + $tmp = explode(';', $value); + if (count($tmp) == 2) + { + if ((($value{0} != '"') && ($value{0} != "'")) || + preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) || + preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value) + ) + { + $value = $tmp[0]; + } + } + else + { + if ($value{0} == '"') + { + $value = preg_replace('/^"(.*)".*/', '$1', $value); + } + elseif ($value{0} == "'") + { + $value = preg_replace("/^'(.*)'.*/", '$1', $value); + } + else + { + $value = $tmp[0]; + } + } + } + $value = trim($value); + $value = trim($value, "'\""); + + if ($i == 0) + { + if (substr($line, -1, 2) == '[]') + { + $globals[$key][] = $value; + } + else + { + $globals[$key] = $value; + } + } + else + { + if (substr($line, -1, 2) == '[]') + { + $values[$i - 1][$key][] = $value; + } + else + { + $values[$i - 1][$key] = $value; + } + } + } + } + + for ($j = 0; $j < $i; $j++) + { + if ($process_sections === true) + { + $result[$sections[$j]] = $values[$j]; + } + else + { + $result[] = $values[$j]; + } + } + + return $result + $globals; + } + + public function getBrowserLanguage() + { + // Detection code from Full Operating system language detection, by Harald Hope + // Retrieved from http://techpatterns.com/downloads/php_language_detection.php + $user_languages = array(); + //check to see if language is set + if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) + { + $languages = strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"]); + // $languages = ' fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3'; + // need to remove spaces from strings to avoid error + $languages = str_replace(' ', '', $languages); + $languages = explode(",", $languages); + + foreach ($languages as $language_list) + { + // pull out the language, place languages into array of full and primary + // string structure: + $temp_array = array(); + // slice out the part before ; on first step, the part before - on second, place into array + $temp_array[0] = substr($language_list, 0, strcspn($language_list, ';'));//full language + $temp_array[1] = substr($language_list, 0, 2);// cut out primary language + if ((strlen($temp_array[0]) == 5) && ((substr($temp_array[0], 2, 1) == '-') || (substr($temp_array[0], 2, 1) == '_'))) + { + $langLocation = strtoupper(substr($temp_array[0], 3, 2)); + $temp_array[0] = $temp_array[1] . '-' . $langLocation; + } + //place this array into main $user_languages language array + $user_languages[] = $temp_array; + } + } + else// if no languages found + { + $user_languages[0] = array('', ''); //return blank array. + } + + $this->language = null; + $basename = basename(__FILE__, '.php') . '.ini'; + + // Try to match main language part of the filename, irrespective of the location, e.g. de_DE will do if de_CH doesn't exist. + if (class_exists('AKUtilsLister')) + { + $fs = new AKUtilsLister(); + $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); + if (empty($iniFiles) && ($basename != 'kickstart.ini')) + { + $basename = 'kickstart.ini'; + $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); + } + } + else + { + $iniFiles = null; + } + + if (is_array($iniFiles)) + { + foreach ($user_languages as $languageStruct) + { + if (is_null($this->language)) + { + // Get files matching the main lang part + $iniFiles = $fs->getFiles(KSROOTDIR, $languageStruct[1] . '-??.' . $basename); + if (count($iniFiles) > 0) + { + $filename = $iniFiles[0]; + $filename = substr($filename, strlen(KSROOTDIR) + 1); + $this->language = substr($filename, 0, 5); + } + else + { + $this->language = null; + } + } + } + } + + if (is_null($this->language)) + { + // Try to find a full language match + foreach ($user_languages as $languageStruct) + { + if (@file_exists($languageStruct[0] . '.' . $basename) && is_null($this->language)) + { + $this->language = $languageStruct[0]; + } + else + { + + } + } + } + else + { + // Do we have an exact match? + foreach ($user_languages as $languageStruct) + { + if (substr($this->language, 0, strlen($languageStruct[1])) == $languageStruct[1]) + { + if (file_exists($languageStruct[0] . '.' . $basename)) + { + $this->language = $languageStruct[0]; + } + } + } + } + + // Now, scan for full language based on the partial match + + } + + public static function sprintf($key) + { + $text = self::getInstance(); + $args = func_get_args(); + if (count($args) > 0) + { + $args[0] = $text->_($args[0]); + + return @call_user_func_array('sprintf', $args); + } + + return ''; + } + + /** + * Singleton pattern for Language + * + * @return AKText The global AKText instance + */ + public static function &getInstance() + { + static $instance; + + if (!is_object($instance)) + { + $instance = new AKText(); + } + + return $instance; + } + + public static function _($string) + { + $text = self::getInstance(); + + $key = strtoupper($string); + $key = substr($key, 0, 1) == '_' ? substr($key, 1) : $key; + + if (isset ($text->strings[$key])) + { + $string = $text->strings[$key]; + } + else + { + if (defined($string)) + { + $string = constant($string); + } + } + + return $string; + } + + public function dumpLanguage() + { + $out = ''; + foreach ($this->strings as $key => $value) + { + $out .= "$key=$value\n"; + } + + return $out; + } + + public function asJavascript() + { + $out = ''; + foreach ($this->strings as $key => $value) + { + $key = addcslashes($key, '\\\'"'); + $value = addcslashes($value, '\\\'"'); + if (!empty($out)) + { + $out .= ",\n"; + } + $out .= "'$key':\t'$value'"; + } + + return $out; + } + + public function resetTranslation() + { + $this->strings = $this->default_translation; + } + + public function addDefaultLanguageStrings($stringList = array()) + { + if (!is_array($stringList)) + { + return; + } + if (empty($stringList)) + { + return; + } + + $this->strings = array_merge($stringList, $this->strings); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The Akeeba Kickstart Factory class + * + * This class is reponssible for instantiating all Akeeba Kickstart classes + */ +class AKFactory +{ + /** @var array A list of instantiated objects */ + private $objectlist = array(); + + /** @var array Simple hash data storage */ + private $varlist = array(); + + /** @var self Static instance */ + private static $instance = null; + + /** + * AKFactory constructor. + * + * This is a private constructor makes sure we can't instantiate the class unless we go through the static + * getInstance singleton method. This is different than making the class abstract (preventing any kind of object + * instantiation). + */ + private function __construct() + { + } + + /** + * Gets a serialized snapshot of the Factory for safekeeping (hibernate) + * + * @return string The serialized snapshot of the Factory + */ + public static function serialize() + { + $engine = self::getUnarchiver(); + $engine->shutdown(); + $serialized = serialize(self::getInstance()); + + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + $serialized = base64_encode($serialized); + } + + return $serialized; + } + + /** + * Gets the unarchiver engine + * + * @return AKAbstractUnarchiver + */ + public static function &getUnarchiver($configOverride = null) + { + static $class_name; + + if (!empty($configOverride) && isset($configOverride['reset']) && $configOverride['reset']) + { + $class_name = null; + } + + if (empty($class_name)) + { + $filetype = self::get('kickstart.setup.filetype', null); + + if (empty($filetype)) + { + $filename = self::get('kickstart.setup.sourcefile', null); + $basename = basename($filename); + $baseextension = strtoupper(substr($basename, -3)); + + switch ($baseextension) + { + case 'JPA': + $filetype = 'JPA'; + break; + + case 'JPS': + $filetype = 'JPS'; + break; + + case 'ZIP': + $filetype = 'ZIP'; + break; + + default: + die('Invalid archive type or extension in file ' . $filename); + break; + } + } + + $class_name = 'AKUnarchiver' . ucfirst($filetype); + } + + $destdir = self::get('kickstart.setup.destdir', null); + + if (empty($destdir)) + { + $destdir = KSROOTDIR; + } + + /** @var AKAbstractUnarchiver $object */ + $object = self::getClassInstance($class_name); + + if ($object->getState() == 'init') + { + $sourcePath = self::get('kickstart.setup.sourcepath', ''); + $sourceFile = self::get('kickstart.setup.sourcefile', ''); + + if (!empty($sourcePath)) + { + $sourceFile = rtrim($sourcePath, '/\\') . '/' . $sourceFile; + } + + // Initialize the object –– Any change here MUST be reflected to echoHeadJavascript (default values) + $config = array( + 'filename' => $sourceFile, + 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0), + 'post_proc' => self::get('kickstart.procengine', 'direct'), + 'add_path' => self::get('kickstart.setup.targetpath', $destdir), + 'remove_path' => self::get('kickstart.setup.removepath', ''), + 'rename_files' => self::get('kickstart.setup.renamefiles', array( + '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak', 'web.config' => 'web.config.bak', + '.user.ini' => '.user.ini.bak', + )), + 'skip_files' => self::get('kickstart.setup.skipfiles', array( + basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', + 'cacert.pem', + )), + 'ignoredirectories' => self::get('kickstart.setup.ignoredirectories', array( + 'tmp', 'log', 'logs', + )), + ); + + if (!defined('KICKSTART')) + { + // In restore.php mode we have to exclude the restoration.php files + $moreSkippedFiles = array( + // Akeeba Backup for Joomla! + 'administrator/components/com_akeeba/restoration.php', + // Joomla! Update + 'administrator/components/com_joomlaupdate/restoration.php', + // Akeeba Backup for WordPress + 'wp-content/plugins/akeebabackupwp/app/restoration.php', + 'wp-content/plugins/akeebabackupcorewp/app/restoration.php', + 'wp-content/plugins/akeebabackup/app/restoration.php', + 'wp-content/plugins/akeebabackupwpcore/app/restoration.php', + // Akeeba Solo + 'app/restoration.php', + ); + + $config['skip_files'] = array_merge($config['skip_files'], $moreSkippedFiles); + } + + if (!empty($configOverride)) + { + $config = array_merge($config, $configOverride); + } + + $object->setup($config); + } + + return $object; + } + + // ======================================================================== + // Public factory interface + // ======================================================================== + + public static function get($key, $default = null) + { + $self = self::getInstance(); + + if (array_key_exists($key, $self->varlist)) + { + return $self->varlist[$key]; + } + + return $default; + } + + /** + * Gets a single, internally used instance of the Factory + * + * @param string $serialized_data [optional] Serialized data to spawn the instance from + * + * @return AKFactory A reference to the unique Factory object instance + */ + protected static function &getInstance($serialized_data = null) + { + if (!is_object(self::$instance) || !is_null($serialized_data)) + { + if (!is_null($serialized_data)) + { + self::$instance = unserialize($serialized_data); + + return self::$instance; + } + + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Internal function which instantiates a class named $class_name. + * The autoloader + * + * @param string $class_name + * + * @return object + */ + protected static function &getClassInstance($class_name) + { + $self = self::getInstance(); + + if (!isset($self->objectlist[$class_name])) + { + $self->objectlist[$class_name] = new $class_name; + } + + return $self->objectlist[$class_name]; + } + + // ======================================================================== + // Public hash data storage interface + // ======================================================================== + + /** + * Regenerates the full Factory state from a serialized snapshot (resume) + * + * @param string $serialized_data The serialized snapshot to resume from + */ + public static function unserialize($serialized_data) + { + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + $serialized_data = base64_decode($serialized_data); + } + + self::getInstance($serialized_data); + } + + /** + * Reset the internal factory state, freeing all previously created objects + */ + public static function nuke() + { + self::$instance = null; + } + + // ======================================================================== + // Akeeba Kickstart classes + // ======================================================================== + + public static function set($key, $value) + { + $self = self::getInstance(); + $self->varlist[$key] = $value; + } + + /** + * Gets the post processing engine + * + * @param string $proc_engine + * + * @return AKAbstractPostproc + */ + public static function &getPostProc($proc_engine = null) + { + static $class_name; + + if (empty($class_name)) + { + if (empty($proc_engine)) + { + $proc_engine = self::get('kickstart.procengine', 'direct'); + } + + $class_name = 'AKPostproc' . ucfirst($proc_engine); + } + + return self::getClassInstance($class_name); + } + + /** + * Get the a reference to the Akeeba Engine's timer + * + * @return AKCoreTimer + */ + public static function &getTimer() + { + return self::getClassInstance('AKCoreTimer'); + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Interface for AES encryption adapters + */ +interface AKEncryptionAESAdapterInterface +{ + /** + * Decrypts a string. Returns the raw binary ciphertext, zero-padded. + * + * @param string $plainText The plaintext to encrypt + * @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size) + * + * @return string The raw encrypted binary string. + */ + public function decrypt($plainText, $key); + + /** + * Returns the encryption block size in bytes + * + * @return int + */ + public function getBlockSize(); + + /** + * Is this adapter supported? + * + * @return bool + */ + public function isSupported(); +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Abstract AES encryption class + */ +abstract class AKEncryptionAESAdapterAbstract +{ + /** + * Trims or zero-pads a key / IV + * + * @param string $key The key or IV to treat + * @param int $size The block size of the currently used algorithm + * + * @return null|string Null if $key is null, treated string of $size byte length otherwise + */ + public function resizeKey($key, $size) + { + if (empty($key)) + { + return null; + } + + $keyLength = strlen($key); + + if (function_exists('mb_strlen')) + { + $keyLength = mb_strlen($key, 'ASCII'); + } + + if ($keyLength == $size) + { + return $key; + } + + if ($keyLength > $size) + { + if (function_exists('mb_substr')) + { + return mb_substr($key, 0, $size, 'ASCII'); + } + + return substr($key, 0, $size); + } + + return $key . str_repeat("\0", ($size - $keyLength)); + } + + /** + * Returns null bytes to append to the string so that it's zero padded to the specified block size + * + * @param string $string The binary string which will be zero padded + * @param int $blockSize The block size + * + * @return string The zero bytes to append to the string to zero pad it to $blockSize + */ + protected function getZeroPadding($string, $blockSize) + { + $stringSize = strlen($string); + + if (function_exists('mb_strlen')) + { + $stringSize = mb_strlen($string, 'ASCII'); + } + + if ($stringSize == $blockSize) + { + return ''; + } + + if ($stringSize < $blockSize) + { + return str_repeat("\0", $blockSize - $stringSize); + } + + $paddingBytes = $stringSize % $blockSize; + + return str_repeat("\0", $blockSize - $paddingBytes); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +class Mcrypt extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface +{ + protected $cipherType = MCRYPT_RIJNDAEL_128; + + protected $cipherMode = MCRYPT_MODE_CBC; + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('mcrypt_get_key_size')) + { + return false; + } + + if (!function_exists('mcrypt_get_iv_size')) + { + return false; + } + + if (!function_exists('mcrypt_create_iv')) + { + return false; + } + + if (!function_exists('mcrypt_encrypt')) + { + return false; + } + + if (!function_exists('mcrypt_decrypt')) + { + return false; + } + + if (!function_exists('mcrypt_list_algorithms')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = mcrypt_list_algorithms(); + + if (!in_array('rijndael-128', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-192', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-256', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + public function getBlockSize() + { + return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +class OpenSSL extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface +{ + /** + * The OpenSSL options for encryption / decryption + * + * @var int + */ + protected $openSSLOptions = 0; + + /** + * The encryption method to use + * + * @var string + */ + protected $method = 'aes-128-cbc'; + + public function __construct() + { + $this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('openssl_get_cipher_methods')) + { + return false; + } + + if (!function_exists('openssl_random_pseudo_bytes')) + { + return false; + } + + if (!function_exists('openssl_cipher_iv_length')) + { + return false; + } + + if (!function_exists('openssl_encrypt')) + { + return false; + } + + if (!function_exists('openssl_decrypt')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = openssl_get_cipher_methods(); + + if (!in_array('aes-128-cbc', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + /** + * @return int + */ + public function getBlockSize() + { + return openssl_cipher_iv_length($this->method); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * AES implementation in PHP (c) Chris Veness 2005-2016. + * Right to use and adapt is granted for under a simple creative commons attribution + * licence. No warranty of any form is offered. + * + * Heavily modified for Akeeba Backup by Nicholas K. Dionysopoulos + * Also added AES-128 CBC mode (with mcrypt and OpenSSL) on top of AES CTR + * Removed CTR encrypt / decrypt (no longer used) + */ +class AKEncryptionAES +{ + // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1] + protected static $Sbox = + array(0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16); + + // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2] + protected static $Rcon = array( + array(0x00, 0x00, 0x00, 0x00), + array(0x01, 0x00, 0x00, 0x00), + array(0x02, 0x00, 0x00, 0x00), + array(0x04, 0x00, 0x00, 0x00), + array(0x08, 0x00, 0x00, 0x00), + array(0x10, 0x00, 0x00, 0x00), + array(0x20, 0x00, 0x00, 0x00), + array(0x40, 0x00, 0x00, 0x00), + array(0x80, 0x00, 0x00, 0x00), + array(0x1b, 0x00, 0x00, 0x00), + array(0x36, 0x00, 0x00, 0x00)); + + protected static $passwords = array(); + + /** + * The algorithm to use for PBKDF2. Must be a supported hash_hmac algorithm. Default: sha1 + * + * @var string + */ + private static $pbkdf2Algorithm = 'sha1'; + + /** + * Number of iterations to use for PBKDF2 + * + * @var int + */ + private static $pbkdf2Iterations = 1000; + + /** + * Should we use a static salt for PBKDF2? + * + * @var int + */ + private static $pbkdf2UseStaticSalt = 0; + + /** + * The static salt to use for PBKDF2 + * + * @var string + */ + private static $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * AES Cipher function: encrypt 'input' with Rijndael algorithm + * + * @param array $input Message as byte-array (16 bytes) + * @param array $w key schedule as 2D byte-array (Nr+1 x Nb bytes) - + * generated from the cipher key by KeyExpansion() + * + * @return string Ciphertext as byte-array (16 bytes) + */ + protected static function Cipher($input, $w) + { + // main Cipher function [�5.1] + $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) + $Nr = count($w) / $Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys + + $state = array(); // initialise 4xNb byte-array 'state' with input [�3.4] + + for ($i = 0; $i < 4 * $Nb; $i++) + { + $state[$i % 4][floor($i / 4)] = $input[$i]; + } + + $state = self::AddRoundKey($state, $w, 0, $Nb); + + for ($round = 1; $round < $Nr; $round++) + { // apply Nr rounds + $state = self::SubBytes($state, $Nb); + $state = self::ShiftRows($state, $Nb); + $state = self::MixColumns($state); + $state = self::AddRoundKey($state, $w, $round, $Nb); + } + + $state = self::SubBytes($state, $Nb); + $state = self::ShiftRows($state, $Nb); + $state = self::AddRoundKey($state, $w, $Nr, $Nb); + + $output = array(4 * $Nb); // convert state to 1-d array before returning [�3.4] + + for ($i = 0; $i < 4 * $Nb; $i++) + { + $output[$i] = $state[$i % 4][floor($i / 4)]; + } + + return $output; + } + + protected static function AddRoundKey($state, $w, $rnd, $Nb) + { + // xor Round Key into state S [�5.1.4] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $state[$r][$c] ^= $w[$rnd * 4 + $c][$r]; + } + } + + return $state; + } + + protected static function SubBytes($s, $Nb) + { + // apply SBox to state S [�5.1.1] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $s[$r][$c] = self::$Sbox[$s[$r][$c]]; + } + } + + return $s; + } + + protected static function ShiftRows($s, $Nb) + { + // shift row r of state S left by r bytes [�5.1.2] + $t = array(4); + + for ($r = 1; $r < 4; $r++) + { + for ($c = 0; $c < 4; $c++) + { + $t[$c] = $s[$r][($c + $r) % $Nb]; + } // shift into temp copy + + for ($c = 0; $c < 4; $c++) + { + $s[$r][$c] = $t[$c]; + } // and copy back + } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES): + + return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf + } + + protected static function MixColumns($s) + { + // combine bytes of each col of state S [�5.1.3] + for ($c = 0; $c < 4; $c++) + { + $a = array(4); // 'a' is a copy of the current column from 's' + $b = array(4); // 'b' is a�{02} in GF(2^8) + + for ($i = 0; $i < 4; $i++) + { + $a[$i] = $s[$i][$c]; + $b[$i] = $s[$i][$c] & 0x80 ? $s[$i][$c] << 1 ^ 0x011b : $s[$i][$c] << 1; + } + + // a[n] ^ b[n] is a�{03} in GF(2^8) + $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3 + $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3 + $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3 + $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3 + } + + return $s; + } + + /** + * Key expansion for Rijndael Cipher(): performs key expansion on cipher key + * to generate a key schedule + * + * @param array $key Cipher key byte-array (16 bytes) + * + * @return array Key schedule as 2D byte-array (Nr+1 x Nb bytes) + */ + protected static function KeyExpansion($key) + { + // generate Key Schedule from Cipher Key [�5.2] + + // block size (in words): no of columns in state (fixed at 4 for AES) + $Nb = 4; + // key length (in words): 4/6/8 for 128/192/256-bit keys + $Nk = (int) (count($key) / 4); + // no of rounds: 10/12/14 for 128/192/256-bit keys + $Nr = $Nk + 6; + + $w = array(); + $temp = array(); + + for ($i = 0; $i < $Nk; $i++) + { + $r = array($key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]); + $w[$i] = $r; + } + + for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++) + { + $w[$i] = array(); + for ($t = 0; $t < 4; $t++) + { + $temp[$t] = $w[$i - 1][$t]; + } + if ($i % $Nk == 0) + { + $temp = self::SubWord(self::RotWord($temp)); + for ($t = 0; $t < 4; $t++) + { + $rConIndex = (int) ($i / $Nk); + $temp[$t] ^= self::$Rcon[$rConIndex][$t]; + } + } + else if ($Nk > 6 && $i % $Nk == 4) + { + $temp = self::SubWord($temp); + } + for ($t = 0; $t < 4; $t++) + { + $w[$i][$t] = $w[$i - $Nk][$t] ^ $temp[$t]; + } + } + + return $w; + } + + protected static function SubWord($w) + { + // apply SBox to 4-byte word w + for ($i = 0; $i < 4; $i++) + { + $w[$i] = self::$Sbox[$w[$i]]; + } + + return $w; + } + + /* + * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints + * + * @param a number to be shifted (32-bit integer) + * @param b number of bits to shift a to the right (0..31) + * @return a right-shifted and zero-filled by b bits + */ + + protected static function RotWord($w) + { + // rotate 4-byte word w left by one byte + $tmp = $w[0]; + for ($i = 0; $i < 3; $i++) + { + $w[$i] = $w[$i + 1]; + } + $w[3] = $tmp; + + return $w; + } + + protected static function urs($a, $b) + { + $a &= 0xffffffff; + $b &= 0x1f; // (bounds check) + if ($a & 0x80000000 && $b > 0) + { // if left-most bit set + $a = ($a >> 1) & 0x7fffffff; // right-shift one bit & clear left-most bit + $a = $a >> ($b - 1); // remaining right-shifts + } + else + { // otherwise + $a = ($a >> $b); // use normal right-shift + } + + return $a; + } + + /** + * AES decryption in CBC mode. This is the standard mode (the CTR methods + * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). + * + * It supports AES-128 only. It assumes that the last 4 bytes + * contain a little-endian unsigned long integer representing the unpadded + * data length. + * + * @since 3.0.1 + * @author Nicholas K. Dionysopoulos + * + * @param string $ciphertext The data to encrypt + * @param string $password Encryption password + * + * @return string The plaintext + */ + public static function AESDecryptCBC($ciphertext, $password) + { + $adapter = self::getAdapter(); + + if (!$adapter->isSupported()) + { + return false; + } + + // Read the data size + $data_size = unpack('V', substr($ciphertext, -4)); + + // Do I have a PBKDF2 salt? + $salt = substr($ciphertext, -92, 68); + $rightStringLimit = -4; + + $params = self::getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $useStaticSalt = $params['useStaticSalt']; + + if (substr($salt, 0, 4) == 'JPST') + { + // We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes + // (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the + // uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the + // format specification). + $salt = substr($salt, 4); + $rightStringLimit -= 68; + + $key = self::pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); + } + elseif ($useStaticSalt) + { + // We have a static salt. Use it for PBKDF2. + $key = self::getStaticSaltExpandedKey($password); + } + else + { + // Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD. + $key = self::expandKey($password); + } + + // Try to get the IV from the data + $iv = substr($ciphertext, -24, 20); + + if (substr($iv, 0, 4) == 'JPIV') + { + // We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes + // (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length) + $iv = substr($iv, 4); + $rightStringLimit -= 20; + } + else + { + // No stored IV. Do it the dumb way. + $iv = self::createTheWrongIV($password); + } + + // Decrypt + $plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key); + + // Trim padding, if necessary + if (strlen($plaintext) > $data_size) + { + $plaintext = substr($plaintext, 0, $data_size); + } + + return $plaintext; + } + + /** + * That's the old way of creating an IV that's definitely not cryptographically sound. + * + * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA + * + * @param string $password The raw password from which we create an IV in a super bozo way + * + * @return string A 16-byte IV string + */ + public static function createTheWrongIV($password) + { + static $ivs = array(); + + $key = md5($password); + + if (!isset($ivs[$key])) + { + $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes + $pwBytes = array(); + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + $iv = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); + $newIV = ''; + foreach ($iv as $int) + { + $newIV .= chr($int); + } + + $ivs[$key] = $newIV; + } + + return $ivs[$key]; + } + + /** + * Expand the password to an appropriate 128-bit encryption key + * + * @param string $password + * + * @return string + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public static function expandKey($password) + { + // Try to fetch cached key or create it if it doesn't exist + $nBits = 128; + $lookupKey = md5($password . '-' . $nBits); + + if (array_key_exists($lookupKey, self::$passwords)) + { + $key = self::$passwords[$lookupKey]; + + return $key; + } + + // use AES itself to encrypt password to get cipher key (using plain password as source for + // key expansion) - gives us well encrypted key. + $nBytes = $nBits / 8; // Number of bytes in key + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); + $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long + $newKey = ''; + + foreach ($key as $int) + { + $newKey .= chr($int); + } + + $key = $newKey; + + self::$passwords[$lookupKey] = $key; + + return $key; + } + + /** + * Returns the correct AES-128 CBC encryption adapter + * + * @return AKEncryptionAESAdapterInterface + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public static function getAdapter() + { + static $adapter = null; + + if (is_object($adapter) && ($adapter instanceof AKEncryptionAESAdapterInterface)) + { + return $adapter; + } + + $adapter = new OpenSSL(); + + if (!$adapter->isSupported()) + { + $adapter = new Mcrypt(); + } + + return $adapter; + } + + /** + * @return string + */ + public static function getPbkdf2Algorithm() + { + return self::$pbkdf2Algorithm; + } + + /** + * @param string $pbkdf2Algorithm + * @return void + */ + public static function setPbkdf2Algorithm($pbkdf2Algorithm) + { + self::$pbkdf2Algorithm = $pbkdf2Algorithm; + } + + /** + * @return int + */ + public static function getPbkdf2Iterations() + { + return self::$pbkdf2Iterations; + } + + /** + * @param int $pbkdf2Iterations + * @return void + */ + public static function setPbkdf2Iterations($pbkdf2Iterations) + { + self::$pbkdf2Iterations = $pbkdf2Iterations; + } + + /** + * @return int + */ + public static function getPbkdf2UseStaticSalt() + { + return self::$pbkdf2UseStaticSalt; + } + + /** + * @param int $pbkdf2UseStaticSalt + * @return void + */ + public static function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt) + { + self::$pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt; + } + + /** + * @return string + */ + public static function getPbkdf2StaticSalt() + { + return self::$pbkdf2StaticSalt; + } + + /** + * @param string $pbkdf2StaticSalt + * @return void + */ + public static function setPbkdf2StaticSalt($pbkdf2StaticSalt) + { + self::$pbkdf2StaticSalt = $pbkdf2StaticSalt; + } + + /** + * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static + * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block + * to minimize the risk of attacks against the password. + * + * @return array + */ + public static function getKeyDerivationParameters() + { + return array( + 'keySize' => 16, + 'algorithm' => self::$pbkdf2Algorithm, + 'iterations' => self::$pbkdf2Iterations, + 'useStaticSalt' => self::$pbkdf2UseStaticSalt, + 'staticSalt' => self::$pbkdf2StaticSalt, + ); + } + + /** + * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt + * + * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt + * + * This implementation of PBKDF2 was originally created by https://defuse.ca + * With improvements by http://www.variations-of-shadow.com + * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster) + * + * @param string $password The password. + * @param string $salt A salt that is unique to the password. + * @param string $algorithm The hash algorithm to use. Default is sha1. + * @param int $count Iteration count. Higher is better, but slower. Default: 1000. + * @param int $key_length The length of the derived key in bytes. + * + * @return string A string of $key_length bytes + */ + public static function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16) + { + if (function_exists("hash_pbkdf2")) + { + return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true); + } + + $hash_length = akstringlen(hash($algorithm, "", true)); + $block_count = ceil($key_length / $hash_length); + + $output = ""; + + for ($i = 1; $i <= $block_count; $i++) + { + // $i encoded as 4 bytes, big endian. + $last = $salt . pack("N", $i); + + // First iteration + $xorResult = hash_hmac($algorithm, $last, $password, true); + $last = $xorResult; + + // Perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) + { + $last = hash_hmac($algorithm, $last, $password, true); + $xorResult ^= $last; + } + + $output .= $xorResult; + } + + return aksubstr($output, 0, $key_length); + } + + /** + * Get the expanded key from the user supplied password using a static salt. The results are cached for performance + * reasons. + * + * @param string $password The user-supplied password, UTF-8 encoded. + * + * @return string The expanded key + */ + private static function getStaticSaltExpandedKey($password) + { + $params = self::getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $staticSalt = $params['staticSalt']; + + $lookupKey = "PBKDF2-$algorithm-$iterations-" . md5($password . $staticSalt); + + if (!array_key_exists($lookupKey, self::$passwords)) + { + self::$passwords[$lookupKey] = self::pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes); + } + + return self::$passwords[$lookupKey]; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A timing safe equals comparison + * + * @param string $safe The internal (safe) value to be checked + * @param string $user The user submitted (unsafe) value + * + * @return boolean True if the two strings are identical. + * + * @see http://blog.ircmaxell.com/2014/11/its-all-about-time.html + */ +function timingSafeEquals($safe, $user) +{ + $safeLen = strlen($safe); + $userLen = strlen($user); + + if ($userLen != $safeLen) + { + return false; + } + + $result = 0; + + for ($i = 0; $i < $userLen; $i++) + { + $result |= (ord($safe[$i]) ^ ord($user[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; +} + +/** + * The Master Setup will read the configuration parameters from restoration.php or + * the JSON-encoded "configuration" input variable and return the status. + * + * @return bool True if the master configuration was applied to the Factory object + */ +function masterSetup() +{ + // ------------------------------------------------------------ + // 1. Import basic setup parameters + // ------------------------------------------------------------ + + $ini_data = null; + + // In restore.php mode, require restoration.php or fail + if (!defined('KICKSTART')) + { + // This is the standalone mode, used by Akeeba Backup Professional. It looks for a restoration.php + // file to perform its magic. If the file is not there, we will abort. + $setupFile = 'restoration.php'; + + if (!file_exists($setupFile)) + { + AKFactory::set('kickstart.enabled', false); + + return false; + } + + // Load restoration.php. It creates a global variable named $restoration_setup + require_once $setupFile; + + $ini_data = $restoration_setup; + + if (empty($ini_data)) + { + // No parameters fetched. Darn, how am I supposed to work like that?! + AKFactory::set('kickstart.enabled', false); + + return false; + } + + AKFactory::set('kickstart.enabled', true); + } + else + { + // Maybe we have $restoration_setup defined in the head of kickstart.php + global $restoration_setup; + + if (!empty($restoration_setup) && !is_array($restoration_setup)) + { + $ini_data = AKText::parse_ini_file($restoration_setup, false, true); + } + elseif (is_array($restoration_setup)) + { + $ini_data = $restoration_setup; + } + } + + // Import any data from $restoration_setup + if (!empty($ini_data)) + { + foreach ($ini_data as $key => $value) + { + AKFactory::set($key, $value); + } + AKFactory::set('kickstart.enabled', true); + } + + // Reinitialize $ini_data + $ini_data = null; + + // ------------------------------------------------------------ + // 2. Explode JSON parameters into $_REQUEST scope + // ------------------------------------------------------------ + + // Detect a JSON string in the request variable and store it. + $json = getQueryParam('json', null); + + // Detect a password in the request variable and store it. + $userPassword = getQueryParam('password', ''); + + // Remove everything from the request, post and get arrays + if (!empty($_REQUEST)) + { + foreach ($_REQUEST as $key => $value) + { + unset($_REQUEST[$key]); + } + } + + if (!empty($_POST)) + { + foreach ($_POST as $key => $value) + { + unset($_POST[$key]); + } + } + + if (!empty($_GET)) + { + foreach ($_GET as $key => $value) + { + unset($_GET[$key]); + } + } + + // Authentication - Akeeba Restore 5.4.0 or later + $password = AKFactory::get('kickstart.security.password', null); + $isAuthenticated = false; + + /** + * Akeeba Restore 5.3.1 and earlier use a custom implementation of AES-128 in CTR mode to encrypt the JSON data + * between client and server. This is not used as a means to maintain secrecy (it's symmetrical encryption and the + * key is, by necessity, transmitted with the HTML page to the client). It's meant as a form of authentication, so + * that the server part can ensure that it only receives commands by an authorized client. + * + * The downside is that encryption in CTR mode (like CBC) is an all-or-nothing affair. This opens the possibility + * for a padding oracle attack (https://en.wikipedia.org/wiki/Padding_oracle_attack). While Akeeba Restore was + * hardened in 2014 to prevent the bulk of suck attacks it is still possible to attack the encryption using a very + * large number of requests (several dozens of thousands). + * + * Since Akeeba Restore 5.4.0 we have removed this authentication method and replaced it with the transmission of a + * very large length password. On the server side we use a timing safe password comparison. By its very nature, it + * will only leak the (well known, constant and large) length of the password but no more information about the + * password itself. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html As a result this form of + * authentication is many orders of magnitude harder to crack than regular encryption. + * + * Now you may wonder "how is sending a password in the clear hardier than encryption?". If you ask that question + * you were not paying attention. The password needs to be known by BOTH the server AND the client (browser). Since + * this password is generated programmatically by the server, it MUST be sent to the client by the server. If an + * attacker is able to intercept this transmission (man in the middle attack) using encryption is irrelevant: the + * attacker already knows your password. This situation also applies when the user sends their own password to the + * server, e.g. when logging into their site. The ONLY way to avoid security issues regarding information being + * stolen in transit is using HTTPS with a commercially signed SSL certificate. Unlike 2008, when Kickstart was + * originally written, obtaining such a certificate nowadays is trivial and costs absolutely nothing thanks to Let's + * Encrypt (https://letsencrypt.org/). + * + * TL;DR: Use HTTPS with a commercially signed SSL certificate, e.g. a free certificate from Let's Encrypt. Client- + * side cryptography does NOT protect you against an attacker (see + * https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-considered-harmful/). + * Moreover, sending a plaintext password is safer than relying on client-side encryption for authentication as it + * reoves the possibility of an attacker inferring the contents of the authentication key (password) in a relatively + * easy and automated manner. + */ + if (!empty($password)) + { + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) + { + die('###{"status":false,"message":"Invalid login"}###'); + } + } + + // No JSON data? Die. + if (empty($json)) + { + die('###{"status":false,"message":"Invalid JSON data"}###'); + } + + // Handle the JSON string + $raw = json_decode($json, true); + + // Invalid JSON data? + if (empty($raw)) + { + die('###{"status":false,"message":"Invalid JSON data"}###'); + } + + // Pass all JSON data to the request array + if (!empty($raw)) + { + foreach ($raw as $key => $value) + { + $_REQUEST[$key] = $value; + } + } + + // ------------------------------------------------------------ + // 3. Try the "factory" variable + // ------------------------------------------------------------ + // A "factory" variable will override all other settings. + $serialized = getQueryParam('factory', null); + + if (!is_null($serialized)) + { + // Get the serialized factory + AKFactory::unserialize($serialized); + AKFactory::set('kickstart.enabled', true); + + return true; + } + + // ------------------------------------------------------------ + // 4. Try the configuration variable for Kickstart + // ------------------------------------------------------------ + if (defined('KICKSTART')) + { + $configuration = getQueryParam('configuration'); + + if (!is_null($configuration)) + { + // Let's decode the configuration from JSON to array + $ini_data = json_decode($configuration, true); + } + else + { + // Neither exists. Enable Kickstart's interface anyway. + $ini_data = array('kickstart.enabled' => true); + } + + // Import any INI data we might have from other sources + if (!empty($ini_data)) + { + foreach ($ini_data as $key => $value) + { + AKFactory::set($key, $value); + } + + AKFactory::set('kickstart.enabled', true); + + return true; + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +// Mini-controller for restore.php +if (!defined('KICKSTART')) +{ + // The observer class, used to report number of files and bytes processed + class RestorationObserver extends AKAbstractPartObserver + { + public $compressedTotal = 0; + public $uncompressedTotal = 0; + public $filesProcessed = 0; + + public function update($object, $message) + { + if (!is_object($message)) + { + return; + } + + if (!array_key_exists('type', get_object_vars($message))) + { + return; + } + + if ($message->type == 'startfile') + { + $this->filesProcessed++; + $this->compressedTotal += $message->content->compressed; + $this->uncompressedTotal += $message->content->uncompressed; + } + } + + public function __toString() + { + return __CLASS__; + } + + } + + // Import configuration + masterSetup(); + + $retArray = array( + 'status' => true, + 'message' => null + ); + + $enabled = AKFactory::get('kickstart.enabled', false); + + if ($enabled) + { + $task = getQueryParam('task'); + + switch ($task) + { + case 'ping': + // ping task - realy does nothing! + $timer = AKFactory::getTimer(); + $timer->enforce_min_exec_time(); + break; + + /** + * There are two separate steps here since we were using an inefficient restoration intialization method in + * the past. Now both startRestore and stepRestore are identical. The difference in behavior depends + * exclusively on the calling Javascript. If no serialized factory was passed in the request then we start a + * new restoration. If a serialized factory was passed in the request then the restoration is resumed. For + * this reason we should NEVER call AKFactory::nuke() in startRestore anymore: that would simply reset the + * extraction engine configuration which was done in masterSetup() leading to an error about the file being + * invalid (since no file is found). + */ + case 'startRestore': + case 'stepRestore': + $engine = AKFactory::getUnarchiver(); // Get the engine + $observer = new RestorationObserver(); // Create a new observer + $engine->attach($observer); // Attach the observer + $engine->tick(); + $ret = $engine->getStatusArray(); + + if ($ret['Error'] != '') + { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $ret['Error']; + } + elseif (!$ret['HasRun']) + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = true; + } + else + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['factory'] = AKFactory::serialize(); + } + break; + + case 'finalizeRestore': + $root = AKFactory::get('kickstart.setup.destdir'); + // Remove the installation directory + recursive_remove_directory($root . '/installation'); + + $postproc = AKFactory::getPostProc(); + + /** + * Should I rename the htaccess.bak and web.config.bak files back to their live filenames...? + */ + $renameFiles = AKFactory::get('kickstart.setup.postrenamefiles', true); + + if ($renameFiles) + { + // Rename htaccess.bak to .htaccess + if (file_exists($root . '/htaccess.bak')) + { + if (file_exists($root . '/.htaccess')) + { + $postproc->unlink($root . '/.htaccess'); + } + $postproc->rename($root . '/htaccess.bak', $root . '/.htaccess'); + } + + // Rename htaccess.bak to .htaccess + if (file_exists($root . '/web.config.bak')) + { + if (file_exists($root . '/web.config')) + { + $postproc->unlink($root . '/web.config'); + } + $postproc->rename($root . '/web.config.bak', $root . '/web.config'); + } + } + + // Remove restoration.php + $basepath = KSROOTDIR; + $basepath = rtrim(str_replace('\\', '/', $basepath), '/'); + if (!empty($basepath)) + { + $basepath .= '/'; + } + $postproc->unlink($basepath . 'restoration.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/restore_finalisation.php'; + if (file_exists($filename)) + { + // opcode cache busting before including the filename + if (function_exists('opcache_invalidate')) + { + opcache_invalidate($filename); + } + if (function_exists('apc_compile_file')) + { + apc_compile_file($filename); + } + if (function_exists('wincache_refresh_if_changed')) + { + wincache_refresh_if_changed(array($filename)); + } + if (function_exists('xcache_asm')) + { + xcache_asm($filename); + } + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeRestore')) + { + finalizeRestore($root, $basepath); + } + break; + + default: + // Invalid task! + $enabled = false; + break; + } + } + + // Maybe we weren't authorized or the task was invalid? + if (!$enabled) + { + // Maybe the user failed to enter any information + $retArray['status'] = false; + $retArray['message'] = AKText::_('ERR_INVALID_LOGIN'); + } + + // JSON encode the message + $json = json_encode($retArray); + + // Return the message + echo "###$json###"; + +} + +// ------------ lixlpixel recursive PHP functions ------------- +// recursive_remove_directory( directory to delete, empty ) +// expects path to directory and optional TRUE / FALSE to empty +// of course PHP has to have the rights to delete the directory +// you specify and all files and folders inside the directory +// ------------------------------------------------------------ +function recursive_remove_directory($directory) +{ + // if the path has a slash at the end we remove it here + if (substr($directory, -1) == '/') + { + $directory = substr($directory, 0, -1); + } + // if the path is not valid or is not a directory ... + if (!file_exists($directory) || !is_dir($directory)) + { + // ... we return false and exit the function + return false; + // ... if the path is not readable + } + elseif (!is_readable($directory)) + { + // ... we return false and exit the function + return false; + // ... else if the path is readable + } + else + { + // we open the directory + $handle = opendir($directory); + $postproc = AKFactory::getPostProc(); + // and scan through the items inside + while (false !== ($item = readdir($handle))) + { + // if the filepointer is not the current directory + // or the parent directory + if ($item != '.' && $item != '..') + { + // we build the new path to delete + $path = $directory . '/' . $item; + // if the new path is a directory + if (is_dir($path)) + { + // we call this function with the new path + recursive_remove_directory($path); + // if the new path is a file + } + else + { + // we remove the file + $postproc->unlink($path); + } + } + } + // close the directory + closedir($handle); + // try to delete the now empty directory + if (!$postproc->rmdir($directory)) + { + // return false if not possible + return false; + } + + // return success + return true; + } +} + + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ +class AKKickstartUtils +{ + /** + * Guess the best path containing backup archives. The default strategy is check in the current directory first, + * then attempt to find an Akeeba Backup for Joomla!, Akeeba Solo or Akeeba Backup for WordPress default backup + * output directory under the current root. The first one containing backup archives wins. + * + * @return string The path to get archives from + */ + public static function getBestArchivePath() + { + $basePath = self::getPath(); + $basePathSlash = (empty($basePath) ? '.' : rtrim($basePath, '/\\')) . '/'; + + $paths = array( + // Root, same as the directory we're in + $basePath, + // Standard temporary directory + $basePath . '/kicktemp', + // Akeeba Backup for Joomla!, default output directory + $basePathSlash . 'administrator/components/com_akeeba/backup', + // Akeeba Solo, default output directory + $basePathSlash . 'backups', + // Akeeba Backup for WordPress, default output directory + $basePathSlash . 'wp-content/plugins/akeebabackupwp/app/backups', + ); + + foreach ($paths as $path) + { + $archives = self::findArchives($path); + + if (!empty($archives)) + { + return $path; + } + } + + return $basePath; + } + + /** + * Gets the directory the file is in + * + * @return string + */ + public static function getPath() + { + $path = KSROOTDIR; + $path = rtrim(str_replace('\\', '/', $path), '/'); + if (!empty($path)) + { + $path .= '/'; + } + + return $path; + } + + /** + * Scans the current directory for archive files (JPA, JPS and ZIP format) + * + * @param string $path The path to look for archives. null for automatic path + * + * @return array + */ + public static function findArchives($path) + { + $ret = array(); + + if (empty($path)) + { + $path = self::getPath(); + } + + if (empty($path)) + { + $path = '.'; + } + + $dh = @opendir($path); + + if ($dh === false) + { + return $ret; + } + + while (false !== $file = @readdir($dh)) + { + $dotpos = strrpos($file, '.'); + + if ($dotpos === false) + { + continue; + } + + if ($dotpos == strlen($file)) + { + continue; + } + + $extension = strtolower(substr($file, $dotpos + 1)); + + if (in_array($extension, array('jpa', 'zip', 'jps'))) + { + $ret[] = $file; + } + } + + closedir($dh); + + if (!empty($ret)) + { + return $ret; + } + + // On some hosts using opendir doesn't work. Let's try Dir instead + $d = dir($path); + + while (false != ($file = $d->read())) + { + $dotpos = strrpos($file, '.'); + + if ($dotpos === false) + { + continue; + } + + if ($dotpos == strlen($file)) + { + continue; + } + + $extension = strtolower(substr($file, $dotpos + 1)); + + if (in_array($extension, array('jpa', 'zip', 'jps'))) + { + $ret[] = $file; + } + } + + return $ret; + } + + /** + * Gets the most appropriate temporary path + * + * @return string + */ + public static function getTemporaryPath() + { + $path = self::getPath(); + + $candidateDirs = array( + $path, + $path . '/kicktemp', + ); + + if (function_exists('sys_get_temp_dir')) + { + $candidateDirs[] = sys_get_temp_dir(); + } + + foreach ($candidateDirs as $dir) + { + if (is_dir($dir) && is_writable($dir)) + { + return $dir; + } + } + + // Failsafe + return $path; + } + + /** + * Scans the current directory for archive files and returns them as ' . "\n"; + } + + return $ret; + } +} + + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ +class ExtractionObserver extends AKAbstractPartObserver +{ + public $compressedTotal = 0; + public $uncompressedTotal = 0; + public $filesProcessed = 0; + public $totalSize = null; + public $fileList = null; + public $lastFile = ''; + + public function update($object, $message) + { + if (!is_object($message)) + { + return; + } + + if (!array_key_exists('type', get_object_vars($message))) + { + return; + } + + switch ($message->type) + { + // Sent when we read the list of archive parts and their total size + case 'totalsize': + $this->totalSize = $message->content->totalsize; + $this->fileList = $message->content->filelist; + break; + + // Sent when a file header is read from the archive + case 'startfile': + $this->lastFile = $message->content->file; + $this->filesProcessed++; + $this->compressedTotal += $message->content->compressed; + $this->uncompressedTotal += $message->content->uncompressed; + break; + } + + } + + public function __toString() + { + return __CLASS__; + } + +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +function callExtraFeature($method = null, array $params = array()) +{ + static $extraFeatureObjects = null; + + if (!is_array($extraFeatureObjects)) + { + $extraFeatureObjects = array(); + $allClasses = get_declared_classes(); + foreach ($allClasses as $class) + { + if (substr($class, 0, 9) == 'AKFeature') + { + $extraFeatureObjects[] = new $class; + } + } + } + + if (is_null($method)) + { + return; + } + + if (empty($extraFeatureObjects)) + { + return; + } + + $result = null; + foreach ($extraFeatureObjects as $o) + { + if (!method_exists($o, $method)) + { + continue; + } + $result = call_user_func(array($o, $method), $params); + } + + return $result; +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +function TranslateWinPath($p_path) +{ + $is_unc = false; + + if (KSWINDOWS) + { + // Is this a UNC path? + $is_unc = (substr($p_path, 0, 2) == '\\\\') || (substr($p_path, 0, 2) == '//'); + // Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) + { + $p_path = strtr($p_path, '\\', '/'); + } + } + + // Remove multiple slashes + $p_path = str_replace('///', '/', $p_path); + $p_path = str_replace('//', '/', $p_path); + + // Fix UNC paths + if ($is_unc) + { + $p_path = '//' . ltrim($p_path, '/'); + } + + return $p_path; +} + + +/** + * FTP Functions + */ +function getListing($directory, $host, $port, $username, $password, $passive, $ssl) +{ + $directory = resolvePath($directory); + $dir = $directory; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + if (!empty($copy_of_parts)) + { + $parent_directory = '/' . implode('/', $copy_of_parts); + } + else + { + $parent_directory = '/'; + } + } + else + { + $parent_directory = ''; + } + + // Connect to the server + if ($ssl) + { + $con = @ftp_ssl_connect($host, $port); + } + else + { + $con = @ftp_connect($host, $port); + } + if ($con === false) + { + return array( + 'error' => 'FTPBROWSER_ERROR_HOSTNAME' + ); + } + + // Login + $result = @ftp_login($con, $username, $password); + if ($result === false) + { + return array( + 'error' => 'FTPBROWSER_ERROR_USERPASS' + ); + } + + // Set the passive mode -- don't care if it fails, though! + @ftp_pasv($con, $passive); + + // Try to chdir to the specified directory + if (!empty($dir)) + { + $result = @ftp_chdir($con, $dir); + if ($result === false) + { + return array( + 'error' => 'FTPBROWSER_ERROR_NOACCESS' + ); + } + } + else + { + $directory = @ftp_pwd($con); + + $parsed_dir = trim($directory, '/'); + $parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + $parent_directory = $this->directory; + } + + // Get a raw directory listing (hoping it's a UNIX server!) + $list = @ftp_rawlist($con, '.'); + ftp_close($con); + + if ($list === false) + { + return array( + 'error' => 'FTPBROWSER_ERROR_UNSUPPORTED' + ); + } + + // Parse the raw listing into an array + $folders = parse_rawlist($list); + + return array( + 'error' => '', + 'list' => $folders, + 'breadcrumbs' => $parts, + 'directory' => $directory, + 'parent' => $parent_directory + ); +} + +function parse_rawlist($list) +{ + $folders = array(); + foreach ($list as $v) + { + $info = array(); + $vinfo = preg_split("/[\s]+/", $v, 9); + if ($vinfo[0] !== "total") + { + $perms = $vinfo[0]; + if (substr($perms, 0, 1) == 'd') + { + $folders[] = $vinfo[8]; + } + } + } + + asort($folders); + + return $folders; +} + +function getSftpListing($directory, $host, $port, $username, $password) +{ + $directory = resolvePath($directory); + $dir = $directory; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + if (!empty($copy_of_parts)) + { + $parent_directory = '/' . implode('/', $copy_of_parts); + } + else + { + $parent_directory = '/'; + } + } + else + { + $parent_directory = ''; + } + + // Initialise + $connection = null; + $sftphandle = null; + + // Open a connection + if (!function_exists('ssh2_connect')) + { + return array( + 'error' => AKText::_('SFTP_NO_SSH2') + ); + } + + $connection = ssh2_connect($host, $port); + + if ($connection === false) + { + return array( + 'error' => AKText::_('SFTP_WRONG_USER') + ); + } + + if (!ssh2_auth_password($connection, $username, $password)) + { + return array( + 'error' => AKText::_('SFTP_WRONG_USER') + ); + } + + $sftphandle = ssh2_sftp($connection); + + if ($sftphandle === false) + { + return array( + 'error' => AKText::_('SFTP_NO_FTP_SUPPORT') + ); + } + + // Get a raw directory listing (hoping it's a UNIX server!) + $list = array(); + $dir = ltrim($dir, '/'); + + if (empty($dir)) + { + $dir = ssh2_sftp_realpath($sftphandle, "."); + $directory = $dir; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + + if (!empty($copy_of_parts)) + { + $parent_directory = '/' . implode('/', $copy_of_parts); + } + else + { + $parent_directory = '/'; + } + } + else + { + $parent_directory = ''; + } + } + + $handle = opendir("ssh2.sftp://$sftphandle/$dir"); + + if (!is_resource($handle)) + { + return array( + 'error' => AKText::_('SFTPBROWSER_ERROR_NOACCESS') + ); + } + + while (($entry = readdir($handle)) !== false) + { + if (!is_dir("ssh2.sftp://$sftphandle/$dir/$entry")) + { + continue; + } + + $list[] = $entry; + } + + closedir($handle); + + if (!empty($list)) + { + asort($list); + } + + return array( + 'error' => '', + 'list' => $list, + 'breadcrumbs' => $parts, + 'directory' => $directory, + 'parent' => $parent_directory + ); +} + +/** + * Simple function to resolve relative paths. + * Note that it is unable to resolve pathnames any higher than the present working directory. + * I.E. It doesn't know about any directory names that you don't tell it about; hence: ../../foo becomes foo. + * + * @param $filename + * + * @return string + */ +function resolvePath($filename) +{ + $filename = str_replace('//', '/', $filename); + $parts = explode('/', $filename); + $out = array(); + foreach ($parts as $part) + { + if ($part == '.') + { + continue; + } + if ($part == '..') + { + array_pop($out); + continue; + } + $out[] = $part; + } + + return implode('/', $out); +} + +function createStealthURL() +{ + $filename = AKFactory::get('kickstart.stealth.url', ''); + // We need an HTML file! + if (empty($filename)) + { + return; + } + // Make sure it ends in .html or .htm + $filename = basename($filename); + if ((strtolower(substr($filename, -5)) != '.html') && (strtolower(substr($filename, -4)) != '.htm')) + { + return; + } + + $filename_quoted = str_replace('.', '\\.', $filename); + $rewrite_base = trim(dirname(AKFactory::get('kickstart.stealth.url', '')), '/'); + + // Get the IP + $userIP = $_SERVER['REMOTE_ADDR']; + $userIP = str_replace('.', '\.', $userIP); + + // Get the .htaccess contents + $stealthHtaccess = <<unlink('.htaccess'); + $tempfile = $postproc->processFilename('.htaccess'); + @file_put_contents($tempfile, $stealthHtaccess); + $postproc->process(); +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +function echoCSS() +{ + echo << li {display: inline-block; text-shadow: 0 1px 0 #FFFFFF;} +#ak_crumbs span {padding: 1px 3px;} +#ak_crumbs a {cursor: pointer;} +#ftpBrowserFolderList a{cursor:pointer} + +/* Bootstrap porting */ +.table {margin-bottom: 18px;width: 100%;} +.table th, .table td {border-top: 1px solid #DDDDDD; line-height: 18px; padding: 8px; text-align: left; vertical-align: top;} +.table-striped tbody > tr:nth-child(2n+1) > td, .table-striped tbody > tr:nth-child(2n+1) > th { background-color: #F9F9F9;} + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { display: none; } +.ui-helper-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } +.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } +.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } +.ui-helper-clearfix:after { clear: both; } +.ui-helper-clearfix { zoom: 1; } +.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { cursor: default !important; } + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } +.ui-resizable { position: relative;} +.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; } +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } +.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } +.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } +.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } +.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } +.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } +.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } +.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } +.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;} +.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ +.ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; } +.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ +button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ +.ui-button-icons-only { width: 3.4em; } +button.ui-button-icons-only { width: 3.7em; } + +/*button text element */ +.ui-button .ui-button-text { display: block; line-height: 1.4; } +.ui-button-text-only .ui-button-text { padding: 0; } +.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } +.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } +.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } +.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } +/* no icon support for input elements, provide padding by default */ +input.ui-button { padding: .4em 1em; } + +/*button icon element(s) */ +.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } +.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } +.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } +.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } +.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } + +/*button sets*/ +.ui-buttonset { margin-right: 7px; } +.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } + +/* workarounds */ +button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ +.ui-dialog { position: absolute; top: 0; left: 0; padding: .2em; width: 300px; overflow: hidden; } +.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } +.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } +.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; display:none} +.ui-dialog .ui-dialog-titlebar-close span { display: none; margin: 1px; } +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } +.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } +.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } +.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } +.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } +.ui-draggable .ui-dialog-titlebar { cursor: move; } + +/* Component containers +----------------------------------*/ +.ui-widget-content { border: 1px solid #a6c9e2; background: #fcfdfd; color: #222222; } +.ui-widget-content a { color: #222222; } +.ui-widget-header { border: 1px solid #4297d7; background: #5c9ccc ; color: #ffffff; font-weight: bold; } +.ui-widget-header a { color: #ffffff; } + +/* Interaction states +----------------------------------*/ +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited {text-decoration: none; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover{ + background: #4096ee; + background: -moz-linear-gradient(top, #4096ee 0%, #60abf8 56%, #7abcff 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4096ee), color-stop(56%,#60abf8), color-stop(100%,#7abcff)); + background: -webkit-linear-gradient(top, #4096ee 0%,#60abf8 56%,#7abcff 100%); + background: -o-linear-gradient(top, #4096ee 0%,#60abf8 56%,#7abcff 100%); + background: -ms-linear-gradient(top, #4096ee 0%,#60abf8 56%,#7abcff 100%); + background: linear-gradient(top, #4096ee 0%,#60abf8 56%,#7abcff 100%); +} +.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #1d5987; text-decoration: none; } +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #e17009; text-decoration: none; } + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fad42e; background: #fbec88 ; color: #363636; } +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec ; color: #cd0a0a; } +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; } +.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; } +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } +.ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 */ + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { display:none} + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 5px; -webkit-border-top-left-radius: 5px; -khtml-border-top-left-radius: 5px; border-top-left-radius: 5px; } +.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 5px; -webkit-border-top-right-radius: 5px; -khtml-border-top-right-radius: 5px; border-top-right-radius: 5px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 5px; -webkit-border-bottom-left-radius: 5px; -khtml-border-bottom-left-radius: 5px; border-bottom-left-radius: 5px; } +.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 5px; -webkit-border-bottom-right-radius: 5px; -khtml-border-bottom-right-radius: 5px; border-bottom-right-radius: 5px; } + +/* Overlays */ +.ui-widget-overlay { background: #000000 ; opacity: .8;filter:Alpha(Opacity=80); } +.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #000000 ; opacity: .8;filter:Alpha(Opacity=80); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; } + + +.ui-button { + font-family: Calibri,"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 1.4rem; + display: inline-block; + padding: .5em 1em; + margin: 1em .25em; + color:#fff; + text-decoration: none; + background: #7abcff; + border: solid #ddd; + cursor: pointer; + border-radius: .25em; + transition: 0.3s linear all; +} + +CSS; + + callExtraFeature('onExtraHeadCSS'); +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +function echoHeadJavascript() +{ + ?> + + asJavascript(); +} + +function echoPage() +{ + $edition = KICKSTARTPRO ? 'Professional' : 'Core'; + $bestArchivePath = AKKickstartUtils::getBestArchivePath(); + $filelist = AKKickstartUtils::getArchivesAsOptions($bestArchivePath); + ?> + + + + + Akeeba Kickstart <?php echo $edition ?> <?php echo VERSION ?> + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + +

    THINGS_HEADER

    +
      +
    1. THINGS_01
    2. +
    3. THINGS_02
    4. +
    5. THINGS_03
    6. +
    7. THINGS_04
    8. +
    9. THINGS_05
    10. +
    11. THINGS_06
    12. +
    13. THINGS_07
    14. +
    15. THINGS_08
    16. +
    17. THINGS_09
    18. +
    + CLOSE_LIGHTBOX +
    + +
    +
    
    +		
    + + + + + +
    + + +
    + +
    + NEEDSOMEHELPKS QUICKSTART +
    + +
    +
    1
    +

    SELECT_ARCHIVE

    +
    + +
    + + + + + RELOAD_ARCHIVES + +
    + + + + + + + NOARCHIVESCLICKHERE + + +
    + + +
    +
    + +
    + +
    +
    2
    +

    SELECT_EXTRACTION

    +
    + + + +
    + + + + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    + + + + FTP_BROWSE ?> +
    + + + + + BTN_CHECK + BTN_RESET +
    + + BTN_TESTFTPCON + CANTGETITTOWORK +
    +
    + +
    +
    + +
    + +
    +
    3
    +

    FINE_TUNE

    +
    + BTN_SHOW_FINE_TUNE +
    + +
    + +
    + +
    +
    4
    +

    EXTRACT_FILES

    +
    + + BTN_START +
    +
    + +
    + +
    + + +
    + +
    +
    +
    5
    +

    EXTRACTING

    +
    +
    DO_NOT_CLOSE_EXTRACT
    +
    +
     
    +
    +
    +
    +
    + + + + + + +
    + + + +
    + + + + unlink(basename(__FILE__)); + + // Delete translations + removeKickstartTranslationFiles($postProc); + + // Delete feature files + deleteKickstartFeatureFiles($postProc); + + // Delete the temporary directory IF AND ONLY IF it's called "kicktemp" + deleteKickstartTempDirectory($postProc); + + // Delete cacert.pem + $postProc->unlink('cacert.pem'); + + // Delete jquery.min.js and json2.min.js + $postProc->unlink('jquery.min.js'); + $postProc->unlink('json2.min.js'); +} + +/** + * Remove feature files, e.g. kickstart.transfer.php + * + * @param AKAbstractPostproc $postProc + * + * @return void + */ +function deleteKickstartFeatureFiles(AKAbstractPostproc $postProc) +{ + $dh = opendir(AKKickstartUtils::getPath()); + + if ($dh === false) + { + return; + } + + $basename = basename(__FILE__, '.php'); + + while (false !== $file = @readdir($dh)) + { + if ( + (substr($file, 0, strlen($basename) + 1) == $basename . '.') + && (substr($file, -4) == '.php') + ) + { + $postProc->unlink($file); + } + } + + closedir($dh); +} + +/** + * Delete the temporary directory IF AND ONLY IF it's called "kicktemp" + * + * @param AKAbstractPostproc $postProc + * + * @return void + */ +function deleteKickstartTempDirectory(AKAbstractPostproc $postProc) +{ + $tempDir = $postProc->getTempDir(); + $tempDir = trim($tempDir); + + if (empty($tempDir)) + { + return; + } + + $basename = basename($tempDir); + + if (strtolower($basename) != 'kicktemp') + { + return; + } + + recursive_remove_directory($tempDir); +} + +/** + * Delete language files, e.g. el-GR.kickstart.ini + * + * @param AKAbstractPostproc $postProc + * + * @return void + */ +function removeKickstartTranslationFiles(AKAbstractPostproc $postProc) +{ + $dh = opendir(AKKickstartUtils::getPath()); + + if ($dh === false) + { + return; + } + + $basename = basename(__FILE__, '.php'); + + while (false !== $file = @readdir($dh)) + { + if (strstr($file, $basename . '.ini')) + { + $postProc->unlink($file); + } + } + + closedir($dh); +} + +/** + * Finalization after the restoration. Removes the installation directory, the backup archive and rolls back automatic + * file renames. + * + * @param AKAbstractUnarchiver $unarchiver The unarchiver engine used by Akeeba Restore + * @param AKAbstractPostproc $postProc The post-processing engine used by Akeeba Restore + */ +function finalizeAfterRestoration(AKAbstractUnarchiver $unarchiver, AKAbstractPostproc $postProc) +{ + // Remove installation + recursive_remove_directory('installation'); + + // Run the renames, backwards + rollbackAutomaticRenames($unarchiver, $postProc); + + // Delete the archive + foreach ($unarchiver->archiveList as $archive) + { + $postProc->unlink($archive); + } +} + +/** + * Rolls back automatic file renames. + * + * @param AKAbstractUnarchiver $unarchiver The unarchiver engine used by Akeeba Restore + * @param AKAbstractPostproc $postProc The post-processing engine used by Akeeba Restore + */ +function rollbackAutomaticRenames(AKAbstractUnarchiver $unarchiver, AKAbstractPostproc $postProc) +{ + $renameBack = AKFactory::get('kickstart.setup.renameback', true); + + if ($renameBack) + { + $renames = $unarchiver->renameFiles; + + if (!empty($renames)) + { + foreach ($renames as $original => $renamed) + { + $postProc->rename($renamed, $original); + } + } + } +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Utility class to parse CLI parameters in a POSIX way + */ +class AKCliParams +{ + /** + * POSIX-style CLI options. Access them with through the getOption method. + * + * @var array + */ + protected static $cliOptions = array(); + + /** + * Parses POSIX command line options and sets the self::$cliOptions associative array. Each array item contains + * a single dimensional array of values. Arguments without a dash are silently ignored. + * + * @return void + */ + public static function parseOptions() + { + global $argc, $argv; + + // Workaround for PHP-CGI + if (!isset($argc) && !isset($argv)) + { + $query = ""; + + if (!empty($_GET)) + { + foreach ($_GET as $k => $v) + { + $query .= " $k"; + + if ($v != "") + { + $query .= "=$v"; + } + } + } + + $query = ltrim($query); + $argv = explode(' ', $query); + $argc = count($argv); + } + + $currentName = ""; + $options = array(); + + for ($i = 1; $i < $argc; $i++) + { + $argument = $argv[$i]; + + $value = $argument; + + if (strpos($argument, "-") === 0) + { + $argument = ltrim($argument, '-'); + + $name = $argument; + $value = null; + + if (strstr($argument, '=')) + { + list($name, $value) = explode('=', $argument, 2); + } + + $currentName = $name; + + if (!isset($options[$currentName]) || ($options[$currentName] == null)) + { + $options[$currentName] = array(); + } + } + + if ((!is_null($value)) && (!is_null($currentName))) + { + $key = null; + + if (strstr($value, '=')) + { + $parts = explode('=', $value, 2); + $key = $parts[0]; + $value = $parts[1]; + } + + $values = $options[$currentName]; + + if (is_null($values)) + { + $values = array(); + } + + if (is_null($key)) + { + $values[] = $value; + } + else + { + $values[$key] = $value; + } + + $options[$currentName] = $values; + } + } + + self::$cliOptions = $options; + } + + /** + * Returns the value of a command line option + * + * @param string $key The full name of the option, e.g. "foobar" + * @param mixed $default The default value to return + * @param bool $array Should I return an array parameter? + * + * @return mixed The value of the option + */ + public static function getOption($key, $default = null, $array = false) + { + // If the key doesn't exist set it to the default value + if (!array_key_exists($key, self::$cliOptions)) + { + self::$cliOptions[$key] = is_array($default) ? $default : array($default); + } + + if ($array) + { + return self::$cliOptions[$key]; + } + + return self::$cliOptions[$key][0]; + } + + /** + * Is the specified param key used in the command line? + * + * @param string $key The full name of the option, e.g. "foobar" + * + * @return mixed The value of the option + */ + public static function hasOption($key) + { + return array_key_exists($key, self::$cliOptions); + } +} + +class CLIExtractionObserver extends ExtractionObserver +{ + public static $silent = false; + + public function update($object, $message) + { + parent::update($object, $message); + + if (self::$silent) + { + return; + } + + if (!is_object($message)) + { + return; + } + + if (!array_key_exists('type', get_object_vars($message))) + { + return; + } + + if ($message->type == 'startfile') + { + echo $message->content->file . "\n"; + } + } + +} + +/** + * Routes the Kickstart CLI application + */ +function kickstart_application_cli() +{ + AKCliParams::parseOptions(); + $silent = AKCliParams::hasOption('silent'); + $year = gmdate('Y'); + + if (!$silent) + { + echo <<< BANNER +Akeeba Kickstart CLI rev23305F8 +Copyright (c) 2008-$year Akeeba Ltd / Nicholas K. Dionysopoulos +------------------------------------------------------------------------------- +Akeeba Kickstart is Free Software, distributed under the terms of the GNU General +Public License version 3 or, at your option, any later version. +This program comes with ABSOLUTELY NO WARRANTY as per sections 15 & 16 of the +license. See http://www.gnu.org/licenses/gpl-3.0.html for details. +------------------------------------------------------------------------------- + + +BANNER; + } + + $paths = AKCliParams::getOption('', array(), true); + + if (empty($paths)) + { + global $argv; + + echo <<< HOWTOUSE +Usage: {$argv[0]} archive.jpa [output_path] [--password=yourPassword] + [--silent] [--permissions] [--dry-run] [--ignore-errors] + [--extract=[,...]] + + +HOWTOUSE; + + die; + } + + AKFactory::nuke(); + + $targetPath = isset($paths[1]) ? $paths[1] : getcwd(); + $targetPath = realpath($targetPath); + $archive = $paths[0]; + $archive = realpath($archive); + $archivePath = dirname($archive); + $archivePath = empty($archivePath) ? getcwd() : $archivePath; + $archivePath = empty($archivePath) ? __DIR__ : $archivePath; + $archiveName = basename($paths[0]); + + $archiveForDisplay = $archive; + $cwd = getcwd(); + + if ($archivePath == realpath($cwd)) + { + $archiveForDisplay = $archiveName; + } + + if (!$silent) + { + echo <<< BANNER +Extracting $archiveForDisplay +to folder $targetPath + +BANNER; + } + + // What am I extracting? + AKFactory::set('kickstart.setup.sourcepath', $archivePath); + AKFactory::set('kickstart.setup.sourcefile', $archiveName); + // JPS password + AKFactory::set('kickstart.jps.password', AKCliParams::getOption('password')); + // Restore permissions? + AKFactory::set('kickstart.setup.restoreperms', AKCliParams::hasOption('permissions')); + // Dry run? + AKFactory::set('kickstart.setup.dryrun', AKCliParams::hasOption('dry-run')); + // Ignore errors? + AKFactory::set('kickstart.setup.ignoreerrors', AKCliParams::hasOption('ignore-errors')); + // Which files should I extract? + AKFactory::set('kickstart.setup.extract_list', AKCliParams::getOption('extract', '', true)); + // Do not rename any files (this is the CLI...) + AKFactory::set('kickstart.setup.renamefiles', array()); + // Optimize time limits + AKFactory::set('kickstart.tuning.max_exec_time', 20); + AKFactory::set('kickstart.tuning.run_time_bias', 75); + AKFactory::set('kickstart.tuning.min_exec_time', 0); + AKFactory::set('kickstart.procengine', 'direct'); + + // Make sure that the destination directory is always set (req'd by both FTP and Direct Writes modes) + if (empty($targetPath)) + { + $targetPath = AKKickstartUtils::getPath(); + } + + AKFactory::set('kickstart.setup.destdir', $targetPath); + + $unarchiver = AKFactory::getUnarchiver(); + $observer = new CLIExtractionObserver(); + $unarchiver->attach($observer); + + if ($silent) + { + CLIExtractionObserver::$silent = true; + } + + if (!$silent) + { + echo "\n\n"; + } + + $retArray = array( + 'done' => false, + ); + + while (!$retArray['done']) + { + $unarchiver->tick(); + $ret = $unarchiver->getStatusArray(); + + if ($ret['Error'] != '') + { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $ret['Error']; + } + elseif (!$ret['HasRun']) + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = true; + } + else + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = false; + } + + if (!is_null($observer->totalSize)) + { + $retArray['totalsize'] = $observer->totalSize; + $retArray['filelist'] = $observer->fileList; + } + + $retArray['Warnings'] = $ret['Warnings']; + $retArray['lastfile'] = $observer->lastFile; + + if (!empty($retArray['Warnings']) && !$silent) + { + echo "\n\n"; + + foreach ($retArray['Warnings'] as $line) + { + echo "\t$line\n"; + } + + echo "\n"; + } + } + + if (!$silent) + { + echo "\n\n"; + } + + if (!$retArray['status']) + { + if (!$silent) + { + echo "An error has occurred:\n{$retArray['message']}\n\n"; + } + + exit(255); + } + + // Finalize + $postProc = AKFactory::getPostProc(); + + rollbackAutomaticRenames($unarchiver, $postProc); + clearCodeCaches(); +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Routes the Kickstart web application + */ +function kickstart_application_web() +{ + $retArray = array( + 'status' => true, + 'message' => null + ); + + $task = getQueryParam('task', 'display'); + $json = getQueryParam('json'); + $ajax = true; + + switch ($task) + { + case 'checkTempdir': + $retArray['status'] = false; + + if (!empty($json)) + { + $data = json_decode($json, true); + $dir = @$data['kickstart.ftp.tempdir']; + + if (!empty($dir)) + { + $retArray['status'] = is_writable($dir); + } + } + break; + + case 'checkFTP': + $retArray['status'] = false; + + if (!empty($json)) + { + $data = json_decode($json, true); + + foreach ($data as $key => $value) + { + AKFactory::set($key, $value); + } + + if ($data['type'] == 'ftp') + { + $ftp = new AKPostprocFTP(); + } + else + { + $ftp = new AKPostprocSFTP(); + } + + $retArray['message'] = $ftp->getError(); + $retArray['status'] = empty($retArray['message']); + } + break; + + case 'ftpbrowse': + if (!empty($json)) + { + $data = json_decode($json, true); + + $retArray = + getListing($data['directory'], $data['host'], $data['port'], $data['username'], $data['password'], $data['passive'], $data['ssl']); + } + break; + + case 'sftpbrowse': + if (!empty($json)) + { + $data = json_decode($json, true); + + $retArray = + getSftpListing($data['directory'], $data['host'], $data['port'], $data['username'], $data['password']); + } + break; + + case 'startExtracting': + case 'continueExtracting': + // Look for configuration values + $retArray['status'] = false; + + if (!empty($json)) + { + if ($task == 'startExtracting') + { + AKFactory::nuke(); + } + + $oldJSON = $json; + $json = json_decode($json, true); + + if (is_null($json)) + { + $json = stripslashes($oldJSON); + $json = json_decode($json, true); + } + + if (!empty($json)) + { + foreach ($json as $key => $value) + { + if (substr($key, 0, 9) == 'kickstart') + { + AKFactory::set($key, $value); + } + } + } + + // A "factory" variable will override all other settings. + if (array_key_exists('factory', $json)) + { + // Get the serialized factory + $serialized = $json['factory']; + AKFactory::unserialize($serialized); + AKFactory::set('kickstart.enabled', true); + } + + // Make sure that the destination directory is always set (req'd by both FTP and Direct Writes modes) + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (empty($removePath)) + { + AKFactory::set('kickstart.setup.destdir', AKKickstartUtils::getPath()); + } + + if ($task == 'startExtracting') + { + // If the Stealth Mode is enabled, create the .htaccess file + if (AKFactory::get('kickstart.stealth.enable', false)) + { + createStealthURL(); + } + } + + $engine = AKFactory::getUnarchiver(); // Get the engine + $observer = new ExtractionObserver(); // Create a new observer + $engine->attach($observer); // Attach the observer + $engine->tick(); + $ret = $engine->getStatusArray(); + + if ($ret['Error'] != '') + { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $ret['Error']; + } + elseif (!$ret['HasRun']) + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = true; + } + else + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['factory'] = AKFactory::serialize(); + } + + if (!is_null($observer->totalSize)) + { + $retArray['totalsize'] = $observer->totalSize; + $retArray['filelist'] = $observer->fileList; + } + + $retArray['Warnings'] = $ret['Warnings']; + $retArray['lastfile'] = $observer->lastFile; + } + break; + + case 'cleanUp': + if (!empty($json)) + { + $json = json_decode($json, true); + + if (array_key_exists('factory', $json)) + { + // Get the serialized factory + $serialized = $json['factory']; + AKFactory::unserialize($serialized); + AKFactory::set('kickstart.enabled', true); + } + } + + $unarchiver = AKFactory::getUnarchiver(); // Get the engine + $postProc = AKFactory::getPostProc(); + + finalizeAfterRestoration($unarchiver, $postProc); + removeKickstartFiles($postProc); + clearCodeCaches(); + + break; + + case 'display': + $ajax = false; + echoPage(); + break; + + case 'isJoomla': + $ajax = true; + + if (!empty($json)) + { + $json = json_decode($json, true); + + if (array_key_exists('factory', $json)) + { + // Get the serialized factory + $serialized = $json['factory']; + AKFactory::unserialize($serialized); + AKFactory::set('kickstart.enabled', true); + } + } + + $path = AKFactory::get('kickstart.setup.destdir', ''); + $path = rtrim($path, '/\\'); + $isJoomla = @is_dir($path . '/administrator'); + + if ($isJoomla) + { + $isJoomla = @is_dir($path . '/libraries/joomla'); + } + + $retArray = $isJoomla; + + break; + + case 'listArchives': + $ajax = true; + + $path = null; + + if (!empty($json)) + { + $json = json_decode($json, true); + + if (array_key_exists('path', $json)) + { + $path = $json['path']; + } + } + + if (empty($path) || !@is_dir($path)) + { + $filelist = null; + } + else + { + $filelist = AKKickstartUtils::getArchivesAsOptions($path); + } + + if (empty($filelist)) + { + $retArray = + '' . + AKText::_('NOARCHIVESCLICKHERE') + . ''; + } + else + { + $retArray = ''; + } + + break; + + default: + $ajax = true; + + if (!empty($json)) + { + $params = json_decode($json, true); + } + else + { + $params = array(); + } + + $retArray = callExtraFeature($task, $params); + + break; + } + + if ($ajax) + { + // JSON encode the message + $json = json_encode($retArray); + + // Return the message + echo "###$json###"; + } +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +// Register additional feature classes +callExtraFeature(); + +// Is this a CLI call? +$isCli = !isset($_SERVER) || !is_array($_SERVER); + +if (isset($_SERVER) && is_array($_SERVER)) +{ + $isCli = !array_key_exists('REQUEST_METHOD', $_SERVER); +} + +if (isset($_GET) && is_array($_GET) && !empty($_GET)) +{ + if (isset($_GET['cli'])) + { + $isCli = $_GET['cli'] == 1; + } + elseif (isset($_GET['web'])) + { + $isCli = $_GET['web'] != 1; + } +} + +// Route the application +if ($isCli) +{ + kickstart_application_cli(); +} +else +{ + kickstart_application_web(); +} + +/** + * Akeeba Kickstart + * A JSON-powered archive extraction tool + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * This is an Easter Egg feature. Run Kickstart as kickstart.php?george and a hideous orange template with the font face + * everyone loves to hate will burn your eyes out of their socket. HAHAHAHAHA! + */ +class AKFeatureGeorgeWSpecialEdition +{ + /** + * Echoes extra CSS to the head of the page + */ + public function onExtraHeadCSS() + { + if (!isset($_REQUEST['george'])) + { + return; + } + + echo <<< CSS +html { + background: #FFA500; +} + +body { + font-family: "Comic Sans MS"; +} + +#header { + background: #FFA500; + color: red; + font-weight: bold; +} + +#footer { + color: #333; + background: #FFA500; +} + +#footer a { + color: #f33; +} + +h2 { + background: #FFA500; + color: #993300; +} + +.button:active { + border: 1px solid #FFA500; +} + +.ui-button { + font-family: "Comic Sans MS"; +} + +.ribbon-box { + width:100%; + height:400px; + margin-top: -4.5em; + right: 5px; + position: absolute; + pointer-events: none; +} +.ribbon { + position: absolute; + right: -5px; top: -5px; + z-index: 1; + overflow: hidden; + width: 450px; height: 450px; + text-align: right; +} +.ribbon span { + font-size: 12pt; + color: red; + text-transform: uppercase; + text-align: center; + font-weight: bold; + line-height: 20px; + transform: rotate(45deg); + -webkit-transform: rotate(45deg); /* Needed for Safari */ + width: 300px; display: block; + background: #FFA500; + background: linear-gradient(#FFA500 0%, #FFA500 100%); + position: absolute; + border: thin solid firebrick; + top: 80px; + right: -55px; + box-shadow: yellow 1px 1px 20px; + text-shadow: 1px 1px 2px white; +} +CSS; + + } + + public function onExtraHeadJavascript() + { + if (!isset($_REQUEST['george'])) + { + return; + } + + echo <<< JS +// Matrix effect from http://thecodeplayer.com/walkthrough/matrix-rain-animation-html5-canvas-javascript +var c = null; +var ctx = null; + +//japanese characters - taken from the unicode charset +var japanese = "あいうえおかきくけこさしすせそがぎぐげごぱぴぷぺぽ"; +//converting the string into an array of single characters +japanese = japanese.split(""); + +var font_size = 14; +var columns = 0; //number of columns for the rain +//an array of drops - one per column +var drops = []; + +//drawing the characters +function draw() +{ + //Black BG for the canvas + //translucent BG to show trail + ctx.fillStyle = "rgba(0, 0, 0, 0.05)"; + ctx.fillRect(0, 0, c.width, c.height); + + ctx.fillStyle = "#0F0"; //green text + ctx.font = font_size + "px Arial"; + + //looping over drops + for(var i = 0; i < drops.length; i++) + { + //a random japanese character to print + var text = japanese[Math.floor(Math.random()*japanese.length)]; + //x = i*font_size, y = value of drops[i]*font_size + ctx.fillText(text, i*font_size, drops[i]*font_size); + + //sending the drop back to the top randomly after it has crossed the screen + //adding a randomness to the reset to make the drops scattered on the Y axis + if(drops[i]*font_size > c.height && Math.random() > 0.975) + drops[i] = 0; + + //incrementing Y coordinate + drops[i]++; + } +} + +/* + * Konami Code Javascript Object + * 1.3.0, 7 March 2014 + * + * Using the Konami code, easily configure an Easter Egg for your page or any element on the page. + * + * Options: + * - code : set your own custom code, takes array of keycodes / default is original Konami code + * - cheat : the function to call when the proper sequence is entered + * - elem : the element to set the instance on + * + * Copyright 2013 - 2014 Kurtis Kemple, http://kurtiskemple.com + * Released under the MIT License + */ + +var KONAMI = function ( options ) { + var elem, ret, defaults, keycode, config, cache; + + // set the default code,function, and element + defaults = { + code : [38,38,40,40,37,39,37,39,66,65], + cheat : null, + elem : window + }; + + // build our return object + ret = { + + /** + * handles the initialization of the KONAMI instance + * + * @param {object} options the config to pass in to the instance + * @return {none} + * @method init + * @public + */ + init : function ( options ) { + cache = [], config = {}; + + if ( options ) { + + for ( var key in defaults ) { + + if ( defaults.hasOwnProperty( key ) ) { + + if ( !options[ key ] ) { + + config[ key ] = defaults[ key ]; + } else { + + config[ key ] = options[ key ]; + } + } + } + } else { + + config = defaults; + } + + ret.bind( config.elem, 'keyup', ret.konami ); + }, + + /** + * handles disassembling of the instance + * + * @return {none} + * @method destroy + * @public + */ + destroy : function () { + ret.unbind( config.elem, 'keyup', ret.konami ); + cache = config = null; + }, + + /** + * handles adding events to elements + * + * @param {elem} elem DOM element to attach to + * @param {string} evt the event type to bind to + * @param {Function} fn the function to bind + * @return {none} + * @method bind + * @private + */ + bind : function ( elem, evt, fn ) { + if ( elem.addEventListener ) { + + elem.addEventListener( evt, fn, false ); + } else if ( elem.attachEvent ) { + + elem.attachEvent( 'on'+ evt, function( e ) { + fn( e || window.event ); + }); + } + }, + + /** + * handles removing events from elements + * + * @param {elem} elem DOM element to remove from + * @param {string} evt the event type to unbind + * @param {Function} fn the function to unbind + * @return {none} + * @method unbind + * @private + */ + unbind : function ( elem, evt, fn ) { + if ( elem.removeEventListener ) { + + elem.removeEventListener( evt, fn, false ); + } else if ( elem.detachEvent ) { + + elem.detachEvent( 'on' + evt, function( e ) { + fn( e || window.event ); + }); + } + }, + + /** + * handles the business logic for checking for valid konami code + * + * @param {object} e the event object + * @return {none} + * @method konami + * @private + */ + konami : function( e ) { + keycode = e.keyCode || e.which; + + if ( config.code.length > cache.push( keycode ) ) { + + return; + } + + if ( config.code.length < cache.length ) { + + cache.shift(); + } + + if ( config.code.toString() !== cache.toString() ) { + + return; + } + + config.cheat(); + } + }; + + ret.init( options ); + return ret; +}; + +var options = { + cheat : function() { + c = document.createElement('canvas'); + window.jQuery('html').html('').append(c); + window.jQuery(c).attr('id', 'c'); + + //making the canvas full screen + c.height = window.innerHeight; + c.width = window.innerWidth; + c.left = 0; + c.top = 0; + + ctx = c.getContext("2d"); + columns = c.width/font_size; //number of columns for the rain + + p1 = document.getElementById('page1'); + window.jQuery(p1).hide(); + + //x below is the x coordinate + //1 = y co-ordinate of the drop(same for every drop initially) + for(var x = 0; x < columns; x++) + drops[x] = 1; + + setInterval(draw, 33); + } +}; + +var konamiCode = new KONAMI( options ); + +JS; + + } + + /** + * Echoes extra HTML on page 1 of Kickstart + */ + public function onPage1() + { + if (!isset($_REQUEST['george'])) + { + return; + } + + echo <<< HTML +
    +
    + George W. Edition +
    +
    +HTML; + + } + + /** + * Outputs HTML to be shown before Step 1's archive selection pane + */ + public function onPage1Step1() + { + } +} + diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/none.ini b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/none.ini new file mode 100644 index 00000000..75381417 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/none.ini @@ -0,0 +1,8 @@ +[none] +name="No Installer" +package="" +installerroot="" +sqlroot="sql" +databasesini=0 +readme=0 +extrainfo=0 \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/web.config b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/web.config new file mode 100644 index 00000000..87e3d8ac --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Installers/web.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/Master/Stats/usagestats.php b/deployed/akeeba/administrator/components/com_akeeba/Master/Stats/usagestats.php new file mode 100644 index 00000000..e922d5e0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Master/Stats/usagestats.php @@ -0,0 +1,91 @@ +siteId = $siteId; + } + + /** + * Sets the value of a collected variable. Use NULL as value to unset it + * + * @param string $key Variable name + * @param string $value Variable value + */ + public function setValue($key, $value) + { + if(is_null($value) && isset($this->data[$key])) + { + unset($this->data[$key]); + } + else + { + $this->data[$key] = $value; + } + } + + /** + * Uploads collected data to the remote server + * + * @param bool $useIframe Should I create an iframe to upload data or should I use cURL/fopen? + * + * @return string|bool The HTML code if an iframe is requested or a boolean if we're using cURL/fopen + */ + public function sendInfo($useIframe = false) + { + // No site ID? Well, simply do nothing + if(!$this->siteId) + { + return ''; + } + + // First of all let's add the siteId + $this->setValue('sid', $this->siteId); + + // Then let's create the url + $url = array(); + + foreach($this->data as $param => $value) + { + $url[] .= $param.'='.$value; + } + + $url = $this->remoteUrl.'?'.implode('&', $url); + + // Should I create an iframe? + if($useIframe) + { + return ''; + } + else + { + // Do we have cURL installed? + if(function_exists('curl_init')) + { + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + + return curl_exec($ch); + } + else + { + // Nope, let's try with fopen and cross our fingers + return @fopen($url, 'r'); + } + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Backup.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Backup.php new file mode 100644 index 00000000..97caa9fd --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Backup.php @@ -0,0 +1,467 @@ +getState('ajax'); + + switch ($ajaxTask) + { + // Start a new backup + case 'start': + $ret_array = $this->startBackup(); + break; + + // Step through a backup + case 'step': + $ret_array = $this->stepBackup(); + break; + + // Send a push notification for backup failure + case 'pushFail': + $this->pushFail(); + break; + + default: + break; + } + + return $ret_array; + } + + /** + * Starts a new backup. + * + * State variables expected + * backupid The ID of the backup. If none is set up we will create a new one in the form id123 + * tag The backup tag, e.g. "frontend". If none is set up we'll get it through the Platform. + * description The description of the backup (optional) + * comment The comment of the backup (optional) + * jpskey JPS password + * angiekey ANGIE password + * + * @param array $overrides Configuration overrides + * + * @return array An Akeeba Engine return array + */ + public function startBackup(array $overrides = []) + { + // Get information from the session + $tag = $this->getState('tag', null, 'string'); + $backupId = $this->getState('backupid', null, 'string'); + $description = $this->getState('description', '', 'string'); + $comment = $this->getState('comment', '', 'html'); + $jpskey = $this->getState('jpskey', null, 'raw'); + $angiekey = $this->getState('angiekey', null, 'raw'); + + // Try to get a backup ID if none is provided + if (is_null($backupId)) + { + $db = $this->container->db; + $query = $db->getQuery(true) + ->select('MAX(' . $db->qn('id') . ')') + ->from($db->qn('#__ak_stats')); + + try + { + $maxId = $db->setQuery($query)->loadResult(); + } + catch (Exception $e) + { + $maxId = 0; + } + + $backupId = 'id' . ($maxId + 1); + } + + // Use the default description if none specified + if (empty($description)) + { + JLoader::import('joomla.utilities.date'); + $dateNow = new Date(); + $timezone = $this->container->platform->getConfig()->get('offset', 'UTC'); + + if (!$this->getContainer()->platform->isCli()) + { + $user = $this->container->platform->getUser(); + + if (!$user->guest) + { + $timezone = $user->getParam('timezone', $timezone); + } + } + + $tz = new \DateTimeZone($timezone); + $dateNow->setTimezone($tz); + $description = + JText::_('COM_AKEEBA_BACKUP_DEFAULT_DESCRIPTION') . ' ' . + $dateNow->format(JText::_('DATE_FORMAT_LC2'), true); + } + + // Try resetting the engine + Factory::resetState(array( + 'maxrun' => 0 + )); + + // Remove any stale memory files left over from the previous step + if (empty($tag)) + { + $tag = Platform::getInstance()->get_backup_origin(); + } + + $tempVarsTag = $tag; + $tempVarsTag .= empty($backupId) ? '' : ('.' . $backupId); + + Factory::getFactoryStorage()->reset($tempVarsTag); + Factory::nuke(); + Factory::getLog()->log(LogLevel::DEBUG, " -- Resetting Akeeba Engine factory ($tag.$backupId)"); + Platform::getInstance()->load_configuration(); + + // Should I apply any configuration overrides? + if (is_array($overrides) && !empty($overrides)) + { + $config = Factory::getConfiguration(); + $protectedKeys = $config->getProtectedKeys(); + $config->resetProtectedKeys(); + + foreach ($overrides as $k => $v) + { + $config->set($k, $v); + } + + $config->setProtectedKeys($protectedKeys); + } + + // Check if there are critical issues preventing the backup + if (!Factory::getConfigurationChecks()->getShortStatus()) + { + $configChecks = Factory::getConfigurationChecks()->getDetailedStatus(); + + foreach ($configChecks as $checkItem) + { + if ($checkItem['severity'] != 'critical') + { + continue; + } + + return [ + 'HasRun' => 0, + 'Domain' => 'init', + 'Step' => '', + 'Substep' => '', + 'Error' => 'Failed configuration check Q' . $checkItem['code'] . ': ' . $checkItem['description'] . '. Please refer to https://www.akeebabackup.com/documentation/warnings/q' . $checkItem['code'] . '.html for more information and troubleshooting instructions.', + 'Warnings' => array(), + 'Progress' => 0, + ]; + } + } + + // Set up Kettenrad + $options = [ + 'description' => $description, + 'comment' => $comment, + 'jpskey' => $jpskey, + 'angiekey' => $angiekey, + ]; + + if (is_null($jpskey)) + { + unset ($options['jpskey']); + } + + if (is_null($angiekey)) + { + unset ($options['angiekey']); + } + + $kettenrad = Factory::getKettenrad(); + $kettenrad->setBackupId($backupId); + $kettenrad->setup($options); + + $this->setState('backupid', $backupId); + + // Run the first backup step. We need to run tick() twice + /** + * We need to run tick() twice in the first backup step. + * + * The first tick() will reset the backup engine and start a new backup. However, no backup record is created + * at this point. This means that Factory::loadState() cannot find a backup record, therefore it cannot read + * the backup profile being used, therefore it will assume it's profile #1. + * + * The second tick() creates the backup record without doing much else, fixing this issue. + * + * However, if you have conservative settings where the min exec time is MORE than the max exec time the second + * tick would never run. Therefore we need to tell the first tick to ignore the time settings (since it only + * takes a few milliseconds to execute anyway) and then apply the time settings on the second tick (which also + * only takes a few milliseconds). This is why we have setIgnoreMinimumExecutionTime before and after the first + * tick. DO NOT REMOVE THESE. + * + * THEREFORE, DO NOT REMOVE THE SECOND tick(), IT IS THERE ON PURPOSE! + */ + $kettenrad->setIgnoreMinimumExecutionTime(true); + $kettenrad->tick(); // Do not remove the first call to tick()!!! + $kettenrad->setIgnoreMinimumExecutionTime(false); + $kettenrad->tick(); // Do not remove the second call to tick()!!! + $ret_array = $kettenrad->getStatusArray(); + + // So as not to have duplicate warnings reports + $kettenrad->resetWarnings(); + + try + { + Factory::saveState($tag, $backupId); + } + catch (\RuntimeException $e) + { + $ret_array['Error'] = $e->getMessage(); + } + + return $ret_array; + } + + /** + * Steps through a backup. + * + * State variables expected (MUST be set): + * backupid The ID of the backup. + * tag The backup tag, e.g. "frontend". + * profile (optional) The profile ID of the backup. + * + * @param bool $requireBackupId Should the backup ID be required? + * + * @return array An Akeeba Engine return array + */ + public function stepBackup($requireBackupId = true) + { + // Get the tag. If not specified use the AKEEBA_BACKUP_ORIGIN constant. + $tag = $this->getState('tag', null, 'string'); + + if (is_null($tag) && defined('AKEEBA_BACKUP_ORIGIN')) + { + $tag = AKEEBA_BACKUP_ORIGIN; + } + + // Get the Backup ID. If not specified use the AKEEBA_BACKUP_ID constant. + $backupId = $this->getState('backupid', null, 'string'); + + if (is_null($backupId) && defined('AKEEBA_BACKUP_ID')) + { + $backupId = AKEEBA_BACKUP_ID; + } + + // Get the profile from the session, the AKEEBA_PROFILE constant or the model state – in this order + if ($this->container->platform->isCli()) + { + $profile = defined('AKEEBA_PROFILE') ? AKEEBA_PROFILE : 1; + } + else + { + $profile = $this->container->platform->getSessionVar('profile', null); + $profile = defined('AKEEBA_PROFILE') ? AKEEBA_PROFILE : $profile; + $profile = $this->getState('profile', $profile, 'int'); + } + + $profile = max(0, (int) $profile); + + if (empty($profile)) + { + $profile = $this->getLastBackupProfile($tag, $backupId); + } + + // Set the active profile + if (!$this->container->platform->isCli()) + { + $this->container->platform->setSessionVar('profile', $profile); + } + + if (!defined('AKEEBA_PROFILE')) + { + define('AKEEBA_PROFILE', $profile); + } + + // Run a backup step + $ret_array = array( + 'HasRun' => 0, + 'Domain' => 'init', + 'Step' => '', + 'Substep' => '', + 'Error' => '', + 'Warnings' => array(), + 'Progress' => 0, + ); + + try + { + // Reload the configuration + Platform::getInstance()->load_configuration($profile); + + // Load the engine from storage + Factory::loadState($tag, $backupId, $requireBackupId); + + // Set the backup ID and run a backup step + $kettenrad = Factory::getKettenrad(); + $kettenrad->setBackupId($backupId); + $kettenrad->tick(); + $ret_array = $kettenrad->getStatusArray(); + + // Prevent duplicate reporting of warnings + $kettenrad->resetWarnings(); + } + catch (\Exception $e) + { + $ret_array['Error'] = $e->getMessage(); + } + + try + { + if (empty($ret_array['Error']) && ($ret_array['HasRun'] != 1)) + { + Factory::saveState($tag, $backupId); + } + } + catch (\RuntimeException $e) + { + $ret_array['Error'] = $e->getMessage(); + } + + if (!empty($ret_array['Error']) || ($ret_array['HasRun'] == 1)) + { + /** + * Do not nuke the Factory if we're trying to resume after an error. + * + * When the resume after error (retry) feature is enabled AND we are performing a backend backup we MUST + * leave the factory storage intact so we can actually resume the backup. If we were to nuke the Factory + * the resume would report that it cannot load the saved factory and lead to a failed backup. + */ + $config = Factory::getConfiguration(); + + if ($this->container->platform->isBackend() && $config->get('akeeba.advanced.autoresume', 1)) + { + // We are about to resume; abort. + return $ret_array; + } + + // Clean up + Factory::nuke(); + + $tempVarsTag = $tag; + $tempVarsTag .= empty($backupId) ? '' : ('.' . $backupId); + + Factory::getFactoryStorage()->reset($tempVarsTag); + } + + return $ret_array; + } + + /** + * Send a push notification for a failed backup + * + * State variables expected (MUST be set): + * errorMessage The error message + * + * @return void + */ + public function pushFail() + { + $errorMessage = $this->getState('errorMessage'); + + $platform = Platform::getInstance(); + $key = 'COM_AKEEBA_PUSH_ENDBACKUP_FAIL_BODY_WITH_MESSAGE'; + + if (empty($errorMessage)) + { + $key = 'COM_AKEEBA_PUSH_ENDBACKUP_FAIL_BODY'; + } + + $pushSubject = sprintf( + $platform->translate('COM_AKEEBA_PUSH_ENDBACKUP_FAIL_SUBJECT'), + $platform->get_site_name(), + $platform->get_host() + ); + $pushDetails = sprintf( + $platform->translate($key), + $platform->get_site_name(), + $platform->get_host(), + $errorMessage + ); + + $push = new PushMessages(); + $push->message($pushSubject, $pushDetails); + } + + /** + * Get the profile used to take the last backup for the specified tag + * + * @param string $tag The backup tag a.k.a. backup origin (backend, frontend, json, ...) + * @param string $backupId (optional) The Backup ID + * + * @return int The profile ID of the latest backup taken with the specified tag / backup ID + */ + protected function getLastBackupProfile($tag, $backupId = null) + { + $filters = array( + array('field' => 'tag', 'value' => $tag) + ); + + if (!empty($backupId)) + { + $filters[] = array('field' => 'backupid', 'value' => $backupId); + } + + $statList = Platform::getInstance()->get_statistics_list(array( + 'filters' => $filters, + 'order' => array( + 'by' => 'id', 'order' => 'DESC' + ) + ) + ); + + if (is_array($statList)) + { + $stat = array_pop($statList); + + return (int) $stat['profile_id']; + } + + // Backup entry not found. If backupId was specified, try without a backup ID + if (!empty($backupId)) + { + return $this->getLastBackupProfile($tag); + } + + // Else, return the default backup profile + return 1; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Browser.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Browser.php new file mode 100644 index 00000000..c72cf96f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Browser.php @@ -0,0 +1,127 @@ +getState('folder', ''); + $processfolder = $this->getState('processfolder', 0); + + if (empty($folder)) + { + $folder = JPATH_SITE; + } + + $stock_dirs = Platform::getInstance()->get_stock_directories(); + arsort($stock_dirs); + + if ($processfolder == 1) + { + foreach ($stock_dirs as $find => $replace) + { + $folder = str_replace($find, $replace, $folder); + } + } + + // Normalise name, but only if realpath() really, REALLY works... + $old_folder = $folder; + $folder = @realpath($folder); + + if ($folder === false) + { + $folder = $old_folder; + } + + $isFolderThere = @is_dir($folder); + + // Check if it's a subdirectory of the site's root + $isInRoot = (strpos($folder, JPATH_SITE) === 0); + + // Check open_basedir restrictions + $isOpenbasedirRestricted = Factory::getConfigurationChecks()->checkOpenBasedirs($folder); + + // -- Get the meta form of the directory name, if applicable + $folder_raw = $folder; + + foreach ($stock_dirs as $replace => $find) + { + $folder_raw = str_replace($find, $replace, $folder_raw); + } + + $isWritable = false; + $subfolders = []; + + if ($isFolderThere && !$isOpenbasedirRestricted) + { + $isWritable = is_writable($folder); + $subfolders = JFolder::folders($folder); + } + + // In case we can't identify the parent folder, use ourselves. + $parent = $folder; + $breadcrumbs = array(); + + // Try to get the parent directory + $pathparts = explode(DIRECTORY_SEPARATOR, $folder); + + if (is_array($pathparts)) + { + $path = ''; + + foreach ($pathparts as $part) + { + $path .= empty($path) ? $part : DIRECTORY_SEPARATOR . $part; + + if (empty($part)) + { + if (DIRECTORY_SEPARATOR != '\\') + { + $path = DIRECTORY_SEPARATOR; + } + + $part = DIRECTORY_SEPARATOR; + } + + $crumb['label'] = $part; + $crumb['folder'] = $path; + $breadcrumbs[] = $crumb; + } + + $junk = array_pop($pathparts); + $parent = implode(DIRECTORY_SEPARATOR, $pathparts); + } + + $this->setState('folder', $folder); + $this->setState('folder_raw', $folder_raw); + $this->setState('parent', $parent); + $this->setState('exists', $isFolderThere); + $this->setState('inRoot', $isInRoot); + $this->setState('openbasedirRestricted', $isOpenbasedirRestricted); + $this->setState('writable', $isWritable); + $this->setState('subfolders', $subfolders); + $this->setState('breadcrumbs', $breadcrumbs); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Configuration.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Configuration.php new file mode 100644 index 00000000..d1215644 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Configuration.php @@ -0,0 +1,176 @@ +getState('engineconfig', array()); + + // Forbid stupidly selecting the site's root as the output or temporary directory + if (array_key_exists('akeeba.basic.output_directory', $data)) + { + $folder = $data['akeeba.basic.output_directory']; + $folder = Factory::getFilesystemTools()->translateStockDirs($folder, true, true); + $check = Factory::getFilesystemTools()->translateStockDirs('[SITEROOT]', true, true); + + if ($check == $folder) + { + $data['akeeba.basic.output_directory'] = '[DEFAULT_OUTPUT]'; + } + } + + // Unprotect the configuration and merge it + $config = Factory::getConfiguration(); + $protectedKeys = $config->getProtectedKeys(); + $config->resetProtectedKeys(); + $config->mergeArray($data, false, false); + $config->setProtectedKeys($protectedKeys); + + // Save configuration + Platform::getInstance()->save_configuration(); + } + + /** + * Test the FTP connection. + * + * @return void + */ + public function testFTP() + { + $config = array( + 'host' => $this->getState('host'), + 'port' => $this->getState('port'), + 'user' => $this->getState('user'), + 'pass' => $this->getState('pass'), + 'initdir' => $this->getState('initdir'), + 'usessl' => $this->getState('usessl'), + 'passive' => $this->getState('passive'), + ); + + // Check for bad settings + if (substr($config['host'], 0, 6) == 'ftp://') + { + throw new RuntimeException(JText::_('COM_AKEEBA_CONFIG_FTPTEST_BADPREFIX'), 500); + } + + // Perform the FTP connection test + $test = new Directftp(); + $test->initialize('', $config); + $errorMessage = $test->getError(); + + if (!empty($errorMessage) && !$test->connect_ok) + { + throw new RuntimeException($errorMessage, 500); + } + } + + /** + * Test the SFTP connection. + * + * @return void + */ + public function testSFTP() + { + $config = array( + 'host' => $this->getState('host'), + 'port' => $this->getState('port'), + 'user' => $this->getState('user'), + 'pass' => $this->getState('pass'), + 'privkey' => $this->getState('privkey'), + 'pubkey' => $this->getState('pubkey'), + 'initdir' => $this->getState('initdir'), + ); + + // Check for bad settings + if (substr($config['host'], 0, 7) == 'sftp://') + { + throw new RuntimeException(JText::_('COM_AKEEBA_CONFIG_SFTPTEST_BADPREFIX'), 500); + } + + // Perform the FTP connection test + $test = new Directsftp(); + $test->initialize('', $config); + $errorMessages = $test->getWarnings(); + $errorMessage = array_shift($errorMessages); + + if (!empty($errorMessage) && !$test->connect_ok) + { + throw new RuntimeException($errorMessage[0], 500); + } + } + + /** + * Opens an OAuth window for the selected post-processing engine + * + * @return void + */ + public function dpeOuthOpen() + { + $engine = $this->getState('engine'); + $params = $this->getState('params', array()); + + // Get a callback URI for OAuth 2 + $params['callbackURI'] = JUri::base() . '/index.php?option=com_akeeba&view=Configuration&task=dpecustomapiraw&engine=' . $engine; + + // Get the Input object + $params['input'] = $this->input->getData(); + + // Get the engine + $engineObject = Factory::getPostprocEngine($engine); + + if ($engineObject === false) + { + return; + } + + $engineObject->oauthOpen($params); + } + + /** + * Runs a custom API call for the selected post-processing engine + * + * @return mixed + */ + public function dpeCustomAPICall() + { + $engine = $this->getState('engine'); + $method = $this->getState('method'); + $params = $this->getState('params', array()); + + // Get the Input object + $params['input'] = $this->input->getData(); + + $engineObject = Factory::getPostprocEngine($engine); + + if ($engineObject === false) + { + return false; + } + + return $engineObject->customAPICall($method, $params); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/ConfigurationWizard.php b/deployed/akeeba/administrator/components/com_akeeba/Model/ConfigurationWizard.php new file mode 100644 index 00000000..f9031bbf --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/ConfigurationWizard.php @@ -0,0 +1,618 @@ +get_active_profile(); + + // Get the output and temporary directory + $engineConfig = Factory::getConfiguration(); + $outputDirectory = $engineConfig->get('akeeba.basic.output_directory', ''); + + $fixOut = true; + + if (is_dir($outputDirectory)) + { + // Test the writability of the directory + $filename = $outputDirectory . '/test.dat'; + $fixOut = !@file_put_contents($filename, 'test'); + + if (!$fixOut) + { + // Directory writable, remove the temp file + @unlink($filename); + } + else + { + // Try to chmod the directory + $this->chmod($outputDirectory, 511); + // Repeat the test + $fixOut = !@file_put_contents($filename, 'test'); + + if (!$fixOut) + { + // Directory writable, remove the temp file + @unlink($filename); + } + } + } + + // Do I have to fix the output directory? + if ($fixOut && ($dontRecurse < 1)) + { + $engineConfig->set('akeeba.basic.output_directory', '[DEFAULT_OUTPUT]'); + Platform::getInstance()->save_configuration($profile_id); + + // After fixing the directory, run ourselves again + return $this->autofixDirectories(1); + } + elseif ($fixOut) + { + // If we reached this point after recursing, we can't fix the permissions + // and the user has to RTFM and fix the issue! + return false; + } + + return true; + } + + /** + * Creates a temporary file of a specific size + * + * @param int $blocks How many 128Kb blocks to write. Common values: 1, 2, 4, 16, 40, 80, 81 + * @param string|null $tempdir + * + * @return bool TRUE on success + */ + public function createTempFile($blocks = 1, $tempdir = null) + { + if (empty($tempdir)) + { + $aeconfig = Factory::getConfiguration(); + $tempdir = $aeconfig->get('akeeba.basic.output_directory', ''); + } + + $sixtyfourBytes = '012345678901234567890123456789012345678901234567890123456789ABCD'; + $oneKilo = ''; + $oneBlock = ''; + + for ($i = 0; $i < 16; $i ++) + { + $oneKilo .= $sixtyfourBytes; + } + + for ($i = 0; $i < 128; $i ++) + { + $oneBlock .= $oneKilo; + } + + $filename = tempnam($tempdir, 'confwiz'); + @unlink($filename); + + $fp = @fopen($filename, 'w'); + + if ($fp !== false) + { + for ($i = 0; $i < $blocks; $i ++) + { + if (!@fwrite($fp, $oneBlock)) + { + @fclose($fp); + @unlink($filename); + + return false; + } + } + + @fclose($fp); + @unlink($filename); + } + else + { + return false; + } + + return true; + } + + /** + * Sleeps for a given amount of time. Returns false if the sleep time requested is over the maximum execution time. + * + * @param int $secondsDelay Seconds to sleep + * + * @return bool FALSE if we cannot sleep that long + */ + public function doNothing($secondsDelay = 1) + { + // Try to get the maximum execution time and PHP memory limit + if (function_exists('ini_get')) + { + $maxexec = ini_get("max_execution_time"); + $memlimit = ini_get("memory_limit"); + } + else + { + $maxexec = 14; + $memlimit = 16777216; + } + + // Unknown time limit; suppose 10s + if (!is_numeric($maxexec) || ($maxexec == 0)) + { + $maxexec = 10; + } + + // Some servers report silly values, i.e. 30000, which Do Not Work™ :( + if ($maxexec > 180) + { + $maxexec = 10; + } + + // Sometimes memlimit comes with the M or K suffixes. Parse them. + if (is_string($memlimit)) + { + $memlimit = strtoupper(trim(str_replace(' ', '', $memlimit))); + + if (substr($memlimit, - 1) == 'K') + { + $memlimit = 1024 * substr($memlimit, 0, - 1); + } + elseif (substr($memlimit, - 1) == 'M') + { + $memlimit = 1024 * 1024 * substr($memlimit, 0, - 1); + } + elseif (substr($memlimit, - 1) == 'G') + { + $memlimit = 1024 * 1024 * 1024 * substr($memlimit, 0, - 1); + } + } + + // Unknown limit; suppose 16M + if (!is_numeric($memlimit) || ($memlimit === 0)) + { + $memlimit = 16777216; + } + + // No limit; suppose 128M + if ($memlimit === - 1) + { + $memlimit = 134217728; + } + + // Get the current memory usage (or assume one if the metric is not available) + if (function_exists('memory_get_usage')) + { + $usedram = memory_get_usage(); + } + else + { + $usedram = 7340032; // Suppose 7M of RAM usage if the metric isn't available; + } + + // If we have less than 12M of RAM left, we have to limit ourselves to 6 seconds of + // total execution time (emperical value!) to avoid deadly memory outages + if (($memlimit - $usedram) < 12582912) + { + $maxexec = 5; + } + + // If the requested delay is over the $maxexec limit (minus one second + // for application initialization), return false + if ($secondsDelay > ($maxexec - 1)) + { + return false; + } + + // And now, run the silly loop to simulate the CPU usage pattern during backup + $start = microtime(true); + $loop = true; + + while ($loop) + { + // Waste some CPU power... + for ($i = 1; $i < 1000; $i ++) + { + $j = exp(($i * $i / 123 * 864) >> 2); + } + + // ... then sleep for a millisec + usleep(1000); + + // Are we done yet? + $end = microtime(true); + + if (($end - $start) >= $secondsDelay) + { + $loop = false; + } + } + + return true; + } + + /** + * This method will analyze your database tables and try to figure out the optimal batch row count value so that its + * SELECT doesn't return excessive amounts of data. The only drawback is that it only accounts for the core tables, + * but that is usually a good metric. + * + * @return void + */ + public function analyzeDatabase() + { + // Try to get the PHP memory limit + if (function_exists('ini_get')) + { + $memlimit = ini_get("memory_limit"); + } + else + { + $memlimit = 16777216; + } + + if (!is_numeric($memlimit) || ($memlimit === 0)) + { + $memlimit = 16777216; // Unknown limit; suppose 16M + } + + if ($memlimit === - 1) + { + $memlimit = 134217728; // No limit; suppose 128M + } + + // Get the current memory usage (or assume one if the metric is not available) + if (function_exists('memory_get_usage')) + { + $usedram = memory_get_usage(); + } + else + { + $usedram = 7340032; // Suppose 7M of RAM usage if the metric isn't available; + } + + // How much RAM can I spare? It's the max memory minus the current memory usage and an extra + // 5Mb to cater for Akeeba Engine's peak memory usage + $max_mem_usage = $usedram + 5242880; + $ram_allowance = $memlimit - $max_mem_usage; + + // If the RAM allowance is too low, assume 2Mb (emperical value) + if ($ram_allowance < 2097152) + { + $ram_allowance = 2097152; + } + + // If SHOW TABLE STATUS is not supported this is a safe-ish value. + $rowCount = 100; + + // Get the table statistics + $db = $this->container->db; + + if (strtolower(substr($db->name, 0, 5)) == 'mysql') + { + // The table analyzer only works with MySQL + $db->setQuery("SHOW TABLE STATUS"); + + try + { + $metrics = $db->loadAssocList(); + + if (method_exists($db, 'getError') && $db->getError()) + { + $metrics = null; + } + } + catch (\Exception $exc) + { + $metrics = null; + } + + // SHOW TABLE STATUS is supported. + if (!is_null($metrics)) + { + $rowCount = 1000; // Start with the default value + + if (!empty($metrics)) + { + foreach ($metrics as $table) + { + // Get row count and average row length + $rows = $table['Rows']; + $avg_len = $table['Avg_row_length']; + + // Calculate RAM usage with current settings + $max_rows = min($rows, $rowCount); + $max_ram_current = $max_rows * $avg_len; + + if ($max_ram_current > $ram_allowance) + { + // Hm... over the allowance. Let's try to find a sweet spot. + $max_rows = (int) ($ram_allowance / $avg_len); + // Quantize to multiple of 10 rows + $max_rows = 10 * floor($max_rows / 10); + + // Can't really go below 10 rows / batch + if ($max_rows < 10) + { + $max_rows = 10; + } + + // If the new setting is less than the current $rowCount, use the new setting + if ($rowCount > $max_rows) + { + $rowCount = $max_rows; + } + } + } + } + } + } + + $profile_id = Platform::getInstance()->get_active_profile(); + $config = Factory::getConfiguration(); + + // Use the correct database dump engine + $config->set('akeeba.advanced.dump_engine', 'reverse'); + + if (strpos($db->name, 'mysql') !== false) + { + $config->set('akeeba.advanced.dump_engine', 'native'); + } + + // Save the row count per batch + $config->set('engine.dump.common.batchsize', $rowCount); + + // Enable SQL file splitting - default is 512K unless the part_size is less than that! + $splitsize = 524288; + $partsize = $config->get('engine.archiver.common.part_size', 0); + + if (($partsize < $splitsize) && !empty($partsize)) + { + $splitsize = $partsize; + } + + $config->set('engine.dump.common.splitsize', $splitsize); + + // Enable extended INSERTs + $config->set('engine.dump.common.extended_inserts', '1'); + + // Determine optimal packet size (must be at most two fifths of the split size and no more than 256K) + $packet_size = (int) $splitsize * 0.4; + + if ($packet_size > 262144) + { + $packet_size = 262144; + } + + $config->set('engine.dump.common.packet_size', $packet_size); + + // Enable the native dump engine + $config->set('akeeba.advanced.dump_engine', 'native'); + + Platform::getInstance()->save_configuration($profile_id); + } + + /** + * Executes the action requested through AJAX + * + * @return bool + */ + public function runAjax() + { + // Only allowed actions + $allowedActions = [ + 'ping', 'minexec', 'applyminexec', 'directories', 'database', 'maxexec', 'applymaxexec', 'partsize' + ]; + + // Get the requested action from the model state + $action = $this->getState('act'); + + $result = false; + + if (in_array($action, $allowedActions) && method_exists($this, $action)) + { + $result = call_user_func([$this, $action]); + } + + return $result; + } + + /** + * Pings the configuration wizard process and marks the current profile as configured + * + * @return bool TRUE, always + */ + private function ping() + { + // Get the profile ID + $profile_id = Platform::getInstance()->get_active_profile(); + + // Set the embedded installer to the default ANGIE installer + $engineConfig = Factory::getConfiguration(); + $engineConfig->set('akeeba.advanced.embedded_installer', 'angie'); + + // And mark this profile as already configured + $engineConfig->set('akeeba.flag.confwiz', 1); + + Platform::getInstance()->save_configuration($profile_id); + + return true; + } + + /** + * Try different values of minimum execution time + * + * @return bool TRUE, always + */ + private function minexec() + { + $seconds = $this->input->get('seconds', '0.5', 'float'); + + if ($seconds < 1) + { + usleep($seconds * 1000000); + } + else + { + sleep($seconds); + } + + return true; + } + + /** + * Saves the AJAX preference and the minimum execution time + * + * @return bool TRUE, always + */ + private function applyminexec() + { + // Get the user parameters + $iframes = $this->input->get('iframes', 0, 'int'); + $minexec = $this->input->get('minexec', 2.0, 'float'); + + // Save the settings + $profile_id = Platform::getInstance()->get_active_profile(); + $engineConfig = Factory::getConfiguration(); + $engineConfig->set('akeeba.basic.useiframe', $iframes); + $engineConfig->set('akeeba.tuning.min_exec_time', $minexec * 1000); + Platform::getInstance()->save_configuration($profile_id); + + // Enforce the min exec time + $timer = Factory::getTimer(); + $timer->enforce_min_exec_time(false); + + // Done! + return true; + } + + /** + * Try to make the directories writable or provide a set of writable directories + * + * @return bool TRUE if we coud fix the permissions of the directories + */ + private function directories() + { + $timer = Factory::getTimer(); + $result = $this->autofixDirectories(); + $timer->enforce_min_exec_time(false); + + return $result; + } + + /** + * Analyze the database and apply optimized database dump settings + * + * @return bool TRUE if we were successful + */ + private function database() + { + $timer = Factory::getTimer(); + $this->analyzeDatabase(); + $timer->enforce_min_exec_time(false); + + return true; + } + + /** + * Try to apply a specific maximum execution time setting + * + * @return bool + */ + private function maxexec() + { + $seconds = $this->input->get('seconds', 30, 'int'); + $timer = Factory::getTimer(); + $result = $this->doNothing($seconds); + $timer->enforce_min_exec_time(false); + + return $result; + } + + /** + * Save a specific maximum execution time preference to the database + * + * @return bool + */ + private function applymaxexec() + { + // Get the user parameters + $maxexec = $this->input->get('seconds', 2, 'int'); + + // Save the settings + $timer = Factory::getTimer(); + $profile_id = Platform::getInstance()->get_active_profile(); + $config = Factory::getConfiguration(); + $config->set('akeeba.tuning.max_exec_time', $maxexec); + $config->set('akeeba.tuning.run_time_bias', '75'); + $config->set('akeeba.advanced.scan_engine', 'smart'); + $config->set('akeeba.advanced.archiver_engine', 'jpa'); + Platform::getInstance()->save_configuration($profile_id); + + // Enforce the min exec time + $timer->enforce_min_exec_time(false); + + // Done! + return true; + } + + /** + * Creates a dummy file of a given size. Remember to give the filesize query parameter in bytes! + * + * @return bool + */ + public function partsize() + { + $timer = Factory::getTimer(); + $blocks = $this->input->get('blocks', 1, 'int'); + + $result = $this->createTempFile($blocks); + + if ($result) + { + // Save the setting + if ($blocks > 200) + { + $blocks = 16383; // Over 25Mb = 2Gb minus 128Kb limit (safe setting for PHP not running on 64-bit Linux) + } + + $profile_id = Platform::getInstance()->get_active_profile(); + $config = Factory::getConfiguration(); + $config->set('engine.archiver.common.part_size', $blocks * 128 * 1024); + Platform::getInstance()->save_configuration($profile_id); + } + + // Enforce the min exec time + $timer->enforce_min_exec_time(false); + + return $result; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/ControlPanel.php b/deployed/akeeba/administrator/components/com_akeeba/Model/ControlPanel.php new file mode 100644 index 00000000..9c5a94b2 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/ControlPanel.php @@ -0,0 +1,534 @@ +container->db; + + $query = $db->getQuery(true) + ->select(array( + $db->qn('id'), + $db->qn('description') + ))->from($db->qn('#__ak_profiles')) + ->where($db->qn('quickicon') . ' = ' . $db->q(1)) + ->order($db->qn('id') . " ASC"); + + $db->setQuery($query); + + $ret = $db->loadObjectList(); + + if (empty($ret)) + { + $ret = array(); + } + + return $ret; + } + + /** + * Creates an icon definition entry + * + * @param string $iconFile The filename of the icon on the GUI button + * @param string $label The label below the GUI button + * @param string $view The view to fire up when the button is clicked + * + * @return array The icon definition array + */ + public function _makeIconDefinition($iconFile, $label, $view = null, $task = null) + { + return array( + 'icon' => $iconFile, + 'label' => $label, + 'view' => $view, + 'task' => $task + ); + } + + /** + * Was the last backup a failed one? Used to apply magic settings as a means of troubleshooting. + * + * @return bool + */ + public function isLastBackupFailed() + { + // Get the last backup record ID + $list = Platform::getInstance()->get_statistics_list(array('limitstart' => 0, 'limit' => 1)); + + if (empty($list)) + { + return false; + } + + $id = $list[0]; + + $record = Platform::getInstance()->get_statistics($id); + + return ($record['status'] == 'fail'); + } + + /** + * Checks that the media permissions are oh seven double five for directories and oh six double four for files and + * fixes them if they are incorrect. + * + * @param bool $force Forcibly check subresources, even if the parent has correct permissions + * + * @return bool False if we couldn't figure out what's going on + */ + public function fixMediaPermissions($force = false) + { + // Are we on Windows? + $isWindows = (DIRECTORY_SEPARATOR == '\\'); + + if (function_exists('php_uname')) + { + $isWindows = stristr(php_uname(), 'windows'); + } + + // No point changing permissions on Windows, as they have ACLs + if ($isWindows) + { + return true; + } + + // Check the parent permissions + $parent = JPATH_ROOT . '/media/com_akeeba'; + $parentPerms = fileperms($parent); + + // If we can't determine the parent's permissions, bail out + if ($parentPerms === false) + { + return false; + } + + // Fooling some broken file scanners. + $ohSevenFiveFive = 500 - 7; + $ohFourOhSevenFiveFive = 16000 + 900 - 23; + $ohSixFourFour = 450 - 30; + $ohOneDoubleOhSixFourFour = 33000 + 200 - 12; + + // Fix the parent's permissions if required + if (($parentPerms != $ohSevenFiveFive) && ($parentPerms != $ohFourOhSevenFiveFive)) + { + $this->chmod($parent, $ohSevenFiveFive); + } + elseif (!$force) + { + return true; + } + + // During development we use symlinks and we don't wanna see that big fat warning + if (@is_link($parent)) + { + return true; + } + + JLoader::import('joomla.filesystem.folder'); + + $result = true; + + // Loop through subdirectories + $folders = JFolder::folders($parent, '.', 3, true); + + foreach ($folders as $folder) + { + $perms = fileperms($folder); + + if (($perms != $ohSevenFiveFive) && ($perms != $ohFourOhSevenFiveFive)) + { + $result &= $this->chmod($folder, $ohSevenFiveFive); + } + } + + // Loop through files + $files = JFolder::files($parent, '.', 3, true); + + foreach ($files as $file) + { + $perms = fileperms($file); + + if (($perms != $ohSixFourFour) && ($perms != $ohOneDoubleOhSixFourFour)) + { + $result &= $this->chmod($file, $ohSixFourFour); + } + } + + return $result; + } + + /** + * Checks if we should enable settings encryption and applies the change + * + * @return void + */ + public function checkSettingsEncryption() + { + // Do we have a key file? + JLoader::import('joomla.filesystem.file'); + $filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php'; + + if (JFile::exists($filename)) + { + // We have a key file. Do we need to disable it? + if ($this->container->params->get('useencryption', -1) == 0) + { + // User asked us to disable encryption. Let's do it. + $this->disableSettingsEncryption(); + } + + return; + } + + if (!Factory::getSecureSettings()->supportsEncryption()) + { + return; + } + + if ($this->container->params->get('useencryption', -1) != 0) + { + // User asked us to enable encryption (or he left us with the default setting!). Let's do it. + $this->enableSettingsEncryption(); + } + } + + /** + * Disables the encryption of profile settings. If the settings were already encrypted they are automatically + * decrypted. + * + * @return void + */ + private function disableSettingsEncryption() + { + // Load the server key file if necessary + + $filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php'; + $key = Factory::getSecureSettings()->getKey(); + + // Loop all profiles and decrypt their settings + /** @var Profiles $profilesModel */ + $profilesModel = $this->container->factory->model('Profiles')->tmpInstance(); + $profiles = $profilesModel->get(true); + $db = $this->container->db; + + /** @var Profiles $profile */ + foreach ($profiles as $profile) + { + $id = $profile->getId(); + $config = Factory::getSecureSettings()->decryptSettings($profile->configuration, $key); + $sql = $db->getQuery(true) + ->update($db->qn('#__ak_profiles')) + ->set($db->qn('configuration') . ' = ' . $db->q($config)) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $db->setQuery($sql); + $db->execute(); + } + + // Decrypt the Secret Word settings in the database + $params = $this->container->params; + SecretWord::enforceDecrypted($params, 'frontend_secret_word', $key); + + // Finally, remove the key file + if (!@unlink($filename)) + { + JLoader::import('joomla.filesystem.file'); + JFile::delete($filename); + } + } + + /** + * Enabled the encryption of profile settings. Existing settings are automatically encrypted. + * + * @return void + */ + private function enableSettingsEncryption() + { + $key = $this->createSettingsKey(); + + if (empty($key) || ($key == false)) + { + return; + } + + // Loop all profiles and encrypt their settings + /** @var \Akeeba\Backup\Admin\Model\Profiles $profilesModel */ + $profilesModel = $this->container->factory->model('Profiles')->tmpInstance(); + $profiles = $profilesModel->get(true); + $db = $this->container->db; + if (!empty($profiles)) + { + foreach ($profiles as $profile) + { + $id = $profile->id; + $config = Factory::getSecureSettings()->encryptSettings($profile->configuration, $key); + $sql = $db->getQuery(true) + ->update($db->qn('#__ak_profiles')) + ->set($db->qn('configuration') . ' = ' . $db->q($config)) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $db->setQuery($sql); + $db->execute(); + } + } + } + + /** + * Creates an encryption key for the settings and saves it in the /BackupEngine/serverkey.php path + * + * @return bool|string FALSE on failure, the encryptions key otherwise + */ + private function createSettingsKey() + { + $randVal = new RandomValue(); + $rawKey = $randVal->generate(64); + $key = base64_encode($rawKey); + + $filecontents = ""; + $filename = $this->container->backEndPath . '/BackupEngine/serverkey.php'; + + JLoader::import('joomla.filesystem.file'); + $result = JFile::write($filename, $filecontents); + + if (!$result) + { + return false; + } + + return $rawKey; + } + + /** + * Updates some internal settings: + * + * - The stored URL of the site, used for the front-end backup feature (altbackup.php) + * - The detected Joomla! libraries path + * - Marks all existing profiles as configured, if necessary + */ + public function updateMagicParameters() + { + if (!$this->container->params->get('confwiz_upgrade', 0)) + { + $this->markOldProfilesConfigured(); + } + + $this->container->params->set('confwiz_upgrade', 1); + $this->container->params->set('siteurl', str_replace('/administrator', '', JUri::base())); + $this->container->params->set('jlibrariesdir', Factory::getFilesystemTools()->TranslateWinPath(JPATH_LIBRARIES)); + $this->container->params->set('jversion', '1.6'); + $this->container->params->save(); + } + + /** + * Do you have to issue a warning that setting the Download ID in the CORE edition has no effect? + * + * @return bool True if you need to show the warning + */ + public function mustWarnAboutDownloadIDInCore() + { + $ret = false; + $isPro = AKEEBA_PRO; + + if ($isPro) + { + return $ret; + } + + $dlid = $this->container->params->get('update_dlid', ''); + + if (preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $dlid)) + { + $ret = true; + } + + return $ret; + } + + /** + * Does the user need to enter a Download ID in the component's Options page? + * + * @return bool + */ + public function needsDownloadID() + { + // Do I need a Download ID? + $ret = true; + $isPro = AKEEBA_PRO; + + if (!$isPro) + { + $ret = false; + } + else + { + $dlid = $this->container->params->get('update_dlid', ''); + + if (preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $dlid)) + { + $ret = false; + } + } + + return $ret; + } + + /** + * Checks the database for missing / outdated tables and runs the appropriate SQL scripts if necessary. + * + * @throws RuntimeException If the previous database update is stuck + * + * @return $this + */ + public function checkAndFixDatabase() + { + $params = $this->container->params; + + // First of all let's check if we are already updating + $stuck = $params->get('updatedb', 0); + + if ($stuck) + { + throw new RuntimeException('Previous database update is flagged as stuck'); + } + + // Then set the flag + $params->set('updatedb', 1); + $params->save(); + + // Install or update database + $dbInstaller = new Installer( + $this->container->db, + JPATH_ADMINISTRATOR . '/components/com_akeeba/sql/xml' + + ); + + $dbInstaller->updateSchema(); + + // And finally remove the flag if everything went fine + $params->set('updatedb', null); + $params->save(); + + return $this; + } + + /** + * Akeeba Backup 4.3.2 displays a popup if your profile is not already configured by Configuration Wizard, the + * Configuration page or imported from the Profiles page. This bit of code makes sure that existing profiles will + * be marked as already configured just the FIRST time you upgrade to the new version from an old version. + * + * @return void + */ + public function markOldProfilesConfigured() + { + // Get all profiles + $db = $this->container->db; + + $query = $db->getQuery(true) + ->select(array( + $db->qn('id'), + ))->from($db->qn('#__ak_profiles')) + ->order($db->qn('id') . " ASC"); + $db->setQuery($query); + $profiles = $db->loadColumn(); + + // Save the current profile number + $oldProfile = $this->container->platform->getSessionVar('profile', 1, 'akeeba'); + + // Update all profiles + foreach ($profiles as $profile_id) + { + Factory::nuke(); + Platform::getInstance()->load_configuration($profile_id); + $config = Factory::getConfiguration(); + $config->set('akeeba.flag.confwiz', 1); + Platform::getInstance()->save_configuration($profile_id); + } + + // Restore the old profile + Factory::nuke(); + Platform::getInstance()->load_configuration($oldProfile); + } + + /** + * Check the strength of the Secret Word for front-end and remote backups. If it is insecure return the reason it + * is insecure as a string. If the Secret Word is secure return an empty string. + * + * @return string + */ + public function getFrontendSecretWordError() + { + // Is frontend backup enabled? + $febEnabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', 0) != 0; + + if (!$febEnabled) + { + return ''; + } + + $secretWord = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', ''); + + try + { + \Akeeba\Engine\Util\Complexify::isStrongEnough($secretWord); + } + catch (RuntimeException $e) + { + // Ah, the current Secret Word is bad. Create a new one if necessary. + $newSecret = $this->container->platform->getSessionVar('newSecretWord', null, 'akeeba.cpanel'); + + if (empty($newSecret)) + { + $random = new \Akeeba\Engine\Util\RandomValue(); + $newSecret = $random->generateString(32); + $this->container->platform->setSessionVar('newSecretWord', $newSecret, 'akeeba.cpanel'); + } + + return $e->getMessage(); + } + + return ''; + } + + /** + * Checks if the mbstring extension is installed and enabled + * + * @return bool + */ + public function checkMbstring() + { + return function_exists('mb_strlen') && function_exists('mb_convert_encoding') && + function_exists('mb_substr') && function_exists('mb_convert_case'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/DatabaseFilters.php b/deployed/akeeba/administrator/components/com_akeeba/Model/DatabaseFilters.php new file mode 100644 index 00000000..8d1bc0fe --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/DatabaseFilters.php @@ -0,0 +1,274 @@ +knownFilterTypes = ['tables', 'tabledata']; + } + + /** + * Returns a list of the database tables, views, procedures, functions and triggers, + * along with their filter status in array format, for use in the GUI + * + * @param string $root The database root we're working on + * + * @return array Hash array. 'tables' is an array list of tables w/ metadata. 'root' is the current db root. + */ + public function make_listing($root) + { + // Get database inclusion filters + $filters = Factory::getFilters(); + $database_list = $filters->getInclusions('db'); + + // Load the database object for the selected database + $config = $database_list[ $root ]; + $config['user'] = $config['username']; + $db = Factory::getDatabase($config); + + // Load the table data + try + { + $table_data = $db->getTables(); + } + catch (Exception $e) + { + $table_data = array(); + } + + // Process filters + $tables = array(); + + if (!empty($table_data)) + { + foreach ($table_data as $table_name => $table_type) + { + $status = array(); + + // Add table type + $status['type'] = $table_type; + + // Check dbobject/all filter (exclude) + $result = $filters->isFilteredExtended($table_name, $root, 'dbobject', 'all', $byFilter); + $status['tables'] = (!$result) ? 0 : (($byFilter == 'tables') ? 1 : 2); + + // Check dbobject/content filter (skip table data) + $result = $filters->isFilteredExtended($table_name, $root, 'dbobject', 'content', $byFilter); + $status['tabledata'] = (!$result) ? 0 : (($byFilter == 'tabledata') ? 1 : 2); + + // We can't filter contents of views, merge tables, black holes, procedures, functions and triggers + if ($table_type != 'table') + { + $status['tabledata'] = 2; + } + + $tables[ $table_name ] = $status; + } + } + + return array( + 'tables' => $tables, + 'root' => $root + ); + } + + /** + * Returns an array containing a mapping of db root names and their human-readable representation + * + * @return array Array of objects; "value" contains the root name, "text" the human-readable text + */ + public function get_roots() + { + // Get database inclusion filters + $filters = Factory::getFilters(); + $database_list = $filters->getInclusions('db'); + + $ret = array(); + + foreach ($database_list as $name => $definition) + { + $root = $definition['host']; + + if (!empty($definition['port'])) + { + $root .= ':' . $definition['port']; + } + + $root .= '/' . $definition['database']; + + if ($name == '[SITEDB]') + { + $root = JText::_('COM_AKEEBA_DBFILTER_LABEL_SITEDB'); + } + + $ret[] = (object)[ + 'value' => $name, + 'text' => $root, + ]; + } + + return $ret; + } + + /** + * Toggle a filter + * + * @param string $root Database root + * @param string $item The db entity we want to toggle the filter for + * @param string $filter The name of the filter to apply (tables, tabledata) + * + * @return array + */ + public function toggle($root, $item, $filter) + { + return $this->applyExclusionFilter($filter, $root, $item, 'toggle'); + } + + /** + * Set a filter + * + * @param string $root Database root + * @param string $item The db entity we want to toggle the filter for + * @param string $filter The name of the filter to apply (tables, tabledata) + * + * @return array + */ + public function remove($root, $item, $filter) + { + return $this->applyExclusionFilter($filter, $root, $item, 'remove'); + } + + /** + * Set a filter + * + * @param string $root Database root + * @param string $item The db entity we want to toggle the filter for + * @param string $filter The name of the filter to apply (tables, tabledata) + * + * @return array + */ + public function setFilter($root, $item, $filter) + { + return $this->applyExclusionFilter($filter, $root, $item, 'set'); + } + + /** + * Swap a filter + * + * @param string $root Database root + * @param string $old_item The db entity that used to be filtered and will no longer be + * @param string $new_item The db entity that wasn't filtered but now will be + * @param string $filter The name of the filter to apply (tables, tabledata) + * + * @return array + */ + public function swap($root, $old_item, $new_item, $filter) + { + return $this->applyExclusionFilter($filter, $root, $new_item, 'swap', $old_item); + } + + /** + * Retrieves the filters as an array. Used for the tabular filter editor. + * + * @param string $root The root node to search filters on + * + * @return array An array of hash arrays containing node and type for each filtered element + */ + public function &get_filters($root) + { + return $this->getTabularFilters($root); + } + + /** + * Resets all filters + * + * @param string $root Root directory + * + * @return array + */ + public function resetFilters($root) + { + $this->resetAllFilters($root); + + return $this->make_listing($root); + } + + /** + * Handles a request coming in through AJAX. Basically, this is a simple proxy to the model methods. + * + * @return array + */ + public function doAjax() + { + $action = $this->getState('action'); + $verb = array_key_exists('verb', get_object_vars($action)) ? $action->verb : null; + + $ret_array = array(); + + switch ($verb) + { + // Return a listing for the normal view + case 'list': + $ret_array = $this->make_listing($action->root); + break; + + // Toggle a filter's state + case 'toggle': + $ret_array = $this->toggle($action->root, $action->node, $action->filter); + break; + + // Set a filter (used by the editor) + case 'set': + $ret_array = $this->setFilter($action->root, $action->node, $action->filter); + break; + + // Remove a filter (used by the editor) + case 'remove': + $ret_array = $this->remove($action->root, $action->node, $action->filter); + break; + + // Swap a filter (used by the editor) + case 'swap': + $ret_array = $this->swap($action->root, $action->old_node, $action->new_node, $action->filter); + break; + + // Tabular view + case 'tab': + $ret_array = $this->get_filters($action->root); + break; + + // Reset filters + case 'reset': + $ret_array = $this->resetFilters($action->root); + break; + } + + return $ret_array; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Exceptions/TransferFatalError.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Exceptions/TransferFatalError.php new file mode 100644 index 00000000..34ba7cd0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Exceptions/TransferFatalError.php @@ -0,0 +1,16 @@ +directory + * + * @return array + */ + public function getListing() + { + $dir = $this->directory; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $this->parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + $this->parent_directory = ''; + + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + + $this->parent_directory = '/'; + + if (!empty($copy_of_parts)) + { + $this->parent_directory = '/' . implode('/', $copy_of_parts); + } + } + + // Connect to the server + if ($this->ssl) + { + $con = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $con = @ftp_connect($this->host, $this->port); + } + + if ($con === false) + { + throw new RuntimeException(JText::_('COM_AKEEBA_FTPBROWSER_ERROR_HOSTNAME')); + } + + // Login + $result = @ftp_login($con, $this->username, $this->password); + + if ($result === false) + { + throw new RuntimeException(JText::_('COM_AKEEBA_FTPBROWSER_ERROR_USERPASS')); + } + + // Set the passive mode -- don't care if it fails, though! + @ftp_pasv($con, $this->passive); + + // Try to chdir to the specified directory + if (!empty($dir)) + { + $result = @ftp_chdir($con, $dir); + + if ($result === false) + { + throw new RuntimeException(JText::_('COM_AKEEBA_FTPBROWSER_ERROR_NOACCESS')); + } + } + else + { + $this->directory = @ftp_pwd($con); + + $parsed_dir = trim($this->directory, '/'); + $this->parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + $this->parent_directory = $this->directory; + } + + // Get a raw directory listing (hoping it's a UNIX server!) + $list = @ftp_rawlist($con, '.'); + + ftp_close($con); + + if ($list === false) + { + throw new RuntimeException(JText::_('COM_AKEEBA_FTPBROWSER_ERROR_UNSUPPORTED')); + } + + // Parse the raw listing into an array + $folders = $this->parse_rawlist($list); + + return $folders; + } + + /** + * Parse the raw list of folders returned by the server into a usable simple array of folders + * + * @param array $list The raw folder list returned by ftp_rawlist + * + * @return array The parsed list of folders + */ + private function parse_rawlist(array $list) + { + $folders = []; + + foreach ($list as $v) + { + + $vinfo = preg_split("/[\s]+/", $v, 9); + + if ($vinfo[0] !== "total") + { + $perms = $vinfo[0]; + + if (substr($perms, 0, 1) == 'd') + { + $folders[] = $vinfo[8]; + } + } + } + + asort($folders); + + return $folders; + } + + /** + * Perform the actual folder browsing. Returns an array that's usable by the UI. + * + * @return array + */ + public function doBrowse() + { + $error = ''; + $list = []; + + try + { + $list = $this->getListing(); + } + catch (RuntimeException $e) + { + $error = $e->getMessage(); + } + + $response_array = array( + 'error' => $error, + 'list' => $list, + 'breadcrumbs' => $this->parts, + 'directory' => $this->directory, + 'parent' => $this->parent_directory + ); + + return $response_array; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/FileFilters.php b/deployed/akeeba/administrator/components/com_akeeba/Model/FileFilters.php new file mode 100644 index 00000000..3e0a9d43 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/FileFilters.php @@ -0,0 +1,474 @@ +knownFilterTypes = ['directories', 'files', 'skipdirs', 'skipfiles']; + } + + /** + * Returns a listing of contained directories and files, as well as their exclusion status + * + * @param string $root The root directory + * @param string $node The subdirectory to scan + * + * @return array + */ + private function &get_listing($root, $node) + { + // Initialize the absolute directory root + $directory = substr($root, 0); + + // Replace stock directory tags, like [SITEROOT] + $stock_dirs = Platform::getInstance()->get_stock_directories(); + + if (!empty($stock_dirs)) + { + foreach ($stock_dirs as $key => $replacement) + { + $directory = str_replace($key, $replacement, $directory); + } + } + + $directory = Factory::getFilesystemTools()->TranslateWinPath($directory); + + // Clean and add the node + $node = Factory::getFilesystemTools()->TranslateWinPath($node); + + // Just a directory separator is treated as no directory at all + if (($node == '/')) + { + $node = ''; + } + + // Trim leading and trailing slashes + $node = trim($node, '/'); + + // Add node to directory + if (!empty($node)) + { + $directory .= '/' . $node; + } + + // Add any required trailing slash to the node to be used below + if (!empty($node)) + { + $node .= '/'; + } + + // Get a filters instance + $filters = Factory::getFilters(); + + // Get a listing of folders and process it + $folders = Factory::getFileLister()->getFolders($directory); + $folders_out = array(); + + if (!empty($folders)) + { + asort($folders); + + foreach ($folders as $folder) + { + $folder = Factory::getFilesystemTools()->TranslateWinPath($folder); + + // Filter out files whose names result to an empty JSON representation + $json_folder = json_encode($folder); + $folder = json_decode($json_folder); + + if (empty($folder)) + { + continue; + } + + $test = $node . $folder; + $status = array(); + + // Check dir/all filter (exclude) + $result = $filters->isFilteredExtended($test, $root, 'dir', 'all', $byFilter); + $status['directories'] = (!$result) ? 0 : (($byFilter == 'directories') ? 1 : 2); + + // Check dir/content filter (skip_files) + $result = $filters->isFilteredExtended($test, $root, 'dir', 'content', $byFilter); + $status['skipfiles'] = (!$result) ? 0 : (($byFilter == 'skipfiles') ? 1 : 2); + + // Check dir/children filter (skip_dirs) + $result = $filters->isFilteredExtended($test, $root, 'dir', 'children', $byFilter); + $status['skipdirs'] = (!$result) ? 0 : (($byFilter == 'skipdirs') ? 1 : 2); + + // Add to output array + $folders_out[ $folder ] = $status; + } + } + + unset($folders); + $folders = $folders_out; + + // Get a listing of files and process it + $files = Factory::getFileLister()->getFiles($directory); + $files_out = array(); + + if (!empty($files)) + { + asort($files); + + foreach ($files as $file) + { + // Filter out files whose names result to an empty JSON representation + $json_file = json_encode($file); + $file = json_decode($json_file); + + if (empty($file)) + { + continue; + } + + $test = $node . $file; + $status = []; + + // Check file/all filter (exclude) + $result = $filters->isFilteredExtended($test, $root, 'file', 'all', $byFilter); + $status['files'] = (!$result) ? 0 : (($byFilter == 'files') ? 1 : 2); + $status['size'] = $this->formatSize(@filesize($directory . '/' . $file), 1); + + // Add to output array + $files_out[$file] = $status; + } + } + + unset($files); + $files = $files_out; + + // Return a compiled array + $retarray = array( + 'folders' => $folders, + 'files' => $files + ); + + return $retarray; + + /* Return array format + * [array] : + * 'folders' [array] : + * (folder_name) => [array]: + * 'directories' => 0|1|2 + * 'skipfiles' => 0|1|2 + * 'skipdirs' => 0|1|2 + * 'files' [array] : + * (file_name) => [array]: + * 'files' => 0|1|2 + * + * Legend: + * 0 -> Not excluded + * 1 -> Excluded by the direct filter + * 2 -> Excluded by another filter (regex, api, an unknown plugin filter...) + */ + } + + /** + * Glues the current directory crumbs and the child directory into a node string + * + * @param array|string $crumbs Breadcrumbs in array or JSON encoded array format + * @param string $child The child folder (relative to the root defined by crumbs) + * + * @return string The absolute node (path) of the $child + */ + private function glue_crumbs(&$crumbs, $child) + { + // Construct the full node + $node = ''; + + // Some servers do not decode the crumbs. I don't know why! + if (!is_array($crumbs) && (substr($crumbs, 0, 1) == '[')) + { + $crumbs = @json_decode($crumbs); + + if ($crumbs === false) + { + $crumbs = array(); + } + } + + if (!is_array($crumbs)) + { + $crumbs = array(); + } + + array_walk($crumbs, function ($value, $index) { + if (in_array(trim($value), array('.', '..'))) + { + throw new \InvalidArgumentException("Unacceptable folder crumbs"); + } + }); + + if ((stristr($child, '/..') !== false) || (stristr($child, '\..') !== false)) + { + throw new \InvalidArgumentException("Unacceptable child folder"); + } + + if (!empty($crumbs)) + { + $node = implode('/', $crumbs); + } + + if (!empty($node)) + { + $node .= '/'; + } + + if (!empty($child)) + { + $node .= $child; + } + + return $node; + } + + /** + * Returns an array with the listing and filter status of a directory + * + * @param string $root Root directory + * @param array|string $crumbs Breadcrumbs in array or JSON encoded array format, defining the parent directory + * @param string $child The child directory we want to scan + * + * @return array + */ + public function make_listing($root, $crumbs = [], $child = '') + { + // Construct the full node + $node = $this->glue_crumbs($crumbs, $child); + + // Create the new crumbs + if (!is_array($crumbs)) + { + $crumbs = array(); + } + + if (!empty($child)) + { + $crumbs[] = $child; + } + + // Get listing with the filter info + $listing = $this->get_listing($root, $node); + + // Assemble the array + $listing['root'] = $root; + $listing['crumbs'] = $crumbs; + + return $listing; + } + + /** + * Toggle a filter + * + * @param string $root Root directory + * @param array $crumbs Components of the current directory relative to the root + * @param string $item The child item of the current directory we want to toggle the filter for + * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) + * + * @return array + */ + public function toggle($root, $crumbs, $item, $filter) + { + $node = $this->glue_crumbs($crumbs, $item); + + return $this->applyExclusionFilter($filter, $root, $node, 'toggle'); + } + + /** + * Set a filter + * + * @param string $root Root directory + * @param array $crumbs Components of the current directory relative to the root + * @param string $item The child item of the current directory we want to set the filter for + * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) + * + * @return array + */ + public function setFilter($root, $crumbs, $item, $filter) + { + $node = $this->glue_crumbs($crumbs, $item); + + return $this->applyExclusionFilter($filter, $root, $node, 'set'); + } + + /** + * Remove a filter + * + * @param string $root Root directory + * @param array $crumbs Components of the current directory relative to the root + * @param string $item The child item of the current directory we want to remove the filter for + * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) + * + * @return array + */ + public function remove($root, $crumbs, $item, $filter) + { + $node = $this->glue_crumbs($crumbs, $item); + + return $this->applyExclusionFilter($filter, $root, $node, 'remove'); + } + + /** + * Swap a filter + * + * @param string $root Root directory + * @param array $crumbs Components of the current directory relative to the root + * @param string $item The child item of the current directory we want to set the filter for + * @param string $filter The name of the filter to apply (directories, skipfiles, skipdirs, files) + * + * @return array + */ + public function swap($root, $crumbs, $old_item, $new_item, $filter) + { + $new_node = $this->glue_crumbs($crumbs, $new_item); + $old_node = $this->glue_crumbs($crumbs, $old_item); + + return $this->applyExclusionFilter($filter, $root, $new_node, 'swap', $old_node); + } + + /** + * Retrieves the filters as an array. Used for the tabular filter editor. + * + * @param string $root The root node to search filters on + * + * @return array A collection of hash arrays containing node and type for each filtered element + */ + public function &get_filters($root) + { + return $this->getTabularFilters($root); + } + + /** + * Resets the filters + * + * @param string $root Root directory + * + * @return array + */ + public function resetFilters($root) + { + $this->resetAllFilters($root); + + return $this->make_listing($root); + } + + /** + * Handles a request coming in through AJAX. Basically, this is a simple proxy to the model methods. + * + * @return array + */ + public function doAjax() + { + $action = $this->getState('action'); + $verb = array_key_exists('verb', get_object_vars($action)) ? $action->verb : null; + + if (!array_key_exists('crumbs', get_object_vars($action))) + { + $action->crumbs = ''; + } + + $ret_array = array(); + + switch ($verb) + { + // Return a listing for the normal view + case 'list': + $ret_array = $this->make_listing($action->root, $action->crumbs, $action->node); + break; + + // Toggle a filter's state + case 'toggle': + $ret_array = $this->toggle($action->root, $action->crumbs, $action->node, $action->filter); + break; + + // Set a filter (used by the editor) + case 'set': + $ret_array = $this->setFilter($action->root, $action->crumbs, $action->node, $action->filter); + break; + + // Swap a filter (used by the editor) + case 'swap': + $ret_array = + $this->swap($action->root, $action->crumbs, $action->old_node, $action->new_node, $action->filter); + break; + + case 'tab': + $ret_array = $this->get_filters($action->root); + break; + + // Reset filters + case 'reset': + $ret_array = $this->resetFilters($action->root); + break; + } + + return $ret_array; + } + + /** + * Format the size of the file (given in bytes) to something human readable, e.g. 123 MB + * + * @param int $bytes The file size in bytes + * @param int $decimals How many decimals you want (default: 0) + * + * @return string The human-readable, formatted size + */ + private function formatSize($bytes, $decimals = 0) + { + $bytes = empty($bytes) ? 0 : (int) $bytes; + $format = empty($decimals) ? '%0u' : '%0.' . $decimals . 'f'; + + $uom = [ + 'TB' => 1048576 * 1048576, + 'GB' => 1024 * 1048576, + 'MB' => 1048576, + 'KB' => 1024, + 'B' => 1, + ]; + + // Whole bytes cannot have decimal positions + if (!empty($decimals)) + { + unset($uom['B']); + } + + foreach ($uom as $unit => $byteSize) + { + if (doubleval($bytes) >= $byteSize) + { + return sprintf($format, $bytes / $byteSize) . ' ' . $unit; + } + } + + // If the number is either too big or too small, + return sprintf('%0u B', $bytes); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Log.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Log.php new file mode 100644 index 00000000..15d8f560 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Log.php @@ -0,0 +1,122 @@ +get('akeeba.basic.output_directory'); + + $files = Factory::getFileLister()->getFiles($outdir); + $ret = array(); + + if (!empty($files) && is_array($files)) + { + foreach ($files as $filename) + { + $basename = basename($filename); + + if ((substr($basename, 0, 7) == 'akeeba.') && (substr($basename, -4) == '.log') && ($basename != 'akeeba.log')) + { + $tag = str_replace('akeeba.', '', str_replace('.log', '', $basename)); + + if (!empty($tag)) + { + $parts = explode('.', $tag); + $key = array_pop($parts); + $key = str_replace('id', '', $key); + $key = is_numeric($key) ? sprintf('%015u', $key) : $key; + + if (empty($parts)) + { + $key = str_repeat('0', 15) . '.' . $key; + } + else + { + $key .= '.' . implode('.', $parts); + } + + $ret[$key] = $tag; + } + } + } + } + + krsort($ret); + + return $ret; + } + + /** + * Gets the JHtml options list for selecting a log file + * + * @return array + */ + public function getLogList() + { + $options = array(); + + $list = $this->getLogFiles(); + + if (!empty($list)) + { + $options[] = JHtml::_('select.option', null, JText::_('COM_AKEEBA_LOG_CHOOSE_FILE_VALUE')); + + foreach ($list as $item) + { + $text = JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_' . $item); + + if (strstr($item, '.') !== false) + { + list($origin, $backupId) = explode('.', $item, 2); + + $text = JText::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_' . $origin) . ' (' . $backupId . ')'; + } + + $options[] = JHtml::_('select.option', $item, $text); + } + } + + return $options; + } + + /** + * Output the raw text log file to the standard output + * + * @return void + */ + public function echoRawLog() + { + $tag = $this->getState('tag', ''); + + echo "WARNING: Do not copy and paste lines from this file!\r\n"; + echo "You are supposed to ZIP and attach it in your support forum post.\r\n"; + echo "If you fail to do so, we will be unable to provide efficient support.\r\n"; + echo "\r\n"; + echo "--- START OF RAW LOG --\r\n"; + // The at sign (silence operator) is necessary to prevent PHP showing a warning if the file doesn't exist or + // isn't readable for any reason. + @readfile(Factory::getLog()->getLogFilename($tag)); + echo "--- END OF RAW LOG ---\r\n"; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/Chmod.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/Chmod.php new file mode 100644 index 00000000..f0fce318 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/Chmod.php @@ -0,0 +1,75 @@ + $trustMeIKnowWhatImDoing)) + { + $mode = $ohSevenFiveFive; + } + } + + // Initialize variables + JLoader::import('joomla.client.helper'); + $ftpOptions = JClientHelper::getCredentials('ftp'); + + // Check to make sure the path valid and clean + $path = JPath::clean($path); + + if (@chmod($path, $mode)) + { + $ret = true; + } + elseif ($ftpOptions['enabled'] == 1) + { + // Connect the FTP client + JLoader::import('joomla.client.ftp'); + $ftp = JClientFtp::getInstance( + $ftpOptions['host'], $ftpOptions['port'], array(), + $ftpOptions['user'], $ftpOptions['pass'] + ); + + // Translate path and delete + $path = JPath::clean(str_replace(JPATH_ROOT, $ftpOptions['root'], $path), '/'); + // FTP connector throws an error + $ret = $ftp->chmod($path, $mode); + } + else + { + $ret = false; + } + + return $ret; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/ExclusionFilter.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/ExclusionFilter.php new file mode 100644 index 00000000..42a24ae0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Mixin/ExclusionFilter.php @@ -0,0 +1,151 @@ + false, + 'newstate' => false + ]; + + $filter = Factory::getFilterObject($type); + $newState = null; + + switch ($action) + { + case 'set': + $ret['success'] = $filter->set($root, $node); + break; + + case 'remove': + $ret['success'] = $filter->remove($root, $node); + break; + + case 'toggle': + $ret['success'] = $filter->toggle($root, $node, $newState); + break; + + case 'swap': + $ret['success'] = true; + + if (empty($node)) + { + $ret['success'] = false; + } + + if ($ret['success'] && !empty($oldNode)) + { + $ret = $this->applyExclusionFilter($type, $root, $oldNode, 'remove'); + } + + if ($ret['success']) + { + $ret = $this->applyExclusionFilter($type, $root, $node, 'set'); + } + break; + } + + $ret['newstate'] = $newState; + + if (is_null($newState)) + { + $ret['newstate'] = $ret['success']; + } + + if ($ret['success']) + { + $filters = Factory::getFilters(); + $filters->save(); + } + + return $ret; + } + + + /** + * Retrieves the filters as an array. Used for the tabular filter editor. + * + * @param string $root The root node to search filters on + * + * @return array A collection of hash arrays containing node and type for each filtered element + */ + protected function &getTabularFilters($root) + { + // A reference to the global Akeeba Engine filter object + $filters = Factory::getFilters(); + + // Initialize the return array + $ret = array(); + + foreach ($this->knownFilterTypes as $type) + { + $rawFilterData = $filters->getFilterData($type); + + if (array_key_exists($root, $rawFilterData)) + { + if (!empty($rawFilterData[ $root ])) + { + foreach ($rawFilterData[ $root ] as $node) + { + $ret[] = array( + 'node' => substr($node, 0), // Make sure we get a COPY, not a reference to the original data + 'type' => $type + ); + } + } + } + } + + return $ret; + } + + /** + * Resets the filters + * + * @param string $root Root directory + * + * @return array + */ + protected function resetAllFilters($root) + { + // Get a reference to the global Filters object + $filters = Factory::getFilters(); + + foreach ($this->knownFilterTypes as $filterName) + { + $filter = Factory::getFilterObject($filterName); + $filter->reset($root); + } + + $filters->save(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Profiles.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Profiles.php new file mode 100644 index 00000000..8c6d755b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Profiles.php @@ -0,0 +1,178 @@ + '#__ak_profiles', + 'idFieldName' => 'id', + ]; + + if (!is_array($config) || empty($config)) + { + $config = []; + } + + $config = array_merge($defaultConfig, $config); + + parent::__construct($container, $config); + + $this->addBehaviour('filters'); + $this->blacklistFilters([ + 'configuration', + 'filters' + ]); + } + + /** + * Tries to copy the currently loaded to a new record + * + * @return self The new record + */ + public function copy($data = null) + { + $id = $this->getId(); + + // Check for invalid id's (not numeric, or <= 0) + if ((!is_numeric($id)) || ($id <= 0)) + { + throw new DataModel\Exception\RecordNotLoaded('PROFILE_INVALID_ID'); + } + + if (!is_array($data)) + { + $data = []; + } + + $data['id'] = 0; + + return $this->getClone()->save($data); + } + + /** + * Returns an associative array with profile IDs as keys and the post-processing engine as values + * + * @return array + */ + public function getPostProcessingEnginePerProfile() + { + // Cache the current profile's ID + $currentProfileID = $this->container->platform->getSessionVar('profile', null, 'akeeba'); + + // Get the IDs of all profiles + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->qn('id')) + ->from($db->qn('#__ak_profiles')); + $db->setQuery($query); + $profiles = $db->loadColumn(); + + // Initialise return; + $engines = []; + + // Loop all profiles + foreach ($profiles as $profileId) + { + Platform::getInstance()->load_configuration($profileId); + $profileConfiguration = Factory::getConfiguration(); + $engines[ $profileId ] = $profileConfiguration->get('akeeba.advanced.postproc_engine'); + } + + // Reload the current profile + Platform::getInstance()->load_configuration($currentProfileID); + + return $engines; + } + + /** + * Runs before deleting a record + * + * @param int $id The ID of the record being deleted + */ + public function onBeforeDelete(&$id) + { + // You cannot delete the default record + if ($id <= 1) + { + throw new RuntimeException(\JText::_('COM_AKEEBA_PROFILE_ERR_CANNOTDELETEDEFAULT'), 500); + } + + // If you're deleting the current backup profile we have to switch to the default profile (#1) + $activeProfile = Platform::getInstance()->get_active_profile(); + + if ($id == $activeProfile) + { + throw new RuntimeException(\JText::sprintf('COM_AKEEBA_PROFILE_ERR_CANNOTDELETEACTIVE', $id), 500); + } + } + + /** + * Save a profile from imported configuration data. The $data array must contain the keys description (profile + * description), configuration (engine configuration INI data) and filters (inclusion and inclusion filters JSON + * configuration data). + * + * @param array $data See above + * + * @returns void + * + * @throws RuntimeException When an iport error occurs + */ + public function import($data) + { + // Check for data validity + $isValid = + is_array($data) && + !empty($data) && + array_key_exists('description', $data) && + array_key_exists('configuration', $data) && + array_key_exists('filters', $data); + + if (!$isValid) + { + throw new RuntimeException(\JText::_('COM_AKEEBA_PROFILES_ERR_IMPORT_INVALID')); + } + + // Unset the id, if it exists + if (array_key_exists('id', $data)) + { + unset($data['id']); + } + + $data['akeeba.flag.confwiz'] = 1; + + // Try saving the profile + $result = $this->save($data); + + if (!$result) + { + throw new RuntimeException(\JText::_('COM_AKEEBA_PROFILES_ERR_IMPORT_FAILED')); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Restore.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Restore.php new file mode 100644 index 00000000..2cc95ad0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Restore.php @@ -0,0 +1,324 @@ +input->get('cid', array(), 'array'); + $id = $this->input->getInt('id', 0); + + $ids = array(); + + if (is_array($cid) && !empty($cid)) + { + $ids = $cid; + } + elseif (!empty($id)) + { + $ids = array($id); + } + + return $ids; + } + + /** + * Generates a pseudo-random password + * + * @param int $length The length of the password in characters + * + * @return string The requested password string + */ + function makeRandomPassword($length = 32) + { + \JLoader::import('joomla.user.helper'); + + return \JUserHelper::genRandomPassword($length); + } + + /** + * Validates the data passed to the request. + * + * @return mixed True if all is OK, an error string if something is wrong + */ + public function validateRequest() + { + // Is this a valid backup entry? + $ids = $this->getIDsFromRequest(); + $id = array_pop($ids); + $profileID = $this->input->getInt('profileid', 0); + + // No backup IDs in the request and no backup profile (which means I should use its latest backup record) is found. + if (empty($id) && ($profileID <= 0)) + { + return JText::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'); + } + + if (empty($id)) + { + try + { + $id = $this->getLatestBackupForProfile($profileID); + } + catch (\RuntimeException $e) + { + return $e->getMessage(); + } + } + + $data = Platform::getInstance()->get_statistics($id); + + if (empty($data)) + { + return JText::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'); + } + + if ($data['status'] != 'complete') + { + return JText::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'); + } + + // Load the profile ID (so that we can find out the output directory) + $profile_id = $data['profile_id']; + Platform::getInstance()->load_configuration($profile_id); + + $path = $data['absolute_path']; + $exists = @file_exists($path); + + if (!$exists) + { + // Let's try figuring out an alternative path + $config = Factory::getConfiguration(); + $path = $config->get('akeeba.basic.output_directory', '') . '/' . $data['archivename']; + $exists = @file_exists($path); + } + + if (!$exists) + { + return JText::_('COM_AKEEBA_RESTORE_ERROR_ARCHIVE_MISSING'); + } + + $filename = basename($path); + $lastdot = strrpos($filename, '.'); + $extension = strtoupper(substr($filename, $lastdot + 1)); + + if (!in_array($extension, array('JPS', 'JPA', 'ZIP'))) + { + return JText::_('COM_AKEEBA_RESTORE_ERROR_INVALID_TYPE'); + } + + $this->data = $data; + $this->path = $path; + $this->extension = $extension; + + return true; + } + + /** + * Finds the latest backup for a given backup profile with an "OK" status (the archive file exists on your server). + * If none is found a RuntimeException is thrown. + * + * This method uses the code from the Transfer model for DRY reasons. + * + * @param int $profileID The profile in which to locate the latest valid backup + * + * @return int + * + * @throws \RuntimeException + * + * @since 5.3.0 + */ + public function getLatestBackupForProfile($profileID) + { + /** @var Transfer $transferModel */ + $transferModel = $this->container->factory->model('Transfer')->tmpInstance(); + $latestBackup = $transferModel->getLatestBackupInformation($profileID); + + if (empty($latestBackup)) + { + throw new \RuntimeException(JText::sprintf('COM_AKEEBA_RESTORE_ERROR_NO_LATEST', $profileID)); + } + + return $latestBackup['id']; + } + + /** + * Creates the restoration.php file which is used to configure Akeeba Restore (restore.php). Without it, resotre.php + * is completely inert, preventing abuse. + * + * @return bool + */ + function createRestorationINI() + { + // Get a password + $this->password = $this->makeRandomPassword(32); + $this->setState('password', $this->password); + + // Do we have to use FTP? + $procengine = $this->getState('procengine', 'direct'); + + // Get the absolute path to site's root + $siteroot = JPATH_SITE; + + // Get the JPS password + $password = addslashes($this->getState('jps_key')); + + $data = " '{$this->password}', + 'kickstart.tuning.max_exec_time' => '5', + 'kickstart.tuning.run_time_bias' => '75', + 'kickstart.tuning.min_exec_time' => '0', + 'kickstart.procengine' => '$procengine', + 'kickstart.setup.sourcefile' => '{$this->path}', + 'kickstart.setup.destdir' => '$siteroot', + 'kickstart.setup.restoreperms' => '0', + 'kickstart.setup.filetype' => '{$this->extension}', + 'kickstart.setup.dryrun' => '0', + 'kickstart.jps.password' => '$password' +ENDDATA; + + if ($procengine == 'ftp') + { + $ftp_host = $this->getState('ftp_host', ''); + $ftp_port = $this->getState('ftp_port', '21'); + $ftp_user = $this->getState('ftp_user', ''); + $ftp_pass = addcslashes($this->getState('ftp_pass', ''), "'\\"); + $ftp_root = $this->getState('ftp_root', ''); + $ftp_ssl = $this->getState('ftp_ssl', 0); + $ftp_pasv = $this->getState('ftp_root', 1); + $tempdir = $this->getState('tmp_path', ''); + $data .= << '$ftp_ssl', + 'kickstart.ftp.passive' => '$ftp_pasv', + 'kickstart.ftp.host' => '$ftp_host', + 'kickstart.ftp.port' => '$ftp_port', + 'kickstart.ftp.user' => '$ftp_user', + 'kickstart.ftp.pass' => '$ftp_pass', + 'kickstart.ftp.dir' => '$ftp_root', + 'kickstart.ftp.tempdir' => '$tempdir' +ENDDATA; + } + + $data .= ');'; + + // Remove the old file, if it's there... + JLoader::import('joomla.filesystem.file'); + $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'; + clearstatcache(true, $configpath); + + if (@file_exists($configpath)) + { + if (!@unlink($configpath)) + { + JFile::delete($configpath); + } + } + + // Write new file + $result = JFile::write($configpath, $data); + + // Clear opcode caches for the generated .php file + if (function_exists('opcache_invalidate')) + { + opcache_invalidate($configpath); + } + + if (function_exists('apc_compile_file')) + { + apc_compile_file($configpath); + } + + if (function_exists('wincache_refresh_if_changed')) + { + wincache_refresh_if_changed(array($configpath)); + } + + if (function_exists('xcache_asm')) + { + xcache_asm($configpath); + } + + return $result; + } + + /** + * Handles an AJAX request + * + * @return mixed + */ + public function doAjax() + { + $ajax = $this->getState('ajax'); + switch ($ajax) + { + // FTP Connection test for DirectFTP + case 'testftp': + // Grab request parameters + $config = array( + 'host' => $this->input->get('host', '', 'none', 2), + 'port' => $this->input->get('port', 21, 'int'), + 'user' => $this->input->get('user', '', 'none', 2), + 'pass' => $this->input->get('pass', '', 'none', 2), + 'initdir' => $this->input->get('initdir', '', 'none', 2), + 'usessl' => $this->input->get('usessl', 'cmd') == 'true', + 'passive' => $this->input->get('passive', 'cmd') == 'true' + ); + + // Perform the FTP connection test + $test = new \Akeeba\Engine\Archiver\Directftp(); + $test->initialize('', $config); + $errors = $test->getError(); + if (empty($errors)) + { + $result = true; + } + else + { + $result = $errors; + } + break; + + // Unrecognized AJAX task + default: + $result = false; + break; + } + + return $result; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/SFTPBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/Model/SFTPBrowser.php new file mode 100644 index 00000000..d6531e25 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/SFTPBrowser.php @@ -0,0 +1,244 @@ +directory + * + * @return array + */ + public function getListing() + { + $dir = $this->directory; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $this->parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + $this->parent_directory = ''; + + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + + $this->parent_directory = '/'; + + if (!empty($copy_of_parts)) + { + $this->parent_directory = '/' . implode('/', $copy_of_parts); + } + } + + // Initialise + $connection = null; + $sftphandle = null; + + // Open a connection + if (!function_exists('ssh2_connect')) + { + throw new RuntimeException("Your web server does not have the SSH2 PHP module, therefore can not connect and upload archives to SFTP servers."); + } + + $connection = ssh2_connect($this->host, $this->port); + + if ($connection === false) + { + throw new RuntimeException("Invalid SFTP hostname or port ({$this->host}:{$this->port}) or the connection is blocked by your web server's firewall."); + } + + // Connect to the server + + if (!empty($this->pubkey) && !empty($this->privkey)) + { + if (!ssh2_auth_pubkey_file($connection, $this->username, $this->pubkey, $this->privkey, $this->password)) + { + throw new RuntimeException('Certificate error'); + } + } + else + { + if (!ssh2_auth_password($connection, $this->username, $this->password)) + { + throw new RuntimeException('Could not authenticate access to SFTP server; check your username and password.'); + } + } + + $sftphandle = ssh2_sftp($connection); + + if ($sftphandle === false) + { + throw new RuntimeException("Your SSH server does not allow SFTP connections"); + } + + // Get a raw directory listing (hoping it's a UNIX server!) + $list = array(); + $dir = ltrim($dir, '/'); + + if (empty($dir)) + { + $dir = ssh2_sftp_realpath($sftphandle, "."); + + $this->directory = $dir; + + // Parse directory to parts + $parsed_dir = trim($dir, '/'); + $this->parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir); + + // Find the path to the parent directory + $this->parent_directory = ''; + + if (!empty($parts)) + { + $copy_of_parts = $parts; + array_pop($copy_of_parts); + + $this->parent_directory = '/'; + + if (!empty($copy_of_parts)) + { + $this->parent_directory = '/' . implode('/', $copy_of_parts); + } + } + } + + $handle = opendir("ssh2.sftp://$sftphandle/$dir"); + + if (!is_resource($handle)) + { + throw new RuntimeException(JText::_('COM_AKEEBA_SFTPBROWSER_ERROR_NOACCESS')); + } + + while (($entry = readdir($handle)) !== false) + { + if (substr($entry, 0, 1) == '.') + { + continue; + } + + if (!is_dir("ssh2.sftp://$sftphandle/$dir/$entry")) + { + continue; + } + + $list[] = $entry; + } + + closedir($handle); + + if (!empty($list)) + { + asort($list); + } + + return $list; + } + + /** + * Perform the actual folder browsing. Returns an array that's usable by the UI. + * + * @return array + */ + public function doBrowse() + { + $error = ''; + $list = []; + + try + { + $list = $this->getListing(); + } + catch (RuntimeException $e) + { + $error = $e->getMessage(); + } + + $response_array = array( + 'error' => $error, + 'list' => $list, + 'breadcrumbs' => $this->parts, + 'directory' => $this->directory, + 'parent' => $this->parent_directory + ); + + return $response_array; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Schedule.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Schedule.php new file mode 100644 index 00000000..b6b7ba73 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Schedule.php @@ -0,0 +1,187 @@ + (object)array( + 'supported' => false, + 'path' => false + ), + 'altcli' => (object)array( + 'supported' => false, + 'path' => false + ), + 'frontend' => (object)array( + 'supported' => false, + 'path' => false, + ), + 'info' => (object)array( + 'windows' => false, + 'php_path' => false, + 'root_url' => false, + 'secret' => '', + 'feenabled' => false, + ) + ); + + // Get the profile ID + $profileid = Platform::getInstance()->get_active_profile(); + + // Get the absolute path to the site's root + $absolute_root = rtrim(realpath(JPATH_ROOT), DIRECTORY_SEPARATOR); + + // Is this Windows? + $ret->info->windows = (DIRECTORY_SEPARATOR == '\\') || (substr(strtoupper(PHP_OS), 0, 3) == 'WIN'); + + // Get the pseudo-path to PHP CLI + $ret->info->php_path = '/path/to/php'; + + if ($ret->info->windows) + { + $ret->info->php_path = 'c:\path\to\php.exe'; + } + + // Get front-end backup secret key + $ret->info->secret = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', ''); + $ret->info->feenabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', false); + + // Get root URL + $ret->info->root_url = rtrim($this->container->params->get('siteurl', ''), '/'); + + // Get information for CLI CRON script + if (AKEEBA_PRO) + { + $ret->cli->supported = true; + $ret->cli->path = + $absolute_root . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'akeeba-backup.php'; + + if ($profileid != 1) + { + $ret->cli->path .= ' --profile=' . $profileid; + } + } + + // Get information for alternative CLI CRON script + if (AKEEBA_PRO) + { + $ret->altcli->supported = true; + + if (trim($ret->info->secret) && $ret->info->feenabled) + { + $ret->altcli->path = + $absolute_root . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'akeeba-altbackup.php'; + + if ($profileid != 1) + { + $ret->altcli->path .= ' --profile=' . $profileid; + } + } + } + + // Get information for front-end backup + $ret->frontend->supported = true; + if (trim($ret->info->secret) && $ret->info->feenabled) + { + $ret->frontend->path = 'index.php?option=com_akeeba&view=Backup&key=' + . urlencode($ret->info->secret); + + if ($profileid != 1) + { + $ret->frontend->path .= '&profile=' . $profileid; + } + } + + return $ret; + } + + public function getCheckPaths() + { + $ret = (object)array( + 'cli' => (object)array( + 'supported' => false, + 'path' => false + ), + 'altcli' => (object)array( + 'supported' => false, + 'path' => false + ), + 'frontend' => (object)array( + 'supported' => false, + 'path' => false, + ), + 'info' => (object)array( + 'windows' => false, + 'php_path' => false, + 'root_url' => false, + 'secret' => '', + 'feenabled' => false, + ) + ); + + // Get the absolute path to the site's root + $absolute_root = rtrim(realpath(JPATH_ROOT), DIRECTORY_SEPARATOR); + + // Is this Windows? + $ret->info->windows = (DIRECTORY_SEPARATOR == '\\') || (substr(strtoupper(PHP_OS), 0, 3) == 'WIN'); + + // Get the pseudo-path to PHP CLI + $ret->info->php_path = '/path/to/php'; + + if ($ret->info->windows) + { + $ret->info->php_path = 'c:\path\to\php.exe'; + } + + // Get front-end backup secret key + $ret->info->secret = $this->container->params->get('frontend_secret_word', ''); + $ret->info->feenabled = $this->container->params->get('failure_frontend_enable', false); + + // Get root URL + $ret->info->root_url = rtrim($this->container->params->get('siteurl', ''), '/'); + + // Get information for CLI CRON script + if (AKEEBA_PRO) + { + $ret->cli->supported = true; + $ret->cli->path = + $absolute_root . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'akeeba-check-failed.php'; + } + + // Get information for alternative CLI CRON script + if (AKEEBA_PRO) + { + $ret->altcli->supported = true; + + if (trim($ret->info->secret) && $ret->info->feenabled) + { + $ret->altcli->path = + $absolute_root . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'akeeba-altcheck-failed.php'; + } + } + + // Get information for front-end backup + $ret->frontend->supported = true; + + if (trim($ret->info->secret) && $ret->info->feenabled) + { + $ret->frontend->path = 'index.php?option=com_akeeba&view=check&key=' . urlencode($ret->info->secret); + } + + return $ret; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Statistics.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Statistics.php new file mode 100644 index 00000000..0583fe31 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Statistics.php @@ -0,0 +1,709 @@ + '#__ak_stats', + 'idFieldName' => 'id', + ]; + + if (!is_array($config) || empty($config)) + { + $config = []; + } + + $config = array_merge($defaultConfig, $config); + + parent::__construct($container, $config); + + $platform = $this->container->platform; + $defaultLimit = $platform->getConfig()->get('list_limit', 10); + + if ($platform->isCli()) + { + $limit = $this->input->getInt('limit', $defaultLimit); + $limitstart = $this->input->getInt('limitstart', 0); + } + else + { + $limit = $platform->getUserStateFromRequest('global.list.limit', 'limit', $this->input, $defaultLimit); + $limitstart = $platform->getUserStateFromRequest('com_akeeba.stats.limitstart', 'limitstart', $this->input, 0); + } + + if ($platform->isFrontend()) + { + $limit = 0; + $limitstart = 0; + } + + // Set the page pagination variables + $this->setState('limit', $limit); + $this->setState('limitstart', $limitstart); + } + + /** + * Returns the same list as getStatisticsList(), but includes an extra field + * named 'meta' which categorises attempts based on their backup archive status + * + * @param bool $overrideLimits Should I disregard limit, limitStart and filters? + * @param array $filters Filters to apply. See Platform::get_statistics_list + * @param array $order Results ordering. The accepted keys are by (column name) and order (ASC or DESC) + * + * @return array An array of arrays. Each inner array is one backup record. + */ + public function &getStatisticsListWithMeta($overrideLimits = false, $filters = null, $order = null) + { + $limitstart = $this->getState('limitstart', 0); + $limit = $this->getState('limit', 10); + + if ($overrideLimits) + { + $limitstart = 0; + $limit = 0; + $filters = null; + } + + if (is_array($order) && isset($order['order'])) + { + if (strtoupper($order['order']) != 'ASC') + { + $order['order'] = 'desc'; + } + } + + $allStats = Platform::getInstance()->get_statistics_list(array( + 'limitstart' => $limitstart, + 'limit' => $limit, + 'filters' => $filters, + 'order' => $order + )); + + $validRecords = Platform::getInstance()->get_valid_backup_records(); + + if (empty($validRecords)) + { + $validRecords = array(); + } + + // This will hold the entries whose files are no longer present and are + // not already marked as such in the database + $updateObsoleteRecords = []; + + // The list of statistics entries to return + $ret = []; + + if (empty($allStats)) + { + return $ret; + } + + foreach ($allStats as $stat) + { + // Translate backup status and the existence of a remote filename to the backup record's "meta" status. + switch ($stat['status']) + { + case 'run': + $stat['meta'] = 'pending'; + break; + + case 'fail': + $stat['meta'] = 'fail'; + break; + + default: + if ($stat['remote_filename']) + { + // If there is a "remote_filename", the record is "remote", not "obsolete" + $stat['meta'] = 'remote'; + } + else + { + // Else, it's "obsolete" + $stat['meta'] = 'obsolete'; + } + break; + } + + // If the backup is reported to have files still stored on the server we need to investigate further + if (in_array($stat['id'], $validRecords)) + { + $archives = Factory::getStatistics()->get_all_filenames($stat); + $stat['meta'] = (count($archives) > 0) ? 'ok' : 'obsolete'; + + // The archives exist. Set $stat['size'] to the total size of the backup archives. + if ($stat['meta'] == 'ok') + { + $stat['size'] = $stat['total_size']; + + if ($stat['total_size'] <= 0) + { + $stat['size'] = 0; + + foreach ($archives as $filename) + { + $stat['size'] += @filesize($filename); + } + } + + $ret[] = $stat; + + continue; + } + + // The archives do not exist or we can't find them. If the record says otherwise we need to update it. + if ($stat['filesexist']) + { + $updateObsoleteRecords[] = $stat['id']; + } + + // Does the backup record report a total size even though our files no longer exist? + if ($stat['total_size']) + { + $stat['size'] = $stat['total_size']; + } + + // If there is a "remote_filename", the record is "remote", not "obsolete" + if ($stat['remote_filename']) + { + $stat['meta'] = 'remote'; + } + } + + $ret[] = $stat; + } + + // Update records which report that their files exist on the server but, in fact, they don't. + if (count($updateObsoleteRecords)) + { + Platform::getInstance()->invalidate_backup_records($updateObsoleteRecords); + } + + unset($validRecords); + + return $ret; + } + + /** + * Send an email notification for failed backups + * + * @return array See the CLI script + */ + public function notifyFailed() + { + // Invalidate stale backups + Factory::resetState(array( + 'global' => true, + 'log' => false, + 'maxrun' => $this->container->params->get('failure_timeout', 180) + )); + + // Get the last execution and search for failed backups AFTER that date + $last = $this->getLastCheck(); + + // Get failed backups + $filters[] = array('field' => 'status', 'operand' => '=', 'value' => 'fail'); + $filters[] = array('field' => 'origin', 'operand' => '<>', 'value' => 'restorepoint'); + $filters[] = array('field' => 'backupstart', 'operand' => '>', 'value' => $last); + + $failed = Platform::getInstance()->get_statistics_list(array('filters' => $filters)); + + // Well, everything went ok. + if (!$failed) + { + return array( + 'message' => array("No need to run: no failed backups or notifications were already sent."), + 'result' => true + ); + } + + // Whops! Something went wrong, let's start notifing + $superAdmins = array(); + $superAdminEmail = $this->container->params->get('failure_email_address', ''); + + if (!empty($superAdminEmail)) + { + $superAdmins = $this->getSuperUsers($superAdminEmail); + } + + if (empty($superAdmins)) + { + $superAdmins = $this->getSuperUsers(); + } + + if (empty($superAdmins)) + { + return array( + 'message' => array("WARNING! Failed backup(s) detected, but there are no configured Super Administrators to receive notifications"), + 'result' => false + ); + } + + $failedReport = array(); + + foreach ($failed as $fail) + { + $string = "Description : " . $fail['description'] . "\n"; + $string .= "Start time : " . $fail['backupstart'] . "\n"; + $string .= "Origin : " . $fail['origin'] . "\n"; + $string .= "Type : " . $fail['type'] . "\n"; + $string .= "Profile ID : " . $fail['profile_id']; + + $failedReport[] = $string; + } + + $failedReport = implode("\n#-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+#\n", $failedReport); + + $email_subject = $this->container->params->get('failure_email_subject', ''); + + if (!$email_subject) + { + $email_subject = <<container->params->get('failure_email_body', ''); + + if (!$email_body) + { + $email_body = <<container->platform->getConfig(); + + $mailfrom = $jconfig->get('mailfrom'); + $fromname = $jconfig->get('fromname'); + + $email_subject = Factory::getFilesystemTools()->replace_archive_name_variables($email_subject); + $email_body = Factory::getFilesystemTools()->replace_archive_name_variables($email_body); + $email_body = str_replace('[FAILEDLIST]', $failedReport, $email_body); + + foreach ($superAdmins as $sa) + { + try + { + $mailer = JFactory::getMailer(); + + $mailer->setSender(array($mailfrom, $fromname)); + $mailer->addRecipient($sa->email); + $mailer->setSubject($email_subject); + $mailer->setBody($email_body); + $mailer->Send(); + } + catch (\Exception $e) + { + // Joomla! 3.5 is written by incompetent bonobos + } + } + + // Let's update the last time we check, so we will avoid to send + // the same notification several times + $this->updateLastCheck(intval($last)); + + return array( + 'message' => array( + "WARNING! Found " . count($failed) . " failed backup(s)", + "Sent " . count($superAdmins) . " notifications" + ), + 'result' => true + ); + } + + /** + * Delete the backup statistics record whose ID is set in the model + * + * @return bool True on success + */ + public function delete() + { + $db = $this->container->db; + + $id = $this->getState('id', 0); + + if ((!is_numeric($id)) || ($id <= 0)) + { + throw new RecordNotLoaded(JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID')); + } + + // Try to delete files + $this->deleteFile(); + + if (!Platform::getInstance()->delete_statistics($id)) + { + throw new \RuntimeException($db->getError(), 500); + } + + return true; + } + + /** + * Delete the backup file of the stats record whose ID is set in the model + * + * @return bool True on success + */ + public function deleteFile() + { + JLoader::import('joomla.filesystem.file'); + + $id = $this->getState('id', 0); + + if ((!is_numeric($id)) || ($id <= 0)) + { + throw new RecordNotLoaded(JText::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID')); + } + + // Get the backup statistics record and the files to delete + $stat = Platform::getInstance()->get_statistics($id); + $allFiles = Factory::getStatistics()->get_all_filenames($stat, false); + + // Remove the custom log file if necessary + $this->deleteLogs($stat); + + // No files? Nothing to do. + if (empty($allFiles)) + { + return true; + } + + $status = true; + + foreach ($allFiles as $filename) + { + if (!@file_exists($filename)) + { + continue; + } + + $new_status = @unlink($filename); + + if (!$new_status) + { + $new_status = JFile::delete($filename); + } + + $status = $status ? $new_status : false; + } + + return $status; + } + + /** + * Deletes the backup-specific log files of a stats record + * + * @param array $stat The array holding the backup stats record + * + * @return void + */ + protected function deleteLogs(array $stat) + { + // We can't delete logs if there is no backup ID in the record + if (!isset($stat['backupid']) || empty($stat['backupid'])) + { + return; + } + + $logFileName = 'akeeba.' . $stat['tag'] . '.' . $stat['backupid'] . '.log'; + + $logPath = dirname($stat['absolute_path']) . '/' . $logFileName; + + if (@file_exists($logPath)) + { + if (!@unlink($logPath)) + { + JFile::delete($logPath); + } + } + } + + /** + * Get a Joomla! pagination object + * + * @param array $filters Filters to apply. See Platform::get_statistics_list + * + * @return JPagination + * + */ + public function &getPagination($filters = null) + { + if (empty($this->pagination)) + { + // Import the pagination library + JLoader::import('joomla.html.pagination'); + + // Prepare pagination values + $total = Platform::getInstance()->get_statistics_count($filters); + $limitstart = $this->getState('limitstart', 0); + $limit = $this->getState('limit', 10); + + // Create the pagination object + $this->pagination = new JPagination($total, $limitstart, $limit); + } + + return $this->pagination; + } + + /** + * Returns the Super Users' email information. If you provide a comma separated $email list we will check that these + * emails do belong to Super Users and that they have not blocked reception of system emails. + * + * @param null|string $email A list of Super Users to email + * + * @return array The list of Super User emails + */ + private function getSuperUsers($email = null) + { + // Get a reference to the database object + $db = $this->container->db; + + // Convert the email list to an array + if (!empty($email)) + { + $temp = explode(',', $email); + $emails = array(); + + foreach ($temp as $entry) + { + $entry = trim($entry); + $emails[] = $db->q($entry); + } + + $emails = array_unique($emails); + } + else + { + $emails = array(); + } + + // Get a list of groups which have Super User privileges + $ret = array(); + + // Get a list of groups with core.admin (Super User) permissions + try + { + $query = $db->getQuery(true) + ->select($db->qn('rules')) + ->from($db->qn('#__assets')) + ->where($db->qn('parent_id') . ' = ' . $db->q(0)); + $db->setQuery($query, 0, 1); + $rulesJSON = $db->loadResult(); + $rules = json_decode($rulesJSON, true); + + $rawGroups = $rules['core.admin']; + $groups = array(); + + if (empty($rawGroups)) + { + return $ret; + } + + foreach ($rawGroups as $g => $enabled) + { + if ($enabled) + { + $groups[] = $db->q($g); + } + } + + if (empty($groups)) + { + return $ret; + } + } + catch (Exception $exc) + { + return $ret; + } + + // Get the user IDs of users belonging to the groups with the core.admin (Super User) privilege + try + { + $query = $db->getQuery(true) + ->select($db->qn('user_id')) + ->from($db->qn('#__user_usergroup_map')) + ->where($db->qn('group_id') . ' IN(' . implode(',', $groups) . ')' ); + $db->setQuery($query); + $rawUserIDs = $db->loadColumn(0); + + if (empty($rawUserIDs)) + { + return $ret; + } + + $userIDs = array(); + + foreach ($rawUserIDs as $id) + { + $userIDs[] = $db->q($id); + } + } + catch (Exception $exc) + { + return $ret; + } + + // Get the user information for the Super Users + try + { + $query = $db->getQuery(true) + ->select(array( + $db->qn('id'), + $db->qn('username'), + $db->qn('email'), + ))->from($db->qn('#__users')) + ->where($db->qn('id') . ' IN(' . implode(',', $userIDs) . ')') + ->where($db->qn('sendEmail') . ' = ' . $db->q('1')); + + if (!empty($emails)) + { + $query->where($db->qn('email') . 'IN(' . implode(',', $emails) . ')'); + } + + $db->setQuery($query); + $ret = $db->loadObjectList(); + } + catch (Exception $exc) + { + return $ret; + } + + return $ret; + } + + /** + * Update the time we last checked for failed backups + * + * @param int $exists Any non zero value means that we update, not insert, the record + * + * @return void + */ + private function updateLastCheck($exists) + { + $db = $this->container->db; + + $now = new Date(); + $nowToSql = $now->toSql(); + + $query = $db->getQuery(true) + ->insert($db->qn('#__ak_storage')) + ->columns(array($db->qn('tag'), $db->qn('lastupdate'))) + ->values($db->q('akeeba_checkfailed') . ', ' . $db->q($nowToSql)); + + if ($exists) + { + $query = $db->getQuery(true) + ->update($db->qn('#__ak_storage')) + ->set($db->qn('lastupdate') . ' = ' . $db->q($nowToSql)) + ->where($db->qn('tag') . ' = ' . $db->q('akeeba_checkfailed')); + } + + try + { + $db->setQuery($query)->execute(); + } + catch (Exception $exc) + { + } + } + + /** + * Get the last update check date and time stamp + * + * @return string + */ + private function getLastCheck() + { + $db = $this->container->db; + + $query = $db->getQuery(true) + ->select($db->qn('lastupdate')) + ->from($db->qn('#__ak_storage')) + ->where($db->qn('tag') . ' = ' . $db->q('akeeba_checkfailed')); + + $datetime = $db->setQuery($query)->loadResult(); + + if (!intval($datetime)) + { + $datetime = $db->getNullDate(); + } + + return $datetime; + } + + /** + * Set the flag to hide the restoration instructions modal from the Manage Backups page + * + * @return void + */ + public function hideRestorationInstructionsModal() + { + $this->container->params->set('show_howtorestoremodal', 0); + $this->container->params->save(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Transfer.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Transfer.php new file mode 100644 index 00000000..4618cd6d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Transfer.php @@ -0,0 +1,1277 @@ +container->db; + + /** @var Statistics $model */ + $model = $this->container->factory->model('Statistics')->tmpInstance(); + $model->setState('limitstart', 0); + $model->setState('limit', 1); + + if ($profileID > 0) + { + $model->setState('profile_id', $profileID); + } + + $backups = $model->getStatisticsListWithMeta(false, null, $db->qn('id') . ' DESC'); + + // No valid backups? No joy. + if (empty($backups)) + { + return $ret; + } + + // Get the latest backup + $backup = array_shift($backups); + + // If it's not stored on the server (e.g. remote backup), no joy. + if ($backup['meta'] != 'ok') + { + return $ret; + } + + // If it's not a full site backup, no joy. + if ($backup['type'] != 'full') + { + return $ret; + } + + return $backup; + } + + /** + * Returns the amount of space required on the target server. The two array keys are + * size In bytes + * string Pretty formatted, user-friendly string + * + * @return array + */ + public function getApproximateSpaceRequired() + { + $backup = $this->getLatestBackupInformation(); + + if (is_null($backup)) + { + return [ + 'size' => 0, + 'string' => '0.00 KB' + ]; + } + + $approximateSize = 2.5 * (float) $backup['size']; + + $unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB'); + + return [ + 'size' => $approximateSize, + 'string' => @round($approximateSize / pow(1024, ($i = floor(log($approximateSize, 1024)))), 2) . ' ' . $unit[$i] + ]; + } + + /** + * Cleans up a URL and makes sure it is a valid-looking URL + * + * @param string $url The URL to check + * + * @return array status [ok, invalid, same, notexists] (check status); url (the cleaned URL) + */ + public function checkAndCleanUrl($url) + { + // Initialise + $result = [ + 'status' => 'ok', + 'url' => $url + ]; + + // Am I missing the protocol? + if (strpos($url, '://') === false) + { + $url = 'http://' . $url; + } + + $result['url'] = $url; + + // Verify that it is an HTTP or HTTPS URL. + $uri = JUri::getInstance($url); + $protocol = $uri->getScheme(); + + if (!in_array($protocol, ['http', 'https'])) + { + $result['status'] = 'invalid'; + + return $result; + } + + // Verify we are not restoring to the same site we are backing up from + $path = $this->simplifyPath($uri->getPath()); + $uri->setPath('/' . $path); + + $siteUri = JUri::getInstance(); + + if ($siteUri->getHost() == $uri->getHost()) + { + $sitePath = $this->simplifyPath($siteUri->getPath()); + + if ($sitePath == $path) + { + $result['status'] = 'same'; + + return $result; + } + } + + $result['url'] = $uri->toString(['scheme', 'user', 'pass', 'host', 'port', 'path']); + + // Verify we can reach the domain. Since it can be an IP we check both name to IP and IP to name. + $host = $uri->getHost(); + + if (function_exists('idn_to_ascii')) + { + $host = idn_to_ascii($host); + } + + $isValid = ($siteUri->getHost() == $uri->getHost()) || ($host == 'localhost') || ($host == '127.0.0.1') || (($host !== false) && checkdnsrr($host, 'A')); + + // Sometimes we have a domain name without a DNS record which *can* be accessed locally, e.g. through the hosts + // file. We have to cater for that, just in case... + if (!$isValid) + { + $download = new Download($this->container); + $dummy = $download->getFromURL($uri->toString()); + + $isValid = $dummy !== false; + } + + if (!$isValid) + { + $result['status'] = 'notexists'; + + return $result; + } + + // All checks pass + return $result; + } + + /** + * Tries to simplify a server path to get the site's root. It can handle most forms on non-SEF and non-rewrite SEF + * URLs (as in index.php?foo=bar, something.php/this/is?completely=nuts#ok). It can't fix stupid but it tries really + * bloody hard to. + * + * @param string $path The path to simplify. We *expect* this to contain nonsense. + * + * @return string The scrubbed clean URL, hopefully leading to the site's root. + */ + private function simplifyPath($path) + { + $path = ltrim($path, '/'); + + if (empty($path)) + { + return $path; + } + + // Trim out anything after a .php file (including the .php file itself) + if (substr($path, -1) != '/') + { + $parts = explode('/', $path); + $newParts = []; + + foreach ($parts as $part) + { + if (substr($part, -4) == '.php') + { + break; + } + + $newParts[] = $part; + } + + $path = implode('/', $newParts); + } + + if (substr($path, -13) == 'administrator') + { + $path = substr($path, 0, -13); + } + + return $path; + } + + /** + * Determines the status of FTP, FTPS and SFTP support. The returned array has two keys 'supported' and 'firewalled' + * each one being an array. You want the protocol to has its 'supported' value set to true and its 'firewalled' + * value set to false. This would mean that the server supports this protocol AND does not block outbound + * connections over this protocol. + * + * @return array + */ + public function getFTPSupport() + { + // Initialise + $result = [ + 'supported' => [ + 'ftpcurl' => false, + 'ftpscurl' => false, + 'sftpcurl' => false, + 'ftp' => false, + 'ftps' => false, + 'sftp' => false, + ], + 'firewalled' => [ + 'ftpcurl' => false, + 'ftpscurl' => false, + 'sftpcurl' => false, + 'ftp' => false, + 'ftps' => false, + 'sftp' => false, + ] + ]; + + // Necessary functions for each connection method + $supportChecks = [ + 'ftpcurl' => ['curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'], + 'ftpscurl' => ['curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'], + 'sftpcurl' => ['curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'], + 'ftp' => ['ftp_connect', 'ftp_login', 'ftp_close', 'ftp_chdir', 'ftp_mkdir', 'ftp_pasv', 'ftp_put', 'ftp_delete'], + 'ftps' => ['ftp_ssl_connect', 'ftp_login', 'ftp_close', 'ftp_chdir', 'ftp_mkdir', 'ftp_pasv', 'ftp_put', 'ftp_delete'], + 'sftp' => ['ssh2_connect', 'ssh2_auth_password', 'ssh2_auth_pubkey_file', 'ssh2_sftp', 'ssh2_exec', 'ssh2_sftp_unlink', 'ssh2_sftp_stat', 'ssh2_sftp_mkdir'], + ]; + + // Determine which connection methods are supported + $supported = []; + + foreach ($supportChecks as $protocol => $functions) + { + $supported[$protocol] = true; + + foreach ($functions as $function) + { + if (!function_exists($function)) + { + $supported[$protocol] = false; + + break; + } + } + } + + $result['supported'] = $supported; + + // Check firewall settings -- Disabled because the 3PD test server got clogged :( + /** + $result['firewalled'] = array( + 'ftp' => !$result['supported']['ftp'] ? false : EngineTransfer\Ftp::isFirewalled(), + 'ftpcurl' => !$result['supported']['ftp'] ? false : EngineTransfer\FtpCurl::isFirewalled(), + 'ftps' => !$result['supported']['ftps'] ? false : EngineTransfer\Ftp::isFirewalled(['ssl' => true]), + 'ftpscurl' => !$result['supported']['ftp'] ? false : EngineTransfer\FtpCurl::isFirewalled(['ssl' => true]), + 'sftp' => !$result['supported']['sftp'] ? false : EngineTransfer\Sftp::isFirewalled(), + 'sftpcurl' => !$result['supported']['sftp'] ? false : EngineTransfer\SftpCurl::isFirewalled(), + ); + /**/ + + return $result; + } + + /** + * Checks the FTP connection parameters + * + * @param array $config FTP/SFTP connection details + * + * @throws RuntimeException + */ + public function testConnection(array $config) + { + /** @var EngineTransfer\TransferInterface $connector */ + $connector = $this->getConnector($config); + + // Is it the same site we are restoring from? It is if the configuration.php exists and has the same contents as + // the one I read from our server. + $this->checkIfSameSite($connector); + + // Only perform those checks if I'm not forcing the transfer + if (!$config['force']) + { + // Check if there's a special file in this directory, e.g. .htaccess, php.ini, .user.ini or web.config. + $this->checkIfHasSpecialFile($connector); + + // Check if there's another site present in this directory + $this->checkIfExistingSite($connector); + } + + // Does it match the URL to the site? + $this->checkIfMatchesUrl($connector); + } + + /** + * Upload Kickstart, our extra script and check that the target server fullfills our criteria + * + * @param array $config FTP/SFTP connection details + * + * @throws Exception + */ + public function initialiseUpload(array $config) + { + /** @var EngineTransfer\TransferInterface $connector */ + $connector = $this->getConnector($config); + + // Can I upload Kickstart and my extra script? + $files = [ + JPATH_ADMINISTRATOR . '/components/com_akeeba/Master/Installers/kickstart.txt' => 'kickstart.php', + JPATH_ADMINISTRATOR . '/components/com_akeeba/Master/Installers/kickstart.transfer.php' => 'kickstart.transfer.php' + ]; + + $createdFiles = []; + $transferredSize = 0; + $transferTime = 0; + + try + { + foreach ($files as $localFile => $remoteFile) + { + $start = microtime(true); + $connector->upload($localFile, $connector->getPath($remoteFile)); + $end = microtime(true); + $createdFiles[] = $remoteFile; + $transferredSize += filesize($localFile); + $transferTime += $end - $start; + } + } + catch (Exception $e) + { + // An upload failed. Remove existing files. + $this->removeRemoteFiles($connector, $createdFiles, true); + + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADKICKSTART')); + } + + // Get the transfer speed between the two servers in bytes / second + $transferSpeed = $transferredSize / $transferTime; + + try + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $connector->mkdir($connector->getPath('kicktemp'), $trustMeIKnowWhatImDoing); + } + catch (Exception $e) + { + // Don't sweat if we can't create our temporary directory. + } + + // Can I run Kickstart and my extra script? + try + { + $this->checkRemoteServerEnvironment(); + } + catch (Exception $e) + { + $this->removeRemoteFiles($connector, $createdFiles, true); + + throw $e; + } + + // Get the lowest maximum execution time between our local and remote server + $remoteTimeout = $this->container->platform->getSessionVar('transfer.remoteTimeLimit', 5, 'akeeba'); + $localTimeout = 5; + + if (function_exists('ini_get')) + { + $localTimeout = ini_get("max_execution_time"); + } + + $timeout = min($localTimeout, $remoteTimeout); + + if ($localTimeout == 0) + { + $timeout = $remoteTimeout; + } + elseif ($remoteTimeout == 0) + { + $timeout = $localTimeout; + } + + if ($timeout == 0) + { + $timeout = 5; + } + + // Get the maximimum transfer size, rounded down to 512K + $maxTransferSize = $transferSpeed * $timeout; + $maxTransferSize = floor($maxTransferSize / 524288) * 524288; + + if ($maxTransferSize == 0) + { + $maxTransferSize = 524288; + } + + /** + * We never go above a maximum transfer size that depends on the server memory setting and the maximum remote + * upload size (minus 10Kb for overhead data) + */ + $chunkSizeLimit = $this->getMaxChunkSize(); + $maxUploadLimit = $this->container->platform->getSessionVar('transfer.uploadLimit', 5242880, 'akeeba') - 10240; + + /** + * A little explanation for "$maxUploadLimit / 4" below. We are uploading binary data which gets encoded as + * form data. The integer part is a rough estimation of the size discrepancy between raw and encoded data. + */ + if ($config['chunkMode'] == 'post') + { + $maxTransferSize = min(floor($maxUploadLimit / 4), $maxTransferSize, $chunkSizeLimit); + } + + // Save the optimal transfer size in the session + $this->container->platform->setSessionVar('transfer.fragSize', $maxTransferSize, 'akeeba'); + } + + /** + * Upload the next fragment + * + * @param array $config FTP/SFTP connection details + * + * @throws Exception + * + * @return array + */ + public function uploadChunk(array $config) + { + $ret = [ + 'result' => true, + 'done' => false, + 'message' => '', + 'totalSize' => 0, + 'doneSize' => 0 + ]; + + // Get information from the session + $fragSize = $this->container->platform->getSessionVar('transfer.fragSize', 5242880, 'akeeba'); + $backup = $this->container->platform->getSessionVar('transfer.lastBackup', [], 'akeeba'); + $totalSize = $this->container->platform->getSessionVar('transfer.totalSize', 0, 'akeeba'); + $doneSize = $this->container->platform->getSessionVar('transfer.doneSize', 0, 'akeeba'); + $part = $this->container->platform->getSessionVar('transfer.part', -1, 'akeeba'); + $frag = $this->container->platform->getSessionVar('transfer.frag', -1, 'akeeba'); + + // Do I need to update the total size? + if (!$totalSize) + { + $totalSize = $backup['total_size']; + $this->container->platform->setSessionVar('transfer.totalSize', $totalSize, 'akeeba'); + } + + $ret['totalSize'] = $totalSize; + + // First fragment of a new part + if ($frag == -1) + { + $frag = 0; + $part++; + } + + /** + * If the backup is single part then $backup['multipart'] is 0. This means that the next if-block will report + * that the transfer is done. In these cases we have to convert $backup['multipart'] to 1 to let the upload + * actually run at all. + */ + if ($backup['multipart'] == 0) + { + $backup['multipart'] = 1; + } + + // If I'm past the last part I'm done. + if ($part >= $backup['multipart']) + { + + // We are done + $ret['done'] = true; + return $ret; + } + + // Get the information for this part + $fileName = $this->getPartFilename($backup['absolute_path'], $part); + $fileSize = filesize($fileName); + + $intendedSeekPosition = $fragSize * $frag; + + // I am trying to seek past EOF. Oops. Upload the next part. + if ($intendedSeekPosition >= $fileSize) + { + $this->container->platform->setSessionVar('transfer.frag', -1, 'akeeba'); + return $this->uploadChunk($config); + } + + // Open the part + $fp = @fopen($fileName, 'rb'); + + if ($fp === false) + { + $ret['result'] = false; + $ret['message'] = JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTREADLOCALFILE', $fileName); + + return $ret; + } + + // Seek to position + if (fseek($fp, $intendedSeekPosition) == -1) + { + @fclose($fp); + + $ret['result'] = false; + $ret['message'] = JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTREADLOCALFILE', $fileName); + + return $ret; + } + + // Read the data + $data = fread($fp, $fragSize); + $doneSize += strlen($data); + $ret['doneSize'] = $doneSize; + $this->container->platform->setSessionVar('transfer.doneSize', $doneSize, 'akeeba'); + + // Upload the data + $this->container->platform->setSessionVar('transfer.frag', $frag, 'akeeba'); + + try + { + switch ($config['chunkMode']) + { + case 'post': + $dataLength = $this->uploadUsingPost($fileName, $data); + break; + + case 'chunked': + default: + $dataLength = $this->uploadUsingChunked($fileName, $data, $config); + break; + } + } + // A finally{} block is what we really need but it's not supported until PHP 5.5 and I'm stuck supporting 5.4 :( + catch (\RuntimeException $e) + { + // Close the part + fclose($fp); + + // Rethrow the exception + throw $e; + } + + // Close the part + fclose($fp); + + // Update the session data + $this->container->platform->setSessionVar('transfer.fragSize', $fragSize, 'akeeba'); + $this->container->platform->setSessionVar('transfer.totalSize', $totalSize, 'akeeba'); + $this->container->platform->setSessionVar('transfer.doneSize', $doneSize, 'akeeba'); + $this->container->platform->setSessionVar('transfer.part', $part, 'akeeba'); + $this->container->platform->setSessionVar('transfer.frag', ++$frag, 'akeeba'); + + // Did I go past EOF? Then on to the next part + $intendedSeekPosition += $dataLength; + + if ($intendedSeekPosition >= $fileSize) + { + $this->container->platform->setSessionVar('transfer.frag', -1, 'akeeba'); + $this->container->platform->setSessionVar('transfer.part', ++$part, 'akeeba'); + } + + // Did I reach the last part? Then I'm done + if ($part >= $backup['multipart']) + { + // We are done + $ret['done'] = true; + } + + return $ret; + } + + /** + * Reset the upload information. Required to start over. + * + * @return void + */ + public function resetUpload() + { + $this->container->platform->setSessionVar('transfer.totalSize', 0, 'akeeba'); + $this->container->platform->setSessionVar('transfer.doneSize', 0, 'akeeba'); + $this->container->platform->setSessionVar('transfer.part', -1, 'akeeba'); + $this->container->platform->setSessionVar('transfer.frag', -1, 'akeeba'); + } + + /** + * Gets the TransferInterface connector object based on the $config configuration parameters array + * + * @param array $config The configuration array with the FTP/SFTP connection information + * + * @return EngineTransfer\TransferInterface + * + * @throws RuntimeException + */ + private function getConnector(array $config) + { + switch ($config['method']) + { + case 'sftp': + $connector = new EngineTransfer\Sftp($config); + break; + + case 'sftpcurl': + $connector = new EngineTransfer\SftpCurl($config); + break; + + case 'ftpcurl': + case 'ftpscurl': + $connector = new EngineTransfer\FtpCurl($config); + break; + + default: + $connector = new EngineTransfer\Ftp($config); + break; + } + + return $connector; + } + + /** + * Checks if the remote site is the same as the site we are running the wizard from. + * + * @param EngineTransfer\TransferInterface $connector + */ + private function checkIfSameSite(EngineTransfer\TransferInterface $connector) + { + $myConfiguration = @file_get_contents(JPATH_ROOT . '/configuration.php'); + + if ($myConfiguration === false) + { + return; + } + + try + { + $otherConfiguration = $connector->read($connector->getPath('configuration.php')); + } + catch (Exception $e) + { + // File not found. No harm done. + + return; + } + + if ($otherConfiguration == $myConfiguration) + { + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_SAMESITE')); + } + } + + /** + * Check if there's a special file which might prevent site transfer from taking place. + * + * @param EngineTransfer\TransferInterface $connector + */ + private function checkIfHasSpecialFile(EngineTransfer\TransferInterface $connector) + { + $possibleFiles = ['.htaccess', 'web.config', 'php.ini', '.user.ini']; + + foreach ($possibleFiles as $file) + { + try + { + $fileContents = $connector->read($connector->getPath($file)); + } + catch (Exception $e) + { + // File not found. No harm done. + continue; + } + + if (empty($fileContents)) + { + continue; + } + + throw new TransferIgnorableError(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_HTACCESS', $file)); + } + } + + /** + * Check if there's an existing site + * + * @param EngineTransfer\TransferInterface $connector + */ + private function checkIfExistingSite(EngineTransfer\TransferInterface $connector) + { + /** + * I run into a PHP bug. When we try to read 'wordpress/index.php' over FTP to determine if it exists we end up + * with the folder "wordpress" being created. I have only been able to reproduce with with VSFTPd. The VSFTPd + * log claims there is only an unsuccessful read operation. Why the folder is create is a mystery, but I have to + * remove it anyway. I know, right? + */ + // $possibleFiles = ['index.php', 'wordpress/index.php']; + $possibleFiles = ['index.php']; + + foreach ($possibleFiles as $file) + { + try + { + $fileContents = $connector->read($connector->getPath($file)); + } + catch (Exception $e) + { + // File not found. No harm done. + continue; + } + + if (empty($fileContents)) + { + continue; + } + + throw new TransferIgnorableError(JText::_('COM_AKEEBA_TRANSFER_ERR_EXISTINGSITE')); + } + } + + /** + * Check if the connection matches the site's stated URL + * + * @param EngineTransfer\TransferInterface $connector + */ + private function checkIfMatchesUrl(EngineTransfer\TransferInterface $connector) + { + $sourceFile = JPATH_SITE . '/media/com_akeeba/icons/akeeba-16.png'; + + // Try to upload the file + try + { + $connector->upload($sourceFile, $connector->getPath(basename($sourceFile))); + } + catch (Exception $e) + { + $errorMessage = JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADTESTFILE', basename($sourceFile)); + + $errorMessage .= " — [ " . $e->getMessage() . ' ]'; + + throw new RuntimeException($errorMessage); + } + + // Try to fetch the file over HTTP + $url = $this->container->platform->getSessionVar('transfer.url', '', 'akeeba'); + + $url = rtrim($url, '/'); + + $downloader = new Download($this->container); + $data = $downloader->getFromURL($url . '/' . basename($sourceFile)); + + // Delete the temporary file + $connector->delete($connector->getPath(basename($sourceFile))); + + // Could we get it over HTTP? + $originalData = file_get_contents($sourceFile); + + if ($originalData != $data) + { + throw new TransferFatalError(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTACCESSTESTFILE')); + } + } + + /** + * Gets the FTP configuration from the session + * + * @return array + */ + public function getFtpConfig() + { + $transferOption = $this->container->platform->getSessionVar('transfer.transferOption', '', 'akeeba'); + + return array( + 'method' => $transferOption, + 'force' => $this->container->platform->getSessionVar('transfer.force', 0, 'akeeba'), + 'host' => $this->container->platform->getSessionVar('transfer.ftpHost', '', 'akeeba'), + 'port' => $this->container->platform->getSessionVar('transfer.ftpPort', '', 'akeeba'), + 'username' => $this->container->platform->getSessionVar('transfer.ftpUsername', '', 'akeeba'), + 'password' => $this->container->platform->getSessionVar('transfer.ftpPassword', '', 'akeeba'), + 'directory' => $this->container->platform->getSessionVar('transfer.ftpDirectory', '', 'akeeba'), + 'ssl' => $transferOption == 'ftps', + 'passive' => $this->container->platform->getSessionVar('transfer.ftpPassive', 1, 'akeeba'), + 'passive_fix' => $this->container->platform->getSessionVar('transfer.ftpPassiveFix', 1, 'akeeba'), + 'privateKey' => $this->container->platform->getSessionVar('transfer.ftpPrivateKey', '', 'akeeba'), + 'publicKey' => $this->container->platform->getSessionVar('transfer.ftpPubKey', '', 'akeeba'), + 'chunkMode' => $this->container->platform->getSessionVar('transfer.chunkMode', 'chunked', 'akeeba'), + 'chunkSize' => $this->container->platform->getSessionVar('transfer.uploadLimit', '5242880', 'akeeba'), + ); + } + + /** + * Removes files stored remotely + * + * @param EngineTransfer\TransferInterface $connector The transfer object + * @param array $files The list of remote files to delete (relative paths) + * @param bool|true $ignoreExceptions Should I ignore exceptions thrown? + * + * @return void + * + * @throws Exception + */ + private function removeRemoteFiles(EngineTransfer\TransferInterface $connector, array $files, $ignoreExceptions = true) + { + if (empty($files)) + { + return; + } + + foreach ($files as $file) + { + $remoteFile = $connector->getPath($file); + + try + { + $connector->delete($remoteFile); + } + catch (Exception $e) + { + // Only let the exception bubble up if we are told not to ignore exceptions + if (!$ignoreExceptions) + { + throw $e; + } + } + } + } + + /** + * Check if the remote server environment matches our expectations. + * + * @throws Exception + */ + private function checkRemoteServerEnvironment() + { + $baseUrl = $this->container->platform->getSessionVar('transfer.url', '', 'akeeba'); + + $baseUrl = rtrim($baseUrl, '/'); + + $downloader = new Download($this->container); + $rawData = $downloader->getFromURL($baseUrl . '/kickstart.php?task=serverinfo'); + + if ($rawData == false) + { + // Cannot access Kickstart on the remote server + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART')); + } + + // Try to get the raw JSON data + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no leading ### + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART')); + } + + // Remove the leading ### + $rawData = substr($rawData, $pos + 3); + + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no trailing ### + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART')); + } + + // Remove the trailing ### + $rawData = substr($rawData, 0, $pos); + + // Get the JSON response + $data = @json_decode($rawData, true); + + if (empty($data)) + { + // Invalid AJAX data, can't decode this stuff + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART')); + } + + // Does the server have enough disk space? + $freeSpace = $data['freeSpace']; + + $requiredSize = $this->getApproximateSpaceRequired(); + + if ($requiredSize['size'] > $freeSpace) + { + $unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB'); + $freeSpaceString = @round($freeSpace / pow(1024, ($i = floor(log($freeSpace, 1024)))), 2) . ' ' . $unit[$i]; + + throw new RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_NOTENOUGHSPACE', $freeSpaceString, $requiredSize['string'])); + } + + // Can I write to remote files? + $canWrite = $data['canWrite']; + $canWriteTemp = $data['canWriteTemp']; + + if (!$canWrite && !$canWriteTemp) + { + throw new RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANNOTWRITEREMOTEFILES')); + } + + if ($canWrite) + { + $this->container->platform->setSessionVar('transfer.targetPath', '', 'akeeba'); + } + else + { + $this->container->platform->setSessionVar('transfer.targetPath', 'kicktemp', 'akeeba'); + } + + $this->container->platform->setSessionVar('transfer.remoteTimeLimit', $data['maxExecTime'], 'akeeba'); + + // What is my upload limit? + $uploadLimit = min($data['maxPost'], $data['maxUpload']); + + if (empty($data['maxPost'])) + { + $uploadLimit = $data['maxUpload']; + } + elseif (empty($data['maxUpload'])) + { + $uploadLimit = $data['maxPost']; + } + + if (empty($uploadLimit)) + { + $uploadLimit = 1048576; + } + + $this->container->platform->setSessionVar('transfer.uploadLimit', $uploadLimit, 'akeeba'); + } + + /** + * Get the filename for a backup part file, given the base file and the part number + * + * @param string $baseFile Full path to the base file (.jpa, .jps, .zip) + * @param int $part Part number + * + * @return string + */ + private function getPartFilename($baseFile, $part = 0) + { + if ($part == 0) + { + return $baseFile; + } + + $dirname = dirname($baseFile); + $basename = basename($baseFile); + + $pos = strrpos($basename, '.'); + $extension = substr($basename, $pos + 1); + + $newExtension = substr($baseFile, 0, 1) . sprintf('%02u', $part); + + return $dirname . '/' . basename($basename, '.' . $extension) . '.' .$newExtension; + } + + /** + * Returns the PHP memory limit. If ini_get is not available it will assume 8Mb. + * + * @return int + */ + private function getServerMemoryLimit() + { + // Default reported memory limit: 8Mb + $memLimit = 8388608; + + // If we can't find out how much PHP memory we have available use 8Mb by default + if (!function_exists('ini_get')) + { + return $memLimit; + } + + $iniMemLimit = ini_get("memory_limit"); + $iniMemLimit = $this->convertMemoryLimitToBytes($iniMemLimit); + + $memLimit = ($iniMemLimit > 0) ? $iniMemLimit : $memLimit; + + return (int) $memLimit; + } + + /** + * Gets the maximum chunk size the server can handle safely. It does so by finding the PHP memory limit, removing + * the current memory usage (or at least 2Mb) and rounding down to the closest 512Kb. It can never be lower than + * 512Kb. + */ + private function getMaxChunkSize() + { + $memoryLimit = $this->getServerMemoryLimit(); + $usedMemory = max(memory_get_usage(), memory_get_peak_usage(), 2048); + + $maxChunkSize = max(($memoryLimit - $usedMemory) / 2, 524288); + + return floor($maxChunkSize / 524288) * 524288; + } + + /** + * Convert the textual representation of PHP memory limit to an integer, e.g. convert 8M to 8388608 + * + * @param string $setting The PHP memory limit + * + * @return int PHP memory limit as an integer + */ + private function convertMemoryLimitToBytes($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; + } + + /** + * Uploads a chunk of a backup part file using a direct POST to Kickstart. + * + * This is the method supported by the Site Transfer Wizard since its inception. However, it may not work with hosts + * which have a sensitive server protection, e.g. the very tight mod_security2 rules on SiteGround servers. In those + * cases the remote server will respond with a 500 Internal Server Error, a 403 Forbidden or another server error. + * + * @param string $fileName The filename to upload + * @param string $data The data to upload + * + * @return int The length of the data we managed to upload + * + * @since 3.1.0 + */ + private function uploadUsingPost($fileName, $data) + { + $frag = $this->container->platform->getSessionVar('transfer.frag', -1, 'akeeba'); + $fragSize = $this->container->platform->getSessionVar('transfer.fragSize', 5242880, 'akeeba'); + $url = $this->container->platform->getSessionVar('transfer.url', '', 'akeeba'); + $directory = $this->container->platform->getSessionVar('transfer.targetPath', '', 'akeeba'); + + $url = rtrim($url, '/') . '/kickstart.php'; + $uri = JUri::getInstance($url); + $uri->setVar('task', 'uploadFile'); + $uri->setVar('file', basename($fileName)); + $uri->setVar('directory', $directory); + $uri->setVar('frag', $frag); + $uri->setVar('fragSize', $fragSize); + + $downloader = new Download($this->container); + $downloader->setAdapterOptions([ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => [ + 'data' => $data + ] + ]); + $dataLength = strlen($data); + unset($data); + $rawData = $downloader->getFromURL($uri->toString()); + + // Try to get the raw JSON data + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no leading ### + throw new RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + // Remove the leading ### + $rawData = substr($rawData, $pos + 3); + + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no trailing ### + throw new RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + // Remove the trailing ### + $rawData = substr($rawData, 0, $pos); + + // Get the JSON response + $data = @json_decode($rawData, true); + + if (empty($data)) + { + // Invalid AJAX data, can't decode this stuff + throw new RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + if (!$data['status']) + { + throw new RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_ERRORFROMREMOTE', $data['message'])); + } + + return $dataLength; + } + + /** + * Uploads a chunk of a backup part file via FTP and then uses Kickstart to piece the file together. + * + * This is a new upload method which works better on servers with tighter security. The only downside is that we + * have to open many FTP/SFTP upload sessions which may result in the remote server eventually blocking our uploads. + * + * @param string $fileName The filename to upload + * @param string $data The data to upload + * @param array $config The FTP/SFTP configuration + * + * @return int The length of the data we managed to upload + * + * @since 3.1.0 + */ + private function uploadUsingChunked($fileName, $data, $config) + { + // ==== Initialize + $frag = $this->container->platform->getSessionVar('transfer.frag', -1, 'akeeba'); + $fragSize = $this->container->platform->getSessionVar('transfer.fragSize', 5242880, 'akeeba'); + $url = $this->container->platform->getSessionVar('transfer.url', '', 'akeeba'); + $directory = $this->container->platform->getSessionVar('transfer.targetPath', '', 'akeeba'); + + // ==== Upload the data to the same folder as Kickstart, under a temporary name + // Even though the connector has the write() method, it's not very good for over 1M files. So we create a temp file instead. + $engineConfig = Factory::getConfiguration(); + $localTempFile = tempnam($this->container->platform->getConfig()->get('tmp_path', sys_get_temp_dir()), 'stw'); + $localTempFile = ($localTempFile === false) ? tempnam(sys_get_temp_dir(), 'stw') : $localTempFile; + $localTempFile = ($localTempFile === false) ? tempnam($engineConfig->get('akeeba.basic.output_directory', '[DEFAULT_OUTPUT]'), 'stw') : $localTempFile; + + if ($localTempFile === false) + { + throw new \RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANTCREATETEMPCHUNK')); + } + + if (!file_put_contents($localTempFile, $data)) + { + if (!JFile::write($localTempFile, $data)) + { + throw new \RuntimeException(JText::_('COM_AKEEBA_TRANSFER_ERR_CANTCREATETEMPCHUNK')); + } + } + + $random = new RandomValue(); + $tempFile = strtolower($random->generateString(8)) . '.dat'; + $connector = $this->getConnector($config); + + try + { + $connector->upload($localTempFile, $tempFile, true); + } + catch (\RuntimeException $e) + { + $this->container->fileSystem->delete($localTempFile); + + throw $e; + } + + // ==== Call Kickstart to piece together the file + $url = rtrim($url, '/') . '/kickstart.php'; + $uri = JUri::getInstance($url); + $uri->setVar('task', 'uploadFile'); + $uri->setVar('file', basename($fileName)); + $uri->setVar('directory', $directory); + $uri->setVar('frag', $frag); + $uri->setVar('fragSize', $fragSize); + $uri->setVar('dataFile', $tempFile); + + $downloader = new Download($this->container); + $dataLength = strlen($data); + unset($data); + $rawData = $downloader->getFromURL($uri->toString()); + + // ==== Delete the temporary files + if (!@unlink($localTempFile)) + { + JFile::delete($localTempFile); + } + $connector->delete($tempFile); + + // ==== Parse Kickstart's response + + // Try to get the raw JSON data + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no leading ### + throw new \RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + // Remove the leading ### + $rawData = substr($rawData, $pos + 3); + + $pos = strpos($rawData, '###'); + + if ($pos === false) + { + // Invalid AJAX data, no trailing ### + throw new \RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + // Remove the trailing ### + $rawData = substr($rawData, 0, $pos); + + // Get the JSON response + $data = @json_decode($rawData, true); + + if (empty($data)) + { + // Invalid AJAX data, can't decode this stuff + throw new \RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName))); + } + + if (!$data['status']) + { + throw new \RuntimeException(JText::sprintf('COM_AKEEBA_TRANSFER_ERR_ERRORFROMREMOTE', $data['message'])); + } + + return $dataLength; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/Updates.php b/deployed/akeeba/administrator/components/com_akeeba/Model/Updates.php new file mode 100644 index 00000000..37d21110 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/Updates.php @@ -0,0 +1,705 @@ +params->get('update_dlid', ''); + + // If I have a valid Download ID I will need to use a non-blank extra_query in Joomla! 3.2+ + if (preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $dlid)) + { + // Even if the user entered a Download ID in the Core version. Let's switch his update channel to Professional + $isPro = true; + } + + if ($isPro) + { + $config['update_sitename'] = 'Akeeba Backup Professional'; + $config['update_site'] = 'https://cdn.akeebabackup.com/updates/pkgakeebapro.xml'; + $config['update_extraquery'] = 'dlid=' . $dlid; + } + + if (defined('AKEEBA_VERSION') && !in_array(substr(AKEEBA_VERSION, 0, 3), ['dev', 'rev'])) + { + $config['update_version'] = AKEEBA_VERSION; + } + + parent::__construct($config); + + $this->container = $container; + + $this->extension_id = $this->findExtensionId('pkg_akeeba', 'package'); + + if (empty($this->extension_id)) + { + $this->createFakePackageExtension(); + $this->extension_id = $this->findExtensionId('pkg_akeeba', 'package'); + } + } + + /** + * Refreshes the update sites, removing obsolete update sites in the process + */ + public function refreshUpdateSite() + { + // Remove any update sites for the old com_akeeba package + $this->removeObsoleteComponentUpdateSites(); + + // Refresh our update sites + parent::refreshUpdateSite(); + } + + /** + * Handle automatic updates. Sends update notification emails and/or installs a new version automatically. + * + * @return array + */ + public function autoupdate() + { + $return = array( + 'message' => '' + ); + + // First of all let's check if there are any updates + $updateInfo = (object) $this->getUpdates(true); + + // There are no updates, there's no point in continuing + if (!$updateInfo->hasUpdate) + { + return array( + 'message' => array("No available updates found") + ); + } + + $return['message'][] = "Update detected, version: " . $updateInfo->version; + + // Ok, an update is found, what should I do? + $params = $this->container->params; + $autoupdate = $params->get('autoupdateCli', 1); + + // Let's notifiy the user + if ($autoupdate == 1 || $autoupdate == 2) + { + $email = $params->get('notificationEmail'); + + if (!$email) + { + $return['message'][] = "There isn't an email for notifications, no notification will be sent."; + } + else + { + // Ok, I can send it out, but before let's check if the user set any frequency limit + $numfreq = $params->get('notificationFreq', 1); + $freqtime = $params->get('notificationTime', 'day'); + $lastSend = $this->getLastSend(); + $shouldSend = false; + + if (!$numfreq) + { + $shouldSend = true; + } + else + { + $check = strtotime('-' . $numfreq . ' ' . $freqtime); + + if ($lastSend < $check) + { + $shouldSend = true; + } + else + { + $return['message'][] = "Frequency limit hit, I won't send any email"; + } + } + + if ($shouldSend) + { + if ($this->sendNotificationEmail($updateInfo->version, $email)) + { + $return['message'][] = "E-mail(s) correctly sent"; + } + else + { + $return['message'][] = "An error occurred while sending e-mail(s). Please double check your settings"; + } + + $this->setLastSend(); + } + } + } + + // Let's download and install the latest version + if ($autoupdate == 1 || $autoupdate == 3) + { + $return['message'][] = $this->updateComponent(); + } + + return $return; + } + + /** + * Sends an update notification email + * + * @param string $version The newest available version + * @param string $email The email address of the recipient + * + * @return boolean The result from JMailer::send() + */ + public function sendNotificationEmail($version, $email) + { + $email_subject = <<container->platform->getConfig(); + $sitename = $jconfig->get('sitename'); + + $substitutions = array( + '[VERSION]' => $version, + '[SITENAME]' => $sitename + ); + + $email_subject = str_replace(array_keys($substitutions), array_values($substitutions), $email_subject); + $email_body = str_replace(array_keys($substitutions), array_values($substitutions), $email_body); + + try + { + $mailer = \JFactory::getMailer(); + + $mailfrom = $jconfig->get('mailfrom'); + $fromname = $jconfig->get('fromname'); + + $mailer->setSender(array($mailfrom, $fromname)); + $mailer->addRecipient($email); + $mailer->setSubject($email_subject); + $mailer->setBody($email_body); + + return $mailer->Send(); + } + catch (\Exception $e) + { + // Joomla! 3.5 is written by incompetent bonobos + return false; + } + } + + /** + * Automatically download and install the updated version + * + * @return string The message to show in the CLI output + */ + private function updateComponent() + { + \JLoader::import('joomla.updater.update'); + + $db = $this->container->db; + + $updateSiteIDs = $this->getUpdateSiteIds(); + $update_site = array_shift($updateSiteIDs); + + $query = $db->getQuery(true) + ->select($db->qn('update_id')) + ->from($db->qn('#__updates')) + ->where($db->qn('update_site_id') . ' = ' . $update_site); + + $uid = $db->setQuery($query)->loadResult(); + + $update = new \JUpdate(); + $instance = \JTable::getInstance('update'); + $instance->load($uid); + $update->loadFromXML($instance->detailsurl); + + if (isset($update->get('downloadurl')->_data)) + { + $url = trim($update->downloadurl->_data); + } + else + { + return "No download URL found inside XML manifest"; + } + + $extra_query = $instance->extra_query; + + if ($extra_query) + { + if (strpos($url, '?') === false) + { + $url .= '?'; + } + else + { + $url .= '&'; + } + + $url .= $extra_query; + } + + $config = $this->container->platform->getConfig(); + $tmp_dest = $config->get('tmp_path'); + + if (!$tmp_dest) + { + return "Joomla temp directory is empty, please set it before continuing"; + } + elseif (!\JFolder::exists($tmp_dest)) + { + return "Joomla temp directory does not exists, please set the correct path before continuing"; + } + + $p_file = \JInstallerHelper::downloadPackage($url); + + if (!$p_file) + { + return "An error occurred while trying to download the latest version"; + } + + // Unpack the downloaded package file + $package = \JInstallerHelper::unpack($tmp_dest . '/' . $p_file); + + if (!$package) + { + return "An error occurred while unpacking the file, please double check your Joomla temp directory"; + } + + $installer = new \JInstaller; + $installed = $installer->install($package['extractdir']); + + // Let's cleanup the downloaded archive and the temp folder + if (\JFolder::exists($package['extractdir'])) + { + \JFolder::delete($package['extractdir']); + } + + if (\JFile::exists($package['packagefile'])) + { + \JFile::delete($package['packagefile']); + } + + if ($installed) + { + return "Component successfully updated"; + } + else + { + return "An error occurred while trying to update the component"; + } + } + + /** + * Does the user need to provide FTP credentials? It also registers any FTP credentials provided in the URL. + * + * @return bool True if the user needs to provide FTP credentials + */ + public function needsFTPCredentials() + { + // Determine wether FTP credentials have been passed along with the current request + \JLoader::import('joomla.client.helper'); + + $user = $this->input->get('username', null, 'raw'); + $pass = $this->input->get('password', null, 'raw'); + + if (!(($user == '') && ($pass == ''))) + { + // Add credentials to the session + if (\JClientHelper::setCredentials('ftp', $user, $pass)) + { + return false; + } + + return true; + } + + return !\JClientHelper::hasCredentials('ftp'); + } + + /** + * Get the UNIX timestamp of the the last time we sent out an update notification email + * + * @return integer + */ + private function getLastSend() + { + return $this->container->params->get('akeebasubs_autoupdate_lastsend', 0); + } + + /** + * Set the UNIX timestamp of the last time we sent out an update notificatin email to be right now + * + * @return void + */ + private function setLastSend() + { + $this->container->params->set('akeebasubs_autoupdate_lastsend', time()); + $this->container->params->save(); + } + + /** + * Removes the obsolete update sites for the component, since now we're dealing with a package. + * + * Controlled by componentName, packageName and obsoleteUpdateSiteLocations + * + * Depends on getExtensionId, getUpdateSitesFor + * + * @return void + */ + private function removeObsoleteComponentUpdateSites() + { + // Initialize + $deleteIDs = array(); + + // Get component ID + $componentID = $this->findExtensionId('com_akeeba', 'component'); + + // Get package ID + $packageID = $this->findExtensionId('pkg_akeeba', 'package'); + + // Update sites for old extension ID (all) + if ($componentID) + { + // Old component packages + $moreIDs = $this->getUpdateSitesFor($componentID, null); + + if (is_array($moreIDs) && count($moreIDs)) + { + $deleteIDs = array_merge($deleteIDs, $moreIDs); + } + + // Obsolete update sites + $moreIDs = $this->getUpdateSitesFor(null, $componentID, $this->obsoleteUpdateSiteLocations); + + if (is_array($moreIDs) && count($moreIDs)) + { + $deleteIDs = array_merge($deleteIDs, $moreIDs); + } + } + + // Update sites for any but current extension ID, location matching any of the obsolete update sites + if ($packageID) + { + // Update sites for all of the current extension ID update sites + $moreIDs = $this->getUpdateSitesFor($packageID, null); + + if (is_array($moreIDs) && count($moreIDs)) + { + $deleteIDs = array_merge($deleteIDs, $moreIDs); + } + + $deleteIDs = array_unique($deleteIDs); + + // Remove the last update site + if (count($deleteIDs)) + { + $lastID = array_pop($moreIDs); + $pos = array_search($lastID, $deleteIDs); + unset($deleteIDs[$pos]); + } + } + + $db = $this->container->db; + $deleteIDs = array_unique($deleteIDs); + + if (empty($deleteIDs) || !count($deleteIDs)) + { + return; + } + + $deleteIDs = array_map(array($db, 'q'), $deleteIDs); + + $query = $db->getQuery(true) + ->delete($db->qn('#__update_sites')) + ->where($db->qn('update_site_id') . ' IN(' . implode(',', $deleteIDs) . ')'); + + try + { + $db->setQuery($query)->execute(); + } + catch (Exception $e) + { + // Do nothing. + } + + $query = $db->getQuery(true) + ->delete($db->qn('#__update_sites_extensions')) + ->where($db->qn('update_site_id') . ' IN(' . implode(',', $deleteIDs) . ')'); + + try + { + $db->setQuery($query)->execute(); + } + catch (Exception $e) + { + // Do nothing. + } + } + + /** + * Gets the ID of an extension + * + * @param string $element Extension element, e.g. com_foo, mod_foo, lib_foo, pkg_foo or foo (CAUTION: plugin, file!) + * @param string $type Extension type: component, module, library, package, plugin or file + * @param null $folder Plugins: plugin folder. Modules: admin/site + * + * @return int Extension ID or 0 on failure + */ + private function findExtensionId($element, $type = 'component', $folder = null) + { + $db = $this->container->db; + $query = $db->getQuery(true) + ->select($db->qn('extension_id')) + ->from($db->qn('#__extensions')) + ->where($db->qn('element') . ' = ' . $db->q($element)) + ->where($db->qn('type') . ' = ' . $db->q($type)); + + // Plugin? We should look for a folder + if ($type == 'plugin') + { + $folder = empty($folder) ? 'system' : $folder; + + $query->where($db->qn('folder') . ' = ' . $db->q($folder)); + } + + // Module? Use the folder to determine if it's site or admin module. + if ($type == 'module') + { + $folder = empty($folder) ? 'site' : $folder; + + $query->where($db->qn('client_id') . ' = ' . $db->q(($folder == 'site') ? 0 : 1)); + } + + try + { + $id = $db->setQuery($query, 0, 1)->loadResult(); + } + catch (Exception $e) + { + $id = 0; + } + + return empty($id) ? 0 : (int) $id; + } + + /** + * Returns the update site IDs matching the criteria below. All criteria are optional but at least one must be + * defined for the method call to make any sense. + * + * @param int|null $includeEID The update site must belong to this extension ID + * @param int|null $excludeEID The update site must NOT belong to this extension ID + * @param array $locations The update site must match one of these locations + * + * @return array The IDs of the update sites + */ + private function getUpdateSitesFor($includeEID = null, $excludeEID = null, $locations = array()) + { + $db = $this->container->db; + $query = $db->getQuery(true) + ->select($db->qn('s.update_site_id')) + ->from($db->qn('#__update_sites', 's')); + + if (!empty($locations)) + { + $quotedLocations = array_map(array($db, 'q'), $locations); + $query->where($db->qn('location') . 'IN(' . implode(',', $quotedLocations) . ')'); + } + + if (!empty($includeEID) || !empty($excludeEID)) + { + $query->innerJoin($db->qn('#__update_sites_extensions', 'e') . 'ON(' . $db->qn('e.update_site_id') . + ' = ' . $db->qn('s.update_site_id') . ')' + ); + } + + if (!empty($includeEID)) + { + $query->where($db->qn('e.extension_id') . ' = ' . $db->q($includeEID)); + } + elseif (!empty($excludeEID)) + { + $query->where($db->qn('e.extension_id') . ' != ' . $db->q($excludeEID)); + } + + try + { + $ret = $db->setQuery($query)->loadColumn(); + } + catch (Exception $e) + { + $ret = null; + } + + return empty($ret) ? array() : $ret; + } + + private function createFakePackageExtension() + { + $db = $this->container->db; + + $query = $db->getQuery(true) + ->insert($db->qn('#__extensions')) + ->columns(array( + $db->qn('name'), $db->qn('type'), $db->qn('element'), $db->qn('folder'), $db->qn('client_id'), + $db->qn('enabled'), $db->qn('access'), $db->qn('protected'), $db->qn('manifest_cache'), + $db->qn('params'), $db->qn('custom_data'), $db->qn('system_data'), $db->qn('checked_out'), + $db->qn('checked_out_time'), $db->qn('ordering'), $db->qn('state') + )) + ->values(array( + $db->q('Akeeba Backup package') . ',' . + $db->q('package') . ',' . + $db->q('pkg_akeeba') . ',' . + $db->q('') . ',' . + $db->q(0) . ',' . + $db->q(1) . ',' . + $db->q(1) . ',' . + $db->q(0) . ',' . + $db->q('{"name":"Akeeba Backup package","type":"package","creationDate":"2016-04-21","author":"Nicholas K. Dionysopoulos","copyright":"Copyright (c)2006-2017 Akeeba Ltd \/ Nicholas K. Dionysopoulos","authorEmail":"","authorUrl":"","version":"' . $this->version . '","description":"Akeeba Backup installation package, for updating from version 4.x only","group":"","filename":"pkg_akeeba"}') . ',' . + $db->q('{}') . ',' . + $db->q('') . ',' . + $db->q('') . ',' . + $db->q(0) . ',' . + $db->q($db->getNullDate()) . ',' . + $db->q(0) . ',' . + $db->q(0) + )); + + try + { + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + // Your database if FUBAR. + return; + } + + $this->createFakePackageManifest(); + } + + private function createFakePackageManifest() + { + $path = JPATH_ADMINISTRATOR . '/manifests/packages/pkg_akeeba.xml'; + + if (file_exists($path)) + { + return; + } + + + $content = <<< XML + + + Akeeba Backup package + Nicholas K. Dionysopoulos + 2016-04-20 + akeeba + {$this->version} + https://www.akeebabackup.com + Akeeba Ltd + https://www.akeebabackup.com + Copyright (c)2006-2017 Akeeba Ltd / Nicholas K. Dionysopoulos + GNU GPL v3 or later + Akeeba Backup installation package v.revD5C5D46 + + + com_akeeba-pro.zip + file_akeeba-pro.zip + plg_quickicon_akeebabackup.zip + plg_system_akeebaupdatecheck.zip + plg_system_backuponupdate.zip + + + script.akeeba.php + +XML; + + if (!@file_put_contents($content, $path)) + { + JLoader::import('joomla.filesystem.file'); + JFile::write($path, $content); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Model/UsageStatistics.php b/deployed/akeeba/administrator/components/com_akeeba/Model/UsageStatistics.php new file mode 100644 index 00000000..2a7d4477 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Model/UsageStatistics.php @@ -0,0 +1,297 @@ +container; + $dbInstaller = new Installer($container->db, $container->backEndPath . '/sql/common'); + $dbInstaller->updateSchema(); + } + + return $this; + } + + /** + * Get an existing unique site ID or create a new one + * + * @return string + */ + public function getSiteId() + { + // Can I load a site ID from the database? + $siteId = $this->getCommonVariable('stats_siteid', null); + + // Can I load the site Url from the database? + $siteUrl = $this->getCommonVariable('stats_siteurl', null); + + // No id or the saved URL is not the same as the current one (ie site restored to a new url)? + // Create a new, random site ID and save it to the database + if (empty($siteId) || (md5(JUri::base()) != $siteUrl)) + { + $siteUrl = md5(JUri::base()); + $this->setCommonVariable('stats_siteurl', $siteUrl); + + $randomData = JCrypt::genRandomBytes(120); + $siteId = sha1($randomData); + + $this->setCommonVariable('stats_siteid', $siteId); + } + + return $siteId; + } + + /** + * Send site information to the remove collection service + * + * @param bool $useIframe Should I use an IFRAME? + * + * @return bool + */ + public function collectStatistics($useIframe) + { + // Is data collection turned off? + if (!$this->container->params->get('stats_enabled', 1)) + { + return false; + } + + // Do not collect statistics on localhost + // -- Actually, using Akeeba Backup on localhost is a perfectly legitimate use case +/* if ( + (strpos(JUri::root(), 'localhost') !== false) || + (strpos(JUri::root(), '127.0.0.1') !== false) + ) + { + return false; + }*/ + + // Make sure the common tables are installed + $this->checkAndFixCommonTables(); + + // Make sure there is a site ID set + $siteId = $this->getSiteId(); + $container = $this->container; + + // UsageStats file is missing, no need to continue + if (!file_exists($container->backEndPath . '/Master/Stats/usagestats.php')) + { + return false; + } + + if (!class_exists('AkeebaUsagestats', false)) + { + @include_once $container->backEndPath . '/Master/Stats/usagestats.php'; + } + + // UsageStats file is missing, no need to continue + if (!class_exists('AkeebaUsagestats', false)) + { + return false; + } + + $lastrun = $this->getCommonVariable('stats_lastrun', 0); + + // It's not time to collect the stats + if (time() < ($lastrun + 3600 * 24)) + { + return false; + } + + if (!defined('AKEEBA_VERSION')) + { + @include_once $container->backEndPath . '/version.php'; + } + + if (!defined('AKEEBA_VERSION')) + { + define('AKEEBA_VERSION', 'dev'); + define('AKEEBA_DATE', date('Y-m-d')); + } + + $db = $container->db; + + try + { + $stats = new AkeebaUsagestats(); + } + catch (\Exception $e) + { + return false; + } + + $stats->setSiteId($siteId); + + // I can't use list since dev release don't have any dots + $at_parts = explode('.', AKEEBA_VERSION); + $at_major = $at_parts[0]; + $at_minor = isset($at_parts[1]) ? $at_parts[1] : ''; + $at_revision = isset($at_parts[2]) ? $at_parts[2] : ''; + + list($php_major, $php_minor, $php_revision) = explode('.', phpversion()); + $php_qualifier = strpos($php_revision, '~') !== false ? substr($php_revision, strpos($php_revision, '~')) : ''; + + list($cms_major, $cms_minor, $cms_revision) = explode('.', JVERSION); + list($db_major, $db_minor, $db_revision) = explode('.', $db->getVersion()); + $db_qualifier = strpos($db_revision, '~') !== false ? substr($db_revision, strpos($db_revision, '~')) : ''; + + $db_driver = get_class($db); + + if (stripos($db_driver, 'mysql') !== false) + { + $stats->setValue('dt', 1); + } + elseif (stripos($db_driver, 'sqlsrv') !== false || stripos($db_driver, 'sqlazure')) + { + $stats->setValue('dt', 2); + } + elseif (stripos($db_driver, 'postgresql') !== false) + { + $stats->setValue('dt', 3); + } + else + { + $stats->setValue('dt', 0); + } + + $stats->setValue('sw', AKEEBA_PRO ? 2 : 1); // software + $stats->setValue('pro', AKEEBA_PRO); // pro + $stats->setValue('sm', $at_major); // software_major + $stats->setValue('sn', $at_minor); // software_minor + $stats->setValue('sr', $at_revision); // software_revision + $stats->setValue('pm', $php_major); // php_major + $stats->setValue('pn', $php_minor); // php_minor + $stats->setValue('pr', $php_revision); // php_revision + $stats->setValue('pq', $php_qualifier); // php_qualifiers + $stats->setValue('dm', $db_major); // db_major + $stats->setValue('dn', $db_minor); // db_minor + $stats->setValue('dr', $db_revision); // db_revision + $stats->setValue('dq', $db_qualifier); // db_qualifiers + $stats->setValue('ct', 1); // cms_type + $stats->setValue('cm', $cms_major); // cms_major + $stats->setValue('cn', $cms_minor); // cms_minor + $stats->setValue('cr', $cms_revision); // cms_revision + + // Store the last execution time. We must store it even if we fail since we don't want a failed stats collection + // to cause the site to stop responding. + $this->setCommonVariable('stats_lastrun', time()); + + $return = $stats->sendInfo($useIframe); + + return $return; + } + + /** + * Load a variable from the common variables table. If it doesn't exist it returns $default + * + * @param string $key The key to load + * @param mixed $default The default value if the key doesn't exist + * + * @return mixed The contents of the key or null if it's not present + */ + public function getCommonVariable($key, $default = null) + { + $db = $this->container->db; + $query = $db->getQuery(true) + ->select($db->qn('value')) + ->from($db->qn('#__akeeba_common')) + ->where($db->qn('key') . ' = ' . $db->q($key)); + + try + { + $db->setQuery($query); + $result = $db->loadResult(); + } + catch (\Exception $e) + { + $result = $default; + } + + return $result; + } + + /** + * Set a variable to the common variables table. + * + * @param string $key The key to save + * @param mixed $value The value to save + * + * @return void + */ + public function setCommonVariable($key, $value) + { + $db = $this->container->db; + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->qn('#__akeeba_common')) + ->where($db->qn('key') . ' = ' . $db->q($key)); + + try + { + $db->setQuery($query); + $count = $db->loadResult(); + } + catch (\Exception $e) + { + return; + } + + try + { + if (!$count) + { + $insertObject = (object)array( + 'key' => $key, + 'value' => $value, + ); + $db->insertObject('#__akeeba_common', $insertObject); + } + else + { + $keyName = version_compare(JVERSION, '1.7.0', 'lt') ? $db->qn('key') : 'key'; + + $insertObject = (object)array( + $keyName => $key, + 'value' => $value, + ); + + $db->updateObject('#__akeeba_common', $insertObject, $keyName); + } + } + catch (\Exception $e) + { + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/Toolbar/Toolbar.php b/deployed/akeeba/administrator/components/com_akeeba/Toolbar/Toolbar.php new file mode 100644 index 00000000..cf12732b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/Toolbar/Toolbar.php @@ -0,0 +1,299 @@ +'.JText::_('COM_AKEEBA_BACKUP').'','akeeba'); + + if (!$this->container->input->getBool('akeeba_hide_toolbar', false)) + { + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/backup-now.html'); + } + } + + public function onConfigurations() + { + $bar = JToolbar::getInstance('toolbar'); + + JToolbarHelper::title(JText::_('COM_AKEEBA').':: '.JText::_('COM_AKEEBA_CONFIG').'','akeeba'); + JToolbarHelper::preferences('com_akeeba', '500', '660'); + JToolbarHelper::spacer(); + JToolbarHelper::apply(); + JToolbarHelper::save(); + JToolbarHelper::spacer(); + JToolbarHelper::custom('savenew', 'save-new.png', 'save-new_f2.png', 'JTOOLBAR_SAVE_AND_NEW', false); + JToolbarHelper::cancel(); + JToolbarHelper::spacer(); + + // Configuration wizard button. We apply styling to it. + $bar->appendButton('Link', 'lightning', '' . JText::_('COM_AKEEBA_CONFWIZ') . '', 'index.php?option=com_akeeba&view=ConfigurationWizard'); + + JToolbarHelper::spacer(); + + $bar->appendButton('Link', 'calendar', JText::_('COM_AKEEBA_SCHEDULE'), 'index.php?option=com_akeeba&view=Schedule'); + + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/configuration.html'); + + $js = <<< JS +;; + +jQuery(document).ready(function(){ + jQuery('#toolbar-lightning>button').addClass('btn-primary'); +}); + +JS; + JFactory::getDocument()->addScriptDeclaration($js); + } + + public function onConfigurationWizardsMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').':: '.JText::_('COM_AKEEBA_CONFWIZ').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/configuration-wizard.html'); + } + + public function onControlPanelsMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').' :: '.JText::_('COM_AKEEBA_CONTROLPANEL').'','akeeba'); + JToolbarHelper::preferences('com_akeeba', '500', '660'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/using-akeeba-backup-component.html#control-panel'); + } + + public function onDatabaseFiltersMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_DBFILTER').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/database-tables-exclusion.html'); + } + + public function onDiscovers() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_DISCOVER').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/ch03s02s05s03.html'); + } + + public function onFileFiltersMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_FILEFILTERS').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/exclude-data-from-backup.html#files-and-directories-exclusion'); + } + + public function onIncludeFoldersMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_INCLUDEFOLDER').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/off-site-directories-inclusion.html'); + } + + public function onLogs() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_LOG').'','akeeba'); + + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/view-log.html'); + } + + public function onManagesDefault() + { + JToolbarHelper::title(JText::_('COM_AKEEBA') . ': ' . JText::_('COM_AKEEBA_BUADMIN') . '', 'akeeba'); + + if (AKEEBA_PRO) + { + $bar = JToolbar::getInstance('toolbar'); + $bar->appendButton('Link', 'restore', JText::_('COM_AKEEBA_DISCOVER'), 'index.php?option=com_akeeba&view=Discover'); + } + + $user = $this->container->platform->getUser(); + $permissions = [ + 'configure' => $user->authorise('akeeba.configure', 'com_akeeba'), + 'backup' => $user->authorise('akeeba.backup', 'com_akeeba'), + ]; + + if ($permissions['configure']) + { + JToolbarHelper::publish('restore', JText::_('COM_AKEEBA_BUADMIN_LABEL_RESTORE')); + } + + if ($permissions['backup']) + { + JToolbarHelper::editList('showcomment', JText::_('COM_AKEEBA_BUADMIN_LOG_EDITCOMMENT')); + } + + if ($permissions['configure'] || $permissions['backup']) + { + JToolbarHelper::spacer(); + } + + if ($permissions['backup']) + { + JToolbarHelper::deleteList(); + JToolbarHelper::custom('deletefiles', 'delete.png', 'delete_f2.png', JText::_('COM_AKEEBA_BUADMIN_LABEL_DELETEFILES'), true); + JToolbarHelper::spacer(); + } + + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/adminsiter-backup-files.html'); + } + + public function onManagesShowcomment() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_BUADMIN').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::save(); + JToolbarHelper::cancel(); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/adminsiter-backup-files.html'); + } + + public function onMultipleDatabasesMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_MULTIDB').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/include-data-to-archive.html#multiple-db-definitions'); + } + + public function onProfilesAdd() + { + parent::onAdd(); + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_PROFILES_PAGETITLE_NEW').'','akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/using-basic-operations.html#profiles-management'); + } + + public function onProfilesBrowse() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_PROFILES').'','akeeba'); + + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::addNew(); + JToolbarHelper::custom('copy', 'copy.png', 'copy_f2.png', 'COM_AKEEBA_LBL_BATCH_COPY', false); + JToolbarHelper::spacer(); + JToolbarHelper::deleteList(); + JToolbarHelper::spacer(); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/using-basic-operations.html#profiles-management'); + } + + public function onProfilesEdit() + { + parent::onEdit(); + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_PROFILES_PAGETITLE_EDIT').'','akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/using-basic-operations.html#profiles-management'); + } + + public function onRegExDatabaseFiltersMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_REGEXDBFILTERS').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/regex-database-tables-exclusion.html'); + } + + public function onRegExFileFiltersMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_REGEXFSFILTERS').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/regex-files-directories-exclusion.html'); + } + + public function onRemoteFilesDownloadToServer() + { + JToolbarHelper::title(JText::_('COM_AKEEBA_REMOTEFILES'),'akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/ch03s02s05s02.html'); + } + + public function onRemoteFilesListActions() + { + JToolbarHelper::title(JText::_('COM_AKEEBA_REMOTEFILES'),'akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/ch03s02s05s02.html'); + } + + public function onRestores() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_RESTORE').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/adminsiter-backup-files.html#integrated-restoration'); + } + + public function onS3ImportsMain() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_S3IMPORT').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/ch03s02s05s03.html'); + } + + public function onS3ImportsDltoserver() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_S3IMPORT').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/ch03s02s05s03.html'); + } + + public function onSchedules() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').':: '.JText::_('COM_AKEEBA_SCHEDULE').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + JToolbarHelper::spacer(); + JToolbarHelper::help(null, false, 'https://www.akeebabackup.com/documentation/akeeba-backup-documentation/automating-your-backup.html'); + } + + public function onStatisticsMain() + { + $this->onManagesDefault(); + } + + public function onTransfers() + { + JToolbarHelper::title(JText::_('COM_AKEEBA').': '.JText::_('COM_AKEEBA_TRANSFER').'','akeeba'); + JToolbarHelper::back('COM_AKEEBA_CONTROLPANEL', 'index.php?option=com_akeeba'); + + $bar = JToolbar::getInstance('toolbar'); + $bar->appendButton('Link', 'loop', 'COM_AKEEBA_TRANSFER_BTN_RESET', 'index.php?option=com_akeeba&view=Transfer&task=reset'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Backup/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/Html.php new file mode 100644 index 00000000..ce001556 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/Html.php @@ -0,0 +1,325 @@ +addJavascriptFile('media://com_akeeba/js/Backup.min.js'); + + // Preload Joomla! behaviours + JLoader::import('joomla.utilities.date'); + + // Load the models + /** @var Backup $model */ + $model = $this->getModel(); + + /** @var ControlPanel $cpanelmodel */ + $cpanelmodel = $this->container->factory->model('ControlPanel')->tmpInstance(); + + // Load the Status Helper + $helper = Status::getInstance(); + + // Determine default description + $default_description = $this->getDefaultDescription(); + + // Load data from the model state + $backup_description = $model->getState('description', $default_description); + $comment = $model->getState('comment', ''); + $returnurl = $model->getState('returnurl'); + + // Only allow non-empty, internal URLs for the redirection + if (empty($returnurl)) + { + $returnurl = ''; + } + + if (!JUri::isInternal($returnurl)) + { + $returnurl = ''; + } + + // Get the maximum execution time and bias + $engineConfiguration = Factory::getConfiguration(); + $maxexec = $engineConfiguration->get('akeeba.tuning.max_exec_time', 14) * 1000; + $bias = $engineConfiguration->get('akeeba.tuning.run_time_bias', 75); + + // Check if the output directory is writable + $warnings = Factory::getConfigurationChecks()->getDetailedStatus(); + $unwritableOutput = array_key_exists('001', $warnings); + + // Pass on data + $this->getProfileList(); + $this->getProfileIdAndName(); + + $this->hasErrors = !$helper->status; + $this->hasWarnings = $helper->hasQuirks(); + $this->warningsCell = $helper->getQuirksCell(!$helper->status); + $this->description = $backup_description; + $this->defaultDescription = $default_description; + $this->comment = $comment; + $this->domains = json_encode($this->getDomains()); + $this->maxExecutionTime = $maxexec; + $this->runtimeBias = $bias; + $this->useIFRAME = $engineConfiguration->get('akeeba.basic.useiframe', 0) == 1; + $this->returnURL = $returnurl; + $this->unwriteableOutput = $unwritableOutput; + $this->autoStart = $model->getState('autostart'); + $this->desktopNotifications = $this->container->params->get('desktop_notifications', '0') ? 1 : 0; + $this->autoResume = $engineConfiguration->get('akeeba.advanced.autoresume', 1); + $this->autoResumeTimeout = $engineConfiguration->get('akeeba.advanced.autoresume_timeout', 10); + $this->autoResumeRetries = $engineConfiguration->get('akeeba.advanced.autoresume_maxretries', 3); + $this->promptForConfigurationWizard = $engineConfiguration->get('akeeba.flag.confwiz', 0) == 0; + + if ($engineConfiguration->get('akeeba.advanced.archiver_engine', 'jpa') == 'jps') + { + $this->showJPSPassword = 1; + $this->jpsPassword = $engineConfiguration->get('engine.archiver.jps.key', ''); + } + + // Always show ANGIE password: we add that feature to the Core version as well + $this->showANGIEPassword = 1; + $this->ANGIEPassword = $engineConfiguration->get('engine.installer.angie.key', ''); + + // Push language strings to Javascript + JText::script('COM_AKEEBA_BACKUP_TEXT_LASTRESPONSE'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPSTARTED'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPFINISHED'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPHALT'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPRESUME'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPHALT_DESC'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPFAILED'); + JText::script('COM_AKEEBA_BACKUP_TEXT_BACKUPWARNING'); + JText::script('COM_AKEEBA_BACKUP_TEXT_AVGWARNING'); + } + + /** + * Get the default description for this backup attempt + * + * @return string + */ + private function getDefaultDescription() + { + $tzDefault = $this->container->platform->getConfig()->get('offset'); + $user = $this->container->platform->getUser(); + $tz = $user->getParam('timezone', $tzDefault); + $dateNow = new Date('now', $tz); + $default_description = JText::_('COM_AKEEBA_BACKUP_DEFAULT_DESCRIPTION') . ' ' . + $dateNow->format(JText::_('DATE_FORMAT_LC2'), true); + + return $default_description; + } + + /** + * Get a list of backup domain keys and titles + * + * @return array + */ + private function getDomains() + { + $engineConfiguration = Factory::getConfiguration(); + $script = $engineConfiguration->get('akeeba.basic.backup_type', 'full'); + $scripting = Factory::getEngineParamsProvider()->loadScripting(); + $domains = array(); + + if (empty($scripting)) + { + return $domains; + } + + foreach ($scripting['scripts'][ $script ]['chain'] as $domain) + { + $description = JText::_($scripting['domains'][ $domain ]['text']); + $domain_key = $scripting['domains'][ $domain ]['domain']; + $domains[] = array($domain_key, $description); + } + + return $domains; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/default.php new file mode 100644 index 00000000..36b3b221 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/default.php @@ -0,0 +1,321 @@ + + +promptForConfigurationWizard): ?> + loadAnyTemplate('admin:com_akeeba/Configuration/confwiz_modal'); ?> + + + +loadAnyTemplate('admin:com_akeeba/Backup/script'); ?> + + +loadAnyTemplate('admin:com_akeeba/ControlPanel/warning_phpversion'); ?> + + +
    +
    +

    + +

    +
    + + hasWarnings && !$this->unwriteableOutput): ?> +
    +

    + +

    +

    + +

    + warningsCell; ?> + +
    + + + unwriteableOutput): ?> +
    +

    + autoStart ? 'AUTOBACKUP' : 'NORMALBACKUP')); ?> +

    +

    + +

    +
    + + +
    + +
    + + profileList, 'profileid', 'onchange="akeeba.Backup.flipProfile();" class="advancedSelect"', 'value', 'text', $this->profileid); ?> +
    + +
    + +
    + +
    + + + + + + +
    +
    + +
    +
    + + + +
    + + showJPSPassword): ?> +
    + + + +
    + + + showANGIEPassword): ?> +
    + + + +
    + + +
    + + + +
    + +
    +
    + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/script.php b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/script.php new file mode 100644 index 00000000..180426e4 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Backup/tmpl/script.php @@ -0,0 +1,110 @@ +defaultDescription); +$escapedDescription = addslashes(empty($this->description) ? $this->defaultDescription : $this->description); +$escapedComment = addslashes($this->comment); +$escapedAngiePassword = addslashes($this->ANGIEPassword); +$escapedJpsKey = $this->showJPSPassword ? addslashes($this->jpsPassword) : ''; +$autoResume = (int)$this->autoResume; +$autoResumeTimeout = (int)$this->autoResumeTimeout; +$autoResumeRetries = (int)$this->autoResumeRetries; +$maxExecTime = (int)$this->maxExecutionTime; +$runtimeBias = (int)$this->runtimeBias; +$escapedJuriBase = addslashes(JUri::base()); +$escapedDomains = addcslashes($this->domains, "'\\"); +$useIframe = $this->useIFRAME ? 'true' : 'false'; +$innerJS = <<< JS + // Initialization + akeeba.Backup.defaultDescription = "$escapedDefaultDescription"; + akeeba.Backup.currentDescription = "$escapedDescription"; + akeeba.Backup.currentComment = "$escapedComment"; + akeeba.Backup.config_angiekey = "$escapedAngiePassword"; + akeeba.Backup.jpsKey = "$escapedJpsKey"; + + // Auto-resume setup + akeeba.Backup.resume.enabled = $autoResume; + akeeba.Backup.resume.timeout = $autoResumeTimeout; + akeeba.Backup.resume.maxRetries = $autoResumeRetries; + + // The return URL + akeeba.Backup.returnUrl = '{$this->returnURL}'; + + // Used as parameters to start_timeout_bar() + akeeba.Backup.maxExecutionTime = $maxExecTime; + akeeba.Backup.runtimeBias = $runtimeBias; + + // Create a function for saving the editor's contents + akeeba.Backup.commentEditorSave = function() { + }; + + akeeba.System.notification.iconURL = '{$escapedJuriBase}../media/com_akeeba/icons/logo-48.png'; + + //Parse the domain keys + akeeba.Backup.domains = JSON.parse('$escapedDomains'); + + // Setup AJAX proxy URL + akeeba.System.params.AjaxURL = 'index.php?option=com_akeeba&view=Backup&task=ajax'; + + // Setup base View Log URL + akeeba.Backup.URLs.LogURL = '{$escapedJuriBase}index.php?option=com_akeeba&view=Log'; + akeeba.Backup.URLs.AliceURL = '{$escapedJuriBase}index.php?option=com_akeeba&view=Alice'; + + // Setup the IFRAME mode + akeeba.System.params.useIFrame = $useIframe; + +JS; + +if ($this->desktopNotifications) +{ + $innerJS .= <<< JS + akeeba.System.notification.askPermission(); + +JS; +} + +if (!$this->unwriteableOutput && $this->autoStart) +{ + $innerJS .= <<< JS + akeeba.Backup.start(); + +JS; +} +else +{ + $innerJS .= <<< JS + + // Bind start button's click event + akeeba.System.addEventListener(document.getElementById('backup-start'), 'click', function(e){ + akeeba.Backup.start(); + }); + + akeeba.System.addEventListener(document.getElementById('backup-default'), 'click', akeeba.Backup.restoreDefaultOptions); + + // Work around Safari which ignores autocomplete=off (FOR CRYING OUT LOUD!) + setTimeout('akeeba.Backup.restoreCurrentOptions();', 500); + +JS; +} + +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.System.documentReady(function(){ + $innerJS +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Browser/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Browser/Html.php new file mode 100644 index 00000000..08be575a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Browser/Html.php @@ -0,0 +1,97 @@ +getModel(); + + // Pass the data from the model to the view template + $this->folder = $model->getState('folder'); + $this->folder_raw = $model->getState('folder_raw'); + $this->parent = $model->getState('parent'); + $this->exists = $model->getState('exists'); + $this->inRoot = $model->getState('inRoot'); + $this->openbasedirRestricted = $model->getState('openbasedirRestricted'); + $this->writable = $model->getState('writable'); + $this->subfolders = $model->getState('subfolders'); + $this->breadcrumbs = $model->getState('breadcrumbs'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Browser/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Browser/tmpl/default.php new file mode 100644 index 00000000..00cfd0f2 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Browser/tmpl/default.php @@ -0,0 +1,157 @@ +addJavascriptInline(<< + +folder)): ?> +
    + + + + + + + +
    + + +folder))): + $writeableText = \JText::_($this->writable ? 'COM_AKEEBA_CPANEL_LBL_WRITABLE' : 'COM_AKEEBA_CPANEL_LBL_UNWRITABLE'); + $writeableIcon = $this->writable ? 'akion-checkmark-circled' : 'akion-ios-close'; + $writeableClass = $this->writable ? 'akeeba-label--green' : 'akeeba-label--red'; + ?> +
    +
    +
    + + + + + + + + + +
    + + + + + +
    +
    +
    +
    + + breadcrumbs)): ?> +
    +
    + +
    +
    + + +
    +
    + subfolders)): ?> + + + + + subfolders as $subfolder): ?> + + + + +
    + + + + +
    + escape($subfolder); ?> +
    + + exists): ?> +
    + +
    + inRoot): ?> +
    + +
    + openbasedirRestricted): ?> +
    + +
    + + + + + +
    + + + + +
    + + subfolders) */ ?> +
    +
    + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ErrorModal.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ErrorModal.php new file mode 100644 index 00000000..ab9e5475 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ErrorModal.php @@ -0,0 +1,24 @@ + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPBrowser.php new file mode 100644 index 00000000..add57091 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPBrowser.php @@ -0,0 +1,46 @@ + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPConnectionTest.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPConnectionTest.php new file mode 100644 index 00000000..1c572794 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPConnectionTest.php @@ -0,0 +1,19 @@ + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FolderBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FolderBrowser.php new file mode 100644 index 00000000..04666d00 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/FolderBrowser.php @@ -0,0 +1,22 @@ + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ProfileName.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ProfileName.php new file mode 100644 index 00000000..f834b89e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/ProfileName.php @@ -0,0 +1,15 @@ + +
    + : + #escape($this->profileid); ?> escape($this->profilename); ?> + +
    diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/SFTPBrowser.php b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/SFTPBrowser.php new file mode 100644 index 00000000..e91f73c3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/CommonTemplates/tmpl/SFTPBrowser.php @@ -0,0 +1,48 @@ + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/Html.php new file mode 100644 index 00000000..7e5623cb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/Html.php @@ -0,0 +1,108 @@ +addJavascriptFile('media://com_akeeba/js/Configuration.min.js'); + + // Get a JSON representation of GUI data + $json = Factory::getEngineParamsProvider()->getJsonGuiDefinition(); + $this->json = $json; + + $this->getProfileIdAndName(); + + // Are the settings secured? + $this->securesettings = $this->getSecureSettingsOption(); + + // Should I show the Configuration Wizard popup prompt? + $this->promptForConfigurationWizard = Factory::getConfiguration()->get('akeeba.flag.confwiz', 0) != 1; + + // Push translations + JText::script('COM_AKEEBA_CONFIG_UI_BROWSE'); + JText::script('COM_AKEEBA_CONFIG_UI_CONFIG'); + JText::script('COM_AKEEBA_CONFIG_UI_REFRESH'); + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_CONFIG_UI_FTPBROWSER_TITLE'); + JText::script('COM_AKEEBA_CONFIG_DIRECTFTP_TEST_OK'); + JText::script('COM_AKEEBA_CONFIG_DIRECTFTP_TEST_FAIL'); + JText::script('COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_OK'); + JText::script('COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_FAIL'); + } + + /** + * Returns the support status of settings encryption. The possible values are: + * -1 Disabled by the user + * 0 Enabled by inactive (not supported by the server) + * 1 Enabled and active + * + * @return int + */ + private function getSecureSettingsOption() + { + // Encryption is disabled by the user + if (Platform::getInstance()->get_platform_configuration_option('useencryption', -1) == 0) + { + return -1; + } + + // Encryption is not supported by this server + if (!Factory::getSecureSettings()->supportsEncryption()) + { + return 0; + } + + $filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php'; + + // Encryption enabled, supported and a key file is present: encryption enabled + if (is_file($filename)) + { + return 1; + } + + // Encryption enabled, supported but and a key file is NOT present: encryption not available + return 0; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/confwiz_modal.php b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/confwiz_modal.php new file mode 100644 index 00000000..f5c47c18 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/confwiz_modal.php @@ -0,0 +1,69 @@ +getContainer()->template->addJSInline($js); +?> + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/default.php new file mode 100644 index 00000000..30c898a9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Configuration/tmpl/default.php @@ -0,0 +1,130 @@ + addslashes('index.php?option=com_akeeba&view=Browser&processfolder=1&tmpl=component&folder='), + 'ftpBrowser' => addslashes('index.php?option=com_akeeba&view=FTPBrowser'), + 'sftpBrowser' => addslashes('index.php?option=com_akeeba&view=SFTPBrowser'), + 'testFtp' => addslashes('index.php?option=com_akeeba&view=Configuration&task=testftp'), + 'testSftp' => addslashes('index.php?option=com_akeeba&view=Configuration&task=testsftp'), + 'dpeauthopen' => addslashes('index.php?option=com_akeeba&view=Configuration&task=dpeoauthopen&format=raw'), + 'dpecustomapi' => addslashes('index.php?option=com_akeeba&view=Configuration&task=dpecustomapi&format=raw'), +); +$this->json = addcslashes($this->json, "'\\"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.System.documentReady(function(){ + // Push some custom URLs + akeeba.Configuration.URLs['browser'] = '{$urls['browser']}'; + akeeba.Configuration.URLs['ftpBrowser'] = '{$urls['ftpBrowser']}'; + akeeba.Configuration.URLs['sftpBrowser'] = '{$urls['sftpBrowser']}'; + akeeba.Configuration.URLs['testFtp'] = '{$urls['testFtp']}'; + akeeba.Configuration.URLs['testSftp'] = '{$urls['testSftp']}'; + akeeba.Configuration.URLs['dpeauthopen'] = '{$urls['dpeauthopen']}'; + akeeba.Configuration.URLs['dpecustomapi'] = '{$urls['dpecustomapi']}'; + akeeba.System.params.AjaxURL = akeeba.Configuration.URLs['dpecustomapi']; + + // Load the configuration UI data in a timeout to prevent Safari from auto-filling the password fields + var data = JSON.parse('{$this->json}'); + + setTimeout(function () + { + // Work around browsers which blatantly ignore autocomplete=off + setTimeout('akeeba.Configuration.restoreDefaultPasswords();', 1000); + + // Render the configuration UI in the timeout to prevent Safari from auto-filling the password fields + akeeba.Configuration.parseConfigData(data); + + // Enable popovers. Must obviously run after we have the UI set up. + akeeba.Configuration.enablePopoverFor(document.querySelectorAll('[rel="popover"]')); + }, 10); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> + +promptForConfigurationWizard): ?> + loadAnyTemplate('admin:com_akeeba/Configuration/confwiz_modal'); ?> + + + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FTPBrowser'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/SFTPBrowser'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FTPConnectionTest'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FolderBrowser'); ?> + +securesettings == 1): ?> +
    + +
    +securesettings == 0): ?> +
    + +
    + + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
    + +
    + +
    + +
    +
    +
    + +
    +
    + +
    + + +
    + +
    + +
    + quickIcon ? 'checked="checked"' : ''; ?>/> +
    +
    +
    + + +
    +
    + +
    + + + + +
    +
    diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/Html.php new file mode 100644 index 00000000..462bb145 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/Html.php @@ -0,0 +1,57 @@ +addJavascriptFile('media://com_akeeba/js/Backup.min.js'); + $this->addJavascriptFile('media://com_akeeba/js/ConfigurationWizard.min.js'); + $this->addJavascriptInline($js); + + // Set the layour + $this->setLayout('wizard'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/index.html b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/wizard.php b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/wizard.php new file mode 100644 index 00000000..4e81dac3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/wizard.php @@ -0,0 +1,91 @@ + + +
    + +
    +
    + +
    + +
    +

    + +

    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
     
    +
    +
    + +
    + +
    + + + + +
    diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/Html.php new file mode 100644 index 00000000..6eea6122 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/Html.php @@ -0,0 +1,316 @@ +getModel(); + + $statusHelper = Status::getInstance(); + $this->statsIframe = ''; + + try + { + /** @var UsageStatistics $usageStatsModel */ + $usageStatsModel = $this->container->factory->model('UsageStatistics')->tmpInstance(); + + if ( + is_object($usageStatsModel) + && class_exists('Akeeba\\Backup\\Admin\\Model\\UsageStatistics') + && ($usageStatsModel instanceof UsageStatistics) + && method_exists($usageStatsModel, 'collectStatistics') + ) + { + $this->statsIframe = $usageStatsModel->collectStatistics(true); + } + } + catch (\Exception $e) + { + // Don't give a crap if usage stats ain't loaded + } + + $this->getProfileList(); + $this->getProfileIdAndName(); + + $this->quickIconProfiles = $model->getQuickIconProfiles(); + $this->statusCell = $statusHelper->getStatusCell(); + $this->detailsCell = $statusHelper->getQuirksCell(); + $this->latestBackupCell = $statusHelper->getLatestBackupDetails(); + $this->areMediaPermissionsFixed = $model->fixMediaPermissions(); + $this->checkMbstring = $model->checkMbstring(); + $this->needsDownloadID = $model->needsDownloadID() ? 1 : 0; + $this->coreWarningForDownloadID = $model->mustWarnAboutDownloadIDInCore(); + $this->extension_id = $model->getState('extension_id', 0, 'int'); + $this->frontEndSecretWordIssue = $model->getFrontendSecretWordError(); + $this->newSecretWord = $this->container->platform->getSessionVar('newSecretWord', null, 'akeeba.cpanel'); + $this->desktopNotifications = $this->container->params->get('desktop_notifications', '0') ? 1 : 0; + $this->formattedChangelog = $this->formatChangelog(); + $this->promptForConfigurationWizard = Factory::getConfiguration()->get('akeeba.flag.confwiz', 0) == 0; + $this->countWarnings = count(Factory::getConfigurationChecks()->getDetailedStatus()); + $this->stuckUpdates = ($this->container->params->get('updatedb', 0) == 1); + $user = $this->container->platform->getUser(); + $this->permissions = array( + 'configure' => $user->authorise('akeeba.configure', 'com_akeeba'), + 'backup' => $user->authorise('akeeba.backup', 'com_akeeba'), + 'download' => $user->authorise('akeeba.download', 'com_akeeba'), + ); + + + // Load the version constants + Platform::getInstance()->load_version_defines(); + + // Add the Javascript to the document + $this->addJavascriptFile('media://com_akeeba/js/ControlPanel.min.js'); + $this->inlineJavascript(); + } + + /** + * Adds inline Javascript to the document + */ + protected function inlineJavascript() + { + $script = <<desktopNotifications}; +akeeba.ControlPanel.needsDownloadID = {$this->needsDownloadID}; +JS; + $this->addJavascriptInline($script); + } + + protected function formatChangelog($onlyLast = false) + { + $ret = ''; + $file = $this->container->backEndPath . '/CHANGELOG.php'; + $lines = @file($file); + + if (empty($lines)) + { + return $ret; + } + + array_shift($lines); + + foreach ($lines as $line) + { + $line = trim($line); + + if (empty($line)) + { + continue; + } + + $type = substr($line, 0, 1); + + switch ($type) + { + case '=': + continue; + break; + + case '+': + $ret .= "\t" . '
  • ' . htmlentities(trim(substr($line, 2))) . "
  • \n"; + break; + + case '-': + $ret .= "\t" . '
  • ' . htmlentities(trim(substr($line, 2))) . "
  • \n"; + break; + + case '~': + $ret .= "\t" . '
  • ' . htmlentities(trim(substr($line, 2))) . "
  • \n"; + break; + + case '!': + $ret .= "\t" . '
  • ' . htmlentities(trim(substr($line, 2))) . "
  • \n"; + break; + + case '#': + $ret .= "\t" . '
  • ' . htmlentities(trim(substr($line, 2))) . "
  • \n"; + break; + + default: + if (!empty($ret)) + { + $ret .= ""; + if ($onlyLast) + { + return $ret; + } + } + + if (!$onlyLast) + { + $ret .= "

    $line

    \n"; + } + $ret .= "
      \n"; + + break; + } + } + + return $ret; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/default.php new file mode 100644 index 00000000..bf164373 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/default.php @@ -0,0 +1,64 @@ + + +loadAnyTemplate('admin:com_akeeba/ControlPanel/warnings'); ?> + + +
      + +
      +
      + + loadAnyTemplate('admin:com_akeeba/ControlPanel/profile'); ?> + + + quickIconProfiles)) && $this->permissions['backup']): ?> + loadAnyTemplate('admin:com_akeeba/ControlPanel/oneclick'); ?> + + + + loadAnyTemplate('admin:com_akeeba/ControlPanel/icons_basic'); ?> + + + loadAnyTemplate('admin:com_akeeba/ControlPanel/icons_troubleshooting'); ?> + + + loadAnyTemplate('admin:com_akeeba/ControlPanel/icons_advanced'); ?> + + + permissions['configure']): ?> + loadAnyTemplate('admin:com_akeeba/ControlPanel/icons_includeexclude'); ?> + + + +
      +
      + + loadAnyTemplate('admin:com_akeeba/ControlPanel/sidebar_status'); ?> + + + loadAnyTemplate('admin:com_akeeba/ControlPanel/sidebar_backup'); ?> +
      + +
      + + +loadAnyTemplate('admin:com_akeeba/ControlPanel/footer'); ?> + +statsIframe) +{ + echo $this->statsIframe; +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/footer.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/footer.php new file mode 100644 index 00000000..8c1ec0e1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/footer.php @@ -0,0 +1,29 @@ + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_advanced.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_advanced.php new file mode 100644 index 00000000..9bf00c92 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_advanced.php @@ -0,0 +1,46 @@ + +
      +
      +

      +
      + +
      + permissions['configure']): ?> + + + + + + + + permissions['configure']): ?> + + + + + + + permissions['configure']): ?> + + + + + + +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_basic.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_basic.php new file mode 100644 index 00000000..843c3986 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_basic.php @@ -0,0 +1,58 @@ + +
      +
      +

      +
      + +
      + permissions['backup']): ?> + + + + + + + permissions['download']): ?> + + + + + + + + + + + + permissions['configure']): ?> + + + + + + + permissions['configure']): ?> + + + + + +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_includeexclude.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_includeexclude.php new file mode 100644 index 00000000..275d42a7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_includeexclude.php @@ -0,0 +1,61 @@ + + +
      +
      +

      +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_troubleshooting.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_troubleshooting.php new file mode 100644 index 00000000..18d7d581 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_troubleshooting.php @@ -0,0 +1,36 @@ + +
      +
      +

      +
      + +
      + permissions['backup']): ?> + + + + + + + permissions['configure']): ?> + + + + + +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/oneclick.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/oneclick.php new file mode 100644 index 00000000..3e1a5bfb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/oneclick.php @@ -0,0 +1,30 @@ + +
      + +
      +

      +
      + +
      + quickIconProfiles as $qiProfile): ?> + + + escape($qiProfile->description); ?> + + +
      + +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/profile.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/profile.php new file mode 100644 index 00000000..94ca5ec7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/profile.php @@ -0,0 +1,41 @@ + 'index.php?......' + * ] + * to set up a custom return URL + */ +?> +
      +
      + + + + + + + + + + profileList, 'profileid', 'onchange="document.forms.switchActiveProfileForm.submit()" class="advancedSelect"', 'value', 'text', $this->profileid); ?> + +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_backup.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_backup.php new file mode 100644 index 00000000..a814c3c3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_backup.php @@ -0,0 +1,19 @@ + +
      +
      +

      +
      +
      latestBackupCell ?>
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_status.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_status.php new file mode 100644 index 00000000..3270a3b7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_status.php @@ -0,0 +1,69 @@ + +
      +
      +

      +
      + +
      + + statusCell ?> + + + countWarnings): ?> +
      + detailsCell ?> +
      +
      + + + +

      + () +

      + + + CHANGELOG + + + + + +
      + + + + +
      + + + + + + +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warning_phpversion.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warning_phpversion.php new file mode 100644 index 00000000..a7a32fe7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warning_phpversion.php @@ -0,0 +1,35 @@ + + + + +
      +

      +

      + format(JText::_('DATE_FORMAT_LC1')), + $akeebaCommonDateObsolescence->format(JText::_('DATE_FORMAT_LC1')), + '5.6' + ); ?> +

      +
      + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warnings.php b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warnings.php new file mode 100644 index 00000000..d4ce96ca --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ControlPanel/tmpl/warnings.php @@ -0,0 +1,133 @@ + + +promptForConfigurationWizard): ?> + loadAnyTemplate('admin:com_akeeba/Configuration/confwiz_modal'); ?> + + + +stuckUpdates):?> +
      +

      + getContainer()->db->getPrefix(), + 'index.php?option=com_akeeba&view=ControlPanel&task=forceUpdateDb' + )?> +

      +
      + + + +checkMbstring)): ?> +
      + +
      + + + +frontEndSecretWordIssue))): ?> +
      +

      +

      +

      frontEndSecretWordIssue; ?>

      +

      + + newSecretWord); ?> +

      +

      + + + + +

      +
      + + + +loadAnyTemplate('admin:com_akeeba/ControlPanel/warning_phpversion'); ?> + + +areMediaPermissionsFixed)): ?> +
      +

      +

      +

      +
        +
      1. +
      2. +
      +

      +
      + + + +needsDownloadID): ?> +
      +

      + +

      +

      + +

      +
      + + + + +
      + + + + +
      +
      +
      + + + +coreWarningForDownloadID): ?> +
      + +
      + + +getContainer()->template->parsePath('media://com_akeeba/js/ControlPanel.min.js'); + $testfile .= '?'.$this->getContainer()->mediaVersion; +?> + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/Html.php new file mode 100644 index 00000000..514ab56d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/Html.php @@ -0,0 +1,134 @@ +addJavascriptFile('media://com_akeeba/js/FileFilters.min.js'); + $this->addJavascriptFile('media://com_akeeba/js/DatabaseFilters.min.js'); + + /** @var DatabaseFilters $model */ + $model = $this->getModel(); + + // Add custom submenus + $task = $model->getState('browse_task', 'normal'); + $toolbar = $this->container->toolbar; + + $toolbar->appendLink( + JText::_('COM_AKEEBA_FILEFILTERS_LABEL_NORMALVIEW'), + JUri::base() . 'index.php?option=com_akeeba&view=DatabaseFilters&task=normal', + ($task == 'normal') + ); + $toolbar->appendLink( + JText::_('COM_AKEEBA_FILEFILTERS_LABEL_TABULARVIEW'), + JUri::base() . 'index.php?option=com_akeeba&view=DatabaseFilters&task=tabular', + ($task == 'tabular') + ); + + // Get a JSON representation of the available roots + $root_info = $model->get_roots(); + $roots = array(); + $options = array(); + + if (!empty($root_info)) + { + // Loop all dir definitions + foreach ($root_info as $def) + { + $roots[] = $def->value; + $options[] = JHtml::_('select.option', $def->value, $def->text); + } + } + + $site_root = '[SITEDB]'; + $attributes = 'onchange="akeeba.Dbfilters.activeRootChanged ();"'; + $this->root_select = + JHtml::_('select.genericlist', $options, 'root', $attributes, 'value', 'text', $site_root, 'active_root'); + $this->roots = $roots; + + switch ($task) + { + case 'normal': + default: + $this->setLayout('default'); + + // Get a JSON representation of the database data + $json = json_encode($model->make_listing($site_root)); + $this->json = $json; + + break; + + case 'tabular': + $this->setLayout('tabular'); + + // Get a JSON representation of the tabular filter data + $json = json_encode($model->get_filters($site_root)); + $this->json = $json; + + break; + } + + // Translations + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIERRORFILTER'); + JText::script('COM_AKEEBA_DBFILTER_TYPE_TABLES'); + JText::script('COM_AKEEBA_DBFILTER_TYPE_TABLEDATA'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_MISC'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_TABLE'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_VIEW'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_PROCEDURE'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_FUNCTION'); + JText::script('COM_AKEEBA_DBFILTER_TABLE_TRIGGER'); + + $this->getProfileIdAndName(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/default.php new file mode 100644 index 00000000..5f50f7ce --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/default.php @@ -0,0 +1,70 @@ +json = addcslashes($this->json, "'\\"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +/** + * Callback function for changing the active root in Database Table filters + */ +function akeeba_active_root_changed() +{ + var elRoot = document.getElementById('active_root'); + var data = { + 'root': elRoot.options[elRoot.selectedIndex].value + }; + akeeba.Dbfilters.load(data); +} + +akeeba.System.documentReady(function(){ + akeeba.System.params.AjaxURL = '$ajaxUrl'; + var data = JSON.parse('{$this->json}'); + akeeba.Dbfilters.render(data); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
      +
      + + root_select; ?> +
      +
      + + +
      +
      + +
      +
      + +
      +
      +

      + +

      +
      +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/tabular.php b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/tabular.php new file mode 100644 index 00000000..28737f61 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/DatabaseFilters/tmpl/tabular.php @@ -0,0 +1,74 @@ +json = addcslashes($this->json, "'\\"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +/** + * Callback function for changing the active root in Filesystem Filters + */ +function akeeba_active_root_changed() +{ + var elRoot = document.getElementById('active_root'); + akeeba.Dbfilters.loadTab(elRoot.options[elRoot.selectedIndex].value); +} + +akeeba.System.documentReady(function(){ + akeeba.System.params.AjaxURL = '$ajaxUrl'; + var data = JSON.parse('{$this->json}'); + akeeba.Dbfilters.renderTab(data); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
      +
      + + root_select; ?> +
      +
      + + + + + +
      +
      + + +
      +
      + + + + + + + + + +
      +
      +
      diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/Html.php new file mode 100644 index 00000000..acd2cc4a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/Html.php @@ -0,0 +1,143 @@ +addJavascriptFile('media://com_akeeba/js/FileFilters.min.js'); + + /** @var FileFilters $model */ + $model = $this->getModel(); + + // Add custom submenus + $task = $model->getState('browse_task', 'normal'); + $toolbar = $this->container->toolbar; + + $toolbar->appendLink( + JText::_('COM_AKEEBA_FILEFILTERS_LABEL_NORMALVIEW'), + JUri::base() . 'index.php?option=com_akeeba&view=FileFilters&task=normal', + ($task == 'normal') + ); + $toolbar->appendLink( + JText::_('COM_AKEEBA_FILEFILTERS_LABEL_TABULARVIEW'), + JUri::base() . 'index.php?option=com_akeeba&view=FileFilters&task=tabular', + ($task == 'tabular') + ); + + // Get a JSON representation of the available roots + $filters = Factory::getFilters(); + $root_info = $filters->getInclusions('dir'); + $roots = array(); + $options = array(); + + if (!empty($root_info)) + { + // Loop all dir definitions + foreach ($root_info as $dir_definition) + { + if (is_null($dir_definition[1])) + { + // Site root definition has a null element 1. It is always pushed on top of the stack. + array_unshift($roots, $dir_definition[0]); + } + else + { + $roots[] = $dir_definition[0]; + } + + $options[] = JHtml::_('select.option', $dir_definition[0], $dir_definition[0]); + } + } + $site_root = $roots[0]; + $attribs = 'onchange="akeeba.Fsfilters.activeRootChanged();"'; + $this->root_select = + JHtml::_('select.genericlist', $options, 'root', $attribs, 'value', 'text', $site_root, 'active_root'); + $this->roots = $roots; + + switch ($task) + { + case 'normal': + default: + $this->setLayout('default'); + + // Get a JSON representation of the directory data + $model = $this->getModel(); + $json = json_encode($model->make_listing($site_root, array(), '')); + $this->json = $json; + + break; + + case 'tabular': + $this->setLayout('tabular'); + + // Get a JSON representation of the tabular filter data + $model = $this->getModel(); + $json = json_encode($model->get_filters($site_root)); + $this->json = $json; + + break; + } + + // Push translations + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIERRORFILTER'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_DIRECTORIES'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPFILES'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPDIRS'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_FILES'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_DIRECTORIES_ALL'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPFILES_ALL'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPDIRS_ALL'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_FILES_ALL'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_APPLYTOALLDIRS'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_APPLYTOALLFILES'); + + $this->getProfileIdAndName(); + } + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/default.php new file mode 100644 index 00000000..c4430796 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/default.php @@ -0,0 +1,84 @@ +container->template->parsePath('media://com_akeeba/icons/loading.gif')); +$this->json = addcslashes($this->json, "'"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.System.documentReady(function() { + akeeba.System.params.AjaxURL = '$ajaxUrl'; + akeeba.Fsfilters.loadingGif = '$loadingUrl'; + + // Bootstrap the page display + var data = eval({$this->json}); + akeeba.Fsfilters.render(data); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
      +
      + + root_select; ?> +
      +
      + + + + + + +
      +
      + +
      +
      +
        +
        +
        + + +
        +
        +
        +
        +

        + +

        +
        +
        +
        +
        + +
        +
        +
        +

        + +

        +
        +
        +
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/tabular.php b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/tabular.php new file mode 100644 index 00000000..e1ca41ea --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/FileFilters/tmpl/tabular.php @@ -0,0 +1,64 @@ +container->template->parsePath('media://com_akeeba/icons/loading.gif')); +$this->json = addcslashes($this->json, "'"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.System.documentReady(function() { + akeeba.System.params.AjaxURL = '$ajaxUrl'; + akeeba.Fsfilters.loadingGif = '$loadingUrl'; + + // Bootstrap the page display + var data = eval({$this->json}); + akeeba.Fsfilters.renderTab(data); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
        +
        + + root_select; ?> +
        +
        + + + + + +
        +
        + +
        +
        + + + + + + + + + +
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Log/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Log/Html.php new file mode 100644 index 00000000..a8546b53 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Log/Html.php @@ -0,0 +1,117 @@ +getModel(); + $this->logs = $model->getLogList(); + + $tag = $model->getState('tag'); + + if (empty($tag)) + { + $tag = null; + } + + $this->tag = $tag; + + // Let's check if the file is too big to display + if ($this->tag) + { + $file = Factory::getLog()->getLogFilename($this->tag); + + if (@file_exists($file)) + { + $this->logSize = filesize($file); + $this->logTooBig = ($this->logSize >= self::bigLogSize); + } + } + + if ($this->logTooBig) + { + $src = 'index.php?option=com_akeeba&view=Log&task=download&attachment=0&tag=' . urlencode($this->tag); + $js = <<'); + this.parentNode.style.display = 'none'; + }); +}); + +JS; + + $this->addJavascriptInline($js); + } + + + $this->getProfileIdAndName(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Log/Raw.php b/deployed/akeeba/administrator/components/com_akeeba/View/Log/Raw.php new file mode 100644 index 00000000..99d2e188 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Log/Raw.php @@ -0,0 +1,49 @@ +getModel(); + $tag = $model->getState('tag'); + + if (empty($tag)) + { + $tag = null; + } + + $this->tag = $tag; + + $this->setLayout('raw'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/default.php new file mode 100644 index 00000000..45f333ed --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/default.php @@ -0,0 +1,64 @@ + +logs) && count($this->logs)): ?> +
        +
        + + logs, 'tag', 'onchange="submitform();" class="advancedSelect"', 'value', 'text', $this->tag); ?> +
        + + tag)): ?> + + + +
        + + + +
        + +
        + + +tag)): ?> + logTooBig):?> +
        +

        + logSize / (1024 * 1024), 2))?> +

        + + + +
        + + +
        + logTooBig):?> + + +
        + + +logs) && count($this->logs))): ?> +
        + +
        + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/raw.php b/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/raw.php new file mode 100644 index 00000000..2153403a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Log/tmpl/raw.php @@ -0,0 +1,73 @@ +tag; +$logName = Factory::getLog()->getLogFilename($tag); + +// Load JFile class +JLoader::import('joomla.filesystem.file'); + +@ob_end_clean(); + +if(!JFile::exists($logName)) +{ + // Oops! The log doesn't exist! + echo '

        '.JText::_('COM_AKEEBA_LOG_ERROR_LOGFILENOTEXISTS').'

        '; + return; +} +else +{ + // Allright, let's load and render it + $fp = fopen( $logName, "rt" ); + if ($fp === FALSE) + { + // Oops! The log isn't readable?! + echo '

        '.JText::_('COM_AKEEBA_LOG_ERROR_UNREADABLE').'

        '; + return; + } + + while( !feof($fp) ) + { + $line = fgets( $fp ); + if(!$line) return; + $exploded = explode( "|", $line, 3 ); + unset( $line ); + switch( trim($exploded[0]) ) + { + case "ERROR": + $fmtString = "["; + break; + case "WARNING": + $fmtString = "["; + break; + case "INFO": + $fmtString = "["; + break; + case "DEBUG": + $fmtString = "["; + break; + default: + $fmtString = "["; + break; + } + $fmtString .= $exploded[1] . "] " . htmlspecialchars($exploded[2]) . "
        \n"; + unset( $exploded ); + echo $fmtString; + unset( $fmtString ); + } +} + +@ob_start(); diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Manage/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/Html.php new file mode 100644 index 00000000..e9066d8d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/Html.php @@ -0,0 +1,635 @@ +addJavascriptFile('media://com_akeeba/js/Manage.min.js'); + + // Load core classes used in the view template + JLoader::import('joomla.utilities.date'); + + $user = $this->container->platform->getUser(); + $this->permissions = array( + 'configure' => $user->authorise('akeeba.configure', 'com_akeeba'), + 'backup' => $user->authorise('akeeba.backup', 'com_akeeba'), + 'download' => $user->authorise('akeeba.download', 'com_akeeba'), + ); + + + /** @var Profiles $profilesModel */ + $profilesModel = $this->container->factory->model('Profiles')->tmpInstance(); + $enginesPerPprofile = $profilesModel->getPostProcessingEnginePerProfile(); + $this->enginesPerProfile = $enginesPerPprofile; + + // "Show warning first" download button. + $confirmationText = JText::_('COM_AKEEBA_BUADMIN_LOG_DOWNLOAD_CONFIRM', true, false); + $confirmationText = str_replace('\\\\n', '\\n', $confirmationText); + $baseURI = JUri::base(); + $js = <<addJavascriptInline($js); + + JHtml::_('behavior.calendar'); + + $hash = 'akeebamanage'; + + // ...ordering + $platform = $this->container->platform; + $input = $this->input; + $this->order = $platform->getUserStateFromRequest($hash . 'filter_order', 'filter_order', $input, 'backupstart'); + $this->order_Dir = $platform->getUserStateFromRequest($hash . 'filter_order_Dir', 'filter_order_Dir', $input, 'DESC'); + + // ...filter state + $this->fltDescription = $platform->getUserStateFromRequest($hash . 'filter_description', 'description', $input, ''); + $this->fltFrom = $platform->getUserStateFromRequest($hash . 'filter_from', 'from', $input, ''); + $this->fltTo = $platform->getUserStateFromRequest($hash . 'filter_to', 'to', $input, ''); + $this->fltOrigin = $platform->getUserStateFromRequest($hash . 'filter_origin', 'origin', $input, ''); + $this->fltProfile = $platform->getUserStateFromRequest($hash . 'filter_profile', 'profile', $input, ''); + + $filters = $this->getFilters(); + $ordering = $this->getOrdering(); + + /** @var Statistics $model */ + $model = $this->getModel(); + $list = $model->getStatisticsListWithMeta(false, $filters, $ordering); + + // Let's create an array indexed with the profile id for better handling + $profiles = $profilesModel->get(true); + + $profilesList = array( + JHtml::_('select.option', '', '–' . JText::_('COM_AKEEBA_BUADMIN_LABEL_PROFILEID') . '–') + ); + + if (!empty($profiles)) + { + foreach ($profiles as $profile) + { + $profilesList[] = JHtml::_('select.option', $profile->id, '#' . $profile->id . '. ' . $profile->description); + } + } + + // Assign data to the view + $this->profiles = $profiles; // Profiles + $this->profilesList = $profilesList; // Profiles list for select box + $this->list = $list; // Data + $this->pagination = $model->getPagination($filters); // Pagination object + + // Date format + $dateFormat = $this->container->params->get('dateformat', ''); + $dateFormat = trim($dateFormat); + $this->dateFormat = !empty($dateFormat) ? $dateFormat : JText::_('DATE_FORMAT_LC4'); + + // Time zone options + $this->useLocalTime = $this->container->params->get('localtime', '1') == 1; + $this->timeZoneFormat = $this->container->params->get('timezonetext', 'T'); + + // Should I show the prompt for the configuration wizard? + $this->promptForBackupRestoration = $this->container->params->get('show_howtorestoremodal', 1) != 0; + + // Construct the array of sorting fields + $this->sortFields = array( + 'id' => JText::_('COM_AKEEBA_BUADMIN_LABEL_ID'), + 'description' => JText::_('COM_AKEEBA_BUADMIN_LABEL_DESCRIPTION'), + 'backupstart' => JText::_('COM_AKEEBA_BUADMIN_LABEL_START'), + 'profile_id' => JText::_('COM_AKEEBA_BUADMIN_LABEL_PROFILEID'), + ); + } + + /** + * Edit a backup record's description and comment + * + * @return void + */ + public function onBeforeShowcomment() + { + /** @var Statistics $model */ + $model = $this->getModel(); + $id = $model->getState('id', 0); + $record = Platform::getInstance()->get_statistics($id); + $this->record = $record; + $this->record_id = $id; + + $this->setLayout('comment'); + } + + /** + * File size formatting function. COnverts number of bytes to a human readable represenation. + * + * @param int $sizeInBytes Size in bytes + * @param int $decimals How many decimals should I use? Default: 2 + * @param string $decSeparator Decimal separator + * @param string $thousandsSeparator Thousands grouping character + * + * @return string + */ + protected function formatFilesize($sizeInBytes, $decimals = 2, $decSeparator = '.', $thousandsSeparator = '') + { + if ($sizeInBytes <= 0) + { + return '-'; + } + + $units = array('b', 'KB', 'MB', 'GB', 'TB'); + $unit = floor(log($sizeInBytes, 2) / 10); + + if ($unit == 0) + { + $decimals = 0; + } + + return number_format($sizeInBytes / pow(1024, $unit), $decimals, $decSeparator, $thousandsSeparator) . ' ' . $units[$unit]; + } + + /** + * Translates the internal backup type (e.g. cli) to a human readable string + * + * @param string $recordType The internal backup type + * + * @return string + */ + protected function translateBackupType($recordType) + { + static $backup_types = null; + + if (!is_array($backup_types)) + { + // Load a mapping of backup types to textual representation + $scripting = \Akeeba\Engine\Factory::getEngineParamsProvider()->loadScripting(); + $backup_types = array(); + foreach ($scripting['scripts'] as $key => $data) + { + $backup_types[$key] = JText::_($data['text']); + } + } + + if (array_key_exists($recordType, $backup_types)) + { + return $backup_types[$recordType]; + } + + return '–'; + } + + /** + * Returns the origin's translated name and the appropriate icon class + * + * @param array $record A backup record + * + * @return array array(originTranslation, iconClass) + */ + protected function getOriginInformation($record) + { + $originLanguageKey = 'COM_AKEEBA_BUADMIN_LABEL_ORIGIN_' . $record['origin']; + $originDescription = JText::_($originLanguageKey); + + switch (strtolower($record['origin'])) + { + case 'backend': + $originIcon = 'akion-android-desktop'; + break; + + case 'frontend': + $originIcon = 'akion-ios-world'; + break; + + case 'json': + $originIcon = 'akion-android-cloud'; + break; + + case 'cli': + $originIcon = 'akion-ios-paper-outline'; + break; + + case 'xmlrpc': + $originIcon = 'akion-code'; + break; + + case 'restorepoint': + $originIcon = 'akion-refresh'; + break; + + case 'lazy': + $originIcon = 'akion-cube'; + break; + + default: + $originIcon = 'akion-help'; + break; + } + + if (empty($originLanguageKey) || ($originDescription == $originLanguageKey)) + { + $originDescription = '–'; + $originIcon = 'akion-help'; + + return array($originDescription, $originIcon); + } + + return array($originDescription, $originIcon); + } + + /** + * Get the start time and duration of a backup record + * + * @param array $record A backup record + * + * @return array array(startTimeAsString, durationAsString) + */ + protected function getTimeInformation($record) + { + $utcTimeZone = new DateTimeZone('UTC'); + $startTime = new Date($record['backupstart'], $utcTimeZone); + $endTime = new Date($record['backupend'], $utcTimeZone); + + $duration = $endTime->toUnix() - $startTime->toUnix(); + + if ($duration > 0) + { + $seconds = $duration % 60; + $duration = $duration - $seconds; + + $minutes = ($duration % 3600) / 60; + $duration = $duration - $minutes * 60; + + $hours = $duration / 3600; + $duration = sprintf('%02d', $hours) . ':' . sprintf('%02d', $minutes) . ':' . sprintf('%02d', $seconds); + } + else + { + $duration = ''; + } + + $user = $this->container->platform->getUser(); + $userTZ = $user->getParam('timezone', 'UTC'); + $tz = new DateTimeZone($userTZ); + $startTime->setTimezone($tz); + + $timeZoneSuffix = ''; + + if (!empty($this->timeZoneFormat)) + { + $timeZoneSuffix = $startTime->format($this->timeZoneFormat, $this->useLocalTime); + } + + return array( + $startTime->format($this->dateFormat, $this->useLocalTime), + $duration, + $timeZoneSuffix + ); + } + + /** + * Get the class and icon for the backup status indicator + * + * @param array $record A backup record + * + * @return array array(class, icon) + */ + protected function getStatusInformation($record) + { + $statusClass = ''; + + switch ($record['meta']) + { + case 'ok': + $statusIcon = 'akion-checkmark'; + $statusClass = 'akeeba-label--green'; + break; + case 'pending': + $statusIcon = 'akion-play'; + $statusClass = 'akeeba-label--orange'; + break; + case 'fail': + $statusIcon = 'akion-android-cancel'; + $statusClass = 'akeeba-label--red'; + break; + case 'remote': + $statusIcon = 'akion-cloud'; + $statusClass = 'akeeba-label--teal'; + break; + default: + $statusIcon = 'akion-trash-a'; + $statusClass = 'akeeba-label--grey'; + break; + } + + return array($statusClass, $statusIcon); + } + + /** + * Get the profile name for the backup record (or "–" if the profile no longer exists) + * + * @param array $record A backup record + * + * @return string + */ + protected function getProfileName($record) + { + $profileName = '—'; + + if (isset($this->profiles[$record['profile_id']])) + { + $profileName = $this->escape($this->profiles[$record['profile_id']]->description); + + return $profileName; + } + + return $profileName; + } + + /** + * Get the filters in a format that Akeeba Engine understands + * + * @return array + */ + private function getFilters() + { + $filters = array(); + + if ($this->fltDescription) + { + $filters[] = array( + 'field' => 'description', + 'operand' => 'LIKE', + 'value' => $this->fltDescription + ); + } + + if ($this->fltFrom && $this->fltTo) + { + $filters[] = array( + 'field' => 'backupstart', + 'operand' => 'BETWEEN', + 'value' => $this->fltFrom, + 'value2' => $this->fltTo + ); + } + elseif ($this->fltFrom) + { + $filters[] = array( + 'field' => 'backupstart', + 'operand' => '>=', + 'value' => $this->fltFrom, + ); + } + elseif ($this->fltTo) + { + JLoader::import('joomla.utilities.date'); + $toDate = new Date($this->fltTo); + $to = $toDate->format('Y-m-d') . ' 23:59:59'; + + $filters[] = array( + 'field' => 'backupstart', + 'operand' => '<=', + 'value' => $to, + ); + } + if ($this->fltOrigin) + { + $filters[] = array( + 'field' => 'origin', + 'operand' => '=', + 'value' => $this->fltOrigin + ); + } + if ($this->fltProfile) + { + $filters[] = array( + 'field' => 'profile_id', + 'operand' => '=', + 'value' => (int) $this->fltProfile + ); + } + + $filters[] = array( + 'field' => 'tag', + 'operand' => '<>', + 'value' => 'restorepoint' + ); + + + if (empty($filters)) + { + $filters = null; + } + + return $filters; + } + + /** + * Get the list ordering in a format that Akeeba Engine understands + * + * @return array + */ + private function getOrdering() + { + $order = array( + 'by' => $this->order, + 'order' => strtoupper($this->order_Dir) + ); + + return $order; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/comment.php b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/comment.php new file mode 100644 index 00000000..8fd48952 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/comment.php @@ -0,0 +1,38 @@ + +
        +
        + + +
        + +
        + + container->platform->getConfig()->get('editor', 'tinymce'))->display('comment', $this->record['comment'], '100%', '400', '60', '20', array()); ?> +
        + +
        + + + + + +
        + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/default.php new file mode 100644 index 00000000..975936c0 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/default.php @@ -0,0 +1,203 @@ +order); +$this->getContainer()->template->addJSInline($js); + +?> + +promptForBackupRestoration): ?> +loadAnyTemplate('admin:com_akeeba/Manage/howtorestore_modal'); ?> + + +
        +

        +

        + +

        +
        + +
        +
        + +
        +
        +
        + +
        + +
        + fltFrom, 'from', 'from', '%Y-%m-%d', array('class' => 'input-small')); ?> +
        + +
        + fltTo, 'to', 'to', '%Y-%m-%d', array('class' => 'input-small')); ?> +
        + +
        + +
        + +
        + profilesList, 'profile', 'onchange="document.forms.adminForm.submit()" class="advancedSelect"', 'value', 'text', $this->fltProfile); ?> +
        +
        + + getPagination(), $this->sortFields, $this->order, $this->order_Dir)?> + +
        + + + + + + + + + + + + + + + + + + + + + list)): ?> + + + + + list))): ?> + + list as $record): ?> + getOriginInformation($record); + list($startTime, $duration, $timeZoneText) = $this->getTimeInformation($record); + list($statusClass, $statusIcon) = $this->getStatusInformation($record); + $profileName = $this->getProfileName($record); + ?> + + + + + + + + + + + + + +
        + + + order_Dir, $this->order, 'default'); ?> + + order_Dir, $this->order, 'default'); ?> + + order_Dir, $this->order, 'default'); ?> + + + + + + + + +
        + pagination->getListFooter(); ?> + +
        + +
        + escape($record['id']); ?> + + + + + + + + escape(empty($record['description']) ? JText::_('COM_AKEEBA_BUADMIN_LABEL_NODESCRIPTION') : $record['description']); ?> + + +
        +
        + + + escape($startTime); ?> escape($timeZoneText); ?> + +
        +
        + #escape((int)$record['profile_id']); ?>. escape($profileName); ?> + +
        + + escape($this->translateBackupType($record['type'])); ?> + +
        + escape($duration); ?> + + + + + + + + escape($this->formatFilesize($record['size'])); ?> + + 0): ?> + formatFilesize($record['total_size']); ?> + + — + + + loadAnyTemplate('admin:com_akeeba/Manage/manage_column', [ + 'record' => &$record + ]); ?> +
        + +
        + + + + + + + +
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/howtorestore_modal.php b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/howtorestore_modal.php new file mode 100644 index 00000000..e6d4497a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/howtorestore_modal.php @@ -0,0 +1,63 @@ +getContainer()->template->addJSInline($js); +?> + +
        +
        +

        + +

        + +

        + +

        + +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/manage_column.php b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/manage_column.php new file mode 100644 index 00000000..5d42f522 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Manage/tmpl/manage_column.php @@ -0,0 +1,157 @@ +permissions['backup'] && $archiveExists && empty($record['remote_filename']) && ($this->enginesPerProfile[$record['profile_id']] != 'none') && ($record['meta'] != 'obsolete') && (AKEEBA_PRO == 1); +$showDownload = $this->permissions['download'] && $archiveExists; +$showViewLog = $this->permissions['backup'] && isset($record['backupid']) && !empty($record['backupid']); +$postProcEngine = ''; +$thisPart = ''; +$thisID = urlencode($record['id']); + +if ($showUploadRemote) +{ + $postProcEngine = $this->enginesPerProfile[$record['profile_id']]; + $showUploadRemote = !empty($postProcEngine); +} + +?> +
        +
        +
        +

        + +

        + +
        + + + + + + + + + +

        +

        + +
        + + escape(Utils::getRelativePath(JPATH_SITE, dirname($record['absolute_path']))); ?> + + +

        +

        + +
        + + escape($record['archivename']); ?> + +

        +
        + +
        + + + + +
        + + + + + "> + + + (escape($postProcEngine); ?>) + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Html.php new file mode 100644 index 00000000..72350998 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Html.php @@ -0,0 +1,86 @@ +getProfileIdAndName(); + + // Get Sort By fields + $this->sortFields = array( + 'id' => JText::_('JGRID_HEADING_ID'), + 'description' => JText::_('COM_AKEEBA_PROFILES_COLLABEL_DESCRIPTION'), + ); + + parent::onBeforeBrowse(); + + $js = <<< JS + Joomla.orderTable = function () + { + table = document.getElementById("sortTable"); + direction = document.getElementById("directionTable"); + order = table.options[table.selectedIndex].value; + + if (order != '{$this->lists->order}') + { + dirn = 'asc'; + } + else + { + dirn = direction.options[direction.selectedIndex].value; + } + + Joomla.tableOrdering(order, dirn); + } + +JS; + $this->addJavascriptInline($js); + + JHtml::_('behavior.multiselect'); + JHtml::_('dropdown.init'); + } + + /** + * The edit layout, editing a profile's name + */ + protected function onBeforeEdit() + { + parent::onBeforeEdit(); + + // Include tooltip support + JHtml::_('behavior.tooltip'); + } + + +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Json.php b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Json.php new file mode 100644 index 00000000..7bd0cfcf --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/Json.php @@ -0,0 +1,18 @@ + + +
        + loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
        +
        +
        + +
        +
        +
        +
        + + pagination->getLimitBox(); ?> +
        + +
        + + +
        + +
        + + +
        +
        +
        + + + + + + + + + + + + + + + + + + items as $profile ): ?> + + + + + + + + + +
        + + + lists->order_Dir, $this->lists->order, 'browse'); ?> + + lists->order_Dir, $this->lists->order, 'browse'); ?> +
        + pagination->getListFooter(); ?> + +
        + id); ?> + + id; ?> + + + + + + +   + + + + + + + escape($profile->description); ?> + + +
        + +
        + + + + + + + + +
        +
        + +
        + + + + + + + + + + + + + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/form.php b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/form.php new file mode 100644 index 00000000..0bc86003 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/form.php @@ -0,0 +1,31 @@ + +
        +
        +
        + +
        + + +
        + +
        + + + + + + +
        +
        + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/item_json.php b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/item_json.php new file mode 100644 index 00000000..32a3dc7a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Profiles/tmpl/item_json.php @@ -0,0 +1,38 @@ +item->getData(); + +if (substr($data['configuration'], 0, 12) == '###AES128###') +{ + // Load the server key file if necessary + if (!defined('AKEEBA_SERVERKEY')) + { + $filename = JPATH_COMPONENT_ADMINISTRATOR . '/BackupEngine/serverkey.php'; + + include_once $filename; + } + + $key = Factory::getSecureSettings()->getKey(); + + $data['configuration'] = Factory::getSecureSettings()->decryptSettings($data['configuration'], $key); +} + +$defaultName = $this->input->get('view', 'joomla', 'cmd'); +$filename = $this->input->get('basename', $defaultName, 'cmd'); + +/** @var JDocumentJson $document */ +$document = JFactory::getDocument(); +$document->setName($filename); + +echo json_encode($data); diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/Html.php new file mode 100644 index 00000000..f6fb6111 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/Html.php @@ -0,0 +1,102 @@ +addJavascriptFile('media://com_akeeba/js/FileFilters.min.js'); + $this->addJavascriptFile('media://com_akeeba/js/RegExFileFilter.min.js'); + + /** @var RegExFileFilters $model */ + $model = $this->getModel(); + + // Get a JSON representation of the available roots + $filters = Factory::getFilters(); + $root_info = $filters->getInclusions('dir'); + $roots = array(); + $options = array(); + + if (!empty($root_info)) + { + // Loop all dir definitions + foreach ($root_info as $dir_definition) + { + if (is_null($dir_definition[1])) + { + // Site root definition has a null element 1. It is always pushed on top of the stack. + array_unshift($roots, $dir_definition[0]); + } + else + { + $roots[] = $dir_definition[0]; + } + + $options[] = JHtml::_('select.option', $dir_definition[0], $dir_definition[0]); + } + } + $site_root = $roots[0]; + $attribs = 'onchange="akeeba.Regexfsfilters.activeRootChanged();"'; + $this->root_select = JHtml::_('select.genericlist', $options, 'root', $attribs, 'value', 'text', $site_root, 'active_root'); + $this->roots = $roots; + + // Get a JSON representation of the directory data + $json = json_encode($model->get_regex_filters($site_root)); + $this->json = $json; + + $this->getProfileIdAndName(); + + // Push translations + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIERRORFILTER'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_DIRECTORIES'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPFILES'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_SKIPDIRS'); + JText::script('COM_AKEEBA_FILEFILTERS_TYPE_FILES'); + + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/tmpl/default.php new file mode 100644 index 00000000..5ff38a42 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/RegExFileFilter/tmpl/default.php @@ -0,0 +1,57 @@ +container->template->parsePath('media://com_akeeba/icons/loading.gif')); +$this->json = addcslashes($this->json, "'\\"); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.System.documentReady(function(){ + akeeba.System.params.AjaxURL = '$ajaxUrl'; + var data = JSON.parse('{$this->json}'); + akeeba.Regexfsfilters.render(data); +}); + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ProfileName'); ?> + +
        +
        +
        + + + root_select; ?> + +
        +
        +
        + +
        +
        + + + + + + + + + + +
         
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Restore/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/Html.php new file mode 100644 index 00000000..60e5635c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/Html.php @@ -0,0 +1,108 @@ +loadCommonJavascript(); + + /** @var Restore $model */ + $model = $this->getModel(); + + $this->id = $model->getState('id'); + $this->ftpparams = $this->getFTPParams(); + $this->extractionmodes = $this->getExtractionModes(); + + $backup = Platform::getInstance()->get_statistics($this->id); + $this->extension = strtolower(substr($backup['absolute_path'], -3)); + } + + protected function onBeforeStart() + { + $this->loadCommonJavascript(); + + /** @var Restore $model */ + $model = $this->getModel(); + + $password = $model->getState('password'); + $this->password = $password; + $this->setLayout('restore'); + } + + /** + * Returns the available extraction modes for use by JHtml + * + * @return array + */ + private function getExtractionModes() + { + $options = array(); + $options[] = JHtml::_('select.option', 'hybrid', JText::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_HYBRID')); + $options[] = JHtml::_('select.option', 'direct', JText::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_DIRECT')); + $options[] = JHtml::_('select.option', 'ftp', JText::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_FTP')); + + return $options; + } + + /** + * Returns the FTP parameters from the Global Configuration + * + * @return array + */ + private function getFTPParams() + { + $config = $this->container->platform->getConfig(); + + return array( + 'procengine' => $config->get('ftp_enable', 0) ? 'hybrid' : 'direct', + 'ftp_host' => $config->get('ftp_host', 'localhost'), + 'ftp_port' => $config->get('ftp_port', '21'), + 'ftp_user' => $config->get('ftp_user', ''), + 'ftp_pass' => $config->get('ftp_pass', ''), + 'ftp_root' => $config->get('ftp_root', ''), + 'tempdir' => $config->get('tmp_path', '') + ); + } + + private function loadCommonJavascript() + { + $this->addJavascriptFile('media://com_akeeba/js/Encryption.min.js'); + $this->addJavascriptFile('media://com_akeeba/js/Configuration.min.js'); + $this->addJavascriptFile('media://com_akeeba/js/Restore.min.js'); + + // Push translations + JText::script('COM_AKEEBA_CONFIG_UI_BROWSE'); + JText::script('COM_AKEEBA_CONFIG_UI_CONFIG'); + JText::script('COM_AKEEBA_CONFIG_UI_REFRESH'); + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_CONFIG_UI_FTPBROWSER_TITLE'); + JText::script('COM_AKEEBA_CONFIG_DIRECTFTP_TEST_OK'); + JText::script('COM_AKEEBA_CONFIG_DIRECTFTP_TEST_FAIL'); + JText::script('COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_OK'); + JText::script('COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_FAIL'); + JText::script('COM_AKEEBA_BACKUP_TEXT_LASTRESPONSE'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/default.php new file mode 100644 index 00000000..b1685717 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/default.php @@ -0,0 +1,155 @@ +getContainer()->template->addJSInline($js); + +?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FTPBrowser'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FTPConnectionTest'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/ErrorModal'); ?> + +
        + + + + + + +

        +
        + + extractionmodes, 'procengine', '', 'value', 'text', $this->ftpparams['procengine']); ?> +

        + +

        +
        + + extension == 'jps'): ?> +

        + +
        + + +
        + + +
        +

        + +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        + +
        +
        +
        + +
        +
        + + +
        +
        + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/restore.php b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/restore.php new file mode 100644 index 00000000..9cc49924 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Restore/tmpl/restore.php @@ -0,0 +1,118 @@ +loadHelper('escape'); + +$escapedPassword = addslashes($this->password); +$escapedJuriBase = addslashes(JUri::base()); +$js = <<< JS + +;// This comment is intentionally put here to prevent badly written plugins from causing a Javascript error +// due to missing trailing semicolon and/or newline in their code. +akeeba.Restore.password = '$escapedPassword'; +akeeba.Restore.ajaxURL = '{$escapedJuriBase}/components/com_akeeba/restore.php'; +akeeba.Restore.mainURL = '{$escapedJuriBase}/index.php'; + +akeeba.System.documentReady(function(){ + akeeba.Restore.pingRestoration(); + + akeeba.System.addEventListener(document.getElementById('restoration-runinstaller'), 'click', akeeba.Restore.runInstaller); + akeeba.System.addEventListener(document.getElementById('restoration-finalize'), 'click', akeeba.Restore.finalize); +}); + + +JS; + +$this->getContainer()->template->addJSInline($js); + +?> +
        +

        + +

        +
        + + +
        +

        + + + + + + + + + + + + + + +
        + + + +
        + + + +
        + + + +
        + +
        +
        +
        +
        +
        + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/Html.php new file mode 100644 index 00000000..d5e23671 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/Html.php @@ -0,0 +1,52 @@ +getProfileIdAndName(); + + // Get the CRON paths + /** @var Schedule $model */ + $model = $this->getModel(); + $this->croninfo = $model->getPaths(); + $this->checkinfo = $model->getCheckPaths(); + + \JHtml::_('bootstrap.framework'); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default.php new file mode 100644 index 00000000..ea98cdc3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default.php @@ -0,0 +1,19 @@ + + 'akeebabackup-scheduling-backups')); ?> + +loadAnyTemplate('admin:com_akeeba/Schedule/default_runbackups'); ?> + + +loadAnyTemplate('admin:com_akeeba/Schedule/default_checkbackups'); ?> + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_checkbackups.php b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_checkbackups.php new file mode 100644 index 00000000..95f3b735 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_checkbackups.php @@ -0,0 +1,302 @@ + +

        + +

        + +

        + ; +

        + +
        +
        +

        +
        + + +
        +

        + +

        +

        + + + + +

        +
        + +

        + + + escape($this->checkinfo->info->php_path); ?> + escape($this->checkinfo->cli->path); ?> + + +

        + +

        + + croninfo->info->php_path); ?> +

        + + + +
        +

        +

        +

        + + + + +

        +
        + +
        + +
        +
        +

        +
        + + +
        +

        + +

        +

        + + + + +

        +
        + + checkinfo->info->feenabled): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && !trim($this->croninfo->info->secret)): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && trim($this->croninfo->info->secret)): ?> +

        + + + escape($this->checkinfo->info->php_path); ?> + escape($this->checkinfo->altcli->path); ?> + + +

        +

        + + checkinfo->info->php_path); ?> +

        + + + + +
        +

        +

        +

        + + + + +

        +
        + + +
        + +
        +
        +

        +
        + +
        +

        + +

        +

        + + + + +

        +
        + + croninfo->info->feenabled): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && !trim($this->croninfo->info->secret)): ?> +
        +

        + +

        +
        + + + checkinfo->info->feenabled && trim($this->checkinfo->info->secret)): ?> +

        + +

        + +

        + +

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + +
        + + + +
        + + + escape($this->checkinfo->info->root_url); ?>/escape($this->checkinfo->frontend->path); ?> + +
        + + + +
        + + + +
        + + + +
        + + + +
        + +
        + +

        + +

        + + + wget --max-redirect=10000 "escape($this->checkinfo->info->root_url); ?>/escape($this->checkinfo->frontend->path); ?>" -O - 1>/dev/null 2>/dev/null + +

        + +

        + +

        + + + curl -L --max-redirs 1000 -v "escape($this->checkinfo->info->root_url); ?>/escape($this->checkinfo->frontend->path); ?>" 1>/dev/null 2>/dev/null + +

        + +

        + +

        + +

        +
        +<?php
        +    $curl_handle=curl_init();
        +    curl_setopt($curl_handle, CURLOPT_URL, 'escape($this->checkinfo->info->root_url); ?>/escape($this->checkinfo->frontend->path); ?> ?>');
        +    curl_setopt($curl_handle,CURLOPT_FOLLOWLOCATION, TRUE);
        +    curl_setopt($curl_handle,CURLOPT_MAXREDIRS, 10000);
        +    curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER, 1);
        +    $buffer = curl_exec($curl_handle);
        +    curl_close($curl_handle);
        +    if (empty($buffer))
        +        echo "Sorry, the check didn't work.";
        +    else
        +        echo $buffer;
        +?>
        +        
        + +

        + +

        + + + escape($this->checkinfo->info->root_url); ?>/escape($this->checkinfo->frontend->path); ?> + +

        + + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_runbackups.php b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_runbackups.php new file mode 100644 index 00000000..b2d02897 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Schedule/tmpl/default_runbackups.php @@ -0,0 +1,299 @@ + +

        + +

        + +

        + +

        + +
        +
        +

        +
        + + +
        +

        + +

        +

        + + + + +

        +
        + +

        + + + escape($this->croninfo->info->php_path); ?> + escape($this->croninfo->cli->path); ?> + +

        +

        + + croninfo->info->php_path); ?> +

        + + + +
        +

        +

        +

        + + + + +

        +
        + +
        + +
        +
        +

        +
        + + +
        +

        + +

        + + + + +
        + + croninfo->info->feenabled): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && !trim($this->croninfo->info->secret)): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && trim($this->croninfo->info->secret)): ?> +

        + + + escape($this->croninfo->info->php_path); ?> + escape($this->croninfo->altcli->path); ?> + +

        +

        + + croninfo->info->php_path); ?> +

        + + + + + +
        +

        +

        +

        + + + + +

        +
        + +
        + +
        +
        +

        +
        + +
        +

        + +

        +

        + + + + +

        +
        + + croninfo->info->feenabled): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && !trim($this->croninfo->info->secret)): ?> +
        +

        + +

        +
        + + + croninfo->info->feenabled && trim($this->croninfo->info->secret)): ?> +

        + +

        + +

        + +

        + +

        + +

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + +
        + + + +
        + + + escape($this->croninfo->info->root_url); ?>/escape($this->croninfo->frontend->path); ?> + +
        + + + +
        + + + +
        + + + +
        + + + +
        + +
        + +

        + +

        + + + wget --max-redirect=10000 "escape($this->croninfo->info->root_url); ?>/escape($this->croninfo->frontend->path); ?>" -O - 1>/dev/null 2>/dev/null + +

        + +

        + +

        + + + curl -L --max-redirs 1000 -v "escape($this->croninfo->info->root_url); ?>/escape($this->croninfo->frontend->path); ?>" 1>/dev/null 2>/dev/null + +

        + +

        + +

        + +

        +
        +<?php
        +    $curl_handle=curl_init();
        +    curl_setopt($curl_handle, CURLOPT_URL, 'escape($this->croninfo->info->root_url); ?>/escape($this->croninfo->frontend->path); ?>');
        +    curl_setopt($curl_handle,CURLOPT_FOLLOWLOCATION, TRUE);
        +    curl_setopt($curl_handle,CURLOPT_MAXREDIRS, 10000);
        +    curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER, 1);
        +    $buffer = curl_exec($curl_handle);
        +    curl_close($curl_handle);
        +    if (empty($buffer))
        +        echo "Sorry, the backup didn't work.";
        +    else
        +        echo $buffer;
        +?>
        +            
        + +

        + +

        + + + croninfo->info->root_url ?>/croninfo->frontend->path ?> + +

        + + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/Html.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/Html.php new file mode 100644 index 00000000..6b548dca --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/Html.php @@ -0,0 +1,272 @@ + 0, + 'string' => '0.00 Kb' + ]; + + /** @var string The URL to the site we are restoring to (from the session) */ + public $newSiteUrl = ''; + + /** @var string */ + public $newSiteUrlResult = ''; + + /** @var array Results of support and firewall status of the known file transfer methods */ + public $ftpSupport = [ + 'supported' => [ + 'ftp' => false, + 'ftps' => false, + 'sftp' => false, + ], + 'firewalled' => [ + 'ftp' => false, + 'ftps' => false, + 'sftp' => false + ] + ]; + + /** @var array Available transfer options, for use by JHTML */ + public $transferOptions = []; + + /** @var array Available chunk options, for use by JHTML */ + public $chunkOptions = array(); + + /** @var array Available chunk size options, for use by JHTML */ + public $chunkSizeOptions = array(); + + /** @var bool Do I have supported but firewalled methods? */ + public $hasFirewalledMethods = false; + + /** @var string Currently selected transfer option */ + public $transferOption = 'manual'; + + /** @var string Currently selected chunk option */ + public $chunkMode = 'chunked'; + + /** @var string Currently selected chunk size */ + public $chunkSize = 5242880; + + /** @var string FTP/SFTP host name */ + public $ftpHost = ''; + + /** @var string FTP/SFTP port (empty for default port) */ + public $ftpPort = ''; + + /** @var string FTP/SFTP username */ + public $ftpUsername = ''; + + /** @var string FTP/SFTP password – or certificate password if you're using SFTP with SSL certificates */ + public $ftpPassword = ''; + + /** @var string SFTP public key certificate path */ + public $ftpPubKey = ''; + + /** @var string SFTP private key certificate path */ + public $ftpPrivateKey = ''; + + /** @var string FTP/SFTP directory to the new site's root */ + public $ftpDirectory = ''; + + /** @var string FTP passive mode (default is true) */ + public $ftpPassive = true; + + /** @var string FTP passive mode workaround, for FTP/FTPS over cURL (default is true) */ + public $ftpPassiveFix = true; + + /** @var int Forces the transfer by skipping some checks on the target site */ + public $force = 0; + + /** + * Translations to pass to the view + * + * @var array + */ + public $translations = []; + + protected function onBeforeMain() + { + $this->addJavascriptFile('media://com_akeeba/js/Transfer.min.js'); + + /** @var Transfer $model */ + $model = $this->getModel(); + + $this->latestBackup = $model->getLatestBackupInformation(); + $this->spaceRequired = $model->getApproximateSpaceRequired(); + $this->newSiteUrl = $this->container->platform->getSessionVar('transfer.url', '', 'akeeba'); + $this->newSiteUrlResult = $this->container->platform->getSessionVar('transfer.url_status', '', 'akeeba'); + $this->ftpSupport = $this->container->platform->getSessionVar('transfer.ftpsupport', null, 'akeeba'); + $this->transferOption = $this->container->platform->getSessionVar('transfer.transferOption', null, 'akeeba'); + $this->chunkMode = $this->container->platform->getSessionVar('transfer.chunkMode', 'chunked', 'akeeba'); + $this->chunkSize = $this->container->platform->getSessionVar('transfer.uploadLimit', 5242880, 'akeeba'); + $this->ftpHost = $this->container->platform->getSessionVar('transfer.ftpHost', null, 'akeeba'); + $this->ftpPort = $this->container->platform->getSessionVar('transfer.ftpPort', null, 'akeeba'); + $this->ftpUsername = $this->container->platform->getSessionVar('transfer.ftpUsername', null, 'akeeba'); + $this->ftpPassword = $this->container->platform->getSessionVar('transfer.ftpPassword', null, 'akeeba'); + $this->ftpPubKey = $this->container->platform->getSessionVar('transfer.ftpPubKey', null, 'akeeba'); + $this->ftpPrivateKey = $this->container->platform->getSessionVar('transfer.ftpPrivateKey', null, 'akeeba'); + $this->ftpDirectory = $this->container->platform->getSessionVar('transfer.ftpDirectory', null, 'akeeba'); + $this->ftpPassive = $this->container->platform->getSessionVar('transfer.ftpPassive', 1, 'akeeba'); + $this->ftpPassiveFix = $this->container->platform->getSessionVar('transfer.ftpPassiveFix', 1, 'akeeba'); + + // We get this option from the request + $this->force = $this->input->getInt('force', 0); + + if (!empty($this->latestBackup)) + { + $lastBackupDate = $this->getContainer()->platform->getDate($this->latestBackup['backupstart'], 'UTC'); + $tz = new \DateTimeZone($this->container->platform->getUser()->getParam('timezone', $this->container->platform->getConfig()->get('offset'))); + $lastBackupDate->setTimezone($tz); + + $this->lastBackupDate = $lastBackupDate->format(JText::_('DATE_FORMAT_LC2'), true); + + $this->container->platform->setSessionVar('transfer.lastBackup', $this->latestBackup, 'akeeba'); + } + + if (empty($this->ftpSupport)) + { + $this->ftpSupport = $model->getFTPSupport(); + $this->container->platform->setSessionVar('transfer.ftpsupport', $this->ftpSupport, 'akeeba'); + } + + $this->transferOptions = $this->getTransferMethodOptions(); + $this->chunkOptions = $this->getChunkOptions(); + $this->chunkSizeOptions = $this->getChunkSizeOptions(); + + /* + foreach ($this->ftpSupport['firewalled'] as $method => $isFirewalled) + { + if ($isFirewalled && $this->ftpSupport['supported'][$method]) + { + $this->hasFirewalledMethods = true; + + break; + } + } + */ + + JText::script('COM_AKEEBA_FILEFILTERS_LABEL_UIROOT'); + JText::script('COM_AKEEBA_CONFIG_DIRECTFTP_TEST_FAIL'); + + $js = <<< JS +akeeba.System.documentReady(function(){ + // AJAX URL endpoint + akeeba.System.params.AjaxURL = 'index.php?option=com_akeeba&view=Transfer&format=raw&force={$this->force}'; + + // Last results of new site URL processing + akeeba.Transfer.lastUrl = '{$this->newSiteUrl}'; + akeeba.Transfer.lastResult = '{$this->newSiteUrlResult}'; + + // Auto-process URL change event + if (document.getElementById('akeeba-transfer-url').value) + { + akeeba.Transfer.onUrlChange(); + } + + // Remote connection hooks + if (document.getElementById('akeeba-transfer-ftp-method')) + { + akeeba.System.addEventListener(document.getElementById('akeeba-transfer-ftp-method'), 'change', akeeba.Transfer.onTransferMethodChange); + //akeeba.System.addEventListener(document.getElementById('akeeba-transfer-ftp-directory-browse'), 'click', akeeba.Transfer.initFtpSftpBrowser); + akeeba.System.addEventListener(document.getElementById('akeeba-transfer-btn-apply'), 'click', akeeba.Transfer.applyConnection); + akeeba.System.addEventListener(document.getElementById('akeeba-transfer-err-url-notexists-btn-ignore'), 'click', akeeba.Transfer.showConnectionDetails); + } +}); +JS; + + $this->addJavascriptInline($js); + } + + /** + * Returns the JHTML options for a transfer methods drop-down, filtering out the unsupported and firewalled methods + * + * @return array + */ + private function getTransferMethodOptions() + { + $options = []; + + foreach ($this->ftpSupport['supported'] as $method => $supported) + { + if (!$supported) + { + continue; + } + + $methodName = JText::_('COM_AKEEBA_TRANSFER_LBL_TRANSFERMETHOD_' . $method); + + if ($this->ftpSupport['firewalled'][$method]) + { + $methodName = '🔒 ' . $methodName; + } + + $options[] = JHtml::_('select.option', $method, $methodName); + } + + $options[] = JHtml::_('select.option', 'manual', JText::_('COM_AKEEBA_TRANSFER_LBL_TRANSFERMETHOD_MANUALLY')); + + return $options; + } + + /** + * Returns the JHTML options for a chunk methods drop-down + * + * @return array + */ + private function getChunkOptions() + { + $options = array(); + + $options[] = array('value' => 'chunked', 'text' => JText::_('COM_AKEEBA_TRANSFER_LBL_TRANSFERMODE_CHUNKED')); + $options[] = array('value' => 'post', 'text' => JText::_('COM_AKEEBA_TRANSFER_LBL_TRANSFERMODE_POST')); + + return $options; + } + + /** + * Returns the JHTML options for a chunk size drop-down + * + * @return array + */ + private function getChunkSizeOptions() + { + $options = array(); + $multiplier = 1048576; + + $options[] = array('value' => 0.5 * $multiplier, 'text' => '512 KB'); + $options[] = array('value' => 1 * $multiplier, 'text' => '1 MB'); + $options[] = array('value' => 2 * $multiplier, 'text' => '2 MB'); + $options[] = array('value' => 5 * $multiplier, 'text' => '5 MB'); + $options[] = array('value' => 10 * $multiplier, 'text' => '10 MB'); + $options[] = array('value' => 20 * $multiplier, 'text' => '20 MB'); + $options[] = array('value' => 30 * $multiplier, 'text' => '30 MB'); + $options[] = array('value' => 50 * $multiplier, 'text' => '50 MB'); + $options[] = array('value' => 100 * $multiplier, 'text' => '100 MB'); + + return $options; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default.php new file mode 100644 index 00000000..2d5b393a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default.php @@ -0,0 +1,30 @@ + + +force):?> +
        +

        +

        +
        + + +loadAnyTemplate('admin:com_akeeba/CommonTemplates/FTPBrowser'); ?> +loadAnyTemplate('admin:com_akeeba/CommonTemplates/SFTPBrowser'); ?> + +loadAnyTemplate('admin:com_akeeba/transfer/default_prerequisites'); ?> + +latestBackup))): ?> + loadAnyTemplate('admin:com_akeeba/transfer/default_remoteconnection'); ?> + loadAnyTemplate('admin:com_akeeba/transfer/default_manualtransfer'); ?> + loadAnyTemplate('admin:com_akeeba/transfer/default_upload'); ?> + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_manualtransfer.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_manualtransfer.php new file mode 100644 index 00000000..13ccf162 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_manualtransfer.php @@ -0,0 +1,67 @@ +latestBackup['archivename'], '.'); +$extension = substr($this->latestBackup['archivename'], $dotPos + 1); +$bareName = basename($this->latestBackup['archivename'], '.' . $extension); + +?> + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_prerequisites.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_prerequisites.php new file mode 100644 index 00000000..deea9f5c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_prerequisites.php @@ -0,0 +1,67 @@ +latestBackup) ? 'red' : 'information'; + +?> +
        +
        +

        + +

        +
        + + + + + + + + latestBackup))): ?> + + + + + + +
        + + + + +
        + + latestBackup)): ?> + + + lastBackupDate); ?> + + +
        + latestBackup)): ?> + + + + +
        + + spaceRequired['string']); ?> + +
        + +
        +
        +
        + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_remoteconnection.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_remoteconnection.php new file mode 100644 index 00000000..370146c6 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_remoteconnection.php @@ -0,0 +1,220 @@ + + +
        +
        +

        + +

        +
        + +
        +
        +
        + + +
        + + + + +
        +
        + +
        + + +
        + +
        +

        + +

        +
        + + + +
        +
        + + +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_upload.php b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_upload.php new file mode 100644 index 00000000..f10bc0e1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/Transfer/tmpl/default_upload.php @@ -0,0 +1,56 @@ + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileIdAndName.php b/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileIdAndName.php new file mode 100644 index 00000000..3cff246e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileIdAndName.php @@ -0,0 +1,62 @@ +container->factory->model('Profiles')->tmpInstance(); + $profileId = Platform::getInstance()->get_active_profile(); + + try + { + $this->profilename = $profilesModel->findOrFail($profileId)->description; + $this->profileid = $profileId; + $this->quickIcon = $profilesModel->quickicon; + } + catch (\Exception $e) + { + $this->container->platform->setSessionVar('profile', 1, 'akeeba'); + + $this->profileid = 1; + $this->profilename = $profilesModel->findOrFail(1)->description; + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileList.php b/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileList.php new file mode 100644 index 00000000..1ac4bca1 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/ViewTraits/ProfileList.php @@ -0,0 +1,66 @@ +container->db; + + $query = $db->getQuery(true) + ->select(array( + $db->qn('id'), + $db->qn('description') + ))->from($db->qn('#__ak_profiles')) + ->order($db->qn('id') . " ASC"); + + $db->setQuery($query); + $rawList = $db->loadAssocList(); + + $this->profileList = array(); + + if (!is_array($rawList)) + { + return; + } + + foreach ($rawList as $row) + { + $description = $row['description']; + + if ($includeId) + { + $description = '#' . $row['id'] . '. ' . $description; + } + + $this->profileList[] = JHtml::_('select.option', $row['id'], $description); + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/eaccelerator.php b/deployed/akeeba/administrator/components/com_akeeba/View/eaccelerator.php new file mode 100644 index 00000000..2a641aac --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/eaccelerator.php @@ -0,0 +1,35 @@ + + +
        +

        eAccelerator is not compatible with PHP 5.4 and later

        +
        +

        + Your host is using a broken, abandoned PHP extension which doesn't allow modern software to run. Ask your + host to disable eAccelerator before trying to run this component. +

        +

        + Your host is using eAccelerator, a code cache which is broken (example) + and abandoned in 2012. In fact, there is an open + issue on its project site about it being dead. Simple facts: +

        +
          +
        • eAccelerator breaks perfectly working PHP 5.4 code.
        • +
        • eAccelerator was abandoned in 2012.
        • +
        • No sane person should ever use eAccelerator on a production server because of the above.
        • +
        +

        + Please let your host know that they are using outdated software on their site and demand that they deactivate + it at once. Once eAccelerator is deactivated this message will go away and Akeeba Backup will work just fine. +

        +

        + Please note that PHP 5.5 and later come with Zend Opcache built in. It is a much better solution which is + actively supported by the people who make PHP. Ask your server to use it. +

        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/errorhandler.php b/deployed/akeeba/administrator/components/com_akeeba/View/errorhandler.php new file mode 100644 index 00000000..9a4eca1b --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/errorhandler.php @@ -0,0 +1,269 @@ +getCode(); +$code = !empty($code) ? $code : 500; +$app = \JFactory::getApplication(); +$isFrontend = $app instanceof JApplicationSite; +$hideTheError = $isFrontend && !(defined('JDEBUG') && (JDEBUG == 1)) && !JFactory::getUser()->authorise('core.admin'); + +// 403 and 404 are re-thrown +if (in_array($code, [403, 404])) +{ + throw $e; +} + +if (version_compare(JVERSION, '4', 'lt')) +{ + $app->setHeader('HTTP/1.1', $code); +} +else +{ + // In Joomla 4 we have to use the "Status" header, otherwise we get a fatal error saying that + // HTTP/1.1 is not a valid header + $app->setHeader('Status', $code); +} + +if (!$isFrontend) +{ + JToolbarHelper::title($title . ' Unhandled Exception'); +} + +?> + + +

        The application has stopped responding

        +

        + Please contact the administrator of the site and let them know of this error and what you were doing when this happened. +

        + + +

        - An unhandled Exception has been detected

        +

        + getMessage() ?> +

        +

        + File getFile()) ?> + Line getLine() ?> +

        + + +
        +

        + Would you like us to help you faster? +

        +

        + Save this page as PDF or HTML. When filing a support ticket please attach that PDF or HTML file. +

        +
        +

        + Why do we need all that information? This information is an x-ray of your site at the time the error + occurred. It lets us reproduce the issue or, if it's not a bug in our software, help you pinpoint the external reason which + led to it. +

        +

        + What about privacy? + Attachments are private in our ticket system: only you and us can see them, even if you file a public ticket, and + they are automatically deleted after a month. +

        + + +
        +

        + + + The content below this point is for developers and power users. + +

        +
        + +

        Debug information

        +

        + Exception type: +

        +
        getTraceAsString() ?>
        + +

        System information

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Operating System (reported by PHP)
        PHP version (as reported by your server)
        PHP Built On
        PHP SAPI
        Server identity
        Browser identity
        Joomla! version
        Database driver namegetName() ?>
        Database driver typegetServerType() ?>
        Database server versiongetVersion() ?>
        Database collationgetCollation()?>
        Database connection collationgetConnectionCollation()?>
        PHP Memory limit
        Peak Memory usage
        PHP Timeout (seconds)
        + +

        Request information

        +

        $_GET

        +
        +

        $_POST

        +
        +

        $_COOKIE

        +
        +

        $_REQUEST

        +
        + +

        Session state

        +
        getSession()->getData()->toArray());
        +	}
        +	else
        +	{
        +		print_r($app->getSession()->all());
        +	}
        +?>
        + +getDirectory(); + $extensions = $model->getExtensions(); + $phpSettings = $model->getPhpSettings(); + $hasPHPInfo = $model->phpinfoEnabled(); +} +catch (\Exception $e) +{ + /** + * If you are here, Joomla! had an unhandled exception inside its own code, typically decoding JSON. The only thing + * you can do is die. If you try returning the unhandled exception will bubble up Joomla's error handler and you're + * stuck with a misleading error. Sorry :( + */ + die; +} +?> + +

        PHP Settings

        + + $v): ?> + + + + + +
        + +getPhpInfoArray(); ?> +

        Loaded PHP Extensions

        + + $data): + if ($section == 'Core') continue; ?> + + + + + +
        + +
        + +
        + + +

        Enabled Extensions

        + + $info): + if (strtoupper($info['state']) != 'ENABLED') continue; ?> + + + + + + + + +
        + +

        Directory Status

        + + $v): ?> + + + + + +
        + + + + + Writeable + + Unwriteable + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/fef.php b/deployed/akeeba/administrator/components/com_akeeba/View/fef.php new file mode 100644 index 00000000..15a30364 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/fef.php @@ -0,0 +1,40 @@ + + +
        +

        Akeeba Frontend Framework (FEF) could not be found on this site

        +
        +
        +

        + This component requires the Akeeba Frontend Framework (FEF) to be installed on your site. Please go to our download page to download it, then install it on your site. +

        +
        +
        +

        Further information

        +

        + FEF is the name of our custom CSS framework. It's responsible for rendering the interface of our Joomla! + extensions. It is automatically installed when you install our extensions on your site. +

        +

        + FEF can be missing from your site either because Joomla failed to install it or because you, another Super User, + or another extension mistakenly uninstalled it. +

        +

        + If it's missing we cannot display the interface to this component. That's why you see this message. +

        +

        + You do not have to worry about adding bloat to your site. FEF is very small. It will also be automatically + uninstalled when you uninstall all components which depend on it. +

        +

        + FEF is installed in the media/fef folder under your site's root. It appears in Joomla's Extensions, + Manage page as file_fef. Please do not remove it from your site. +

        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/fof.php b/deployed/akeeba/administrator/components/com_akeeba/View/fof.php new file mode 100644 index 00000000..57780da9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/fof.php @@ -0,0 +1,58 @@ + + +
        +

        Akeeba Framework-on-Framework (FOF) version 3 could not be found on this site

        +
        +
        +

        + This component requires the Akeeba FOF framework package to be installed on your site. Please go to our download page to download it, then install it on your site. +

        +
        +
        +

        Further information

        +

        + FOF is a Joomla component framework. It's the low level code which sits between our Joomla! extensions and + Joomla! itself. It is automatically installed when you install our extensions on your site. +

        +

        + FOF can be missing from your site either because Joomla failed to install it or because you, another Super User, + or another extension mistakenly uninstalled it. +

        +

        + If it's missing, our components cannot talk to Joomla — or vice versa. Because of that they can not run. + That's why you see this message. +

        +

        + You do not have to worry about adding bloat to your site. FOF is very small. It will also be automatically + uninstalled when you uninstall all components which depend on it. +

        +

        + FOF is installed in the /fof30 folder on your + server. It appears in Joomla's Extensions, Manage page as FOF30. Please do not remove it from your + site. +

        + +

        Why do I have multiple FOF entries in Joomla?

        +

        + Joomla includes an old, obsolete version of FOF - version 2.x. It is installed + in the fof folder on your server. It appears in + Joomla's Extensions, Manage page as FOF. Please do not remove it from your site; Joomla needs it + to function properly. +

        +

        + We discontinued FOF 2.x in 2015 — that's years ago. Ever since, we replaced it + with FOF 3.x. The two versions are incompatible with each other but both are required; FOF 2.x for Joomla! + itself and FOF 3.x for our extensions. That's why you see both. You must not remove either of them or something + will break! +

        + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/hhvm.php b/deployed/akeeba/administrator/components/com_akeeba/View/hhvm.php new file mode 100644 index 00000000..35b3df3a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/hhvm.php @@ -0,0 +1,24 @@ + + +
        +

        We have detected that you are running HHVM instead of PHP. This software WILL NOT WORK properly on HHVM. Please switch to PHP 7 instead.

        +
        +

        + HHVM was Facebook's attempt at modernizing the PHP 5.x language and making it faster. Unfortunately it's also incompatible with PHP proper. + PHP 7 has solved all these issues. It's fast, modern and fully compatible with our software. + Please switch to PHP 7. If you are unsure how to do that, contact your host or the person responsible for maintaining your server. + They are the only people who can help you configure your server. +

        +

        + Kindly note that HHVM is not -and has never been- a supported execution environment for our software. + As a result, if you see this message on your site you are unfortunately ineligible for support and / or filing bug reports. + Please switch to PHP 7. If your problem persists after that we can help you / accept your bug report. Thank you for your understanding. +

        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/View/wrongphp.php b/deployed/akeeba/administrator/components/com_akeeba/View/wrongphp.php new file mode 100644 index 00000000..cae07dc7 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/View/wrongphp.php @@ -0,0 +1,80 @@ + + +
        +

        Outdated PHP version detected

        +
        +

        + Akeeba Backup requires PHP or any later version to work. +

        +

        + We strongly urge you to update to PHP or later. If you are + unsure how to do this, please ask your host. +

        +

        + Version numbers don't make sense? +

        + +
        + +

        Security advice

        +

        + Your version of PHP, , has reached the end + of its life on format(JText::_('DATE_FORMAT_LC1')) ?>. You are + strongly urged to upgrade to a current version, as using older versions may expose you to security + vulnerabilities and bugs that have been fixed in more recent versions of PHP. +

        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/access.xml b/deployed/akeeba/administrator/components/com_akeeba/access.xml new file mode 100644 index 00000000..13515a62 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/access.xml @@ -0,0 +1,15 @@ + + + +
        + + + + + +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/akeeba.php b/deployed/akeeba/administrator/components/com_akeeba/akeeba.php new file mode 100644 index 00000000..0ce14a4e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/akeeba.php @@ -0,0 +1,115 @@ +dispatcher->dispatch(); +}; + +function errorHandlerAkeebaBackupForJoomla($e) +{ + $title = 'Akeeba Backup'; + $isPro = defined(AKEEBA_PRO) ? AKEEBA_PRO : file_exists(__DIR__ . '/View/RegExDatabaseFilters/Html.php'); + if (!(include_once __DIR__ . '/View/errorhandler.php')) + { + throw $e; + } +} + +if (version_compare(PHP_VERSION, '7.0.0', 'lt')) +{ + // PHP 5.4, 5.5 and 5.6. Only user exceptions can be caught. + try + { + mainLoopAkeebaBackupForJoomla(); + } + catch (Exception $e) + { + errorHandlerAkeebaBackupForJoomla($e); + } +} +else +{ + // PHP 7.0 or later; we can catch PHP Fatal Errors as well + try + { + mainLoopAkeebaBackupForJoomla(); + } + catch (Throwable $e) + { + errorHandlerAkeebaBackupForJoomla($e); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/akeeba.xml b/deployed/akeeba/administrator/components/com_akeeba/akeeba.xml new file mode 100644 index 00000000..06ed2c9c --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/akeeba.xml @@ -0,0 +1,74 @@ + + + Akeeba + 2018-06-05 + Nicholas K. Dionysopoulos + nicholas@dionysopoulos.me + http://www.akeebabackup.com + Copyright (c)2006-2017 Akeeba Ltd / Nicholas K. Dionysopoulos + GNU GPL v3 or later + 6.1.1 + Akeeba Backup Core - Full Joomla! site backup solution, Core Edition. + + + + Controller + Dispatcher + Model + akeeba.php + + + + + en-GB/en-GB.com_akeeba.ini + + + + + css + js + icons + + + + + + COM_AKEEBA + + + + backup + BackupEngine + BackupPlatform + Controller + Dispatcher + fields + Helper + Master + Model + sql + Toolbar + View + views + + akeeba.php + fof.xml + restore.php + version.php + config.xml + access.xml + CHANGELOG.php + + + + + en-GB/en-GB.com_akeeba.ini + en-GB/en-GB.com_akeeba.sys.ini + + + + + + script.com_akeeba.php + diff --git a/deployed/akeeba/administrator/components/com_akeeba/backup/.htaccess b/deployed/akeeba/administrator/components/com_akeeba/backup/.htaccess new file mode 100644 index 00000000..5a72d653 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/backup/.htaccess @@ -0,0 +1,9 @@ + +Order deny,allow +Deny from all + + + + Require all denied + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.backend.id1.log b/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.backend.id1.log new file mode 100644 index 00000000..7b9b8d7a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.backend.id1.log @@ -0,0 +1,1209 @@ +DEBUG |180718 11:08:16|*** Batching successive steps (nesting level 1) +DEBUG |180718 11:08:16|====== Starting Step number 1 ====== +DEBUG |180718 11:08:16|Kettenrad :: Switching domains +DEBUG |180718 11:08:16|Kettenrad :: BREAKING STEP BEFORE SWITCHING DOMAIN +DEBUG |180718 11:08:16|Switching to domain init, class Init +DEBUG |180718 11:08:16|Akeeba\Engine\Core\Domain\Init :: New instance +DEBUG |180718 11:08:16|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:16|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:16|Kettenrad :: Break flag status: YES +DEBUG |180718 11:08:16|----- Finished operation 1 ------ +DEBUG |180718 11:08:16|Successful Smart algorithm on Akeeba\Engine\Core\Domain\Init +DEBUG |180718 11:08:16|====== Finished Step number 1 ====== +DEBUG |180718 11:08:16|Kettenrad :: Setting the break flag between domains +DEBUG |180718 11:08:16|*** Engine steps batching: Break flag detected. +DEBUG |180718 11:08:16|*** Batching of engine steps finished. I will now return control to the caller. +DEBUG |180718 11:08:16|====== Starting Step number 2 ====== +DEBUG |180718 11:08:16|Kettenrad :: Ticking the domain object +INFO |180718 11:08:16|-------------------------------------------------------------------------------- +INFO |180718 11:08:16|Akeeba Backup 6.1.1 (2018-06-05) +INFO |180718 11:08:16|Got backup? +INFO |180718 11:08:16|-------------------------------------------------------------------------------- +INFO |180718 11:08:16|--- System Information --- +INFO |180718 11:08:16|PHP Version :5.5.9-1ubuntu4.25 +INFO |180718 11:08:16|PHP OS :Linux +INFO |180718 11:08:16|PHP SAPI :cgi-fcgi +INFO |180718 11:08:16|OS Version :Linux +INFO |180718 11:08:16|DB Version :5.5.60-0ubuntu0.14.04.1 +INFO |180718 11:08:16|Web Server :Apache/2.4.7 (Ubuntu) +INFO |180718 11:08:16|Joomla! version :3.5.1 +INFO |180718 11:08:16|User agent :Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36 +INFO |180718 11:08:16|Safe mode : +INFO |180718 11:08:16|Display errors : +INFO |180718 11:08:16|Error reporting :E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE +INFO |180718 11:08:16|Error display :off +INFO |180718 11:08:16|Disabled functions : +INFO |180718 11:08:16|open_basedir restr.: +INFO |180718 11:08:16|Max. exec. time :0 +INFO |180718 11:08:16|Memory limit :512M +INFO |180718 11:08:16|Current mem. usage :5912864 +INFO |180718 11:08:16|GZIP Compression : available (good) +INFO |180718 11:08:16|JPATH_BASE :/administrator +INFO |180718 11:08:16|JPATH_SITE : +INFO |180718 11:08:16|JPATH_ROOT : +INFO |180718 11:08:16|JPATH_CACHE :/administrator/cache +INFO |180718 11:08:16|Computed root : +INFO |180718 11:08:16|Min/Max/Bias :2000/14/75 +INFO |180718 11:08:16|Output directory :/administrator/components/com_akeeba/backup +INFO |180718 11:08:16|Part size (bytes) :0 +INFO |180718 11:08:16|-------------------------------------------------------------------------------- +WARNING |180718 11:08:16|You are using PHP 5.5.9-1ubuntu4.25 which is officially End of Life. We recommend using PHP 7.0 or later for best results. Your version of PHP, 5.5.9-1ubuntu4.25, will stop being supported by this backup software in the future. +INFO |180718 11:08:16|Loaded profile #1 +DEBUG |180718 11:08:16|Archive template name: site-[HOST]-[DATE]-[TIME_TZ] +DEBUG |180718 11:08:16|Expanded template name: site-www.archlinexp.com-20180718-090816utc +DEBUG |180718 11:08:16|Backup type is now set to 'full' +DEBUG |180718 11:08:16|Expanded archive file name: /administrator/components/com_akeeba/backup/site-www.archlinexp.com-20180718-090816utc.jpa +DEBUG |180718 11:08:16|Initializing archiver engine +DEBUG |180718 11:08:16|Akeeba\Engine\Archiver\Jpa :: new instance - archive /administrator/components/com_akeeba/backup/site-www.archlinexp.com-20180718-090816utc.jpa +DEBUG |180718 11:08:16|Akeeba\Engine\Archiver\BaseArchiver :: Killing old archive +DEBUG |180718 11:08:16|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:16|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:16|----- Finished operation 1 ------ +DEBUG |180718 11:08:16|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:16|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:16|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:16|----- Finished operation 2 ------ +DEBUG |180718 11:08:16|Successful Smart algorithm on Akeeba\Engine\Core\Domain\Init +DEBUG |180718 11:08:16|Kettenrad :: Domain 'init' has finished. +DEBUG |180718 11:08:16|====== Finished Step number 2 ====== +DEBUG |180718 11:08:16|Kettenrad :: Setting the break flag between domains +DEBUG |180718 11:08:16|*** Engine steps batching: Break flag detected. +DEBUG |180718 11:08:16|*** Batching of engine steps finished. I will now return control to the caller. +DEBUG |180718 11:08:16|Sleeping for 1908.0557823181 msec, using usleep() +DEBUG |180718 11:08:18|Saving Kettenrad instance backend +DEBUG |180718 11:08:18|Kettenrad :: Attempting to load from database (backend) [backend.id1] +DEBUG |180718 11:08:18| -- Loaded stored Akeeba Factory (backend) [backend.id1] +DEBUG |180718 11:08:18|====== Starting Step number 3 ====== +DEBUG |180718 11:08:18|Kettenrad :: Switching domains +DEBUG |180718 11:08:18|Kettenrad :: BREAKING STEP BEFORE SWITCHING DOMAIN +DEBUG |180718 11:08:18|Switching to domain installer, class Installer +DEBUG |180718 11:08:18|Akeeba\Engine\Core\Domain\Installer :: New instance +DEBUG |180718 11:08:18|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:18|Truncating backup archive file /administrator/components/com_akeeba/backup/site-www.archlinexp.com-20180718-090816utc.jpa to 19 bytes +DEBUG |180718 11:08:18|-- Adding installation/README.html to archive (virtual data) +DEBUG |180718 11:08:18|-- Adding installation/extrainfo.ini to archive (virtual data) +DEBUG |180718 11:08:18|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:18|Kettenrad :: Break flag status: YES +DEBUG |180718 11:08:18|----- Finished operation 1 ------ +DEBUG |180718 11:08:18|Successful Smart algorithm on Akeeba\Engine\Core\Domain\Installer +DEBUG |180718 11:08:18|====== Finished Step number 3 ====== +DEBUG |180718 11:08:18|Kettenrad :: Setting the break flag between domains +DEBUG |180718 11:08:18|*** Engine steps batching: Break flag detected. +DEBUG |180718 11:08:18|*** Batching of engine steps finished. I will now return control to the caller. +DEBUG |180718 11:08:18|Sleeping for 1998.4760284424 msec, using usleep() +DEBUG |180718 11:08:20|Saving Kettenrad instance backend +DEBUG |180718 11:08:20|Kettenrad :: Attempting to load from database (backend) [backend.id1] +DEBUG |180718 11:08:20| -- Loaded stored Akeeba Factory (backend) [backend.id1] +DEBUG |180718 11:08:20|====== Starting Step number 4 ====== +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20|Initializing with JPA package /administrator/components/com_akeeba/Master/Installers/angie.jpa +DEBUG |180718 11:08:20| Adding installation/index.php; Next offset:1327 +DEBUG |180718 11:08:20|Truncating backup archive file /administrator/components/com_akeeba/backup/site-www.archlinexp.com-20180718-090816utc.jpa to 635 bytes +DEBUG |180718 11:08:20|-- Adding installation/index.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 1 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/version.php; Next offset:1618 +DEBUG |180718 11:08:20|-- Adding installation/version.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 2 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/defines.php; Next offset:2095 +DEBUG |180718 11:08:20|-- Adding installation/defines.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 3 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/css/bootstrap-responsive.min.css; Next offset:6151 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/css/bootstrap-responsive.min.css to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 4 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/css/footer.css; Next offset:6856 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/css/footer.css to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 5 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/css/bootstrap.min.css; Next offset:23970 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/css/bootstrap.min.css to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 6 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/index.php; Next offset:24949 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/index.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 7 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/js/jquery.js; Next offset:57767 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/js/jquery.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 8 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/js/bootstrap.min.js; Next offset:65285 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/js/bootstrap.min.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 9 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/js/jquery.simulate.js; Next offset:68051 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/js/jquery.simulate.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 10 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/php/messages.php; Next offset:68550 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/php/messages.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 11 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/php/head.php; Next offset:69257 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/php/head.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 12 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/php/buttonsfooter.php; Next offset:70007 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/php/buttonsfooter.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 13 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/php/buttons.php; Next offset:70595 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/php/buttons.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 14 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/img/loading_small.gif; Next offset:74511 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/img/loading_small.gif to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 15 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/img/loading_big.gif; Next offset:82068 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/img/loading_big.gif to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 16 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/img/glyphicons-halflings-white.png; Next offset:90665 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/img/glyphicons-halflings-white.png to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 17 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/img/glyphicons-halflings.png; Next offset:103349 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/img/glyphicons-halflings.png to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 18 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/template/angie/error.php; Next offset:104209 +DEBUG |180718 11:08:20|-- Adding installation/template/angie/error.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 19 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/tmp/index.html; Next offset:104293 +DEBUG |180718 11:08:20|-- Adding installation/tmp/index.html to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 20 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/tmp/web.config; Next offset:104455 +DEBUG |180718 11:08:20|-- Adding installation/tmp/web.config to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 21 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/tmp/.htaccess; Next offset:104606 +DEBUG |180718 11:08:20|-- Adding installation/tmp/.htaccess to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 22 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/view/view.php; Next offset:109411 +DEBUG |180718 11:08:20|-- Adding installation/framework/view/view.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 23 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/document/raw.php; Next offset:109761 +DEBUG |180718 11:08:20|-- Adding installation/framework/document/raw.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 24 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/document/document.php; Next offset:111764 +DEBUG |180718 11:08:20|-- Adding installation/framework/document/document.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 25 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/document/json.php; Next offset:112125 +DEBUG |180718 11:08:20|-- Adding installation/framework/document/json.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 26 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/document/html.php; Next offset:112544 +DEBUG |180718 11:08:20|-- Adding installation/framework/document/html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 27 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/download.php; Next offset:116268 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/download.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 28 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/adapter/cacert.pem; Next offset:282673 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/adapter/cacert.pem to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 29 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/adapter/curl.php; Next offset:284759 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/adapter/curl.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 30 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/adapter/fopen.php; Next offset:286379 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/adapter/fopen.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 31 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/adapter/abstract.php; Next offset:287661 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/adapter/abstract.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 32 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/download/interface.php; Next offset:288772 +DEBUG |180718 11:08:20|-- Adding installation/framework/download/interface.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 33 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/dispatcher/dispatcher.php; Next offset:290973 +DEBUG |180718 11:08:20|-- Adding installation/framework/dispatcher/dispatcher.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 34 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/model/model.php; Next offset:294610 +DEBUG |180718 11:08:20|-- Adding installation/framework/model/model.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 35 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/filter/input.php; Next offset:300562 +DEBUG |180718 11:08:20|-- Adding installation/framework/filter/input.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 36 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/session/session.php; Next offset:304369 +DEBUG |180718 11:08:20|-- Adding installation/framework/session/session.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 37 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/ftp/ftp.php; Next offset:309825 +DEBUG |180718 11:08:20|-- Adding installation/framework/ftp/ftp.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 38 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/timer/timer.php; Next offset:311661 +DEBUG |180718 11:08:20|-- Adding installation/framework/timer/timer.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 39 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Autoloader/Autoloader.php; Next offset:313514 +DEBUG |180718 11:08:20|-- Adding installation/framework/Autoloader/Autoloader.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 40 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/object/object.php; Next offset:314670 +DEBUG |180718 11:08:20|-- Adding installation/framework/object/object.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 41 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/README.md; Next offset:314968 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/README.md to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 42 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/phpunit.xml.dist; Next offset:315371 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/phpunit.xml.dist to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 43 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Parser.php; Next offset:320832 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Parser.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 44 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Unescaper.php; Next offset:322125 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Unescaper.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 45 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Inline.php; Next offset:326386 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Inline.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 46 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/CHANGELOG.md; Next offset:326606 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/CHANGELOG.md to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 47 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/LICENSE; Next offset:327310 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/LICENSE to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 48 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Exception/ExceptionInterface.php; Next offset:327674 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Exception/ExceptionInterface.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 49 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Exception/ParseException.php; Next offset:328760 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Exception/ParseException.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 50 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Exception/RuntimeException.php; Next offset:329167 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Exception/RuntimeException.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 51 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Exception/DumpException.php; Next offset:329539 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Exception/DumpException.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 52 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Dumper.php; Next offset:330521 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Dumper.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 53 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/composer.json; Next offset:330929 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/composer.json to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 54 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Yaml.php; Next offset:332197 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Yaml.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 55 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/Symfony/Component/Yaml/Escaper.php; Next offset:333397 +DEBUG |180718 11:08:20|-- Adding installation/framework/Symfony/Component/Yaml/Escaper.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 56 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/log/log.php; Next offset:334073 +DEBUG |180718 11:08:20|-- Adding installation/framework/log/log.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 57 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/input/input.php; Next offset:336219 +DEBUG |180718 11:08:20|-- Adding installation/framework/input/input.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 58 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/pimple/pimple.php; Next offset:338752 +DEBUG |180718 11:08:20|-- Adding installation/framework/pimple/pimple.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 59 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/pimple/ServiceProviderInterface.php; Next offset:339272 +DEBUG |180718 11:08:20|-- Adding installation/framework/pimple/ServiceProviderInterface.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 60 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/exception/download.php; Next offset:339607 +DEBUG |180718 11:08:20|-- Adding installation/framework/exception/download.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 61 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/exception/dispatch.php; Next offset:339940 +DEBUG |180718 11:08:20|-- Adding installation/framework/exception/dispatch.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 62 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/exception/app.php; Next offset:340265 +DEBUG |180718 11:08:20|-- Adding installation/framework/exception/app.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 63 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/exception/interface.php; Next offset:340577 +DEBUG |180718 11:08:20|-- Adding installation/framework/exception/interface.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 64 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/path.php; Next offset:341989 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/path.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 65 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/serialised.php; Next offset:344812 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/serialised.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 66 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/ip.php; Next offset:346211 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/ip.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 67 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/randval.php; Next offset:347857 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/randval.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 68 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/phptokenizer.php; Next offset:350439 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/phptokenizer.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 69 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/servertechnology.php; Next offset:351139 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/servertechnology.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 70 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/PasswordHash.php; Next offset:353621 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/PasswordHash.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 71 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/buffer.php; Next offset:355158 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/buffer.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 72 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/utils/password.php; Next offset:357913 +DEBUG |180718 11:08:20|-- Adding installation/framework/utils/password.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 73 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/uri/uri.php; Next offset:362489 +DEBUG |180718 11:08:20|-- Adding installation/framework/uri/uri.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 74 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/autoloader.php; Next offset:363430 +DEBUG |180718 11:08:20|-- Adding installation/framework/autoloader.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 75 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/controller/controller.php; Next offset:369861 +DEBUG |180718 11:08:20|-- Adding installation/framework/controller/controller.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 76 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/text/text.php; Next offset:371758 +DEBUG |180718 11:08:20|-- Adding installation/framework/text/text.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 77 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/application/application.php; Next offset:375247 +DEBUG |180718 11:08:20|-- Adding installation/framework/application/application.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 78 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/container/container.php; Next offset:375969 +DEBUG |180718 11:08:20|-- Adding installation/framework/container/container.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 79 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/postgresql.php; Next offset:384532 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/postgresql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 80 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/sqlazure.php; Next offset:385129 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/sqlazure.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 81 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/sqlite.php; Next offset:388146 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/sqlite.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 82 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/pdo.php; Next offset:394013 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/pdo.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 83 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/pdomysql.php; Next offset:398121 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/pdomysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 84 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/mysql.php; Next offset:401595 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/mysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 85 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/sqlsrv.php; Next offset:408057 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/sqlsrv.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 86 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver/mysqli.php; Next offset:413711 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver/mysqli.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 87 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query.php; Next offset:420494 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 88 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore.php; Next offset:426344 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 89 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/postgresql.php; Next offset:429586 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/postgresql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 90 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/sqlazure.php; Next offset:430274 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/sqlazure.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 91 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/sqlite.php; Next offset:432391 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/sqlite.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 92 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/preparable.php; Next offset:433363 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/preparable.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 93 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/pdo.php; Next offset:433675 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/pdo.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 94 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/pdomysql.php; Next offset:434169 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/pdomysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 95 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/mysql.php; Next offset:434657 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/mysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 96 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/sqlsrv.php; Next offset:436318 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/sqlsrv.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 97 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/mysqli.php; Next offset:437490 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/mysqli.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 98 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/query/limitable.php; Next offset:438443 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/query/limitable.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 99 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/factory.php; Next offset:440013 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/factory.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 100 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/postgresql.php; Next offset:442915 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/postgresql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 101 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/sqlazure.php; Next offset:443251 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/sqlazure.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 102 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/pdomysql.php; Next offset:443589 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/pdomysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 103 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/mysql.php; Next offset:443920 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/mysql.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 104 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/sqlsrv.php; Next offset:446624 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/sqlsrv.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 105 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/restore/mysqli.php; Next offset:452030 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/restore/mysqli.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 106 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/framework/database/driver.php; Next offset:461205 +DEBUG |180718 11:08:20|-- Adding installation/framework/database/driver.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 107 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/database.php; Next offset:462745 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/database.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 108 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/session.php; Next offset:463861 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/session.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 109 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/steps.php; Next offset:466152 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/steps.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 110 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/base/main.php; Next offset:467741 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/base/main.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 111 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/base/setup.php; Next offset:468906 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/base/setup.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 112 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/base/finalise.php; Next offset:470161 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/base/finalise.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 113 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/base/offsitedirs.php; Next offset:471383 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/base/offsitedirs.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 114 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/base/configuration.php; Next offset:472646 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/base/configuration.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 115 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/finalise.php; Next offset:472937 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/finalise.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 116 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/offsitedirs.php; Next offset:473232 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/offsitedirs.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 117 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/models/ftpbrowser.php; Next offset:474756 +DEBUG |180718 11:08:20|-- Adding installation/angie/models/ftpbrowser.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 118 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/database.js; Next offset:476408 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/database.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 119 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/ajax.js; Next offset:477682 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/ajax.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 120 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/ftpbrowser.js; Next offset:478046 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/ftpbrowser.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 121 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/finalise.js; Next offset:478733 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/finalise.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 122 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/offsitedirs.js; Next offset:479654 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/offsitedirs.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 123 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/js/json.js; Next offset:481157 +DEBUG |180718 11:08:20|-- Adding installation/angie/js/json.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 124 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/application.php; Next offset:481677 +DEBUG |180718 11:08:20|-- Adding installation/angie/application.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 125 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/language/en-GB.ini; Next offset:494324 +DEBUG |180718 11:08:20|-- Adding installation/angie/language/en-GB.ini to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 126 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/language/index.html; Next offset:494419 +DEBUG |180718 11:08:20|-- Adding installation/angie/language/index.html to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 127 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/dbrestore.php; Next offset:495271 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/dbrestore.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 128 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/session.php; Next offset:495784 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/session.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 129 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/base/main.php; Next offset:496266 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/base/main.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 130 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/base/setup.php; Next offset:496902 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/base/setup.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 131 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/base/finalise.php; Next offset:497532 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/base/finalise.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 132 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/finalise.php; Next offset:497832 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/finalise.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 133 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/offsitedirs.php; Next offset:498578 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/offsitedirs.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 134 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/controllers/password.php; Next offset:499130 +DEBUG |180718 11:08:20|-- Adding installation/angie/controllers/password.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 135 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/autoloader.php; Next offset:500088 +DEBUG |180718 11:08:20|-- Adding installation/angie/autoloader.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 136 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/dispatcher.php; Next offset:501218 +DEBUG |180718 11:08:20|-- Adding installation/angie/dispatcher.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 137 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/helpers/setup.php; Next offset:501856 +DEBUG |180718 11:08:20|-- Adding installation/angie/helpers/setup.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 138 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/helpers/ini.php; Next offset:503499 +DEBUG |180718 11:08:20|-- Adding installation/angie/helpers/ini.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 139 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/helpers/select.php; Next offset:507597 +DEBUG |180718 11:08:20|-- Adding installation/angie/helpers/select.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 140 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/assets/runscripts.php; Next offset:508730 +DEBUG |180718 11:08:20|-- Adding installation/angie/assets/runscripts.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 141 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/finalise/tmpl/success.php; Next offset:509088 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/finalise/tmpl/success.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 142 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/finalise/tmpl/config.php; Next offset:509534 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/finalise/tmpl/config.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 143 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/finalise/tmpl/default.php; Next offset:510704 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/finalise/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 144 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/session/view.html.php; Next offset:511089 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/session/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 145 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/session/tmpl/default.php; Next offset:512562 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/session/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 146 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/session/tmpl/blocked.php; Next offset:513354 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/session/tmpl/blocked.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 147 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/ftpbrowser/view.html.php; Next offset:513916 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/ftpbrowser/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 148 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/ftpbrowser/tmpl/default.php; Next offset:514877 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/ftpbrowser/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 149 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/offsitedirs/view.html.php; Next offset:515396 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/offsitedirs/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 150 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/offsitedirs/tmpl/default.php; Next offset:516643 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/offsitedirs/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 151 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/steps/tmpl/steps.php; Next offset:517567 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/steps/tmpl/steps.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 152 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/steps/tmpl/buttons.php; Next offset:518542 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/steps/tmpl/buttons.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 153 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/password/tmpl/default.php; Next offset:519292 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/password/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 154 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/runscripts/view.raw.php; Next offset:520130 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/runscripts/view.raw.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 155 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/main/view.raw.php; Next offset:521109 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/main/view.raw.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 156 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/main/tmpl/default.php; Next offset:521880 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/main/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 157 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/database/view.html.php; Next offset:522460 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/database/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 158 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/angie/views/database/tmpl/default.php; Next offset:524975 +DEBUG |180718 11:08:20|-- Adding installation/angie/views/database/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 159 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Skipping installation/sql/ +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 160 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Done with package angie.jpa +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 161 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20|Initializing with JPA package /administrator/components/com_akeeba/Master/Installers/angie-joomla.jpa +DEBUG |180718 11:08:20| Adding installation/platform/models/joomlaconfiguration.php; Next offset:2866 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/joomlaconfiguration.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 162 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/serverconfig/htaccess.txt; Next offset:4298 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/serverconfig/htaccess.txt to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 163 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/serverconfig/web.config.txt; Next offset:4924 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/serverconfig/web.config.txt to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 164 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/jconfig/j15.php; Next offset:5922 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/jconfig/j15.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 165 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/jconfig/j25.php; Next offset:6680 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/jconfig/j25.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 166 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/jconfig/j40.php; Next offset:8056 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/jconfig/j40.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 167 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/jconfig/j30.php; Next offset:8803 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/jconfig/j30.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 168 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/joomlasetup.php; Next offset:15001 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/joomlasetup.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 169 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/models/joomlamain.php; Next offset:16537 +DEBUG |180718 11:08:20|-- Adding installation/platform/models/joomlamain.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 170 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/js/setup.js; Next offset:17477 +DEBUG |180718 11:08:20|-- Adding installation/platform/js/setup.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 171 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/js/main.js; Next offset:18019 +DEBUG |180718 11:08:20|-- Adding installation/platform/js/main.js to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 172 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/defines.php; Next offset:18335 +DEBUG |180718 11:08:20|-- Adding installation/platform/defines.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 173 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/controllers/joomlasetup.php; Next offset:18643 +DEBUG |180718 11:08:20|-- Adding installation/platform/controllers/joomlasetup.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 174 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/controllers/joomlamain.php; Next offset:19299 +DEBUG |180718 11:08:20|-- Adding installation/platform/controllers/joomlamain.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 175 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/views/finalise/view.html.php; Next offset:20055 +DEBUG |180718 11:08:20|-- Adding installation/platform/views/finalise/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 176 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/views/main/tmpl/init.php; Next offset:21463 +DEBUG |180718 11:08:20|-- Adding installation/platform/views/main/tmpl/init.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 177 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/views/setup/view.html.php; Next offset:22552 +DEBUG |180718 11:08:20|-- Adding installation/platform/views/setup/view.html.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 178 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Adding installation/platform/views/setup/tmpl/default.php; Next offset:25774 +DEBUG |180718 11:08:20|-- Adding installation/platform/views/setup/tmpl/default.php to archive (virtual data) +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 179 ------ +DEBUG |180718 11:08:20|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:20| Done with package angie-joomla.jpa +DEBUG |180718 11:08:20| Done with installer seeding. +DEBUG |180718 11:08:20|Initializing with JPA package has finished +DEBUG |180718 11:08:20|Akeeba\Engine\Core\Domain\Installer:: archive is initialized +DEBUG |180718 11:08:20|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:20|Kettenrad :: Break flag status: no +DEBUG |180718 11:08:20|----- Finished operation 180 ------ +DEBUG |180718 11:08:20|Successful Smart algorithm on Akeeba\Engine\Core\Domain\Installer +DEBUG |180718 11:08:20|Kettenrad :: Domain 'installer' has finished. +DEBUG |180718 11:08:20|====== Finished Step number 4 ====== +DEBUG |180718 11:08:20|Kettenrad :: Setting the break flag between domains +DEBUG |180718 11:08:20|*** Engine steps batching: Break flag detected. +DEBUG |180718 11:08:20|*** Batching of engine steps finished. I will now return control to the caller. +DEBUG |180718 11:08:20|Sleeping for 1926.9299507141 msec, using usleep() +DEBUG |180718 11:08:22|Saving Kettenrad instance backend +DEBUG |180718 11:08:22|Kettenrad :: Attempting to load from database (backend) [backend.id1] +DEBUG |180718 11:08:22| -- Loaded stored Akeeba Factory (backend) [backend.id1] +DEBUG |180718 11:08:22|====== Starting Step number 5 ====== +DEBUG |180718 11:08:22|Kettenrad :: Switching domains +DEBUG |180718 11:08:22|Kettenrad :: BREAKING STEP BEFORE SWITCHING DOMAIN +DEBUG |180718 11:08:22|Switching to domain PackDB, class Db +DEBUG |180718 11:08:22|Akeeba\Engine\Core\Domain\Db :: New instance +DEBUG |180718 11:08:22|Kettenrad :: Ticking the domain object +DEBUG |180718 11:08:22|Akeeba\Engine\Core\Domain\Db :: Preparing instance +DEBUG |180718 11:08:22|Kettenrad :: Domain object returned; propagating +DEBUG |180718 11:08:22|Kettenrad :: Break flag status: YES +DEBUG |180718 11:08:22|----- Finished operation 1 ------ +DEBUG |180718 11:08:22|Successful Smart algorithm on Akeeba\Engine\Core\Domain\Db +DEBUG |180718 11:08:22|====== Finished Step number 5 ====== +DEBUG |180718 11:08:22|Kettenrad :: Setting the break flag between domains +DEBUG |180718 11:08:22|*** Engine steps batching: Break flag detected. +DEBUG |180718 11:08:22|*** Batching of engine steps finished. I will now return control to the caller. +DEBUG |180718 11:08:22|Sleeping for 1998.6479282379 msec, using usleep() +DEBUG |180718 11:08:24|Saving Kettenrad instance backend +DEBUG |180718 11:09:38|Kettenrad :: Attempting to load from database (backend.id1) [backend.id1] +DEBUG |180718 11:09:38| -- Loaded stored Akeeba Factory (backend.id1) [backend.id1] diff --git a/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.backend.log b/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.backend.log new file mode 100644 index 00000000..e69de29b diff --git a/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.log b/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.log new file mode 100644 index 00000000..2aa6b483 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/backup/akeeba.log @@ -0,0 +1,368 @@ +DEBUG |180718 11:09:57|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:09:57|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:10:08|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:10:08|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:10:19|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:10:19|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:10:30|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:10:30|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:10:40|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:10:40|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:10:51|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:10:51|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:11:02|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:02|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:11:12|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:12|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:11:23|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:23|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:11:34|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:34|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:11:44|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:44|mysql_close(): 36 is not a valid MySQL-Link resource +DEBUG |180718 11:11:55|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:11:55|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:06|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:06|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:16|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:16|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:27|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:27|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:38|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:38|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:49|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:49|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:12:59|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:12:59|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:13:10|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:13:10|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:13:21|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:13:21|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:13:31|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:13:31|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:13:42|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:13:42|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:13:53|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:13:53|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:03|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:03|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:14|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:14|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:25|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:25|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:35|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:35|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:46|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:46|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:14:57|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:14:57|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:15:07|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:15:07|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:15:18|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:15:18|mysql_close(): 36 is not a valid MySQL-Link resource +DEBUG |180718 11:15:29|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:15:29|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:15:40|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:15:40|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:15:50|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:15:50|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:01|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:01|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:12|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:12|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:23|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:23|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:33|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:33|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:44|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:44|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:16:55|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:16:55|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:05|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:05|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:16|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:16|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:27|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:27|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:37|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:37|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:48|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:48|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:17:59|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:17:59|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:18:09|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:18:09|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:18:20|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:18:20|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:18:31|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:18:31|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:18:42|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:18:42|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:18:53|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:18:53|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:19:03|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:19:03|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:19:14|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:19:14|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:19:25|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:19:25|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:19:29|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:19:29|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180718 11:19:29|PHP WARNING (not an error; you can ignore) on line 198 in file /administrator/components/com_akeeba/BackupEngine/Driver/Mysql.php: +DEBUG |180718 11:19:29|mysql_close(): 31 is not a valid MySQL-Link resource +DEBUG |180720 09:49:05|Fetching filter data from database +DEBUG |180720 09:49:05|Loading filters +DEBUG |180720 09:49:05|-- Loading filter Regextables +DEBUG |180720 09:49:05|-- Loading filter Regexdirectories +DEBUG |180720 09:49:05|-- Loading filter Files +DEBUG |180720 09:49:05|-- Loading filter Regexskipfiles +DEBUG |180720 09:49:05|-- Loading filter Tables +DEBUG |180720 09:49:05|-- Loading filter Skipfiles +DEBUG |180720 09:49:05|-- Loading filter Incremental +DEBUG |180720 09:49:05|-- Loading filter Tabledata +DEBUG |180720 09:49:05|-- Loading filter Multidb +DEBUG |180720 09:49:05|-- Loading filter Regexskipdirs +DEBUG |180720 09:49:05|-- Loading filter Regexfiles +DEBUG |180720 09:49:05|-- Loading filter Regextabledata +DEBUG |180720 09:49:05|-- Loading filter Extradirs +DEBUG |180720 09:49:05|-- Loading filter Skipdirs +DEBUG |180720 09:49:05|-- Loading filter Directories +DEBUG |180720 09:49:05|-- Loading filter Excludetabledata +DEBUG |180720 09:49:05|-- Loading filter Excludefiles +DEBUG |180720 09:49:05|-- Loading filter Systemcachefiles +DEBUG |180720 09:49:05|-- Loading filter Cvsfolders +DEBUG |180720 09:49:05|-- Loading filter Siteroot +DEBUG |180720 09:49:05|-- Loading filter Libraries +DEBUG |180720 09:49:05|-- Loading filter Joomlaskipdirs +DEBUG |180720 09:49:05|-- Loading filter Excludefolders +DEBUG |180720 09:49:05|-- Loading filter Joomlaskipfiles +DEBUG |180720 09:49:05|-- Loading filter Sitedb +DEBUG |180720 09:49:05|Loading optional filters +DEBUG |180720 09:49:05|-- Loading optional filter Stack\StackErrorlogs +DEBUG |180720 09:49:05|-- Loading optional filter Stack\StackHoststats +DEBUG |180720 09:49:05|-- Loading optional filter Stack\StackFinder +DEBUG |180720 09:49:05|-- Loading optional filter Stack\StackMyjoomla +DEBUG |190129 09:04:20|Fetching filter data from database +DEBUG |190129 09:04:20|Loading filters +DEBUG |190129 09:04:20|-- Loading filter Regextables +DEBUG |190129 09:04:20|-- Loading filter Regexdirectories +DEBUG |190129 09:04:20|-- Loading filter Files +DEBUG |190129 09:04:20|-- Loading filter Regexskipfiles +DEBUG |190129 09:04:20|-- Loading filter Tables +DEBUG |190129 09:04:20|-- Loading filter Skipfiles +DEBUG |190129 09:04:20|-- Loading filter Incremental +DEBUG |190129 09:04:20|-- Loading filter Tabledata +DEBUG |190129 09:04:20|-- Loading filter Multidb +DEBUG |190129 09:04:20|-- Loading filter Regexskipdirs +DEBUG |190129 09:04:20|-- Loading filter Regexfiles +DEBUG |190129 09:04:20|-- Loading filter Regextabledata +DEBUG |190129 09:04:20|-- Loading filter Extradirs +DEBUG |190129 09:04:20|-- Loading filter Skipdirs +DEBUG |190129 09:04:20|-- Loading filter Directories +DEBUG |190129 09:04:20|-- Loading filter Excludetabledata +DEBUG |190129 09:04:20|-- Loading filter Excludefiles +DEBUG |190129 09:04:20|-- Loading filter Systemcachefiles +DEBUG |190129 09:04:20|-- Loading filter Cvsfolders +DEBUG |190129 09:04:20|-- Loading filter Siteroot +DEBUG |190129 09:04:20|-- Loading filter Libraries +DEBUG |190129 09:04:20|-- Loading filter Joomlaskipdirs +DEBUG |190129 09:04:20|-- Loading filter Excludefolders +DEBUG |190129 09:04:20|-- Loading filter Joomlaskipfiles +DEBUG |190129 09:04:20|-- Loading filter Sitedb +DEBUG |190129 09:04:20|Loading optional filters +DEBUG |190129 09:04:20|-- Loading optional filter Stack\StackErrorlogs +DEBUG |190129 09:04:20|-- Loading optional filter Stack\StackHoststats +DEBUG |190129 09:04:20|-- Loading optional filter Stack\StackFinder +DEBUG |190129 09:04:20|-- Loading optional filter Stack\StackMyjoomla +DEBUG |191022 15:03:07|Fetching filter data from database +DEBUG |191022 15:03:07|Loading filters +DEBUG |191022 15:03:07|-- Loading filter Regextables +DEBUG |191022 15:03:07|-- Loading filter Regexdirectories +DEBUG |191022 15:03:07|-- Loading filter Files +DEBUG |191022 15:03:07|-- Loading filter Regexskipfiles +DEBUG |191022 15:03:07|-- Loading filter Tables +DEBUG |191022 15:03:07|-- Loading filter Skipfiles +DEBUG |191022 15:03:07|-- Loading filter Incremental +DEBUG |191022 15:03:07|-- Loading filter Tabledata +DEBUG |191022 15:03:07|-- Loading filter Multidb +DEBUG |191022 15:03:07|-- Loading filter Regexskipdirs +DEBUG |191022 15:03:07|-- Loading filter Regexfiles +DEBUG |191022 15:03:07|-- Loading filter Regextabledata +DEBUG |191022 15:03:07|-- Loading filter Extradirs +DEBUG |191022 15:03:07|-- Loading filter Skipdirs +DEBUG |191022 15:03:07|-- Loading filter Directories +DEBUG |191022 15:03:07|-- Loading filter Excludetabledata +DEBUG |191022 15:03:07|-- Loading filter Excludefiles +DEBUG |191022 15:03:07|-- Loading filter Systemcachefiles +DEBUG |191022 15:03:07|-- Loading filter Cvsfolders +DEBUG |191022 15:03:07|-- Loading filter Siteroot +DEBUG |191022 15:03:07|-- Loading filter Libraries +DEBUG |191022 15:03:07|-- Loading filter Joomlaskipdirs +DEBUG |191022 15:03:07|-- Loading filter Excludefolders +DEBUG |191022 15:03:07|-- Loading filter Joomlaskipfiles +DEBUG |191022 15:03:07|-- Loading filter Sitedb +DEBUG |191022 15:03:07|Loading optional filters +DEBUG |191022 15:03:07|-- Loading optional filter Stack\StackErrorlogs +DEBUG |191022 15:03:07|-- Loading optional filter Stack\StackHoststats +DEBUG |191022 15:03:07|-- Loading optional filter Stack\StackFinder +DEBUG |191022 15:03:07|-- Loading optional filter Stack\StackMyjoomla +DEBUG |191029 08:02:26|Fetching filter data from database +DEBUG |191029 08:02:26|Loading filters +DEBUG |191029 08:02:26|-- Loading filter Regextables +DEBUG |191029 08:02:26|-- Loading filter Regexdirectories +DEBUG |191029 08:02:26|-- Loading filter Files +DEBUG |191029 08:02:26|-- Loading filter Regexskipfiles +DEBUG |191029 08:02:26|-- Loading filter Tables +DEBUG |191029 08:02:26|-- Loading filter Skipfiles +DEBUG |191029 08:02:26|-- Loading filter Incremental +DEBUG |191029 08:02:26|-- Loading filter Tabledata +DEBUG |191029 08:02:26|-- Loading filter Multidb +DEBUG |191029 08:02:26|-- Loading filter Regexskipdirs +DEBUG |191029 08:02:26|-- Loading filter Regexfiles +DEBUG |191029 08:02:26|-- Loading filter Regextabledata +DEBUG |191029 08:02:26|-- Loading filter Extradirs +DEBUG |191029 08:02:26|-- Loading filter Skipdirs +DEBUG |191029 08:02:26|-- Loading filter Directories +DEBUG |191029 08:02:26|-- Loading filter Excludetabledata +DEBUG |191029 08:02:26|-- Loading filter Excludefiles +DEBUG |191029 08:02:26|-- Loading filter Systemcachefiles +DEBUG |191029 08:02:26|-- Loading filter Cvsfolders +DEBUG |191029 08:02:26|-- Loading filter Siteroot +DEBUG |191029 08:02:26|-- Loading filter Libraries +DEBUG |191029 08:02:26|-- Loading filter Joomlaskipdirs +DEBUG |191029 08:02:26|-- Loading filter Excludefolders +DEBUG |191029 08:02:26|-- Loading filter Joomlaskipfiles +DEBUG |191029 08:02:26|-- Loading filter Sitedb +DEBUG |191029 08:02:26|Loading optional filters +DEBUG |191029 08:02:26|-- Loading optional filter Stack\StackErrorlogs +DEBUG |191029 08:02:26|-- Loading optional filter Stack\StackHoststats +DEBUG |191029 08:02:26|-- Loading optional filter Stack\StackFinder +DEBUG |191029 08:02:26|-- Loading optional filter Stack\StackMyjoomla +DEBUG |191104 10:09:59|Fetching filter data from database +DEBUG |191104 10:09:59|Loading filters +DEBUG |191104 10:09:59|-- Loading filter Regextables +DEBUG |191104 10:09:59|-- Loading filter Regexdirectories +DEBUG |191104 10:09:59|-- Loading filter Files +DEBUG |191104 10:09:59|-- Loading filter Regexskipfiles +DEBUG |191104 10:09:59|-- Loading filter Tables +DEBUG |191104 10:09:59|-- Loading filter Skipfiles +DEBUG |191104 10:09:59|-- Loading filter Incremental +DEBUG |191104 10:09:59|-- Loading filter Tabledata +DEBUG |191104 10:09:59|-- Loading filter Multidb +DEBUG |191104 10:09:59|-- Loading filter Regexskipdirs +DEBUG |191104 10:09:59|-- Loading filter Regexfiles +DEBUG |191104 10:09:59|-- Loading filter Regextabledata +DEBUG |191104 10:09:59|-- Loading filter Extradirs +DEBUG |191104 10:09:59|-- Loading filter Skipdirs +DEBUG |191104 10:09:59|-- Loading filter Directories +DEBUG |191104 10:09:59|-- Loading filter Excludetabledata +DEBUG |191104 10:09:59|-- Loading filter Excludefiles +DEBUG |191104 10:09:59|-- Loading filter Systemcachefiles +DEBUG |191104 10:09:59|-- Loading filter Cvsfolders +DEBUG |191104 10:09:59|-- Loading filter Siteroot +DEBUG |191104 10:09:59|-- Loading filter Libraries +DEBUG |191104 10:09:59|-- Loading filter Joomlaskipdirs +DEBUG |191104 10:09:59|-- Loading filter Excludefolders +DEBUG |191104 10:09:59|-- Loading filter Joomlaskipfiles +DEBUG |191104 10:09:59|-- Loading filter Sitedb +DEBUG |191104 10:09:59|Loading optional filters +DEBUG |191104 10:09:59|-- Loading optional filter Stack\StackErrorlogs +DEBUG |191104 10:09:59|-- Loading optional filter Stack\StackHoststats +DEBUG |191104 10:09:59|-- Loading optional filter Stack\StackFinder +DEBUG |191104 10:09:59|-- Loading optional filter Stack\StackMyjoomla +DEBUG |200630 13:06:17|Fetching filter data from database +DEBUG |200630 13:06:17|Loading filters +DEBUG |200630 13:06:17|-- Loading filter Regextabledata +DEBUG |200630 13:06:17|-- Loading filter Skipdirs +DEBUG |200630 13:06:17|-- Loading filter Regexdirectories +DEBUG |200630 13:06:17|-- Loading filter Files +DEBUG |200630 13:06:17|-- Loading filter Regextables +DEBUG |200630 13:06:17|-- Loading filter Skipfiles +DEBUG |200630 13:06:17|-- Loading filter Regexfiles +DEBUG |200630 13:06:17|-- Loading filter Multidb +DEBUG |200630 13:06:17|-- Loading filter Tables +DEBUG |200630 13:06:17|-- Loading filter Regexskipdirs +DEBUG |200630 13:06:17|-- Loading filter Tabledata +DEBUG |200630 13:06:17|-- Loading filter Regexskipfiles +DEBUG |200630 13:06:17|-- Loading filter Extradirs +DEBUG |200630 13:06:17|-- Loading filter Incremental +DEBUG |200630 13:06:17|-- Loading filter Directories +DEBUG |200630 13:06:17|-- Loading filter Joomlaskipdirs +DEBUG |200630 13:06:17|-- Loading filter Cvsfolders +DEBUG |200630 13:06:17|-- Loading filter Siteroot +DEBUG |200630 13:06:17|-- Loading filter Systemcachefiles +DEBUG |200630 13:06:17|-- Loading filter Excludetabledata +DEBUG |200630 13:06:17|-- Loading filter Excludefiles +DEBUG |200630 13:06:17|-- Loading filter Libraries +DEBUG |200630 13:06:17|-- Loading filter Joomlaskipfiles +DEBUG |200630 13:06:17|-- Loading filter Excludefolders +DEBUG |200630 13:06:17|-- Loading filter Sitedb +DEBUG |200630 13:06:17|Loading optional filters +DEBUG |200630 13:06:17|-- Loading optional filter Stack\StackHoststats +DEBUG |200630 13:06:17|-- Loading optional filter Stack\StackErrorlogs +DEBUG |200630 13:06:17|-- Loading optional filter Stack\StackMyjoomla +DEBUG |200630 13:06:17|-- Loading optional filter Stack\StackFinder +DEBUG |221107 11:26:48|Fetching filter data from database +DEBUG |221107 11:26:48|Loading filters +DEBUG |221107 11:26:48|-- Loading filter Regextabledata +DEBUG |221107 11:26:48|-- Loading filter Skipdirs +DEBUG |221107 11:26:48|-- Loading filter Regexdirectories +DEBUG |221107 11:26:48|-- Loading filter Files +DEBUG |221107 11:26:48|-- Loading filter Regextables +DEBUG |221107 11:26:48|-- Loading filter Skipfiles +DEBUG |221107 11:26:48|-- Loading filter Regexfiles +DEBUG |221107 11:26:48|-- Loading filter Multidb +DEBUG |221107 11:26:48|-- Loading filter Tables +DEBUG |221107 11:26:48|-- Loading filter Regexskipdirs +DEBUG |221107 11:26:48|-- Loading filter Tabledata +DEBUG |221107 11:26:48|-- Loading filter Regexskipfiles +DEBUG |221107 11:26:48|-- Loading filter Extradirs +DEBUG |221107 11:26:48|-- Loading filter Incremental +DEBUG |221107 11:26:48|-- Loading filter Directories +DEBUG |221107 11:26:48|-- Loading filter Joomlaskipdirs +DEBUG |221107 11:26:48|-- Loading filter Cvsfolders +DEBUG |221107 11:26:48|-- Loading filter Siteroot +DEBUG |221107 11:26:48|-- Loading filter Systemcachefiles +DEBUG |221107 11:26:48|-- Loading filter Excludetabledata +DEBUG |221107 11:26:48|-- Loading filter Excludefiles +DEBUG |221107 11:26:48|-- Loading filter Libraries +DEBUG |221107 11:26:48|-- Loading filter Joomlaskipfiles +DEBUG |221107 11:26:48|-- Loading filter Excludefolders +DEBUG |221107 11:26:48|-- Loading filter Sitedb +DEBUG |221107 11:26:48|Loading optional filters +DEBUG |221107 11:26:48|-- Loading optional filter Stack\StackHoststats +DEBUG |221107 11:26:48|-- Loading optional filter Stack\StackErrorlogs +DEBUG |221107 11:26:48|-- Loading optional filter Stack\StackMyjoomla +DEBUG |221107 11:26:48|-- Loading optional filter Stack\StackFinder +DEBUG |250911 12:54:47|Fetching filter data from database +DEBUG |250911 12:54:47|Loading filters +DEBUG |250911 12:54:47|-- Loading filter Regextabledata +DEBUG |250911 12:54:47|-- Loading filter Skipdirs +DEBUG |250911 12:54:47|-- Loading filter Regexdirectories +DEBUG |250911 12:54:47|-- Loading filter Files +DEBUG |250911 12:54:47|-- Loading filter Regextables +DEBUG |250911 12:54:47|-- Loading filter Skipfiles +DEBUG |250911 12:54:47|-- Loading filter Regexfiles +DEBUG |250911 12:54:47|-- Loading filter Multidb +DEBUG |250911 12:54:47|-- Loading filter Tables +DEBUG |250911 12:54:47|-- Loading filter Regexskipdirs +DEBUG |250911 12:54:47|-- Loading filter Tabledata +DEBUG |250911 12:54:47|-- Loading filter Regexskipfiles +DEBUG |250911 12:54:47|-- Loading filter Extradirs +DEBUG |250911 12:54:47|-- Loading filter Incremental +DEBUG |250911 12:54:47|-- Loading filter Directories +DEBUG |250911 12:54:47|-- Loading filter Joomlaskipdirs +DEBUG |250911 12:54:47|-- Loading filter Cvsfolders +DEBUG |250911 12:54:47|-- Loading filter Siteroot +DEBUG |250911 12:54:47|-- Loading filter Systemcachefiles +DEBUG |250911 12:54:47|-- Loading filter Excludetabledata +DEBUG |250911 12:54:47|-- Loading filter Excludefiles +DEBUG |250911 12:54:47|-- Loading filter Libraries +DEBUG |250911 12:54:47|-- Loading filter Joomlaskipfiles +DEBUG |250911 12:54:47|-- Loading filter Excludefolders +DEBUG |250911 12:54:47|-- Loading filter Sitedb +DEBUG |250911 12:54:47|Loading optional filters +DEBUG |250911 12:54:47|-- Loading optional filter Stack\StackHoststats +DEBUG |250911 12:54:47|-- Loading optional filter Stack\StackErrorlogs +DEBUG |250911 12:54:47|-- Loading optional filter Stack\StackMyjoomla +DEBUG |250911 12:54:47|-- Loading optional filter Stack\StackFinder diff --git a/deployed/akeeba/administrator/components/com_akeeba/backup/web.config b/deployed/akeeba/administrator/components/com_akeeba/backup/web.config new file mode 100644 index 00000000..ce3e300e --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/backup/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/config.xml b/deployed/akeeba/administrator/components/com_akeeba/config.xml new file mode 100644 index 00000000..8330ae47 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/config.xml @@ -0,0 +1,203 @@ + + + +
        + + +
        + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + + + + + + + + + + + + + + + + + + + +
        + +
        + + + + +
        + +
        + + + + + + + + + + + + + + +
        + +
        + + + + + + + + + + + +
        + +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/fields/akencrypted.php b/deployed/akeeba/administrator/components/com_akeeba/fields/akencrypted.php new file mode 100644 index 00000000..e775958f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/fields/akencrypted.php @@ -0,0 +1,59 @@ +value = $this->conditionalDecrypt($this->value); + + return parent::getInput(); + } + + private function conditionalDecrypt($value) + { + // If the Factory is not already loaded we have to load the + if (!class_exists('Akeeba\Engine\Factory')) + { + if (!defined('FOF30_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof30/include.php')) + { + return $value; + } + + $container = \FOF30\Container\Container::getInstance('com_akeeba', array(), 'admin'); + + /** @var \Akeeba\Backup\Admin\Dispatcher\Dispatcher $dispatcher */ + $dispatcher = $container->dispatcher; + + try + { + $dispatcher->loadAkeebaEngine(); + $dispatcher->loadAkeebaEngineConfiguration(); + } + catch (Exception $e) + { + return $value; + } + } + + $secureSettings = \Akeeba\Engine\Factory::getSecureSettings(); + + return $secureSettings->decryptSettings($this->value); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/fields/backupprofiles.php b/deployed/akeeba/administrator/components/com_akeeba/fields/backupprofiles.php new file mode 100644 index 00000000..db6b5716 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/fields/backupprofiles.php @@ -0,0 +1,71 @@ +getQuery(true) + ->select(array( + $db->qn('id'), + $db->qn('description'), + ))->from($db->qn('#__ak_profiles')); + $db->setQuery($query); + $key = 'id'; + $val = 'description'; + + $objectList = $db->loadObjectList(); + + if (!is_array($objectList)) + { + $objectList = array(); + } + + foreach ($objectList as $o) + { + $o->description = "#{$o->id}: {$o->description}"; + } + + $showNone = $this->element['show_none'] ? (string)$this->element['show_none'] : ''; + $showNone = in_array(strtolower($showNone), array('yes', '1', 'true', 'on')); + + if ($showNone) + { + $defaultItem = (object)array( + 'id' => '0', + 'description' => JText::_('COM_AKEEBA_FORMFIELD_BACKUPPROFILES_NONE') + ); + + array_unshift($objectList, $defaultItem); + } + + return JHTML::_('select.genericlist', $objectList, $this->name, 'class="inputbox"', $key, $val, $this->value, $this->id); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/fields/urlencoded.php b/deployed/akeeba/administrator/components/com_akeeba/fields/urlencoded.php new file mode 100644 index 00000000..d4e63089 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/fields/urlencoded.php @@ -0,0 +1,28 @@ +value = urlencode($this->value); + + return parent::getInput(); + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/fof.xml b/deployed/akeeba/administrator/components/com_akeeba/fof.xml new file mode 100644 index 00000000..fb163b6f --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/fof.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + Filters + + + Filters + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/index.html b/deployed/akeeba/administrator/components/com_akeeba/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/restore.php b/deployed/akeeba/administrator/components/com_akeeba/restore.php new file mode 100644 index 00000000..e4aa9e5a --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/restore.php @@ -0,0 +1,8475 @@ +|'), + array('*' => '.*', '?' => '.?')) . '$/i', $string + ); + } +} + +// Unicode-safe binary data length function +if (!function_exists('akstringlen')) +{ + if (function_exists('mb_strlen')) + { + function akstringlen($string) + { + return mb_strlen($string, '8bit'); + } + } + else + { + function akstringlen($string) + { + return strlen($string); + } + } +} + +if (!function_exists('aksubstr')) +{ + if (function_exists('mb_strlen')) + { + function aksubstr($string, $start, $length = null) + { + return mb_substr($string, $start, $length, '8bit'); + } + } + else + { + function aksubstr($string, $start, $length = null) + { + return substr($string, $start, $length); + } + } +} + +/** + * Gets a query parameter from GET or POST data + * + * @param $key + * @param $default + */ +function getQueryParam($key, $default = null) +{ + $value = $default; + + if (array_key_exists($key, $_REQUEST)) + { + $value = $_REQUEST[$key]; + } + + if (get_magic_quotes_gpc() && !is_null($value)) + { + $value = stripslashes($value); + } + + return $value; +} + +// Debugging function +function debugMsg($msg) +{ + if (!defined('KSDEBUG')) + { + return; + } + + $fp = fopen('debug.txt', 'at'); + + fwrite($fp, $msg . "\n"); + fclose($fp); + + // Echo to stdout if KSDEBUGCLI is defined + if (defined('KSDEBUGCLI')) + { + echo $msg . "\n"; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * 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 AKAbstractObject +{ + /** @var array The queue size of the $_errors array. Set to 0 for infinite size. */ + protected $_errors_queue_size = 0; + /** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */ + protected $_warnings_queue_size = 0; + /** @var array An array of errors */ + private $_errors = array(); + /** @var array An array of warnings */ + private $_warnings = array(); + + /** + * Public constructor, makes sure we are instantiated only by the factory class + */ + public function __construct() + { + /* + // Assisted Singleton pattern + if(function_exists('debug_backtrace')) + { + $caller=debug_backtrace(); + if( + ($caller[1]['class'] != 'AKFactory') && + ($caller[2]['class'] != 'AKFactory') && + ($caller[3]['class'] != 'AKFactory') && + ($caller[4]['class'] != 'AKFactory') + ) { + var_dump(debug_backtrace()); + trigger_error("You can't create direct descendants of ".__CLASS__, E_USER_ERROR); + } + } + */ + } + + /** + * 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); + } + + /** + * 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 + */ + private function getItemFromArray($array, $i = null) + { + // Find the item + if ($i === null) + { + // Default, return the last item + $item = end($array); + } + else if (!array_key_exists($i, $array)) + { + // If $i has been specified but does not exist, return false + return false; + } + else + { + $item = $array[$i]; + } + + return $item; + } + + /** + * Return all errors, if any + * + * @return array Array of error messages + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Resets all error messages + */ + 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; + } + + /** + * Resets all warning messages + */ + public function resetWarnings() + { + $this->_warnings = array(); + } + + /** + * Propagates errors and warnings to a foreign object. The foreign object SHOULD + * implement the setError() and/or setWarning() methods but DOESN'T HAVE TO be of + * AKAbstractObject type. For example, this can even be used to propagate to a + * JObject instance in Joomla!. Propagated items will be removed from ourselves. + * + * @param object $object The object to propagate errors and warnings to. + */ + 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 object $object The object to propagate errors and warnings from + */ + public function propagateFromObject(&$object) + { + if (method_exists($object, 'getErrors')) + { + $errors = $object->getErrors(); + if (!empty($errors)) + { + foreach ($errors as $error) + { + $this->setError($error); + } + } + 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); + } + } + if (method_exists($object, 'resetWarnings')) + { + $object->resetWarnings(); + } + } + } + + /** + * Add an error message + * + * @param string $error Error message + */ + public function setError($error) + { + if ($this->_errors_queue_size > 0) + { + if (count($this->_errors) >= $this->_errors_queue_size) + { + array_shift($this->_errors); + } + } + + $this->_errors[] = $error; + } + + /** + * Add an error message + * + * @param string $error Error message + */ + public function setWarning($warning) + { + if ($this->_warnings_queue_size > 0) + { + if (count($this->_warnings) >= $this->_warnings_queue_size) + { + array_shift($this->_warnings); + } + } + + $this->_warnings[] = $warning; + } + + /** + * 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. + */ + 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. + */ + protected function setWarningsQueueSize($newSize = 0) + { + $this->_warnings_queue_size = (int) $newSize; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The superclass of all Akeeba Kickstart 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 AKAbstractPart extends AKAbstractObject +{ + /** + * 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 array An array of observers */ + protected $observers = array(); + /** @var int Last reported warnings's position in array */ + private $warnings_pointer = -1; + + /** + * 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. + * + * @return array A Response Array + */ + final public function tick() + { + // 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; + } + + // Send a Return Table back to the caller + $out = $this->_makeReturnTable(); + + return $out; + } + + /** + * 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. + */ + final public function getState() + { + if ($this->getError()) + { + return "error"; + } + + if (!($this->isPrepared)) + { + return "init"; + } + + if (!($this->isFinished) && !($this->isRunning) && !($this->hasRun) && ($this->isPrepared)) + { + return "prepared"; + } + + if (!($this->isFinished) && $this->isRunning && !($this->hasRun)) + { + return "running"; + } + + if (!($this->isFinished) && !($this->isRunning) && $this->hasRun) + { + return "postrun"; + } + + if ($this->isFinished) + { + return "finished"; + } + } + + /** + * Runs the preparation for this part. Should set _isPrepared + * to true + */ + abstract protected function _prepare(); + + /** + * 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. + */ + abstract protected function _run(); + + /** + * Runs the finalisation process for this part. Should set + * _isFinished to true. + */ + abstract protected function _finalize(); + + /** + * Constructs a Response Array based on the engine part's state. + * + * @return array The Response Array for the current state + */ + final 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; + } + + /** + * Returns a copy of the class's status array + * + * @return 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. + */ + final public function setup($parametersArray) + { + if ($this->isPrepared) + { + $this->setState('error', "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']; + } + } + } + + /** + * 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 + */ + protected function setState($state = 'init', $errorMessage = 'Invalid setState argument') + { + switch ($state) + { + case 'init': + $this->isPrepared = false; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'prepared': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'running': + $this->isPrepared = true; + $this->isRunning = true; + $this->isFinished = false; + $this->hasRun = false; + break; + + case 'postrun': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = false; + $this->hasRun = true; + break; + + case 'finished': + $this->isPrepared = true; + $this->isRunning = false; + $this->isFinished = true; + $this->hasRun = false; + break; + + case 'error': + default: + $this->setError($errorMessage); + break; + } + } + + final public function getDomain() + { + return $this->active_domain; + } + + final public function getStep() + { + return $this->active_step; + } + + final public function getSubstep() + { + return $this->active_substep; + } + + /** + * Attaches an observer object + * + * @param AKAbstractPartObserver $obs + */ + function attach(AKAbstractPartObserver $obs) + { + $this->observers["$obs"] = $obs; + } + + /** + * Detaches an observer object + * + * @param AKAbstractPartObserver $obs + */ + function detach(AKAbstractPartObserver $obs) + { + delete($this->observers["$obs"]); + } + + /** + * Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately, + * in fear of timing out. + */ + protected function setBreakFlag() + { + AKFactory::set('volatile.breakflag', true); + } + + final protected function setDomain($new_domain) + { + $this->active_domain = $new_domain; + } + + final protected function setStep($new_step) + { + $this->active_step = $new_step; + } + + final protected function setSubstep($new_substep) + { + $this->active_substep = $new_substep; + } + + /** + * Notifies observers each time something interesting happened to the part + * + * @param mixed $message The event object + */ + protected function notify($message) + { + foreach ($this->observers as $obs) + { + $obs->update($this, $message); + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The base class of unarchiver classes + */ +abstract class AKAbstractUnarchiver extends AKAbstractPart +{ + /** @var array List of the names of all archive parts */ + public $archiveList = array(); + /** @var int The total size of all archive parts */ + public $totalSize = array(); + /** @var array Which files to rename */ + public $renameFiles = array(); + /** @var array Which directories to rename */ + public $renameDirs = array(); + /** @var array Which files to skip */ + public $skipFiles = array(); + /** @var string Archive filename */ + protected $filename = null; + /** @var integer Current archive part number */ + protected $currentPartNumber = -1; + /** @var integer The offset inside the current part */ + protected $currentPartOffset = 0; + /** @var bool Should I restore permissions? */ + protected $flagRestorePermissions = false; + /** @var AKAbstractPostproc Post processing class */ + protected $postProcEngine = null; + /** @var string Absolute path to prepend to extracted files */ + protected $addPath = ''; + /** @var string Absolute path to remove from extracted files */ + protected $removePath = ''; + /** @var integer Chunk size for processing */ + protected $chunkSize = 524288; + + /** @var resource File pointer to the current archive part file */ + protected $fp = null; + + /** @var int Run state when processing the current archive file */ + protected $runState = null; + + /** @var stdClass File header data, as read by the readFileHeader() method */ + protected $fileHeader = null; + + /** @var int How much of the uncompressed data we've read so far */ + protected $dataReadLength = 0; + + /** @var array Unwriteable files in these directories are always ignored and do not cause errors when not extracted */ + protected $ignoreDirectories = array(); + + /** + * Public constructor + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Wakeup function, called whenever the class is unserialized + */ + public function __wakeup() + { + if ($this->currentPartNumber >= 0) + { + $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); + if ((is_resource($this->fp)) && ($this->currentPartOffset > 0)) + { + @fseek($this->fp, $this->currentPartOffset); + } + } + } + + /** + * Sleep function, called whenever the class is serialized + */ + public function shutdown() + { + if (is_resource($this->fp)) + { + $this->currentPartOffset = @ftell($this->fp); + @fclose($this->fp); + } + } + + /** + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory + */ + public function isIgnoredDirectory($shortFilename) + { + // return false; + + if (substr($shortFilename, -1) == '/') + { + $check = rtrim($shortFilename, '/'); + } + else + { + $check = dirname($shortFilename); + } + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Implements the abstract _prepare() method + */ + final protected function _prepare() + { + parent::__construct(); + + if (count($this->_parametersArray) > 0) + { + foreach ($this->_parametersArray as $key => $value) + { + switch ($key) + { + // Archive's absolute filename + case 'filename': + $this->filename = $value; + + // Sanity check + if (!empty($value)) + { + $value = strtolower($value); + + if (strlen($value) > 6) + { + if ( + (substr($value, 0, 7) == 'http://') + || (substr($value, 0, 8) == 'https://') + || (substr($value, 0, 6) == 'ftp://') + || (substr($value, 0, 7) == 'ssh2://') + || (substr($value, 0, 6) == 'ssl://') + ) + { + $this->setState('error', 'Invalid archive location'); + } + } + } + + + break; + + // Should I restore permissions? + case 'restore_permissions': + $this->flagRestorePermissions = $value; + break; + + // Should I use FTP? + case 'post_proc': + $this->postProcEngine = AKFactory::getpostProc($value); + break; + + // Path to add in the beginning + case 'add_path': + $this->addPath = $value; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + if (!empty($this->addPath)) + { + $this->addPath .= '/'; + } + break; + + // Path to remove from the beginning + case 'remove_path': + $this->removePath = $value; + $this->removePath = str_replace('\\', '/', $this->removePath); + $this->removePath = rtrim($this->removePath, '/'); + if (!empty($this->removePath)) + { + $this->removePath .= '/'; + } + break; + + // Which files to rename (hash array) + case 'rename_files': + $this->renameFiles = $value; + break; + + // Which files to rename (hash array) + case 'rename_dirs': + $this->renameDirs = $value; + break; + + // Which files to skip (indexed array) + case 'skip_files': + $this->skipFiles = $value; + break; + + // Which directories to ignore when we can't write files in them (indexed array) + case 'ignoredirectories': + $this->ignoreDirectories = $value; + break; + } + } + } + + $this->scanArchives(); + + $this->readArchiveHeader(); + $errMessage = $this->getError(); + if (!empty($errMessage)) + { + $this->setState('error', $errMessage); + } + else + { + $this->runState = AK_STATE_NOFILE; + $this->setState('prepared'); + } + } + + /** + * Scans for archive parts + */ + private function scanArchives() + { + if (defined('KSDEBUG')) + { + @unlink('debug.txt'); + } + debugMsg('Preparing to scan archives'); + + $privateArchiveList = array(); + + // Get the components of the archive filename + $dirname = dirname($this->filename); + $base_extension = $this->getBaseExtension(); + $basename = basename($this->filename, $base_extension); + $this->totalSize = 0; + + // Scan for multiple parts until we don't find any more of them + $count = 0; + $found = true; + $this->archiveList = array(); + while ($found) + { + ++$count; + $extension = substr($base_extension, 0, 2) . sprintf('%02d', $count); + $filename = $dirname . DIRECTORY_SEPARATOR . $basename . $extension; + $found = file_exists($filename); + if ($found) + { + debugMsg('- Found archive ' . $filename); + // Add yet another part, with a numeric-appended filename + $this->archiveList[] = $filename; + + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + else + { + debugMsg('- Found archive ' . $this->filename); + // Add the last part, with the regular extension + $this->archiveList[] = $this->filename; + + $filename = $this->filename; + $filesize = @filesize($filename); + $this->totalSize += $filesize; + + $privateArchiveList[] = array($filename, $filesize); + } + } + debugMsg('Total archive parts: ' . $count); + + $this->currentPartNumber = -1; + $this->currentPartOffset = 0; + $this->runState = AK_STATE_NOFILE; + + // Send start of file notification + $message = new stdClass; + $message->type = 'totalsize'; + $message->content = new stdClass; + $message->content->totalsize = $this->totalSize; + $message->content->filelist = $privateArchiveList; + $this->notify($message); + } + + /** + * Returns the base extension of the file, e.g. '.jpa' + * + * @return string + */ + private function getBaseExtension() + { + static $baseextension; + + if (empty($baseextension)) + { + $basename = basename($this->filename); + $lastdot = strrpos($basename, '.'); + $baseextension = substr($basename, $lastdot); + } + + return $baseextension; + } + + /** + * Concrete classes are supposed to use this method in order to read the archive's header and + * prepare themselves to the point of being ready to extract the first file. + */ + protected abstract function readArchiveHeader(); + + protected function _run() + { + if ($this->getState() == 'postrun') + { + return; + } + + $this->setState('running'); + + $timer = AKFactory::getTimer(); + + $status = true; + while ($status && ($timer->getTimeLeft() > 0)) + { + switch ($this->runState) + { + case AK_STATE_NOFILE: + debugMsg(__CLASS__ . '::_run() - Reading file header'); + $status = $this->readFileHeader(); + if ($status) + { + // Send start of file notification + $message = new stdClass; + $message->type = 'startfile'; + $message->content = new stdClass; + $message->content->realfile = $this->fileHeader->file; + $message->content->file = $this->fileHeader->file; + $message->content->uncompressed = $this->fileHeader->uncompressed; + + if (array_key_exists('realfile', get_object_vars($this->fileHeader))) + { + $message->content->realfile = $this->fileHeader->realFile; + } + + if (array_key_exists('compressed', get_object_vars($this->fileHeader))) + { + $message->content->compressed = $this->fileHeader->compressed; + } + else + { + $message->content->compressed = 0; + } + + debugMsg(__CLASS__ . '::_run() - Preparing to extract ' . $message->content->realfile); + + $this->notify($message); + } + else + { + debugMsg(__CLASS__ . '::_run() - Could not read file header'); + } + break; + + case AK_STATE_HEADER: + case AK_STATE_DATA: + debugMsg(__CLASS__ . '::_run() - Processing file data'); + $status = $this->processFileData(); + break; + + case AK_STATE_DATAREAD: + case AK_STATE_POSTPROC: + debugMsg(__CLASS__ . '::_run() - Calling post-processing class'); + $this->postProcEngine->timestamp = $this->fileHeader->timestamp; + $status = $this->postProcEngine->process(); + $this->propagateFromObject($this->postProcEngine); + $this->runState = AK_STATE_DONE; + break; + + case AK_STATE_DONE: + default: + if ($status) + { + debugMsg(__CLASS__ . '::_run() - Finished extracting file'); + // Send end of file notification + $message = new stdClass; + $message->type = 'endfile'; + $message->content = new stdClass; + if (array_key_exists('realfile', get_object_vars($this->fileHeader))) + { + $message->content->realfile = $this->fileHeader->realFile; + } + else + { + $message->content->realfile = $this->fileHeader->file; + } + $message->content->file = $this->fileHeader->file; + if (array_key_exists('compressed', get_object_vars($this->fileHeader))) + { + $message->content->compressed = $this->fileHeader->compressed; + } + else + { + $message->content->compressed = 0; + } + $message->content->uncompressed = $this->fileHeader->uncompressed; + $this->notify($message); + } + $this->runState = AK_STATE_NOFILE; + continue; + } + } + + $error = $this->getError(); + if (!$status && ($this->runState == AK_STATE_NOFILE) && empty($error)) + { + debugMsg(__CLASS__ . '::_run() - Just finished'); + // We just finished + $this->setState('postrun'); + } + elseif (!empty($error)) + { + debugMsg(__CLASS__ . '::_run() - Halted with an error:'); + debugMsg($error); + $this->setState('error', $error); + } + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected abstract function readFileHeader(); + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected abstract function processFileData(); + + protected function _finalize() + { + // Nothing to do + $this->setState('finished'); + } + + /** + * Opens the next part file for reading + */ + protected function nextFile() + { + debugMsg('Current part is ' . $this->currentPartNumber . '; opening the next part'); + ++$this->currentPartNumber; + + if ($this->currentPartNumber > (count($this->archiveList) - 1)) + { + $this->setState('postrun'); + + return false; + } + else + { + if (is_resource($this->fp)) + { + @fclose($this->fp); + } + debugMsg('Opening file ' . $this->archiveList[$this->currentPartNumber]); + $this->fp = @fopen($this->archiveList[$this->currentPartNumber], 'rb'); + if ($this->fp === false) + { + debugMsg('Could not open file - crash imminent'); + $this->setError(AKText::sprintf('ERR_COULD_NOT_OPEN_ARCHIVE_PART', $this->archiveList[$this->currentPartNumber])); + } + fseek($this->fp, 0); + $this->currentPartOffset = 0; + + return true; + } + } + + /** + * Returns true if we have reached the end of file + * + * @param $local bool True to return EOF of the local file, false (default) to return if we have reached the end of + * the archive set + * + * @return bool True if we have reached End Of File + */ + protected function isEOF($local = false) + { + $eof = @feof($this->fp); + + if (!$eof) + { + // Border case: right at the part's end (eeeek!!!). For the life of me, I don't understand why + // feof() doesn't report true. It expects the fp to be positioned *beyond* the EOF to report + // true. Incredible! :( + $position = @ftell($this->fp); + $filesize = @filesize($this->archiveList[$this->currentPartNumber]); + if ($filesize <= 0) + { + // 2Gb or more files on a 32 bit version of PHP tend to get screwed up. Meh. + $eof = false; + } + elseif ($position >= $filesize) + { + $eof = true; + } + } + + if ($local) + { + return $eof; + } + else + { + return $eof && ($this->currentPartNumber >= (count($this->archiveList) - 1)); + } + } + + /** + * Tries to make a directory user-writable so that we can write a file to it + * + * @param $path string A path to a file + */ + protected function setCorrectPermissions($path) + { + static $rootDir = null; + + if (is_null($rootDir)) + { + $rootDir = rtrim(AKFactory::get('kickstart.setup.destdir', ''), '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + if ($directory != $rootDir) + { + // Is this an unwritable directory? + if (!is_writeable($directory)) + { + $this->postProcEngine->chmod($directory, 0755); + } + } + $this->postProcEngine->chmod($path, 0644); + } + + /** + * Reads data from the archive and notifies the observer with the 'reading' message + * + * @param $fp + * @param $length + */ + protected function fread($fp, $length = null) + { + if (is_numeric($length)) + { + if ($length > 0) + { + $data = fread($fp, $length); + } + else + { + $data = fread($fp, PHP_INT_MAX); + } + } + else + { + $data = fread($fp, PHP_INT_MAX); + } + if ($data === false) + { + $data = ''; + } + + // Send start of file notification + $message = new stdClass; + $message->type = 'reading'; + $message->content = new stdClass; + $message->content->length = strlen($data); + $this->notify($message); + + return $data; + } + + /** + * Removes the configured $removePath from the path $path + * + * @param string $path The path to reduce + * + * @return string The reduced path + */ + protected function removePath($path) + { + if (empty($this->removePath)) + { + return $path; + } + + if (strpos($path, $this->removePath) === 0) + { + $path = substr($path, strlen($this->removePath)); + $path = ltrim($path, '/\\'); + } + + return $path; + } + + /** + * Am I supposed to skip the extraction of the current file? This depends on + * + * @return bool + */ + protected function mustSkip() + { + static $isDryRun = null; + + // List of files (and patterns) to extract + static $extractList = null; + + // Internal cache of the last file we checked and whether it must be skipped + static $lastFileName = ''; + static $mustSkip = false; + + // Make sure the dry run flag is, indeed, populated + if (is_null($isDryRun)) + { + $isDryRun = AKFactory::get('kickstart.setup.dryrun', '0'); + } + + // If it's a Kickstart dry run we have to skip the extraction of the file + if ($isDryRun) + { + return true; + } + + // Make sure I have a list of files and patterns to extract + if (is_null($extractList)) + { + $extractList = $this->getExtractList(); + } + + // No list of files to extract is given; we must extract everything. + if (empty($extractList)) + { + return false; + } + + // I am asked about the same file again. Return the cached result. + if ($this->fileHeader->file == $lastFileName) + { + return $mustSkip; + } + + // Does the current file match the extract patterns or not? + $lastFileName = $this->fileHeader->file; + $lastFileName = (strpos($lastFileName, $this->addPath) === 0) ? substr($lastFileName, strlen(rtrim($this->addPath, "\\/")) + 1) : $lastFileName; + $mustSkip = !$this->matchesGlobPatterns($lastFileName, $extractList); + + return $mustSkip; + } + + /** + * Get the list of files / folders to extract. The list can contain filenames or glob patterns. + * + * @return array + */ + private function getExtractList() + { + $rawList = AKFactory::get('kickstart.setup.extract_list', ''); + + // Sometimes I could get an array, e.g. from CLI + if (is_array($rawList)) + { + $rawList = implode("\n", $rawList); + } + + // Remove any whitespace + $rawList = trim($rawList); + + if (empty($rawList)) + { + return array(); + } + + // Convert commas to newlines so we can support both ways to express lists + $rawList = str_replace(",", "\n", $rawList); + $rawList = trim($rawList); + + // Convert the list to an array and clean it + $list = explode("\n", $rawList); + $list = array_map('trim', $list); + + return array_unique($list); + } + + /** + * Tests whether the item $item matches the list of shell patterns $list. + * + * @param string $item The file name to test + * @param array $list The list of glob patterns to match + * + * @return bool + */ + private function matchesGlobPatterns($item, array $list) + { + if (empty($list)) + { + return true; + } + + foreach ($list as $pattern) + { + if (fnmatch($pattern, $item)) + { + return true; + } + } + + return false; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * File post processor engines base class + */ +abstract class AKAbstractPostproc extends AKAbstractObject +{ + /** @var int The UNIX timestamp of the file's desired modification date */ + public $timestamp = 0; + /** @var string The current (real) file path we'll have to process */ + protected $filename = null; + /** @var int The requested permissions */ + protected $perms = 0755; + /** @var string The temporary file path we gave to the unarchiver engine */ + protected $tempFilename = null; + /** @var string The temporary directory where the data will be stored */ + protected $tempDir = ''; + + /** + * Processes the current file, e.g. moves it from temp to final location by FTP + */ + abstract public function process(); + + /** + * The unarchiver tells us the path to the filename it wants to extract and we give it + * a different path instead. + * + * @param string $filename The path to the real file + * @param int $perms The permissions we need the file to have + * + * @return string The path to the temporary file + */ + abstract public function processFilename($filename, $perms = 0755); + + /** + * Recursively creates a directory if it doesn't exist + * + * @param string $dirName The directory to create + * @param int $perms The permissions to give to that directory + */ + abstract public function createDirRecursive($dirName, $perms); + + abstract public function chmod($file, $perms); + + abstract public function unlink($file); + + abstract public function rmdir($directory); + + abstract public function rename($from, $to); + + /** + * Returns the configured temporary directory + * + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Descendants of this class can be used in the unarchiver's observer methods (attach, detach and notify) + * + * @author Nicholas + * + */ +abstract class AKAbstractPartObserver +{ + abstract public function update($object, $message); +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Direct file writer + */ +class AKPostprocDirect extends AKAbstractPostproc +{ + public function process() + { + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if ($restorePerms) + { + @chmod($this->filename, $this->perms); + } + else + { + if (@is_file($this->filename)) + { + @chmod($this->filename, 0644); + } + else + { + @chmod($this->filename, 0755); + } + } + if ($this->timestamp > 0) + { + @touch($this->filename, $this->timestamp); + } + + return true; + } + + public function processFilename($filename, $perms = 0755) + { + $this->perms = $perms; + $this->filename = $filename; + + return $filename; + } + + public function createDirRecursive($dirName, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + if (@mkdir($dirName, 0755, true)) + { + @chmod($dirName, 0755); + + return true; + } + + $root = AKFactory::get('kickstart.setup.destdir'); + $root = rtrim(str_replace('\\', '/', $root), '/'); + $dir = rtrim(str_replace('\\', '/', $dirName), '/'); + if (strpos($dir, $root) === 0) + { + $dir = ltrim(substr($dir, strlen($root)), '/'); + $root .= '/'; + } + else + { + $root = ''; + } + + if (empty($dir)) + { + return true; + } + + $dirArray = explode('/', $dir); + $path = ''; + foreach ($dirArray as $dir) + { + $path .= $dir . '/'; + $ret = is_dir($root . $path) ? true : @mkdir($root . $path); + if (!$ret) + { + // Is this a file instead of a directory? + if (is_file($root . $path)) + { + @unlink($root . $path); + $ret = @mkdir($root . $path); + } + if (!$ret) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $path)); + + return false; + } + } + // Try to set new directory permissions to 0755 + @chmod($root . $path, $perms); + } + + return true; + } + + public function chmod($file, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + return @chmod($file, $perms); + } + + public function unlink($file) + { + return @unlink($file); + } + + public function rmdir($directory) + { + return @rmdir($directory); + } + + public function rename($from, $to) + { + return @rename($from, $to); + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + /** @var resource The FTP handle */ + private $handle = null; + + public function __construct() + { + parent::__construct(); + + $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); + $this->passive = AKFactory::get('kickstart.ftp.passive', true); + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 21); + if (trim($this->port) == '') + { + $this->port = 21; + } + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + public function connect() + { + // Connect to server, using SSL if so required + if ($this->useSSL) + { + $this->handle = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $this->handle = @ftp_connect($this->host, $this->port); + } + if ($this->handle === false) + { + $this->setError(AKText::_('WRONG_FTP_HOST')); + + return false; + } + + // Login + if (!@ftp_login($this->handle, $this->user, $this->pass)) + { + $this->setError(AKText::_('WRONG_FTP_USER')); + @ftp_close($this->handle); + + return false; + } + + // Change to initial directory + if (!@ftp_chdir($this->handle, $this->dir)) + { + $this->setError(AKText::_('WRONG_FTP_PATH1')); + @ftp_close($this->handle); + + return false; + } + + // Enable passive mode if the user requested it + if ($this->passive) + { + @ftp_pasv($this->handle, true); + } + else + { + @ftp_pasv($this->handle, false); + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + fclose($tempHandle); + + return true; + } + + private function isDirWritable($dir) + { + $fp = @fopen($dir . '/kickstart.dat', 'wb'); + if ($fp === false) + { + return false; + } + else + { + @fclose($fp); + unlink($dir . '/kickstart.dat'); + + return true; + } + } + + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if (empty($dirName)) + { + $dirName = ''; + } // 'cause the substr() above may return FALSE. + + $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir); + foreach ($alldirs as $curdir) + { + $check = $previousDir . '/' . $curdir; + if (!$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + @ftp_delete($this->handle, $check); + + if (@ftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($removePath . $check); + if (@ftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if (!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + $trustMeIKnowWhatImDoing = + 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($check, $trustMeIKnowWhatImDoing); + + return true; + } + } + } + @ftp_chmod($this->handle, $perms, $check); + } + $previousDir = $check; + } + + return true; + } + + private function is_dir($dir) + { + return @ftp_chdir($this->handle, $dir); + } + + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = @error_reporting(E_NONE); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + if (!empty($basePath)) + { + $basePath .= '/'; + } + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + if (is_dir($oldPath . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + function __wakeup() + { + $this->connect(); + } + + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $removePath = ltrim($removePath, "/"); + $remotePath = ltrim($remotePath, "/"); + $left = substr($remotePath, 0, strlen($removePath)); + if ($left == $removePath) + { + $remotePath = substr($remotePath, strlen($removePath)); + } + } + + $absoluteFSPath = dirname($this->filename); + $relativeFTPPath = trim($remotePath, '/'); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if ($ret === false) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if ($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } + } + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + if ($restorePerms) + { + @ftp_chmod($this->_handle, $this->perms, $remoteName); + } + else + { + @ftp_chmod($this->_handle, 0644, $remoteName); + } + + return true; + } + + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + + public function unlink($file) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + return @ftp_delete($this->handle, $check); + } + + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + public function close() + { + @ftp_close($this->handle); + } + + public function chmod($file, $perms) + { + return @ftp_chmod($this->handle, $perms, $file); + } + + public function rmdir($directory) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($directory, 0, strlen($removePath)); + if ($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + return @ftp_rmdir($this->handle, $check); + } + + public function rename($from, $to) + { + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($from, 0, strlen($removePath)); + if ($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } + } + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + + if (!empty($removePath)) + { + $left = substr($to, 0, strlen($removePath)); + if ($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } + } + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $result = @ftp_rename($this->handle, $from, $to); + if ($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } + } + +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * FTP file writer + */ +class AKPostprocSFTP extends AKAbstractPostproc +{ + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + /** @var bool use Passive mode? */ + public $passive = true; + /** @var string FTP host name */ + public $host = ''; + /** @var int FTP port */ + public $port = 21; + /** @var string FTP user name */ + public $user = ''; + /** @var string FTP password */ + public $pass = ''; + /** @var string FTP initial directory */ + public $dir = ''; + + /** @var resource SFTP resource handle */ + private $handle = null; + + /** @var resource SSH2 connection resource handle */ + private $_connection = null; + + /** @var string Current remote directory, including the remote directory string */ + private $_currentdir; + + public function __construct() + { + parent::__construct(); + + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 22); + + if (trim($this->port) == '') + { + $this->port = 22; + } + + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + $connected = $this->connect(); + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('SFTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + public function connect() + { + $this->_connection = false; + + if (!function_exists('ssh2_connect')) + { + $this->setError(AKText::_('SFTP_NO_SSH2')); + + return false; + } + + $this->_connection = @ssh2_connect($this->host, $this->port); + + if (!@ssh2_auth_password($this->_connection, $this->user, $this->pass)) + { + $this->setError(AKText::_('SFTP_WRONG_USER')); + + $this->_connection = false; + + return false; + } + + $this->handle = @ssh2_sftp($this->_connection); + + // I must have an absolute directory + if (!$this->dir) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + return false; + } + + // Change to initial directory + if (!$this->sftp_chdir('/')) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + unset($this->_connection); + unset($this->handle); + + return false; + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $basePath = '/' . trim($this->dir, '/'); + + if (@fopen("ssh2.sftp://{$this->handle}$basePath/$testFilename", 'r+') === false) + { + $this->setError(AKText::_('SFTP_WRONG_STARTING_DIR')); + + unset($this->_connection); + unset($this->handle); + + return false; + } + + return true; + } + + /** + * Changes to the requested directory in the remote server. You give only the + * path relative to the initial directory and it does all the rest by itself, + * including doing nothing if the remote directory is the one we want. + * + * @param string $dir The (realtive) remote directory + * + * @return bool True if successful, false otherwise. + */ + private function sftp_chdir($dir) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dir = str_replace('\\', '/', $dir); + + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dir = rtrim($dir, '/\\') . '/'; + + // Process the path removal + $left = substr($dir, 0, strlen($removePath)); + + if ($left == $removePath) + { + $dir = substr($dir, strlen($removePath)); + } + } + + if (empty($dir)) + { + // Because the substr() above may return FALSE. + $dir = ''; + } + + // Calculate "real" (absolute) SFTP path + $realdir = substr($this->dir, -1) == '/' ? substr($this->dir, 0, strlen($this->dir) - 1) : $this->dir; + $realdir .= '/' . $dir; + $realdir = substr($realdir, 0, 1) == '/' ? $realdir : '/' . $realdir; + + if ($this->_currentdir == $realdir) + { + // Already there, do nothing + return true; + } + + $result = @ssh2_sftp_stat($this->handle, $realdir); + + if ($result === false) + { + return false; + } + else + { + // Update the private "current remote directory" variable + $this->_currentdir = $realdir; + + return true; + } + } + + private function isDirWritable($dir) + { + if (@fopen("ssh2.sftp://{$this->handle}$dir/kickstart.dat", 'wb') === false) + { + return false; + } + else + { + @ssh2_sftp_unlink($this->handle, $dir . '/kickstart.dat'); + + return true; + } + } + + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + if (empty($dirName)) + { + $dirName = ''; + } // 'cause the substr() above may return FALSE. + + $check = '/' . trim($this->dir, '/ ') . '/' . trim($dirName, '/'); + + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir, '/ '); + + foreach ($alldirs as $curdir) + { + if (!$curdir) + { + continue; + } + + $check = $previousDir . '/' . $curdir; + + if (!$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + @ssh2_sftp_unlink($this->handle, $check); + + if (@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($check); + + if (@ssh2_sftp_mkdir($this->handle, $check) === false) + { + // Can we fall back to pure PHP mode, sire? + if (!@mkdir($check)) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + else + { + // Since the directory was built by PHP, change its permissions + $trustMeIKnowWhatImDoing = + 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($check, $trustMeIKnowWhatImDoing); + + return true; + } + } + } + + @ssh2_sftp_chmod($this->handle, $check, $perms); + } + + $previousDir = $check; + } + + return true; + } + + private function is_dir($dir) + { + return $this->sftp_chdir($dir); + } + + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = @error_reporting(E_NONE); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + + if (!empty($basePath)) + { + $basePath .= '/'; + } + + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + + if (is_dir($oldPath . '/' . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . '/' . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + function __wakeup() + { + $this->connect(); + } + + /* + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * @param $path string The full path to a directory or file + */ + + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $absoluteFSPath = dirname($this->filename); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + $ret = $this->sftp_chdir($absoluteFTPPath); + + if ($ret === false) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + $ret = $this->sftp_chdir($absoluteFTPPath); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + // Create the file + $ret = $this->write($this->tempFilename, $remoteName); + + // If I got a -1 it means that I wasn't able to open the file, so I have to stop here + if ($ret === -1) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $ret = $this->write($this->tempFilename, $remoteName); + } + + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('SFTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if ($restorePerms) + { + $this->chmod($remoteName, $this->perms); + } + else + { + $this->chmod($remoteName, 0644); + } + + return true; + } + + private function write($local, $remote) + { + $fp = @fopen("ssh2.sftp://{$this->handle}$remote", 'w'); + $localfp = @fopen($local, 'rb'); + + if ($fp === false) + { + return -1; + } + + if ($localfp === false) + { + @fclose($fp); + + return -1; + } + + $res = true; + + while (!feof($localfp) && ($res !== false)) + { + $buffer = @fread($localfp, 65567); + $res = @fwrite($fp, $buffer); + } + + @fclose($fp); + @fclose($localfp); + + return $res; + } + + public function unlink($file) + { + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + return @ssh2_sftp_unlink($this->handle, $check); + } + + public function chmod($file, $perms) + { + return @ssh2_sftp_chmod($this->handle, $file, $perms); + } + + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + public function close() + { + unset($this->_connection); + unset($this->handle); + } + + public function rmdir($directory) + { + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + return @ssh2_sftp_rmdir($this->handle, $check); + } + + public function rename($from, $to) + { + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $result = @ssh2_sftp_rename($this->handle, $from, $to); + + if ($result !== true) + { + return @rename($from, $to); + } + else + { + return true; + } + } + +} + + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Hybrid direct / FTP mode file writer + */ +class AKPostprocHybrid extends AKAbstractPostproc +{ + + /** @var bool Should I use the FTP layer? */ + public $useFTP = false; + + /** @var bool Should I use FTP over implicit SSL? */ + public $useSSL = false; + + /** @var bool use Passive mode? */ + public $passive = true; + + /** @var string FTP host name */ + public $host = ''; + + /** @var int FTP port */ + public $port = 21; + + /** @var string FTP user name */ + public $user = ''; + + /** @var string FTP password */ + public $pass = ''; + + /** @var string FTP initial directory */ + public $dir = ''; + + /** @var resource The FTP handle */ + private $handle = null; + + /** @var null The FTP connection handle */ + private $_handle = null; + + /** + * Public constructor. Tries to connect to the FTP server. + */ + public function __construct() + { + parent::__construct(); + + $this->useFTP = true; + $this->useSSL = AKFactory::get('kickstart.ftp.ssl', false); + $this->passive = AKFactory::get('kickstart.ftp.passive', true); + $this->host = AKFactory::get('kickstart.ftp.host', ''); + $this->port = AKFactory::get('kickstart.ftp.port', 21); + $this->user = AKFactory::get('kickstart.ftp.user', ''); + $this->pass = AKFactory::get('kickstart.ftp.pass', ''); + $this->dir = AKFactory::get('kickstart.ftp.dir', ''); + $this->tempDir = AKFactory::get('kickstart.ftp.tempdir', ''); + + if (trim($this->port) == '') + { + $this->port = 21; + } + + // If FTP is not configured, skip it altogether + if (empty($this->host) || empty($this->user) || empty($this->pass)) + { + $this->useFTP = false; + } + + // Try to connect to the FTP server + $connected = $this->connect(); + + // If the connection fails, skip FTP altogether + if (!$connected) + { + $this->useFTP = false; + } + + if ($connected) + { + if (!empty($this->tempDir)) + { + $tempDir = rtrim($this->tempDir, '/\\') . '/'; + $writable = $this->isDirWritable($tempDir); + } + else + { + $tempDir = ''; + $writable = false; + } + + if (!$writable) + { + // Default temporary directory is the current root + $tempDir = KSROOTDIR; + if (empty($tempDir)) + { + // Oh, we have no directory reported! + $tempDir = '.'; + } + $absoluteDirToHere = $tempDir; + $tempDir = rtrim(str_replace('\\', '/', $tempDir), '/'); + if (!empty($tempDir)) + { + $tempDir .= '/'; + } + $this->tempDir = $tempDir; + // Is this directory writable? + $writable = $this->isDirWritable($tempDir); + } + + if (!$writable) + { + // Nope. Let's try creating a temporary directory in the site's root. + $tempDir = $absoluteDirToHere . '/kicktemp'; + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + $this->createDirRecursive($tempDir, $trustMeIKnowWhatImDoing); + // Try making it writable... + $this->fixPermissions($tempDir); + $writable = $this->isDirWritable($tempDir); + } + + // Was the new directory writable? + if (!$writable) + { + // Let's see if the user has specified one + $userdir = AKFactory::get('kickstart.ftp.tempdir', ''); + if (!empty($userdir)) + { + // Is it an absolute or a relative directory? + $absolute = false; + $absolute = $absolute || (substr($userdir, 0, 1) == '/'); + $absolute = $absolute || (substr($userdir, 1, 1) == ':'); + $absolute = $absolute || (substr($userdir, 2, 1) == ':'); + if (!$absolute) + { + // Make absolute + $tempDir = $absoluteDirToHere . $userdir; + } + else + { + // it's already absolute + $tempDir = $userdir; + } + // Does the directory exist? + if (is_dir($tempDir)) + { + // Yeah. Is it writable? + $writable = $this->isDirWritable($tempDir); + } + } + } + $this->tempDir = $tempDir; + + if (!$writable) + { + // No writable directory found!!! + $this->setError(AKText::_('FTP_TEMPDIR_NOT_WRITABLE')); + } + else + { + AKFactory::set('kickstart.ftp.tempdir', $tempDir); + $this->tempDir = $tempDir; + } + } + } + + /** + * Tries to connect to the FTP server + * + * @return bool + */ + public function connect() + { + if (!$this->useFTP) + { + return false; + } + + // Connect to server, using SSL if so required + if ($this->useSSL) + { + $this->handle = @ftp_ssl_connect($this->host, $this->port); + } + else + { + $this->handle = @ftp_connect($this->host, $this->port); + } + if ($this->handle === false) + { + $this->setError(AKText::_('WRONG_FTP_HOST')); + + return false; + } + + // Login + if (!@ftp_login($this->handle, $this->user, $this->pass)) + { + $this->setError(AKText::_('WRONG_FTP_USER')); + @ftp_close($this->handle); + + return false; + } + + // Change to initial directory + if (!@ftp_chdir($this->handle, $this->dir)) + { + $this->setError(AKText::_('WRONG_FTP_PATH1')); + @ftp_close($this->handle); + + return false; + } + + // Enable passive mode if the user requested it + if ($this->passive) + { + @ftp_pasv($this->handle, true); + } + else + { + @ftp_pasv($this->handle, false); + } + + // Try to download ourselves + $testFilename = defined('KSSELFNAME') ? KSSELFNAME : basename(__FILE__); + $tempHandle = fopen('php://temp', 'r+'); + + if (@ftp_fget($this->handle, $tempHandle, $testFilename, FTP_ASCII, 0) === false) + { + $this->setError(AKText::_('WRONG_FTP_PATH2')); + @ftp_close($this->handle); + fclose($tempHandle); + + return false; + } + + fclose($tempHandle); + + return true; + } + + /** + * Is the directory writeable? + * + * @param string $dir The directory ti check + * + * @return bool + */ + private function isDirWritable($dir) + { + $fp = @fopen($dir . '/kickstart.dat', 'wb'); + + if ($fp === false) + { + return false; + } + + @fclose($fp); + unlink($dir . '/kickstart.dat'); + + return true; + } + + /** + * Create a directory, recursively + * + * @param string $dirName The directory to create + * @param int $perms The permissions to give to the directory + * + * @return bool + */ + public function createDirRecursive($dirName, $perms) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + // UNIXize the paths + $removePath = str_replace('\\', '/', $removePath); + $dirName = str_replace('\\', '/', $dirName); + // Make sure they both end in a slash + $removePath = rtrim($removePath, '/\\') . '/'; + $dirName = rtrim($dirName, '/\\') . '/'; + // Process the path removal + $left = substr($dirName, 0, strlen($removePath)); + + if ($left == $removePath) + { + $dirName = substr($dirName, strlen($removePath)); + } + } + + // 'cause the substr() above may return FALSE. + if (empty($dirName)) + { + $dirName = ''; + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($dirName, '/'); + $checkFS = $removePath . trim($dirName, '/'); + + if ($this->is_dir($check)) + { + return true; + } + + $alldirs = explode('/', $dirName); + $previousDir = '/' . trim($this->dir); + $previousDirFS = rtrim($removePath, '/\\'); + + foreach ($alldirs as $curdir) + { + $check = $previousDir . '/' . $curdir; + $checkFS = $previousDirFS . '/' . $curdir; + + if (!is_dir($checkFS) && !$this->is_dir($check)) + { + // Proactively try to delete a file by the same name + if (!@unlink($checkFS) && $this->useFTP) + { + @ftp_delete($this->handle, $check); + } + + $createdDir = @mkdir($checkFS, 0755); + + if (!$createdDir && $this->useFTP) + { + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) + { + // If we couldn't create the directory, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($checkFS); + + $createdDir = @mkdir($checkFS, 0755); + if (!$createdDir && $this->useFTP) + { + $createdDir = @ftp_mkdir($this->handle, $check); + } + + if ($createdDir === false) + { + $this->setError(AKText::sprintf('FTP_CANT_CREATE_DIR', $check)); + + return false; + } + } + + if (!@chmod($checkFS, $perms) && $this->useFTP) + { + @ftp_chmod($this->handle, $perms, $check); + } + } + + $previousDir = $check; + $previousDirFS = $checkFS; + } + + return true; + } + + private function is_dir($dir) + { + if ($this->useFTP) + { + return @ftp_chdir($this->handle, $dir); + } + + return false; + } + + /** + * Tries to fix directory/file permissions in the PHP level, so that + * the FTP operation doesn't fail. + * + * @param $path string The full path to a directory or file + */ + private function fixPermissions($path) + { + // Turn off error reporting + if (!defined('KSDEBUG')) + { + $oldErrorReporting = error_reporting(0); + } + + // Get UNIX style paths + $relPath = str_replace('\\', '/', $path); + $basePath = rtrim(str_replace('\\', '/', KSROOTDIR), '/'); + $basePath = rtrim($basePath, '/'); + + if (!empty($basePath)) + { + $basePath .= '/'; + } + + // Remove the leading relative root + if (substr($relPath, 0, strlen($basePath)) == $basePath) + { + $relPath = substr($relPath, strlen($basePath)); + } + + $dirArray = explode('/', $relPath); + $pathBuilt = rtrim($basePath, '/'); + + foreach ($dirArray as $dir) + { + if (empty($dir)) + { + continue; + } + + $oldPath = $pathBuilt; + $pathBuilt .= '/' . $dir; + + if (is_dir($oldPath . $dir)) + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + @chmod($oldPath . $dir, $trustMeIKnowWhatImDoing); + } + else + { + $trustMeIKnowWhatImDoing = 500 + 10 + 1; // working around overzealous scanners written by bozos + if (@chmod($oldPath . $dir, $trustMeIKnowWhatImDoing) === false) + { + @unlink($oldPath . $dir); + } + } + } + + // Restore error reporting + if (!defined('KSDEBUG')) + { + @error_reporting($oldErrorReporting); + } + } + + /** + * Called after unserialisation, tries to reconnect to FTP + */ + function __wakeup() + { + if ($this->useFTP) + { + $this->connect(); + } + } + + function __destruct() + { + if (!$this->useFTP) + { + @ftp_close($this->handle); + } + } + + /** + * Post-process an extracted file, using FTP or direct file writes to move it + * + * @return bool + */ + public function process() + { + if (is_null($this->tempFilename)) + { + // If an empty filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + return true; + } + + $remotePath = dirname($this->filename); + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + $root = rtrim($removePath, '/\\'); + + if (!empty($removePath)) + { + $removePath = ltrim($removePath, "/"); + $remotePath = ltrim($remotePath, "/"); + $left = substr($remotePath, 0, strlen($removePath)); + + if ($left == $removePath) + { + $remotePath = substr($remotePath, strlen($removePath)); + } + } + + $absoluteFSPath = dirname($this->filename); + $relativeFTPPath = trim($remotePath, '/'); + $absoluteFTPPath = '/' . trim($this->dir, '/') . '/' . trim($remotePath, '/'); + $onlyFilename = basename($this->filename); + + $remoteName = $absoluteFTPPath . '/' . $onlyFilename; + + // Does the directory exist? + if (!is_dir($root . '/' . $absoluteFSPath)) + { + $ret = $this->createDirRecursive($absoluteFSPath, 0755); + + if (($ret === false) && ($this->useFTP)) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + } + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + } + + if ($this->useFTP) + { + $ret = @ftp_chdir($this->handle, $absoluteFTPPath); + } + + // Try copying directly + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + + if ($ret === false) + { + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $ret = @copy($this->tempFilename, $root . '/' . $this->filename); + } + + if ($this->useFTP && ($ret === false)) + { + $ret = @ftp_put($this->handle, $remoteName, $this->tempFilename, FTP_BINARY); + + if ($ret === false) + { + // If we couldn't create the file, attempt to fix the permissions in the PHP level and retry! + $this->fixPermissions($this->filename); + $this->unlink($this->filename); + + $fp = @fopen($this->tempFilename, 'rb'); + if ($fp !== false) + { + $ret = @ftp_fput($this->handle, $remoteName, $fp, FTP_BINARY); + @fclose($fp); + } + else + { + $ret = false; + } + } + } + + @unlink($this->tempFilename); + + if ($ret === false) + { + $this->setError(AKText::sprintf('FTP_COULDNT_UPLOAD', $this->filename)); + + return false; + } + + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + $perms = $restorePerms ? $this->perms : 0644; + + $ret = @chmod($root . '/' . $this->filename, $perms); + + if ($this->useFTP && ($ret === false)) + { + @ftp_chmod($this->_handle, $perms, $remoteName); + } + + return true; + } + + public function unlink($file) + { + $ret = @unlink($file); + + if (!$ret && $this->useFTP) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($file, '/'); + + $ret = @ftp_delete($this->handle, $check); + } + + return $ret; + } + + /** + * Create a temporary filename + * + * @param string $filename The original filename + * @param int $perms The file permissions + * + * @return string + */ + public function processFilename($filename, $perms = 0755) + { + // Catch some error conditions... + if ($this->getError()) + { + return false; + } + + // If a null filename is passed, it means that we shouldn't do any post processing, i.e. + // the entity was a directory or symlink + if (is_null($filename)) + { + $this->filename = null; + $this->tempFilename = null; + + return null; + } + + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + $left = substr($filename, 0, strlen($removePath)); + + if ($left == $removePath) + { + $filename = substr($filename, strlen($removePath)); + } + } + + // Trim slash on the left + $filename = ltrim($filename, '/'); + + $this->filename = $filename; + $this->tempFilename = tempnam($this->tempDir, 'kickstart-'); + $this->perms = $perms; + + if (empty($this->tempFilename)) + { + // Oops! Let's try something different + $this->tempFilename = $this->tempDir . '/kickstart-' . time() . '.dat'; + } + + return $this->tempFilename; + } + + /** + * Closes the FTP connection + */ + public function close() + { + if (!$this->useFTP) + { + @ftp_close($this->handle); + } + } + + public function chmod($file, $perms) + { + if (AKFactory::get('kickstart.setup.dryrun', '0')) + { + return true; + } + + $ret = @chmod($file, $perms); + + if (!$ret && $this->useFTP) + { + // Strip absolute filesystem path to website's root + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + + if (!empty($removePath)) + { + $left = substr($file, 0, strlen($removePath)); + + if ($left == $removePath) + { + $file = substr($file, strlen($removePath)); + } + } + + // Trim slash on the left + $file = ltrim($file, '/'); + + $ret = @ftp_chmod($this->handle, $perms, $file); + } + + return $ret; + } + + public function rmdir($directory) + { + $ret = @rmdir($directory); + + if (!$ret && $this->useFTP) + { + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($directory, 0, strlen($removePath)); + if ($left == $removePath) + { + $directory = substr($directory, strlen($removePath)); + } + } + + $check = '/' . trim($this->dir, '/') . '/' . trim($directory, '/'); + + $ret = @ftp_rmdir($this->handle, $check); + } + + return $ret; + } + + public function rename($from, $to) + { + $ret = @rename($from, $to); + + if (!$ret && $this->useFTP) + { + $originalFrom = $from; + $originalTo = $to; + + $removePath = AKFactory::get('kickstart.setup.destdir', ''); + if (!empty($removePath)) + { + $left = substr($from, 0, strlen($removePath)); + if ($left == $removePath) + { + $from = substr($from, strlen($removePath)); + } + } + $from = '/' . trim($this->dir, '/') . '/' . trim($from, '/'); + + if (!empty($removePath)) + { + $left = substr($to, 0, strlen($removePath)); + if ($left == $removePath) + { + $to = substr($to, strlen($removePath)); + } + } + $to = '/' . trim($this->dir, '/') . '/' . trim($to, '/'); + + $ret = @ftp_rename($this->handle, $from, $to); + } + + return $ret; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * JPA archive extraction class + */ +class AKUnarchiverJPA extends AKAbstractUnarchiver +{ + protected $archiveHeaderData = array(); + + protected function readArchiveHeader() + { + debugMsg('Preparing to read archive header'); + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + debugMsg('Opening the first part'); + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + debugMsg('Could not open the first part'); + + return false; + } + + // Read the signature + $sig = fread($this->fp, 3); + + if ($sig != 'JPA') + { + // Not a JPA file + debugMsg('Invalid archive signature'); + $this->setError(AKText::_('ERR_NOT_A_JPA_FILE')); + + return false; + } + + // Read and parse header length + $header_length_array = unpack('v', fread($this->fp, 2)); + $header_length = $header_length_array[1]; + + // Read and parse the known portion of header data (14 bytes) + $bin_data = fread($this->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->fp, $rest_length); + } + else + { + $junk = ''; + } + + // Temporary array with all the data we read + $temp = array( + 'signature' => $sig, + 'length' => $header_length, + 'major' => $header_data['major'], + 'minor' => $header_data['minor'], + 'filecount' => $header_data['count'], + 'uncompressedsize' => $header_data['uncsize'], + 'compressedsize' => $header_data['csize'], + 'unknowndata' => $junk + ); + + // Array-to-object conversion + foreach ($temp as $key => $value) + { + $this->archiveHeaderData->{$key} = $value; + } + + debugMsg('Header data:'); + debugMsg('Length : ' . $header_length); + debugMsg('Major : ' . $header_data['major']); + debugMsg('Minor : ' . $header_data['minor']); + debugMsg('File count : ' . $header_data['count']); + debugMsg('Uncompressed size : ' . $header_data['uncsize']); + debugMsg('Compressed size : ' . $header_data['csize']); + + $this->currentPartOffset = @ftell($this->fp); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + debugMsg('Archive part EOF; moving to next file'); + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + debugMsg("Reading file signature; part {$this->currentPartNumber}, offset {$this->currentPartOffset}"); + // Get and decode Entity Description Block + $signature = fread($this->fp, 3); + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Check signature + if ($signature != 'JPF') + { + if ($this->isEOF(true)) + { + // This file is finished; make sure it's the last one + $this->nextFile(); + + if (!$this->isEOF(false)) + { + debugMsg('Invalid file signature before end of archive encountered'); + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + + // We're just finished + return false; + } + else + { + $screwed = true; + + if (AKFactory::get('kickstart.setup.ignoreerrors', false)) + { + debugMsg('Invalid file block signature; launching heuristic file block signature scanner'); + $screwed = !$this->heuristicFileHeaderLocator(); + + if (!$screwed) + { + $signature = 'JPF'; + } + else + { + debugMsg('Heuristics failed. Brace yourself for the imminent crash.'); + } + } + + if ($screwed) + { + debugMsg('Invalid file block signature'); + // This is not a file block! The archive is corrupt. + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + } + } + // This a JPA Entity Block. Process the header. + + $isBannedFile = false; + + // Read length of EDB and of the Entity Path Data + $length_array = unpack('vblocksize/vpathsize', fread($this->fp, 4)); + // Read the path data + if ($length_array['pathsize'] > 0) + { + $file = fread($this->fp, $length_array['pathsize']); + } + else + { + $file = ''; + } + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($file, $this->renameFiles)) + { + $file = $this->renameFiles[$file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($file), $this->renameDirs)) + { + $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read and parse the known data portion + $bin_data = fread($this->fp, 14); + $header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data); + // Read any unknown data + $restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']); + + if ($restBytes > 0) + { + // Start reading the extra fields + while ($restBytes >= 4) + { + $extra_header_data = fread($this->fp, 4); + $extra_header = unpack('vsignature/vlength', $extra_header_data); + $restBytes -= 4; + $extra_header['length'] -= 4; + + switch ($extra_header['signature']) + { + case 256: + // File modified timestamp + if ($extra_header['length'] > 0) + { + $bindata = fread($this->fp, $extra_header['length']); + $restBytes -= $extra_header['length']; + $timestamps = unpack('Vmodified', substr($bindata, 0, 4)); + $filectime = $timestamps['modified']; + $this->fileHeader->timestamp = $filectime; + } + break; + + default: + // Unknown field + if ($extra_header['length'] > 0) + { + $junk = fread($this->fp, $extra_header['length']); + $restBytes -= $extra_header['length']; + } + break; + } + } + + if ($restBytes > 0) + { + $junk = fread($this->fp, $restBytes); + } + } + + $compressionType = $header_data['compression']; + + // Populate the return array + $this->fileHeader->file = $file; + $this->fileHeader->compressed = $header_data['compsize']; + $this->fileHeader->uncompressed = $header_data['uncompsize']; + + switch ($header_data['type']) + { + case 0: + $this->fileHeader->type = 'dir'; + break; + + case 1: + $this->fileHeader->type = 'file'; + break; + + case 2: + $this->fileHeader->type = 'link'; + break; + } + + switch ($compressionType) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 1: + $this->fileHeader->compression = 'gzip'; + break; + case 2: + $this->fileHeader->compression = 'bzip2'; + break; + } + + $this->fileHeader->permissions = $header_data['perms']; + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + debugMsg('Skipping file ' . $this->fileHeader->file); + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + while ($seekleft > 0) + { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->archiveList[$this->currentPartNumber]); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + if ($canSeek > $seekleft) + { + $canSeek = $seekleft; + } + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + if ($seekleft) + { + $this->nextFile(); + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + // Regular file; ask the postproc engine to process its filename + if ($restorePerms) + { + $this->fileHeader->realFile = + $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + } + elseif ($this->fileHeader->type == 'dir') + { + $dir = $this->fileHeader->file; + + // Directory; just create it + if ($restorePerms) + { + $this->postProcEngine->createDirRecursive($dir, $this->fileHeader->permissions); + } + else + { + $this->postProcEngine->createDirRecursive($dir, 0755); + } + + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + // Header is read + $this->runState = AK_STATE_HEADER; + + $this->dataReadLength = 0; + + return true; + } + + protected function heuristicFileHeaderLocator() + { + $ret = false; + $fullEOF = false; + + while (!$ret && !$fullEOF) + { + $this->currentPartOffset = @ftell($this->fp); + + if ($this->isEOF(true)) + { + $this->nextFile(); + } + + if ($this->isEOF(false)) + { + $fullEOF = true; + continue; + } + + // Read 512Kb + $chunk = fread($this->fp, 524288); + $size_read = mb_strlen($chunk, '8bit'); + //$pos = strpos($chunk, 'JPF'); + $pos = mb_strpos($chunk, 'JPF', 0, '8bit'); + + if ($pos !== false) + { + // We found it! + $this->currentPartOffset += $pos + 3; + @fseek($this->fp, $this->currentPartOffset, SEEK_SET); + $ret = true; + } + else + { + // Not yet found :( + $this->currentPartOffset = @ftell($this->fp); + } + } + + return $ret; + } + + /** + * Creates the directory this file points to + */ + protected function createDirectory() + { + if ($this->mustSkip()) + { + return true; + } + + // Do we need to create a directory? + if (empty($this->fileHeader->realFile)) + { + $this->fileHeader->realFile = $this->fileHeader->file; + } + + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = $this->flagRestorePermissions ? $this->fileHeader->permissions : 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); + + if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); + + return false; + } + else + { + return true; + } + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected function processFileData() + { + switch ($this->fileHeader->type) + { + case 'dir': + return $this->processTypeDir(); + break; + + case 'link': + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) + { + case 'none': + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + return $this->processTypeFileCompressedSimple(); + break; + + } + break; + + default: + debugMsg('Unknown file type ' . $this->fileHeader->type); + break; + } + } + + /** + * Process the file data of a directory entry + * + * @return bool + */ + private function processTypeDir() + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return bool + */ + private function processTypeLink() + { + $readBytes = 0; + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) + { + $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + } + else + { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + } + + $filename = isset($this->fileHeader->realFile) ? $this->fileHeader->realFile : $this->fileHeader->file; + + if (!$this->mustSkip()) + { + // Try to remove an existing file or directory by the same name + if (file_exists($filename)) + { + @unlink($filename); + @rmdir($filename); + } + + // Remove any trailing slash + if (substr($filename, -1) == '/') + { + $filename = substr($filename, 0, -1); + } + // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( + @symlink($data, $filename); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; // No matter if the link was created! + } + + private function processTypeFileUncompressed() + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + + if ($this->dataReadLength == 0) + { + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + } + else + { + $outfp = @fopen($this->fileHeader->realFile, 'ab'); + } + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) + { + // An error occurred + debugMsg('Could not write to output file'); + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (!$this->mustSkip() && is_resource($outfp)) + { + @fclose($outfp); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Reference to the global timer + $timer = AKFactory::getTimer(); + + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) + { + $toReadBytes = ($leftBytes > $this->chunkSize) ? $this->chunkSize : $leftBytes; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + $leftBytes -= $reallyReadBytes; + $this->dataReadLength += $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + } + else + { + // Nope. The archive is corrupt + debugMsg('Not enough data in file. The archive is truncated or corrupt.'); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fwrite($outfp, $data); + } + } + } + + // Close the file pointer + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) + { + $this->runState = AK_STATE_DATA; + } + else + { + // Oh! We just finished! + $this->runState = AK_STATE_DATAREAD; + $this->dataReadLength = 0; + } + + return true; + } + + private function processTypeFileCompressedSimple() + { + if (!$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) + { + // An error occurred + debugMsg('Could not write to output file'); + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) + { + // No file data! + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Simple compressed files are processed as a whole; we can't do chunk processing + $zipData = $this->fread($this->fp, $this->fileHeader->compressed); + while (akstringlen($zipData) < $this->fileHeader->compressed) + { + // End of local file before reading all data, but have more archive parts? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Read from the next file + $this->nextFile(); + $bytes_left = $this->fileHeader->compressed - akstringlen($zipData); + $zipData .= $this->fread($this->fp, $bytes_left); + } + else + { + debugMsg('End of local file before reading all data with no more parts left. The archive is corrupt or truncated.'); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + if ($this->fileHeader->compression == 'gzip') + { + $unzipData = gzinflate($zipData); + } + elseif ($this->fileHeader->compression == 'bzip2') + { + $unzipData = bzdecompress($zipData); + } + unset($zipData); + + // Write to the file. + if (!$this->mustSkip() && is_resource($outfp)) + { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + unset($unzipData); + + $this->runState = AK_STATE_DATAREAD; + + return true; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * ZIP archive extraction class + * + * Since the file data portion of ZIP and JPA are similarly structured (it's empty for dirs, + * linked node name for symlinks, dumped binary data for no compressions and dumped gzipped + * binary data for gzip compression) we just have to subclass AKUnarchiverJPA and change the + * header reading bits. Reusable code ;) + */ +class AKUnarchiverZIP extends AKUnarchiverJPA +{ + var $expectDataDescriptor = false; + + protected function readArchiveHeader() + { + debugMsg('Preparing to read archive header'); + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + debugMsg('Opening the first part'); + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + debugMsg('The first part is not readable'); + + return false; + } + + // Read a possible multipart signature + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // Roll back if it's not a multipart archive + if ($headerData['sig'] == 0x04034b50) + { + debugMsg('The archive is not multipart'); + fseek($this->fp, -4, SEEK_CUR); + } + else + { + debugMsg('The archive is multipart'); + } + + $multiPartSigs = array( + 0x08074b50, // Multi-part ZIP + 0x30304b50, // Multi-part ZIP (alternate) + 0x04034b50 // Single file + ); + if (!in_array($headerData['sig'], $multiPartSigs)) + { + debugMsg('Invalid header signature ' . dechex($headerData['sig'])); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + + $this->currentPartOffset = @ftell($this->fp); + debugMsg('Current part offset after reading header: ' . $this->currentPartOffset); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + debugMsg('Opening next archive part'); + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + if ($this->expectDataDescriptor) + { + // The last file had bit 3 of the general purpose bit flag set. This means that we have a + // 12 byte data descriptor we need to skip. To make things worse, there might also be a 4 + // byte optional data descriptor header (0x08074b50). + $junk = @fread($this->fp, 4); + $junk = unpack('Vsig', $junk); + if ($junk['sig'] == 0x08074b50) + { + // Yes, there was a signature + $junk = @fread($this->fp, 12); + debugMsg('Data descriptor (w/ header) skipped at ' . (ftell($this->fp) - 12)); + } + else + { + // No, there was no signature, just read another 8 bytes + $junk = @fread($this->fp, 8); + debugMsg('Data descriptor (w/out header) skipped at ' . (ftell($this->fp) - 8)); + } + + // And check for EOF, too + if ($this->isEOF(true)) + { + debugMsg('EOF before reading header'); + + $this->nextFile(); + } + } + + // Get and decode Local File Header + $headerBinary = fread($this->fp, 30); + $headerData = + unpack('Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/Vuncomp/vfnamelen/veflen', $headerBinary); + + // Check signature + if (!($headerData['sig'] == 0x04034b50)) + { + debugMsg('Not a file signature at ' . (ftell($this->fp) - 4)); + + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) + { + debugMsg('EOCD signature at ' . (ftell($this->fp) - 4)); + // End of ZIP file detected. We'll just skip to the end of file... + while ($this->nextFile()) + { + }; + @fseek($this->fp, 0, SEEK_END); // Go to EOF + return false; + } + else + { + debugMsg('Invalid signature ' . dechex($headerData['sig']) . ' at ' . ftell($this->fp)); + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // If bit 3 of the bitflag is set, expectDataDescriptor is true + $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Read the last modified data and time + $lastmodtime = $headerData['lastmodtime']; + $lastmoddate = $headerData['lastmoddate']; + + if ($lastmoddate && $lastmodtime) + { + // ----- Extract time + $v_hour = ($lastmodtime & 0xF800) >> 11; + $v_minute = ($lastmodtime & 0x07E0) >> 5; + $v_seconde = ($lastmodtime & 0x001F) * 2; + + // ----- Extract date + $v_year = (($lastmoddate & 0xFE00) >> 9) + 1980; + $v_month = ($lastmoddate & 0x01E0) >> 5; + $v_day = $lastmoddate & 0x001F; + + // ----- Get UNIX date format + $this->fileHeader->timestamp = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year); + } + + $isBannedFile = false; + + $this->fileHeader->compressed = $headerData['compsize']; + $this->fileHeader->uncompressed = $headerData['uncomp']; + $nameFieldLength = $headerData['fnamelen']; + $extraFieldLength = $headerData['eflen']; + + // Read filename field + $this->fileHeader->file = fread($this->fp, $nameFieldLength); + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($this->fileHeader->file, $this->renameFiles)) + { + $this->fileHeader->file = $this->renameFiles[$this->fileHeader->file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($this->fileHeader->file), $this->renameDirs)) + { + $file = + rtrim($this->renameDirs[dirname($this->fileHeader->file)], '/') . '/' . basename($this->fileHeader->file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read extra field if present + if ($extraFieldLength > 0) + { + $extrafield = fread($this->fp, $extraFieldLength); + } + + debugMsg('*' . ftell($this->fp) . ' IS START OF ' . $this->fileHeader->file . ' (' . $this->fileHeader->compressed . ' bytes)'); + + + // Decide filetype -- Check for directories + $this->fileHeader->type = 'file'; + if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) + { + $this->fileHeader->type = 'dir'; + } + // Decide filetype -- Check for symbolic links + if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) + { + $this->fileHeader->type = 'link'; + } + + switch ($headerData['compmethod']) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 8: + $this->fileHeader->compression = 'gzip'; + break; + } + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + while ($seekleft > 0) + { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->archiveList[$this->currentPartNumber]); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + if ($canSeek > $seekleft) + { + $canSeek = $seekleft; + } + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + if ($seekleft) + { + $this->nextFile(); + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + elseif ($this->fileHeader->type == 'dir') + { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + $this->postProcEngine->createDirRecursive($dir, 0755); + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + // Header is read + $this->runState = AK_STATE_HEADER; + + return true; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * JPS archive extraction class + */ +class AKUnarchiverJPS extends AKUnarchiverJPA +{ + /** + * Header data for the archive + * + * @var array + */ + protected $archiveHeaderData = array(); + + /** + * Plaintext password from which the encryption key will be derived with PBKDF2 + * + * @var string + */ + protected $password = ''; + + /** + * Which hash algorithm should I use for key derivation with PBKDF2. + * + * @var string + */ + private $pbkdf2Algorithm = 'sha1'; + + /** + * How many iterations should I use for key derivation with PBKDF2 + * + * @var int + */ + private $pbkdf2Iterations = 1000; + + /** + * Should I use a static salt for key derivation with PBKDF2? + * + * @var bool + */ + private $pbkdf2UseStaticSalt = 0; + + /** + * Static salt for key derivation with PBKDF2 + * + * @var string + */ + private $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * How much compressed data I have read since the last file header read + * + * @var int + */ + private $compressedSizeReadSinceLastFileHeader = 0; + + public function __construct() + { + parent::__construct(); + + $this->password = AKFactory::get('kickstart.jps.password', ''); + } + + public function __wakeup() + { + parent::__wakeup(); + + // Make sure the decryption is all set up (required!) + AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); + AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); + AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); + AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); + } + + + protected function readArchiveHeader() + { + // Initialize header data array + $this->archiveHeaderData = new stdClass(); + + // Open the first part + $this->nextFile(); + + // Fail for unreadable files + if ($this->fp === false) + { + return false; + } + + // Read the signature + $sig = fread($this->fp, 3); + + if ($sig != 'JPS') + { + // Not a JPA file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + // Read and parse the known portion of header data (5 bytes) + $bin_data = fread($this->fp, 5); + $header_data = unpack('Cmajor/Cminor/cspanned/vextra', $bin_data); + + // Is this a v2 archive? + $versionHumanReadable = $header_data['major'] . '.' . $header_data['minor']; + $isV2Archive = version_compare($versionHumanReadable, '2.0', 'ge'); + + // Load any remaining header data + $rest_length = $header_data['extra']; + + if ($isV2Archive && $rest_length) + { + // V2 archives only have one kind of extra header + if (!$this->readKeyExpansionExtraHeader()) + { + return false; + } + } + elseif ($rest_length > 0) + { + $junk = fread($this->fp, $rest_length); + } + + // Temporary array with all the data we read + $temp = array( + 'signature' => $sig, + 'major' => $header_data['major'], + 'minor' => $header_data['minor'], + 'spanned' => $header_data['spanned'] + ); + // Array-to-object conversion + foreach ($temp as $key => $value) + { + $this->archiveHeaderData->{$key} = $value; + } + + $this->currentPartOffset = @ftell($this->fp); + + $this->dataReadLength = 0; + + return true; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return bool True if reading the file was successful, false if an error occurred or we reached end of archive + */ + protected function readFileHeader() + { + // If the current part is over, proceed to the next part please + if ($this->isEOF(true)) + { + $this->nextFile(); + } + + $this->currentPartOffset = ftell($this->fp); + + // Get and decode Entity Description Block + $signature = fread($this->fp, 3); + + // Check for end-of-archive siganture + if ($signature == 'JPE') + { + $this->setState('postrun'); + + return true; + } + + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Check signature + if ($signature != 'JPF') + { + if ($this->isEOF(true)) + { + // This file is finished; make sure it's the last one + $this->nextFile(); + if (!$this->isEOF(false)) + { + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + + // We're just finished + return false; + } + else + { + fseek($this->fp, -6, SEEK_CUR); + $signature = fread($this->fp, 3); + if ($signature == 'JPE') + { + return false; + } + + $this->setError(AKText::sprintf('INVALID_FILE_HEADER', $this->currentPartNumber, $this->currentPartOffset)); + + return false; + } + } + + // This a JPS Entity Block. Process the header. + + $isBannedFile = false; + + // Make sure the decryption is all set up + AKEncryptionAES::setPbkdf2Algorithm($this->pbkdf2Algorithm); + AKEncryptionAES::setPbkdf2Iterations($this->pbkdf2Iterations); + AKEncryptionAES::setPbkdf2UseStaticSalt($this->pbkdf2UseStaticSalt); + AKEncryptionAES::setPbkdf2StaticSalt($this->pbkdf2StaticSalt); + + // Read and decrypt the header + $edbhData = fread($this->fp, 4); + $edbh = unpack('vencsize/vdecsize', $edbhData); + $bin_data = fread($this->fp, $edbh['encsize']); + + // Add the header length to the data read + $this->compressedSizeReadSinceLastFileHeader += $edbh['encsize'] + 4; + + // Decrypt and truncate + $bin_data = AKEncryptionAES::AESDecryptCBC($bin_data, $this->password); + $bin_data = substr($bin_data, 0, $edbh['decsize']); + + // Read length of EDB and of the Entity Path Data + $length_array = unpack('vpathsize', substr($bin_data, 0, 2)); + // Read the path data + $file = substr($bin_data, 2, $length_array['pathsize']); + + // Handle file renaming + $isRenamed = false; + if (is_array($this->renameFiles) && (count($this->renameFiles) > 0)) + { + if (array_key_exists($file, $this->renameFiles)) + { + $file = $this->renameFiles[$file]; + $isRenamed = true; + } + } + + // Handle directory renaming + $isDirRenamed = false; + if (is_array($this->renameDirs) && (count($this->renameDirs) > 0)) + { + if (array_key_exists(dirname($file), $this->renameDirs)) + { + $file = rtrim($this->renameDirs[dirname($file)], '/') . '/' . basename($file); + $isRenamed = true; + $isDirRenamed = true; + } + } + + // Read and parse the known data portion + $bin_data = substr($bin_data, 2 + $length_array['pathsize']); + $header_data = unpack('Ctype/Ccompression/Vuncompsize/Vperms/Vfilectime', $bin_data); + + $this->fileHeader->timestamp = $header_data['filectime']; + $compressionType = $header_data['compression']; + + // Populate the return array + $this->fileHeader->file = $file; + $this->fileHeader->uncompressed = $header_data['uncompsize']; + switch ($header_data['type']) + { + case 0: + $this->fileHeader->type = 'dir'; + break; + + case 1: + $this->fileHeader->type = 'file'; + break; + + case 2: + $this->fileHeader->type = 'link'; + break; + } + switch ($compressionType) + { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 1: + $this->fileHeader->compression = 'gzip'; + break; + case 2: + $this->fileHeader->compression = 'bzip2'; + break; + } + $this->fileHeader->permissions = $header_data['perms']; + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) + { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && (!$isRenamed)) + { + if (in_array($this->fileHeader->file, $this->skipFiles)) + { + $isBannedFile = true; + } + } + + // If we have a banned file, let's skip it + if ($isBannedFile) + { + $done = false; + while (!$done) + { + // Read the Data Chunk Block header + $binMiniHead = fread($this->fp, 8); + if (in_array(substr($binMiniHead, 0, 3), array('JPF', 'JPE'))) + { + // Not a Data Chunk Block header, I am done skipping the file + @fseek($this->fp, -8, SEEK_CUR); // Roll back the file pointer + $done = true; // Mark as done + continue; // Exit loop + } + else + { + // Skip forward by the amount of compressed data + $miniHead = unpack('Vencsize/Vdecsize', $binMiniHead); + @fseek($this->fp, $miniHead['encsize'], SEEK_CUR); + $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHead['encsize']; + } + } + + $this->currentPartOffset = @ftell($this->fp); + $this->runState = AK_STATE_DONE; + $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; + $this->compressedSizeReadSinceLastFileHeader = 0; + + return true; + } + + // Remove the removePath, if any + $this->fileHeader->file = $this->removePath($this->fileHeader->file); + + // Last chance to prepend a path to the filename + if (!empty($this->addPath) && !$isDirRenamed) + { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + $restorePerms = AKFactory::get('kickstart.setup.restoreperms', false); + + if (!$this->mustSkip()) + { + if ($this->fileHeader->type == 'file') + { + // Regular file; ask the postproc engine to process its filename + if ($restorePerms) + { + $this->fileHeader->realFile = + $this->postProcEngine->processFilename($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->fileHeader->realFile = $this->postProcEngine->processFilename($this->fileHeader->file); + } + } + elseif ($this->fileHeader->type == 'dir') + { + $dir = $this->fileHeader->file; + $this->fileHeader->realFile = $dir; + + // Directory; just create it + if ($restorePerms) + { + $this->postProcEngine->createDirRecursive($this->fileHeader->file, $this->fileHeader->permissions); + } + else + { + $this->postProcEngine->createDirRecursive($this->fileHeader->file, 0755); + } + + $this->postProcEngine->processFilename(null); + } + else + { + // Symlink; do not post-process + $this->postProcEngine->processFilename(null); + } + + $this->createDirectory(); + } + + + $this->fileHeader->compressed = $this->compressedSizeReadSinceLastFileHeader; + $this->compressedSizeReadSinceLastFileHeader = 0; + + // Header is read + $this->runState = AK_STATE_HEADER; + + $this->dataReadLength = 0; + + return true; + } + + /** + * Creates the directory this file points to + */ + protected function createDirectory() + { + if ($this->mustSkip()) + { + return true; + } + + // Do we need to create a directory? + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = 0755; + $ignore = AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($dirName); + + if (($this->postProcEngine->createDirRecursive($dirName, $perms) == false) && (!$ignore)) + { + $this->setError(AKText::sprintf('COULDNT_CREATE_DIR', $dirName)); + + return false; + } + + return true; + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return bool True if processing the file data was successful, false if an error occurred + */ + protected function processFileData() + { + switch ($this->fileHeader->type) + { + case 'dir': + return $this->processTypeDir(); + break; + + case 'link': + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) + { + case 'none': + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + return $this->processTypeFileCompressedSimple(); + break; + + } + break; + } + } + + /** + * Process the file data of a directory entry + * + * @return bool + */ + private function processTypeDir() + { + // Directory entries in the JPA do not have file data, therefore we're done processing the entry + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return bool + */ + private function processTypeLink() + { + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + // Read the mini header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + + if ($reallyReadBytes < 8) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Retry reading the header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + // Still not enough data? If so, the archive is corrupt or missing parts. + if ($reallyReadBytes < 8) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Read the encrypted data + $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); + $toReadBytes = $miniHeader['encsize']; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + $this->compressedSizeReadSinceLastFileHeader += 8 + $miniHeader['encsize']; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Read the rest of the data + $toReadBytes -= $reallyReadBytes; + $restData = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + if ($reallyReadBytes < $toReadBytes) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + $data .= $restData; + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Decrypt the data + $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); + + // Is the length of the decrypted data less than expected? + $data_length = akstringlen($data); + if ($data_length < $miniHeader['decsize']) + { + $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); + + return false; + } + + // Trim the data + $data = substr($data, 0, $miniHeader['decsize']); + + if (!$this->mustSkip()) + { + // Try to remove an existing file or directory by the same name + if (file_exists($this->fileHeader->file)) + { + @unlink($this->fileHeader->file); + @rmdir($this->fileHeader->file); + } + // Remove any trailing slash + if (substr($this->fileHeader->file, -1) == '/') + { + $this->fileHeader->file = substr($this->fileHeader->file, 0, -1); + } + // Create the symlink - only possible within PHP context. There's no support built in the FTP protocol, so no postproc use is possible here :( + @symlink($data, $this->fileHeader->file); + } + + $this->runState = AK_STATE_DATAREAD; + + return true; // No matter if the link was created! + } + + private function processTypeFileUncompressed() + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + if ($this->dataReadLength == 0) + { + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + } + else + { + $outfp = @fopen($this->fileHeader->realFile, 'ab'); + } + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + if (!$this->mustSkip() && is_resource($outfp)) + { + @fclose($outfp); + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + $this->setError('An uncompressed file was detected; this is not supported by this archive extraction utility'); + + return false; + } + + private function processTypeFileCompressedSimple() + { + $timer = AKFactory::getTimer(); + + // Files are being processed in small chunks, to avoid timeouts + if (($this->dataReadLength == 0) && !$this->mustSkip()) + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + if (!$this->mustSkip()) + { + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = + AKFactory::get('kickstart.setup.ignoreerrors', false) || $this->isIgnoredDirectory($this->fileHeader->file); + if (($outfp === false) && (!$ignore)) + { + // An error occurred + $this->setError(AKText::sprintf('COULDNT_WRITE_FILE', $this->fileHeader->realFile)); + + return false; + } + } + + // Does the file have any data, at all? + if ($this->fileHeader->uncompressed == 0) + { + // No file data! + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + $this->runState = AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; + + // Loop while there's data to write and enough time to do it + while (($leftBytes > 0) && ($timer->getTimeLeft() > 0)) + { + // Read the mini header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + if ($reallyReadBytes < 8) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Retry reading the header + $binMiniHeader = fread($this->fp, 8); + $reallyReadBytes = akstringlen($binMiniHeader); + // Still not enough data? If so, the archive is corrupt or missing parts. + if ($reallyReadBytes < 8) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Read the encrypted data + $miniHeader = unpack('Vencsize/Vdecsize', $binMiniHeader); + $toReadBytes = $miniHeader['encsize']; + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($data); + + $this->compressedSizeReadSinceLastFileHeader += $miniHeader['encsize'] + 8; + + if ($reallyReadBytes < $toReadBytes) + { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF(true) && !$this->isEOF(false)) + { + // Yeap. Let's go to the next file + $this->nextFile(); + // Read the rest of the data + $toReadBytes -= $reallyReadBytes; + $restData = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = akstringlen($restData); + if ($reallyReadBytes < $toReadBytes) + { + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + if (akstringlen($data) == 0) + { + $data = $restData; + } + else + { + $data .= $restData; + } + } + else + { + // Nope. The archive is corrupt + $this->setError(AKText::_('ERR_CORRUPT_ARCHIVE')); + + return false; + } + } + + // Decrypt the data + $data = AKEncryptionAES::AESDecryptCBC($data, $this->password); + + // Is the length of the decrypted data less than expected? + $data_length = akstringlen($data); + if ($data_length < $miniHeader['decsize']) + { + $this->setError(AKText::_('ERR_INVALID_JPS_PASSWORD')); + + return false; + } + + // Trim the data + $data = substr($data, 0, $miniHeader['decsize']); + + // Decompress + $data = gzinflate($data); + $unc_len = akstringlen($data); + + // Write the decrypted data + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fwrite($outfp, $data, akstringlen($data)); + } + } + + // Update the read length + $this->dataReadLength += $unc_len; + $leftBytes = $this->fileHeader->uncompressed - $this->dataReadLength; + } + + // Close the file pointer + if (!$this->mustSkip()) + { + if (is_resource($outfp)) + { + @fclose($outfp); + } + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) + { + $this->runState = AK_STATE_DATA; + } + else + { + // Oh! We just finished! + $this->runState = AK_STATE_DATAREAD; + $this->dataReadLength = 0; + } + + return true; + } + + private function readKeyExpansionExtraHeader() + { + $signature = fread($this->fp, 4); + + if ($signature != "JH\x00\x01") + { + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + $bin_data = fread($this->fp, 8); + $header_data = unpack('vlength/Calgo/Viterations/CuseStaticSalt', $bin_data); + + if ($header_data['length'] != 76) + { + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + } + + switch ($header_data['algo']) + { + case 0: + $algorithm = 'sha1'; + break; + + case 1: + $algorithm = 'sha256'; + break; + + case 2: + $algorithm = 'sha512'; + break; + + default: + // Not a valid JPS file + $this->setError(AKText::_('ERR_NOT_A_JPS_FILE')); + + return false; + break; + } + + $this->pbkdf2Algorithm = $algorithm; + $this->pbkdf2Iterations = $header_data['iterations']; + $this->pbkdf2UseStaticSalt = $header_data['useStaticSalt']; + $this->pbkdf2StaticSalt = fread($this->fp, 64); + + return true; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Timer class + */ +class AKCoreTimer extends AKAbstractObject +{ + /** @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 AECoreTimer + */ + public function __construct() + { + parent::__construct(); + + // Initialize start time + $this->start_time = $this->microtime_float(); + + // Get configured max time per step and bias + $config_max_exec_time = AKFactory::get('kickstart.tuning.max_exec_time', 14); + $bias = AKFactory::get('kickstart.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; + } + } + + /** + * Returns the current timestampt in decimal seconds + */ + private function microtime_float() + { + list($usec, $sec) = explode(" ", microtime()); + + return ((float) $usec + (float) $sec); + } + + /** + * 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; + } + + /** + * Enforce the minimum execution time + */ + public function enforce_min_exec_time() + { + // 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 + $minexectime = AKFactory::get('kickstart.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; + + // 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 (function_exists('usleep')) + { + usleep(1000 * $sleep_msec); + } + elseif (function_exists('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')) + { + $until_timestamp = time() + $sleep_msec / 1000; + time_sleep_until($until_timestamp); + } + elseif (function_exists('sleep')) + { + $sleep_sec = ceil($sleep_msec / 1000); + sleep($sleep_sec); + } + } + elseif ($elapsed_time > 0) + { + // No sleep required, even if user configured us to be able to do so. + } + } + + /** + * Reset the timer. It should only be used in CLI mode! + */ + public function resetTime() + { + $this->start_time = $this->microtime_float(); + } + + /** + * @param int $max_exec_time + */ + public function setMaxExecTime($max_exec_time) + { + $this->max_exec_time = $max_exec_time; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A filesystem scanner which uses opendir() + */ +class AKUtilsLister extends AKAbstractObject +{ + public function &getFiles($folder, $pattern = '*') + { + // Initialize variables + $arr = array(); + $false = false; + + if (!is_dir($folder)) + { + return $false; + } + + $handle = @opendir($folder); + // If directory is not accessible, just return FALSE + if ($handle === false) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + while (($file = @readdir($handle)) !== false) + { + if (!fnmatch($pattern, $file)) + { + continue; + } + + if (($file != '.') && ($file != '..')) + { + $ds = + ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? + '' : DIRECTORY_SEPARATOR; + $dir = $folder . $ds . $file; + $isDir = is_dir($dir); + if (!$isDir) + { + $arr[] = $dir; + } + } + } + @closedir($handle); + + return $arr; + } + + public function &getFolders($folder, $pattern = '*') + { + // Initialize variables + $arr = array(); + $false = false; + + if (!is_dir($folder)) + { + return $false; + } + + $handle = @opendir($folder); + // If directory is not accessible, just return FALSE + if ($handle === false) + { + $this->setWarning('Unreadable directory ' . $folder); + + return $false; + } + + while (($file = @readdir($handle)) !== false) + { + if (!fnmatch($pattern, $file)) + { + continue; + } + + if (($file != '.') && ($file != '..')) + { + $ds = + ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? + '' : DIRECTORY_SEPARATOR; + $dir = $folder . $ds . $file; + $isDir = is_dir($dir); + if ($isDir) + { + $arr[] = $dir; + } + } + } + @closedir($handle); + + return $arr; + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A simple INI-based i18n engine + */ +class AKText extends AKAbstractObject +{ + /** + * The default (en_GB) translation used when no other translation is available + * + * @var array + */ + private $default_translation = array( + 'AUTOMODEON' => 'Auto-mode enabled', + 'ERR_NOT_A_JPA_FILE' => 'The file is not a JPA archive', + 'ERR_CORRUPT_ARCHIVE' => 'The archive file is corrupt, truncated or archive parts are missing', + 'ERR_INVALID_LOGIN' => 'Invalid login', + 'COULDNT_CREATE_DIR' => 'Could not create %s folder', + 'COULDNT_WRITE_FILE' => 'Could not open %s for writing.', + 'WRONG_FTP_HOST' => 'Wrong FTP host or port', + 'WRONG_FTP_USER' => 'Wrong FTP username or password', + 'WRONG_FTP_PATH1' => 'Wrong FTP initial directory - the directory doesn\'t exist', + 'FTP_CANT_CREATE_DIR' => 'Could not create directory %s', + 'FTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', + 'SFTP_TEMPDIR_NOT_WRITABLE' => 'Could not find or create a writable temporary directory', + 'FTP_COULDNT_UPLOAD' => 'Could not upload %s', + 'THINGS_HEADER' => 'Things you should know about Akeeba Kickstart', + 'THINGS_01' => 'Kickstart is not an installer. It is an archive extraction tool. The actual installer was put inside the archive file at backup time.', + 'THINGS_02' => 'Kickstart is not the only way to extract the backup archive. You can use Akeeba eXtract Wizard and upload the extracted files using FTP instead.', + 'THINGS_03' => 'Kickstart is bound by your server\'s configuration. As such, it may not work at all.', + 'THINGS_04' => 'You should download and upload your archive files using FTP in Binary transfer mode. Any other method could lead to a corrupt backup archive and restoration failure.', + 'THINGS_05' => 'Post-restoration site load errors are usually caused by .htaccess or php.ini directives. You should understand that blank pages, 404 and 500 errors can usually be worked around by editing the aforementioned files. It is not our job to mess with your configuration files, because this could be dangerous for your site.', + 'THINGS_06' => 'Kickstart overwrites files without a warning. If you are not sure that you are OK with that do not continue.', + 'THINGS_07' => 'Trying to restore to the temporary URL of a cPanel host (e.g. http://1.2.3.4/~username) will lead to restoration failure and your site will appear to be not working. This is normal and it\'s just how your server and CMS software work.', + 'THINGS_08' => 'You are supposed to read the documentation before using this software. Most issues can be avoided, or easily worked around, by understanding how this software works.', + 'THINGS_09' => 'This text does not imply that there is a problem detected. It is standard text displayed every time you launch Kickstart.', + 'CLOSE_LIGHTBOX' => 'Click here or press ESC to close this message', + 'SELECT_ARCHIVE' => 'Select a backup archive', + 'ARCHIVE_FILE' => 'Archive file:', + 'SELECT_EXTRACTION' => 'Select an extraction method', + 'WRITE_TO_FILES' => 'Write to files:', + 'WRITE_HYBRID' => 'Hybrid (use FTP only if needed)', + 'WRITE_DIRECTLY' => 'Directly', + 'WRITE_FTP' => 'Use FTP for all files', + 'WRITE_SFTP' => 'Use SFTP for all files', + 'FTP_HOST' => '(S)FTP host name:', + 'FTP_PORT' => '(S)FTP port:', + 'FTP_FTPS' => 'Use FTP over SSL (FTPS)', + 'FTP_PASSIVE' => 'Use FTP Passive Mode', + 'FTP_USER' => '(S)FTP user name:', + 'FTP_PASS' => '(S)FTP password:', + 'FTP_DIR' => '(S)FTP directory:', + 'FTP_TEMPDIR' => 'Temporary directory:', + 'FTP_CONNECTION_OK' => 'FTP Connection Established', + 'SFTP_CONNECTION_OK' => 'SFTP Connection Established', + 'FTP_CONNECTION_FAILURE' => 'The FTP Connection Failed', + 'SFTP_CONNECTION_FAILURE' => 'The SFTP Connection Failed', + 'FTP_TEMPDIR_WRITABLE' => 'The temporary directory is writable.', + 'FTP_TEMPDIR_UNWRITABLE' => 'The temporary directory is not writable. Please check the permissions.', + 'FTPBROWSER_ERROR_HOSTNAME' => "Invalid FTP host or port", + 'FTPBROWSER_ERROR_USERPASS' => "Invalid FTP username or password", + 'FTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'FTPBROWSER_ERROR_UNSUPPORTED' => "Sorry, your FTP server doesn't support our FTP directory browser.", + 'FTPBROWSER_LBL_GOPARENT' => "<up one level>", + 'FTPBROWSER_LBL_INSTRUCTIONS' => 'Click on a directory to navigate into it. Click on OK to select that directory, Cancel to abort the procedure.', + 'FTPBROWSER_LBL_ERROR' => 'An error occurred', + 'SFTP_NO_SSH2' => 'Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', + 'SFTP_NO_FTP_SUPPORT' => 'Your SSH server does not allow SFTP connections', + 'SFTP_WRONG_USER' => 'Wrong SFTP username or password', + 'SFTP_WRONG_STARTING_DIR' => 'You must supply a valid absolute path', + 'SFTPBROWSER_ERROR_NOACCESS' => "Directory doesn't exist or you don't have enough permissions to access it", + 'SFTP_COULDNT_UPLOAD' => 'Could not upload %s', + 'SFTP_CANT_CREATE_DIR' => 'Could not create directory %s', + 'UI-ROOT' => '<root>', + 'CONFIG_UI_FTPBROWSER_TITLE' => 'FTP Directory Browser', + 'FTP_BROWSE' => 'Browse', + 'BTN_CHECK' => 'Check', + 'BTN_RESET' => 'Reset', + 'BTN_TESTFTPCON' => 'Test FTP connection', + 'BTN_TESTSFTPCON' => 'Test SFTP connection', + 'BTN_GOTOSTART' => 'Start over', + 'FINE_TUNE' => 'Fine tune', + 'BTN_SHOW_FINE_TUNE' => 'Show advanced options (for experts)', + 'MIN_EXEC_TIME' => 'Minimum execution time:', + 'MAX_EXEC_TIME' => 'Maximum execution time:', + 'SECONDS_PER_STEP' => 'seconds per step', + 'EXTRACT_FILES' => 'Extract files', + 'BTN_START' => 'Start', + 'EXTRACTING' => 'Extracting', + 'DO_NOT_CLOSE_EXTRACT' => 'Do not close this window while the extraction is in progress', + 'RESTACLEANUP' => 'Restoration and Clean Up', + 'BTN_RUNINSTALLER' => 'Run the Installer', + 'BTN_CLEANUP' => 'Clean Up', + 'BTN_SITEFE' => 'Visit your site\'s frontend', + 'BTN_SITEBE' => 'Visit your site\'s backend', + 'WARNINGS' => 'Extraction Warnings', + 'ERROR_OCCURED' => 'An error occurred', + 'STEALTH_MODE' => 'Stealth mode', + 'STEALTH_URL' => 'HTML file to show to web visitors', + 'ERR_NOT_A_JPS_FILE' => 'The file is not a JPA archive', + 'ERR_INVALID_JPS_PASSWORD' => 'The password you gave is wrong or the archive is corrupt', + 'JPS_PASSWORD' => 'Archive Password (for JPS files)', + 'INVALID_FILE_HEADER' => 'Invalid header in archive file, part %s, offset %s', + 'NEEDSOMEHELPKS' => 'Want some help to use this tool? Read this first:', + 'QUICKSTART' => 'Quick Start Guide', + 'CANTGETITTOWORK' => 'Can\'t get it to work? Click me!', + 'NOARCHIVESCLICKHERE' => 'No archives detected. Click here for troubleshooting instructions.', + 'POSTRESTORATIONTROUBLESHOOTING' => 'Something not working after the restoration? Click here for troubleshooting instructions.', + 'UPDATE_HEADER' => 'An updated version of Akeeba Kickstart (unknown) is available!', + 'UPDATE_NOTICE' => 'You are advised to always use the latest version of Akeeba Kickstart available. Older versions may be subject to bugs and will not be supported.', + 'UPDATE_DLNOW' => 'Download now', + 'UPDATE_MOREINFO' => 'More information', + 'IGNORE_MOST_ERRORS' => 'Ignore most errors', + 'WRONG_FTP_PATH2' => 'Wrong FTP initial directory - the directory doesn\'t correspond to your site\'s web root', + 'ARCHIVE_DIRECTORY' => 'Archive directory:', + 'RELOAD_ARCHIVES' => 'Reload', + 'CONFIG_UI_SFTPBROWSER_TITLE' => 'SFTP Directory Browser', + 'ERR_COULD_NOT_OPEN_ARCHIVE_PART' => 'Could not open archive part file %s for reading. Check that the file exists, is readable by the web server and is not in a directory made out of reach by chroot, open_basedir restrictions or any other restriction put in place by your host.', + 'RENAME_FILES' => 'Rename server configuration files', + 'RESTORE_PERMISSIONS' => 'Restore file permissions', + 'EXTRACT_LIST' => 'Files to extract', + 'EXTRACT_LIST_HELP' => 'Enter a file path such as images/cat.png or shell pattern such as images/*.png on each line. Only files matching this list will be written to disk. Leave empty to extract everything (default).', + ); + + /** + * The array holding the translation keys + * + * @var array + */ + private $strings; + + /** + * The currently detected language (ISO code) + * + * @var string + */ + private $language; + + /* + * Initializes the translation engine + * @return AKText + */ + public function __construct() + { + // Start with the default translation + $this->strings = $this->default_translation; + // Try loading the translation file in English, if it exists + $this->loadTranslation('en-GB'); + // Try loading the translation file in the browser's preferred language, if it exists + $this->getBrowserLanguage(); + if (!is_null($this->language)) + { + $this->loadTranslation(); + } + } + + private function loadTranslation($lang = null) + { + if (defined('KSLANGDIR')) + { + $dirname = KSLANGDIR; + } + else + { + $dirname = KSROOTDIR; + } + $basename = basename(__FILE__, '.php') . '.ini'; + if (empty($lang)) + { + $lang = $this->language; + } + + $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; + if (!@file_exists($translationFilename) && ($basename != 'kickstart.ini')) + { + $basename = 'kickstart.ini'; + $translationFilename = $dirname . DIRECTORY_SEPARATOR . $lang . '.' . $basename; + } + if (!@file_exists($translationFilename)) + { + return; + } + $temp = self::parse_ini_file($translationFilename, false); + + if (!is_array($this->strings)) + { + $this->strings = array(); + } + if (empty($temp)) + { + $this->strings = array_merge($this->default_translation, $this->strings); + } + else + { + $this->strings = array_merge($this->strings, $temp); + } + } + + /** + * A PHP based INI file parser. + * + * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on + * the parse_ini_file page on http://gr.php.net/parse_ini_file + * + * @param string $file Filename to process + * @param bool $process_sections True to also process INI sections + * + * @return array An associative array of sections, keys and values + * @access private + */ + public static function parse_ini_file($file, $process_sections = false, $raw_data = false) + { + $process_sections = ($process_sections !== true) ? false : true; + + if (!$raw_data) + { + $ini = @file($file); + } + else + { + $ini = $file; + } + if (count($ini) == 0) + { + return array(); + } + + $sections = array(); + $values = array(); + $result = array(); + $globals = array(); + $i = 0; + if (!empty($ini)) + { + foreach ($ini as $line) + { + $line = trim($line); + $line = str_replace("\t", " ", $line); + + // Comments + if (!preg_match('/^[a-zA-Z0-9[]/', $line)) + { + continue; + } + + // Sections + if ($line{0} == '[') + { + $tmp = explode(']', $line); + $sections[] = trim(substr($tmp[0], 1)); + $i++; + continue; + } + + // Key-value pair + list($key, $value) = explode('=', $line, 2); + $key = trim($key); + $value = trim($value); + if (strstr($value, ";")) + { + $tmp = explode(';', $value); + if (count($tmp) == 2) + { + if ((($value{0} != '"') && ($value{0} != "'")) || + preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) || + preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value) + ) + { + $value = $tmp[0]; + } + } + else + { + if ($value{0} == '"') + { + $value = preg_replace('/^"(.*)".*/', '$1', $value); + } + elseif ($value{0} == "'") + { + $value = preg_replace("/^'(.*)'.*/", '$1', $value); + } + else + { + $value = $tmp[0]; + } + } + } + $value = trim($value); + $value = trim($value, "'\""); + + if ($i == 0) + { + if (substr($line, -1, 2) == '[]') + { + $globals[$key][] = $value; + } + else + { + $globals[$key] = $value; + } + } + else + { + if (substr($line, -1, 2) == '[]') + { + $values[$i - 1][$key][] = $value; + } + else + { + $values[$i - 1][$key] = $value; + } + } + } + } + + for ($j = 0; $j < $i; $j++) + { + if ($process_sections === true) + { + $result[$sections[$j]] = $values[$j]; + } + else + { + $result[] = $values[$j]; + } + } + + return $result + $globals; + } + + public function getBrowserLanguage() + { + // Detection code from Full Operating system language detection, by Harald Hope + // Retrieved from http://techpatterns.com/downloads/php_language_detection.php + $user_languages = array(); + //check to see if language is set + if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) + { + $languages = strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"]); + // $languages = ' fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3'; + // need to remove spaces from strings to avoid error + $languages = str_replace(' ', '', $languages); + $languages = explode(",", $languages); + + foreach ($languages as $language_list) + { + // pull out the language, place languages into array of full and primary + // string structure: + $temp_array = array(); + // slice out the part before ; on first step, the part before - on second, place into array + $temp_array[0] = substr($language_list, 0, strcspn($language_list, ';'));//full language + $temp_array[1] = substr($language_list, 0, 2);// cut out primary language + if ((strlen($temp_array[0]) == 5) && ((substr($temp_array[0], 2, 1) == '-') || (substr($temp_array[0], 2, 1) == '_'))) + { + $langLocation = strtoupper(substr($temp_array[0], 3, 2)); + $temp_array[0] = $temp_array[1] . '-' . $langLocation; + } + //place this array into main $user_languages language array + $user_languages[] = $temp_array; + } + } + else// if no languages found + { + $user_languages[0] = array('', ''); //return blank array. + } + + $this->language = null; + $basename = basename(__FILE__, '.php') . '.ini'; + + // Try to match main language part of the filename, irrespective of the location, e.g. de_DE will do if de_CH doesn't exist. + if (class_exists('AKUtilsLister')) + { + $fs = new AKUtilsLister(); + $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); + if (empty($iniFiles) && ($basename != 'kickstart.ini')) + { + $basename = 'kickstart.ini'; + $iniFiles = $fs->getFiles(KSROOTDIR, '*.' . $basename); + } + } + else + { + $iniFiles = null; + } + + if (is_array($iniFiles)) + { + foreach ($user_languages as $languageStruct) + { + if (is_null($this->language)) + { + // Get files matching the main lang part + $iniFiles = $fs->getFiles(KSROOTDIR, $languageStruct[1] . '-??.' . $basename); + if (count($iniFiles) > 0) + { + $filename = $iniFiles[0]; + $filename = substr($filename, strlen(KSROOTDIR) + 1); + $this->language = substr($filename, 0, 5); + } + else + { + $this->language = null; + } + } + } + } + + if (is_null($this->language)) + { + // Try to find a full language match + foreach ($user_languages as $languageStruct) + { + if (@file_exists($languageStruct[0] . '.' . $basename) && is_null($this->language)) + { + $this->language = $languageStruct[0]; + } + else + { + + } + } + } + else + { + // Do we have an exact match? + foreach ($user_languages as $languageStruct) + { + if (substr($this->language, 0, strlen($languageStruct[1])) == $languageStruct[1]) + { + if (file_exists($languageStruct[0] . '.' . $basename)) + { + $this->language = $languageStruct[0]; + } + } + } + } + + // Now, scan for full language based on the partial match + + } + + public static function sprintf($key) + { + $text = self::getInstance(); + $args = func_get_args(); + if (count($args) > 0) + { + $args[0] = $text->_($args[0]); + + return @call_user_func_array('sprintf', $args); + } + + return ''; + } + + /** + * Singleton pattern for Language + * + * @return AKText The global AKText instance + */ + public static function &getInstance() + { + static $instance; + + if (!is_object($instance)) + { + $instance = new AKText(); + } + + return $instance; + } + + public static function _($string) + { + $text = self::getInstance(); + + $key = strtoupper($string); + $key = substr($key, 0, 1) == '_' ? substr($key, 1) : $key; + + if (isset ($text->strings[$key])) + { + $string = $text->strings[$key]; + } + else + { + if (defined($string)) + { + $string = constant($string); + } + } + + return $string; + } + + public function dumpLanguage() + { + $out = ''; + foreach ($this->strings as $key => $value) + { + $out .= "$key=$value\n"; + } + + return $out; + } + + public function asJavascript() + { + $out = ''; + foreach ($this->strings as $key => $value) + { + $key = addcslashes($key, '\\\'"'); + $value = addcslashes($value, '\\\'"'); + if (!empty($out)) + { + $out .= ",\n"; + } + $out .= "'$key':\t'$value'"; + } + + return $out; + } + + public function resetTranslation() + { + $this->strings = $this->default_translation; + } + + public function addDefaultLanguageStrings($stringList = array()) + { + if (!is_array($stringList)) + { + return; + } + if (empty($stringList)) + { + return; + } + + $this->strings = array_merge($stringList, $this->strings); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * The Akeeba Kickstart Factory class + * + * This class is reponssible for instantiating all Akeeba Kickstart classes + */ +class AKFactory +{ + /** @var array A list of instantiated objects */ + private $objectlist = array(); + + /** @var array Simple hash data storage */ + private $varlist = array(); + + /** @var self Static instance */ + private static $instance = null; + + /** + * AKFactory constructor. + * + * This is a private constructor makes sure we can't instantiate the class unless we go through the static + * getInstance singleton method. This is different than making the class abstract (preventing any kind of object + * instantiation). + */ + private function __construct() + { + } + + /** + * Gets a serialized snapshot of the Factory for safekeeping (hibernate) + * + * @return string The serialized snapshot of the Factory + */ + public static function serialize() + { + $engine = self::getUnarchiver(); + $engine->shutdown(); + $serialized = serialize(self::getInstance()); + + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + $serialized = base64_encode($serialized); + } + + return $serialized; + } + + /** + * Gets the unarchiver engine + * + * @return AKAbstractUnarchiver + */ + public static function &getUnarchiver($configOverride = null) + { + static $class_name; + + if (!empty($configOverride) && isset($configOverride['reset']) && $configOverride['reset']) + { + $class_name = null; + } + + if (empty($class_name)) + { + $filetype = self::get('kickstart.setup.filetype', null); + + if (empty($filetype)) + { + $filename = self::get('kickstart.setup.sourcefile', null); + $basename = basename($filename); + $baseextension = strtoupper(substr($basename, -3)); + + switch ($baseextension) + { + case 'JPA': + $filetype = 'JPA'; + break; + + case 'JPS': + $filetype = 'JPS'; + break; + + case 'ZIP': + $filetype = 'ZIP'; + break; + + default: + die('Invalid archive type or extension in file ' . $filename); + break; + } + } + + $class_name = 'AKUnarchiver' . ucfirst($filetype); + } + + $destdir = self::get('kickstart.setup.destdir', null); + + if (empty($destdir)) + { + $destdir = KSROOTDIR; + } + + /** @var AKAbstractUnarchiver $object */ + $object = self::getClassInstance($class_name); + + if ($object->getState() == 'init') + { + $sourcePath = self::get('kickstart.setup.sourcepath', ''); + $sourceFile = self::get('kickstart.setup.sourcefile', ''); + + if (!empty($sourcePath)) + { + $sourceFile = rtrim($sourcePath, '/\\') . '/' . $sourceFile; + } + + // Initialize the object –– Any change here MUST be reflected to echoHeadJavascript (default values) + $config = array( + 'filename' => $sourceFile, + 'restore_permissions' => self::get('kickstart.setup.restoreperms', 0), + 'post_proc' => self::get('kickstart.procengine', 'direct'), + 'add_path' => self::get('kickstart.setup.targetpath', $destdir), + 'remove_path' => self::get('kickstart.setup.removepath', ''), + 'rename_files' => self::get('kickstart.setup.renamefiles', array( + '.htaccess' => 'htaccess.bak', 'php.ini' => 'php.ini.bak', 'web.config' => 'web.config.bak', + '.user.ini' => '.user.ini.bak', + )), + 'skip_files' => self::get('kickstart.setup.skipfiles', array( + basename(__FILE__), 'kickstart.php', 'abiautomation.ini', 'htaccess.bak', 'php.ini.bak', + 'cacert.pem', + )), + 'ignoredirectories' => self::get('kickstart.setup.ignoredirectories', array( + 'tmp', 'log', 'logs', + )), + ); + + if (!defined('KICKSTART')) + { + // In restore.php mode we have to exclude the restoration.php files + $moreSkippedFiles = array( + // Akeeba Backup for Joomla! + 'administrator/components/com_akeeba/restoration.php', + // Joomla! Update + 'administrator/components/com_joomlaupdate/restoration.php', + // Akeeba Backup for WordPress + 'wp-content/plugins/akeebabackupwp/app/restoration.php', + 'wp-content/plugins/akeebabackupcorewp/app/restoration.php', + 'wp-content/plugins/akeebabackup/app/restoration.php', + 'wp-content/plugins/akeebabackupwpcore/app/restoration.php', + // Akeeba Solo + 'app/restoration.php', + ); + + $config['skip_files'] = array_merge($config['skip_files'], $moreSkippedFiles); + } + + if (!empty($configOverride)) + { + $config = array_merge($config, $configOverride); + } + + $object->setup($config); + } + + return $object; + } + + // ======================================================================== + // Public factory interface + // ======================================================================== + + public static function get($key, $default = null) + { + $self = self::getInstance(); + + if (array_key_exists($key, $self->varlist)) + { + return $self->varlist[$key]; + } + + return $default; + } + + /** + * Gets a single, internally used instance of the Factory + * + * @param string $serialized_data [optional] Serialized data to spawn the instance from + * + * @return AKFactory A reference to the unique Factory object instance + */ + protected static function &getInstance($serialized_data = null) + { + if (!is_object(self::$instance) || !is_null($serialized_data)) + { + if (!is_null($serialized_data)) + { + self::$instance = unserialize($serialized_data); + + return self::$instance; + } + + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Internal function which instantiates a class named $class_name. + * The autoloader + * + * @param string $class_name + * + * @return object + */ + protected static function &getClassInstance($class_name) + { + $self = self::getInstance(); + + if (!isset($self->objectlist[$class_name])) + { + $self->objectlist[$class_name] = new $class_name; + } + + return $self->objectlist[$class_name]; + } + + // ======================================================================== + // Public hash data storage interface + // ======================================================================== + + /** + * Regenerates the full Factory state from a serialized snapshot (resume) + * + * @param string $serialized_data The serialized snapshot to resume from + */ + public static function unserialize($serialized_data) + { + if (function_exists('base64_encode') && function_exists('base64_decode')) + { + $serialized_data = base64_decode($serialized_data); + } + + self::getInstance($serialized_data); + } + + /** + * Reset the internal factory state, freeing all previously created objects + */ + public static function nuke() + { + self::$instance = null; + } + + // ======================================================================== + // Akeeba Kickstart classes + // ======================================================================== + + public static function set($key, $value) + { + $self = self::getInstance(); + $self->varlist[$key] = $value; + } + + /** + * Gets the post processing engine + * + * @param string $proc_engine + * + * @return AKAbstractPostproc + */ + public static function &getPostProc($proc_engine = null) + { + static $class_name; + + if (empty($class_name)) + { + if (empty($proc_engine)) + { + $proc_engine = self::get('kickstart.procengine', 'direct'); + } + + $class_name = 'AKPostproc' . ucfirst($proc_engine); + } + + return self::getClassInstance($class_name); + } + + /** + * Get the a reference to the Akeeba Engine's timer + * + * @return AKCoreTimer + */ + public static function &getTimer() + { + return self::getClassInstance('AKCoreTimer'); + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Interface for AES encryption adapters + */ +interface AKEncryptionAESAdapterInterface +{ + /** + * Decrypts a string. Returns the raw binary ciphertext, zero-padded. + * + * @param string $plainText The plaintext to encrypt + * @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size) + * + * @return string The raw encrypted binary string. + */ + public function decrypt($plainText, $key); + + /** + * Returns the encryption block size in bytes + * + * @return int + */ + public function getBlockSize(); + + /** + * Is this adapter supported? + * + * @return bool + */ + public function isSupported(); +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * Abstract AES encryption class + */ +abstract class AKEncryptionAESAdapterAbstract +{ + /** + * Trims or zero-pads a key / IV + * + * @param string $key The key or IV to treat + * @param int $size The block size of the currently used algorithm + * + * @return null|string Null if $key is null, treated string of $size byte length otherwise + */ + public function resizeKey($key, $size) + { + if (empty($key)) + { + return null; + } + + $keyLength = strlen($key); + + if (function_exists('mb_strlen')) + { + $keyLength = mb_strlen($key, 'ASCII'); + } + + if ($keyLength == $size) + { + return $key; + } + + if ($keyLength > $size) + { + if (function_exists('mb_substr')) + { + return mb_substr($key, 0, $size, 'ASCII'); + } + + return substr($key, 0, $size); + } + + return $key . str_repeat("\0", ($size - $keyLength)); + } + + /** + * Returns null bytes to append to the string so that it's zero padded to the specified block size + * + * @param string $string The binary string which will be zero padded + * @param int $blockSize The block size + * + * @return string The zero bytes to append to the string to zero pad it to $blockSize + */ + protected function getZeroPadding($string, $blockSize) + { + $stringSize = strlen($string); + + if (function_exists('mb_strlen')) + { + $stringSize = mb_strlen($string, 'ASCII'); + } + + if ($stringSize == $blockSize) + { + return ''; + } + + if ($stringSize < $blockSize) + { + return str_repeat("\0", $blockSize - $stringSize); + } + + $paddingBytes = $stringSize % $blockSize; + + return str_repeat("\0", $blockSize - $paddingBytes); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +class Mcrypt extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface +{ + protected $cipherType = MCRYPT_RIJNDAEL_128; + + protected $cipherMode = MCRYPT_MODE_CBC; + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('mcrypt_get_key_size')) + { + return false; + } + + if (!function_exists('mcrypt_get_iv_size')) + { + return false; + } + + if (!function_exists('mcrypt_create_iv')) + { + return false; + } + + if (!function_exists('mcrypt_encrypt')) + { + return false; + } + + if (!function_exists('mcrypt_decrypt')) + { + return false; + } + + if (!function_exists('mcrypt_list_algorithms')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = mcrypt_list_algorithms(); + + if (!in_array('rijndael-128', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-192', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-256', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + public function getBlockSize() + { + return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +class OpenSSL extends AKEncryptionAESAdapterAbstract implements AKEncryptionAESAdapterInterface +{ + /** + * The OpenSSL options for encryption / decryption + * + * @var int + */ + protected $openSSLOptions = 0; + + /** + * The encryption method to use + * + * @var string + */ + protected $method = 'aes-128-cbc'; + + public function __construct() + { + $this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); + + return $plainText; + } + + public function isSupported() + { + if (!function_exists('openssl_get_cipher_methods')) + { + return false; + } + + if (!function_exists('openssl_random_pseudo_bytes')) + { + return false; + } + + if (!function_exists('openssl_cipher_iv_length')) + { + return false; + } + + if (!function_exists('openssl_encrypt')) + { + return false; + } + + if (!function_exists('openssl_decrypt')) + { + return false; + } + + if (!function_exists('hash')) + { + return false; + } + + if (!function_exists('hash_algos')) + { + return false; + } + + $algorightms = openssl_get_cipher_methods(); + + if (!in_array('aes-128-cbc', $algorightms)) + { + return false; + } + + $algorightms = hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + /** + * @return int + */ + public function getBlockSize() + { + return openssl_cipher_iv_length($this->method); + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * AES implementation in PHP (c) Chris Veness 2005-2016. + * Right to use and adapt is granted for under a simple creative commons attribution + * licence. No warranty of any form is offered. + * + * Heavily modified for Akeeba Backup by Nicholas K. Dionysopoulos + * Also added AES-128 CBC mode (with mcrypt and OpenSSL) on top of AES CTR + * Removed CTR encrypt / decrypt (no longer used) + */ +class AKEncryptionAES +{ + // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1] + protected static $Sbox = + array(0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16); + + // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2] + protected static $Rcon = array( + array(0x00, 0x00, 0x00, 0x00), + array(0x01, 0x00, 0x00, 0x00), + array(0x02, 0x00, 0x00, 0x00), + array(0x04, 0x00, 0x00, 0x00), + array(0x08, 0x00, 0x00, 0x00), + array(0x10, 0x00, 0x00, 0x00), + array(0x20, 0x00, 0x00, 0x00), + array(0x40, 0x00, 0x00, 0x00), + array(0x80, 0x00, 0x00, 0x00), + array(0x1b, 0x00, 0x00, 0x00), + array(0x36, 0x00, 0x00, 0x00)); + + protected static $passwords = array(); + + /** + * The algorithm to use for PBKDF2. Must be a supported hash_hmac algorithm. Default: sha1 + * + * @var string + */ + private static $pbkdf2Algorithm = 'sha1'; + + /** + * Number of iterations to use for PBKDF2 + * + * @var int + */ + private static $pbkdf2Iterations = 1000; + + /** + * Should we use a static salt for PBKDF2? + * + * @var int + */ + private static $pbkdf2UseStaticSalt = 0; + + /** + * The static salt to use for PBKDF2 + * + * @var string + */ + private static $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * AES Cipher function: encrypt 'input' with Rijndael algorithm + * + * @param array $input Message as byte-array (16 bytes) + * @param array $w key schedule as 2D byte-array (Nr+1 x Nb bytes) - + * generated from the cipher key by KeyExpansion() + * + * @return string Ciphertext as byte-array (16 bytes) + */ + protected static function Cipher($input, $w) + { + // main Cipher function [�5.1] + $Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) + $Nr = count($w) / $Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys + + $state = array(); // initialise 4xNb byte-array 'state' with input [�3.4] + + for ($i = 0; $i < 4 * $Nb; $i++) + { + $state[$i % 4][floor($i / 4)] = $input[$i]; + } + + $state = self::AddRoundKey($state, $w, 0, $Nb); + + for ($round = 1; $round < $Nr; $round++) + { // apply Nr rounds + $state = self::SubBytes($state, $Nb); + $state = self::ShiftRows($state, $Nb); + $state = self::MixColumns($state); + $state = self::AddRoundKey($state, $w, $round, $Nb); + } + + $state = self::SubBytes($state, $Nb); + $state = self::ShiftRows($state, $Nb); + $state = self::AddRoundKey($state, $w, $Nr, $Nb); + + $output = array(4 * $Nb); // convert state to 1-d array before returning [�3.4] + + for ($i = 0; $i < 4 * $Nb; $i++) + { + $output[$i] = $state[$i % 4][floor($i / 4)]; + } + + return $output; + } + + protected static function AddRoundKey($state, $w, $rnd, $Nb) + { + // xor Round Key into state S [�5.1.4] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $state[$r][$c] ^= $w[$rnd * 4 + $c][$r]; + } + } + + return $state; + } + + protected static function SubBytes($s, $Nb) + { + // apply SBox to state S [�5.1.1] + for ($r = 0; $r < 4; $r++) + { + for ($c = 0; $c < $Nb; $c++) + { + $s[$r][$c] = self::$Sbox[$s[$r][$c]]; + } + } + + return $s; + } + + protected static function ShiftRows($s, $Nb) + { + // shift row r of state S left by r bytes [�5.1.2] + $t = array(4); + + for ($r = 1; $r < 4; $r++) + { + for ($c = 0; $c < 4; $c++) + { + $t[$c] = $s[$r][($c + $r) % $Nb]; + } // shift into temp copy + + for ($c = 0; $c < 4; $c++) + { + $s[$r][$c] = $t[$c]; + } // and copy back + } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES): + + return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf + } + + protected static function MixColumns($s) + { + // combine bytes of each col of state S [�5.1.3] + for ($c = 0; $c < 4; $c++) + { + $a = array(4); // 'a' is a copy of the current column from 's' + $b = array(4); // 'b' is a�{02} in GF(2^8) + + for ($i = 0; $i < 4; $i++) + { + $a[$i] = $s[$i][$c]; + $b[$i] = $s[$i][$c] & 0x80 ? $s[$i][$c] << 1 ^ 0x011b : $s[$i][$c] << 1; + } + + // a[n] ^ b[n] is a�{03} in GF(2^8) + $s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3 + $s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3 + $s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3 + $s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3 + } + + return $s; + } + + /** + * Key expansion for Rijndael Cipher(): performs key expansion on cipher key + * to generate a key schedule + * + * @param array $key Cipher key byte-array (16 bytes) + * + * @return array Key schedule as 2D byte-array (Nr+1 x Nb bytes) + */ + protected static function KeyExpansion($key) + { + // generate Key Schedule from Cipher Key [�5.2] + + // block size (in words): no of columns in state (fixed at 4 for AES) + $Nb = 4; + // key length (in words): 4/6/8 for 128/192/256-bit keys + $Nk = (int) (count($key) / 4); + // no of rounds: 10/12/14 for 128/192/256-bit keys + $Nr = $Nk + 6; + + $w = array(); + $temp = array(); + + for ($i = 0; $i < $Nk; $i++) + { + $r = array($key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]); + $w[$i] = $r; + } + + for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++) + { + $w[$i] = array(); + for ($t = 0; $t < 4; $t++) + { + $temp[$t] = $w[$i - 1][$t]; + } + if ($i % $Nk == 0) + { + $temp = self::SubWord(self::RotWord($temp)); + for ($t = 0; $t < 4; $t++) + { + $rConIndex = (int) ($i / $Nk); + $temp[$t] ^= self::$Rcon[$rConIndex][$t]; + } + } + else if ($Nk > 6 && $i % $Nk == 4) + { + $temp = self::SubWord($temp); + } + for ($t = 0; $t < 4; $t++) + { + $w[$i][$t] = $w[$i - $Nk][$t] ^ $temp[$t]; + } + } + + return $w; + } + + protected static function SubWord($w) + { + // apply SBox to 4-byte word w + for ($i = 0; $i < 4; $i++) + { + $w[$i] = self::$Sbox[$w[$i]]; + } + + return $w; + } + + /* + * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints + * + * @param a number to be shifted (32-bit integer) + * @param b number of bits to shift a to the right (0..31) + * @return a right-shifted and zero-filled by b bits + */ + + protected static function RotWord($w) + { + // rotate 4-byte word w left by one byte + $tmp = $w[0]; + for ($i = 0; $i < 3; $i++) + { + $w[$i] = $w[$i + 1]; + } + $w[3] = $tmp; + + return $w; + } + + protected static function urs($a, $b) + { + $a &= 0xffffffff; + $b &= 0x1f; // (bounds check) + if ($a & 0x80000000 && $b > 0) + { // if left-most bit set + $a = ($a >> 1) & 0x7fffffff; // right-shift one bit & clear left-most bit + $a = $a >> ($b - 1); // remaining right-shifts + } + else + { // otherwise + $a = ($a >> $b); // use normal right-shift + } + + return $a; + } + + /** + * AES decryption in CBC mode. This is the standard mode (the CTR methods + * actually use Rijndael-128 in CTR mode, which - technically - isn't AES). + * + * It supports AES-128 only. It assumes that the last 4 bytes + * contain a little-endian unsigned long integer representing the unpadded + * data length. + * + * @since 3.0.1 + * @author Nicholas K. Dionysopoulos + * + * @param string $ciphertext The data to encrypt + * @param string $password Encryption password + * + * @return string The plaintext + */ + public static function AESDecryptCBC($ciphertext, $password) + { + $adapter = self::getAdapter(); + + if (!$adapter->isSupported()) + { + return false; + } + + // Read the data size + $data_size = unpack('V', substr($ciphertext, -4)); + + // Do I have a PBKDF2 salt? + $salt = substr($ciphertext, -92, 68); + $rightStringLimit = -4; + + $params = self::getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $useStaticSalt = $params['useStaticSalt']; + + if (substr($salt, 0, 4) == 'JPST') + { + // We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes + // (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the + // uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the + // format specification). + $salt = substr($salt, 4); + $rightStringLimit -= 68; + + $key = self::pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes); + } + elseif ($useStaticSalt) + { + // We have a static salt. Use it for PBKDF2. + $key = self::getStaticSaltExpandedKey($password); + } + else + { + // Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD. + $key = self::expandKey($password); + } + + // Try to get the IV from the data + $iv = substr($ciphertext, -24, 20); + + if (substr($iv, 0, 4) == 'JPIV') + { + // We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes + // (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length) + $iv = substr($iv, 4); + $rightStringLimit -= 20; + } + else + { + // No stored IV. Do it the dumb way. + $iv = self::createTheWrongIV($password); + } + + // Decrypt + $plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key); + + // Trim padding, if necessary + if (strlen($plaintext) > $data_size) + { + $plaintext = substr($plaintext, 0, $data_size); + } + + return $plaintext; + } + + /** + * That's the old way of creating an IV that's definitely not cryptographically sound. + * + * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA + * + * @param string $password The raw password from which we create an IV in a super bozo way + * + * @return string A 16-byte IV string + */ + public static function createTheWrongIV($password) + { + static $ivs = array(); + + $key = md5($password); + + if (!isset($ivs[$key])) + { + $nBytes = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes + $pwBytes = array(); + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + $iv = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); + $newIV = ''; + foreach ($iv as $int) + { + $newIV .= chr($int); + } + + $ivs[$key] = $newIV; + } + + return $ivs[$key]; + } + + /** + * Expand the password to an appropriate 128-bit encryption key + * + * @param string $password + * + * @return string + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public static function expandKey($password) + { + // Try to fetch cached key or create it if it doesn't exist + $nBits = 128; + $lookupKey = md5($password . '-' . $nBits); + + if (array_key_exists($lookupKey, self::$passwords)) + { + $key = self::$passwords[$lookupKey]; + + return $key; + } + + // use AES itself to encrypt password to get cipher key (using plain password as source for + // key expansion) - gives us well encrypted key. + $nBytes = $nBits / 8; // Number of bytes in key + $pwBytes = array(); + + for ($i = 0; $i < $nBytes; $i++) + { + $pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff; + } + + $key = self::Cipher($pwBytes, self::KeyExpansion($pwBytes)); + $key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long + $newKey = ''; + + foreach ($key as $int) + { + $newKey .= chr($int); + } + + $key = $newKey; + + self::$passwords[$lookupKey] = $key; + + return $key; + } + + /** + * Returns the correct AES-128 CBC encryption adapter + * + * @return AKEncryptionAESAdapterInterface + * + * @since 5.2.0 + * @author Nicholas K. Dionysopoulos + */ + public static function getAdapter() + { + static $adapter = null; + + if (is_object($adapter) && ($adapter instanceof AKEncryptionAESAdapterInterface)) + { + return $adapter; + } + + $adapter = new OpenSSL(); + + if (!$adapter->isSupported()) + { + $adapter = new Mcrypt(); + } + + return $adapter; + } + + /** + * @return string + */ + public static function getPbkdf2Algorithm() + { + return self::$pbkdf2Algorithm; + } + + /** + * @param string $pbkdf2Algorithm + * @return void + */ + public static function setPbkdf2Algorithm($pbkdf2Algorithm) + { + self::$pbkdf2Algorithm = $pbkdf2Algorithm; + } + + /** + * @return int + */ + public static function getPbkdf2Iterations() + { + return self::$pbkdf2Iterations; + } + + /** + * @param int $pbkdf2Iterations + * @return void + */ + public static function setPbkdf2Iterations($pbkdf2Iterations) + { + self::$pbkdf2Iterations = $pbkdf2Iterations; + } + + /** + * @return int + */ + public static function getPbkdf2UseStaticSalt() + { + return self::$pbkdf2UseStaticSalt; + } + + /** + * @param int $pbkdf2UseStaticSalt + * @return void + */ + public static function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt) + { + self::$pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt; + } + + /** + * @return string + */ + public static function getPbkdf2StaticSalt() + { + return self::$pbkdf2StaticSalt; + } + + /** + * @param string $pbkdf2StaticSalt + * @return void + */ + public static function setPbkdf2StaticSalt($pbkdf2StaticSalt) + { + self::$pbkdf2StaticSalt = $pbkdf2StaticSalt; + } + + /** + * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static + * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block + * to minimize the risk of attacks against the password. + * + * @return array + */ + public static function getKeyDerivationParameters() + { + return array( + 'keySize' => 16, + 'algorithm' => self::$pbkdf2Algorithm, + 'iterations' => self::$pbkdf2Iterations, + 'useStaticSalt' => self::$pbkdf2UseStaticSalt, + 'staticSalt' => self::$pbkdf2StaticSalt, + ); + } + + /** + * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt + * + * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt + * + * This implementation of PBKDF2 was originally created by https://defuse.ca + * With improvements by http://www.variations-of-shadow.com + * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster) + * + * @param string $password The password. + * @param string $salt A salt that is unique to the password. + * @param string $algorithm The hash algorithm to use. Default is sha1. + * @param int $count Iteration count. Higher is better, but slower. Default: 1000. + * @param int $key_length The length of the derived key in bytes. + * + * @return string A string of $key_length bytes + */ + public static function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16) + { + if (function_exists("hash_pbkdf2")) + { + return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true); + } + + $hash_length = akstringlen(hash($algorithm, "", true)); + $block_count = ceil($key_length / $hash_length); + + $output = ""; + + for ($i = 1; $i <= $block_count; $i++) + { + // $i encoded as 4 bytes, big endian. + $last = $salt . pack("N", $i); + + // First iteration + $xorResult = hash_hmac($algorithm, $last, $password, true); + $last = $xorResult; + + // Perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) + { + $last = hash_hmac($algorithm, $last, $password, true); + $xorResult ^= $last; + } + + $output .= $xorResult; + } + + return aksubstr($output, 0, $key_length); + } + + /** + * Get the expanded key from the user supplied password using a static salt. The results are cached for performance + * reasons. + * + * @param string $password The user-supplied password, UTF-8 encoded. + * + * @return string The expanded key + */ + private static function getStaticSaltExpandedKey($password) + { + $params = self::getKeyDerivationParameters(); + $keySizeBytes = $params['keySize']; + $algorithm = $params['algorithm']; + $iterations = $params['iterations']; + $staticSalt = $params['staticSalt']; + + $lookupKey = "PBKDF2-$algorithm-$iterations-" . md5($password . $staticSalt); + + if (!array_key_exists($lookupKey, self::$passwords)) + { + self::$passwords[$lookupKey] = self::pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes); + } + + return self::$passwords[$lookupKey]; + } + +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +/** + * A timing safe equals comparison + * + * @param string $safe The internal (safe) value to be checked + * @param string $user The user submitted (unsafe) value + * + * @return boolean True if the two strings are identical. + * + * @see http://blog.ircmaxell.com/2014/11/its-all-about-time.html + */ +function timingSafeEquals($safe, $user) +{ + $safeLen = strlen($safe); + $userLen = strlen($user); + + if ($userLen != $safeLen) + { + return false; + } + + $result = 0; + + for ($i = 0; $i < $userLen; $i++) + { + $result |= (ord($safe[$i]) ^ ord($user[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; +} + +/** + * The Master Setup will read the configuration parameters from restoration.php or + * the JSON-encoded "configuration" input variable and return the status. + * + * @return bool True if the master configuration was applied to the Factory object + */ +function masterSetup() +{ + // ------------------------------------------------------------ + // 1. Import basic setup parameters + // ------------------------------------------------------------ + + $ini_data = null; + + // In restore.php mode, require restoration.php or fail + if (!defined('KICKSTART')) + { + // This is the standalone mode, used by Akeeba Backup Professional. It looks for a restoration.php + // file to perform its magic. If the file is not there, we will abort. + $setupFile = 'restoration.php'; + + if (!file_exists($setupFile)) + { + AKFactory::set('kickstart.enabled', false); + + return false; + } + + // Load restoration.php. It creates a global variable named $restoration_setup + require_once $setupFile; + + $ini_data = $restoration_setup; + + if (empty($ini_data)) + { + // No parameters fetched. Darn, how am I supposed to work like that?! + AKFactory::set('kickstart.enabled', false); + + return false; + } + + AKFactory::set('kickstart.enabled', true); + } + else + { + // Maybe we have $restoration_setup defined in the head of kickstart.php + global $restoration_setup; + + if (!empty($restoration_setup) && !is_array($restoration_setup)) + { + $ini_data = AKText::parse_ini_file($restoration_setup, false, true); + } + elseif (is_array($restoration_setup)) + { + $ini_data = $restoration_setup; + } + } + + // Import any data from $restoration_setup + if (!empty($ini_data)) + { + foreach ($ini_data as $key => $value) + { + AKFactory::set($key, $value); + } + AKFactory::set('kickstart.enabled', true); + } + + // Reinitialize $ini_data + $ini_data = null; + + // ------------------------------------------------------------ + // 2. Explode JSON parameters into $_REQUEST scope + // ------------------------------------------------------------ + + // Detect a JSON string in the request variable and store it. + $json = getQueryParam('json', null); + + // Detect a password in the request variable and store it. + $userPassword = getQueryParam('password', ''); + + // Remove everything from the request, post and get arrays + if (!empty($_REQUEST)) + { + foreach ($_REQUEST as $key => $value) + { + unset($_REQUEST[$key]); + } + } + + if (!empty($_POST)) + { + foreach ($_POST as $key => $value) + { + unset($_POST[$key]); + } + } + + if (!empty($_GET)) + { + foreach ($_GET as $key => $value) + { + unset($_GET[$key]); + } + } + + // Authentication - Akeeba Restore 5.4.0 or later + $password = AKFactory::get('kickstart.security.password', null); + $isAuthenticated = false; + + /** + * Akeeba Restore 5.3.1 and earlier use a custom implementation of AES-128 in CTR mode to encrypt the JSON data + * between client and server. This is not used as a means to maintain secrecy (it's symmetrical encryption and the + * key is, by necessity, transmitted with the HTML page to the client). It's meant as a form of authentication, so + * that the server part can ensure that it only receives commands by an authorized client. + * + * The downside is that encryption in CTR mode (like CBC) is an all-or-nothing affair. This opens the possibility + * for a padding oracle attack (https://en.wikipedia.org/wiki/Padding_oracle_attack). While Akeeba Restore was + * hardened in 2014 to prevent the bulk of suck attacks it is still possible to attack the encryption using a very + * large number of requests (several dozens of thousands). + * + * Since Akeeba Restore 5.4.0 we have removed this authentication method and replaced it with the transmission of a + * very large length password. On the server side we use a timing safe password comparison. By its very nature, it + * will only leak the (well known, constant and large) length of the password but no more information about the + * password itself. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html As a result this form of + * authentication is many orders of magnitude harder to crack than regular encryption. + * + * Now you may wonder "how is sending a password in the clear hardier than encryption?". If you ask that question + * you were not paying attention. The password needs to be known by BOTH the server AND the client (browser). Since + * this password is generated programmatically by the server, it MUST be sent to the client by the server. If an + * attacker is able to intercept this transmission (man in the middle attack) using encryption is irrelevant: the + * attacker already knows your password. This situation also applies when the user sends their own password to the + * server, e.g. when logging into their site. The ONLY way to avoid security issues regarding information being + * stolen in transit is using HTTPS with a commercially signed SSL certificate. Unlike 2008, when Kickstart was + * originally written, obtaining such a certificate nowadays is trivial and costs absolutely nothing thanks to Let's + * Encrypt (https://letsencrypt.org/). + * + * TL;DR: Use HTTPS with a commercially signed SSL certificate, e.g. a free certificate from Let's Encrypt. Client- + * side cryptography does NOT protect you against an attacker (see + * https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-considered-harmful/). + * Moreover, sending a plaintext password is safer than relying on client-side encryption for authentication as it + * reoves the possibility of an attacker inferring the contents of the authentication key (password) in a relatively + * easy and automated manner. + */ + if (!empty($password)) + { + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) + { + die('###{"status":false,"message":"Invalid login"}###'); + } + } + + // No JSON data? Die. + if (empty($json)) + { + die('###{"status":false,"message":"Invalid JSON data"}###'); + } + + // Handle the JSON string + $raw = json_decode($json, true); + + // Invalid JSON data? + if (empty($raw)) + { + die('###{"status":false,"message":"Invalid JSON data"}###'); + } + + // Pass all JSON data to the request array + if (!empty($raw)) + { + foreach ($raw as $key => $value) + { + $_REQUEST[$key] = $value; + } + } + + // ------------------------------------------------------------ + // 3. Try the "factory" variable + // ------------------------------------------------------------ + // A "factory" variable will override all other settings. + $serialized = getQueryParam('factory', null); + + if (!is_null($serialized)) + { + // Get the serialized factory + AKFactory::unserialize($serialized); + AKFactory::set('kickstart.enabled', true); + + return true; + } + + // ------------------------------------------------------------ + // 4. Try the configuration variable for Kickstart + // ------------------------------------------------------------ + if (defined('KICKSTART')) + { + $configuration = getQueryParam('configuration'); + + if (!is_null($configuration)) + { + // Let's decode the configuration from JSON to array + $ini_data = json_decode($configuration, true); + } + else + { + // Neither exists. Enable Kickstart's interface anyway. + $ini_data = array('kickstart.enabled' => true); + } + + // Import any INI data we might have from other sources + if (!empty($ini_data)) + { + foreach ($ini_data as $key => $value) + { + AKFactory::set($key, $value); + } + + AKFactory::set('kickstart.enabled', true); + + return true; + } + } +} + +/** + * Akeeba Restore + * A JSON-powered JPA, JPS and ZIP archive extraction library + * + * @copyright Copyright (c)2008-2018 Nicholas K. Dionysopoulos / Akeeba Ltd + * @license GNU GPL v2 or - at your option - any later version + * @package akeebabackup + * @subpackage kickstart + */ + +// Mini-controller for restore.php +if (!defined('KICKSTART')) +{ + // The observer class, used to report number of files and bytes processed + class RestorationObserver extends AKAbstractPartObserver + { + public $compressedTotal = 0; + public $uncompressedTotal = 0; + public $filesProcessed = 0; + + public function update($object, $message) + { + if (!is_object($message)) + { + return; + } + + if (!array_key_exists('type', get_object_vars($message))) + { + return; + } + + if ($message->type == 'startfile') + { + $this->filesProcessed++; + $this->compressedTotal += $message->content->compressed; + $this->uncompressedTotal += $message->content->uncompressed; + } + } + + public function __toString() + { + return __CLASS__; + } + + } + + // Import configuration + masterSetup(); + + $retArray = array( + 'status' => true, + 'message' => null + ); + + $enabled = AKFactory::get('kickstart.enabled', false); + + if ($enabled) + { + $task = getQueryParam('task'); + + switch ($task) + { + case 'ping': + // ping task - realy does nothing! + $timer = AKFactory::getTimer(); + $timer->enforce_min_exec_time(); + break; + + /** + * There are two separate steps here since we were using an inefficient restoration intialization method in + * the past. Now both startRestore and stepRestore are identical. The difference in behavior depends + * exclusively on the calling Javascript. If no serialized factory was passed in the request then we start a + * new restoration. If a serialized factory was passed in the request then the restoration is resumed. For + * this reason we should NEVER call AKFactory::nuke() in startRestore anymore: that would simply reset the + * extraction engine configuration which was done in masterSetup() leading to an error about the file being + * invalid (since no file is found). + */ + case 'startRestore': + case 'stepRestore': + $engine = AKFactory::getUnarchiver(); // Get the engine + $observer = new RestorationObserver(); // Create a new observer + $engine->attach($observer); // Attach the observer + $engine->tick(); + $ret = $engine->getStatusArray(); + + if ($ret['Error'] != '') + { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $ret['Error']; + } + elseif (!$ret['HasRun']) + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = true; + } + else + { + $retArray['files'] = $observer->filesProcessed; + $retArray['bytesIn'] = $observer->compressedTotal; + $retArray['bytesOut'] = $observer->uncompressedTotal; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['factory'] = AKFactory::serialize(); + } + break; + + case 'finalizeRestore': + $root = AKFactory::get('kickstart.setup.destdir'); + // Remove the installation directory + recursive_remove_directory($root . '/installation'); + + $postproc = AKFactory::getPostProc(); + + /** + * Should I rename the htaccess.bak and web.config.bak files back to their live filenames...? + */ + $renameFiles = AKFactory::get('kickstart.setup.postrenamefiles', true); + + if ($renameFiles) + { + // Rename htaccess.bak to .htaccess + if (file_exists($root . '/htaccess.bak')) + { + if (file_exists($root . '/.htaccess')) + { + $postproc->unlink($root . '/.htaccess'); + } + $postproc->rename($root . '/htaccess.bak', $root . '/.htaccess'); + } + + // Rename htaccess.bak to .htaccess + if (file_exists($root . '/web.config.bak')) + { + if (file_exists($root . '/web.config')) + { + $postproc->unlink($root . '/web.config'); + } + $postproc->rename($root . '/web.config.bak', $root . '/web.config'); + } + } + + // Remove restoration.php + $basepath = KSROOTDIR; + $basepath = rtrim(str_replace('\\', '/', $basepath), '/'); + if (!empty($basepath)) + { + $basepath .= '/'; + } + $postproc->unlink($basepath . 'restoration.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/restore_finalisation.php'; + if (file_exists($filename)) + { + // opcode cache busting before including the filename + if (function_exists('opcache_invalidate')) + { + opcache_invalidate($filename); + } + if (function_exists('apc_compile_file')) + { + apc_compile_file($filename); + } + if (function_exists('wincache_refresh_if_changed')) + { + wincache_refresh_if_changed(array($filename)); + } + if (function_exists('xcache_asm')) + { + xcache_asm($filename); + } + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeRestore')) + { + finalizeRestore($root, $basepath); + } + break; + + default: + // Invalid task! + $enabled = false; + break; + } + } + + // Maybe we weren't authorized or the task was invalid? + if (!$enabled) + { + // Maybe the user failed to enter any information + $retArray['status'] = false; + $retArray['message'] = AKText::_('ERR_INVALID_LOGIN'); + } + + // JSON encode the message + $json = json_encode($retArray); + + // Return the message + echo "###$json###"; + +} + +// ------------ lixlpixel recursive PHP functions ------------- +// recursive_remove_directory( directory to delete, empty ) +// expects path to directory and optional TRUE / FALSE to empty +// of course PHP has to have the rights to delete the directory +// you specify and all files and folders inside the directory +// ------------------------------------------------------------ +function recursive_remove_directory($directory) +{ + // if the path has a slash at the end we remove it here + if (substr($directory, -1) == '/') + { + $directory = substr($directory, 0, -1); + } + // if the path is not valid or is not a directory ... + if (!file_exists($directory) || !is_dir($directory)) + { + // ... we return false and exit the function + return false; + // ... if the path is not readable + } + elseif (!is_readable($directory)) + { + // ... we return false and exit the function + return false; + // ... else if the path is readable + } + else + { + // we open the directory + $handle = opendir($directory); + $postproc = AKFactory::getPostProc(); + // and scan through the items inside + while (false !== ($item = readdir($handle))) + { + // if the filepointer is not the current directory + // or the parent directory + if ($item != '.' && $item != '..') + { + // we build the new path to delete + $path = $directory . '/' . $item; + // if the new path is a directory + if (is_dir($path)) + { + // we call this function with the new path + recursive_remove_directory($path); + // if the new path is a file + } + else + { + // we remove the file + $postproc->unlink($path); + } + } + } + // close the directory + closedir($handle); + // try to delete the now empty directory + if (!$postproc->rmdir($directory)) + { + // return false if not possible + return false; + } + + // return success + return true; + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/script.com_akeeba.php b/deployed/akeeba/administrator/components/com_akeeba/script.com_akeeba.php new file mode 100644 index 00000000..b5f34cbb --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/script.com_akeeba.php @@ -0,0 +1,1026 @@ + array( + // Pro component features + 'administrator/components/com_akeeba/BackupEngine/Archiver/Directftp.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/directftp.ini', + 'administrator/components/com_akeeba/BackupEngine/Archiver/Directftpcurl.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/directftpcurl.ini', + 'administrator/components/com_akeeba/BackupEngine/Archiver/Directsftp.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/directsftp.ini', + 'administrator/components/com_akeeba/BackupEngine/Archiver/Directsftpcurl.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/directsftpcurl.ini', + 'administrator/components/com_akeeba/BackupEngine/Archiver/Jps.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/jps.ini', + 'administrator/components/com_akeeba/BackupEngine/Archiver/Zipnative.php', + 'administrator/components/com_akeeba/BackupEngine/Archiver/zipnative.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/amazons3.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Amazons3.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/azure.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Azure.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/backblaze.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Backblaze.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/cloudfiles.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Cloudfiles.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/cloudme.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Cloudme.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/dreamobjects.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Dreamobjects.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/dropbox.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Dropbox.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/dropbox2.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Dropbox2.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/ftp.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Ftp.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/ftpcurl.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Ftpcurl.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/googledrive.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Googledrive.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/googlestorage.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Googlestorage.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/googlestoragejson.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Googlestoragejson.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/idrivesync.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Idrivesync.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/onedrive.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Onedrive.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/onedrivebusiness.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Onedrivebusiness.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/s3.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/S3.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/sftp.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Sftp.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/sftpcurl.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Sftpcurl.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/sugarsync.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Sugarsync.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/webdav.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Webdav.php', + 'administrator/components/com_akeeba/BackupEngine/Scan/large.ini', + 'administrator/components/com_akeeba/BackupEngine/Scan/Large.php', + + 'administrator/components/com_akeeba/Controller/Alice.php', + 'administrator/components/com_akeeba/Controller/Discover.php', + 'administrator/components/com_akeeba/Controller/IncludeFolders.php', + 'administrator/components/com_akeeba/Controller/MultipleDatabases.php', + 'administrator/components/com_akeeba/Controller/RegExDatabaseFilters.php', + 'administrator/components/com_akeeba/Controller/RegExFileFilters.php', + 'administrator/components/com_akeeba/Controller/RemoteFiles.php', + 'administrator/components/com_akeeba/Controller/S3Import.php', + 'administrator/components/com_akeeba/Controller/Upload.php', + + 'administrator/components/com_akeeba/Model/Alice.php', + 'administrator/components/com_akeeba/Model/Discover.php', + 'administrator/components/com_akeeba/Model/IncludeFolders.php', + 'administrator/components/com_akeeba/Model/MultipleDatabases.php', + 'administrator/components/com_akeeba/Model/RegExDatabaseFilters.php', + 'administrator/components/com_akeeba/Model/RegExFileFilters.php', + 'administrator/components/com_akeeba/Model/RemoteFiles.php', + 'administrator/components/com_akeeba/Model/S3Import.php', + 'administrator/components/com_akeeba/Model/Upload.php', + + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Components.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Extensiondirs.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Extensionfiles.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Languages.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Modules.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Plugins.php', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Filter/Templates.php', + + // Additional ANGIE installers which are not used in Core + 'administrator/components/com_akeeba/Master/Installers/abi.ini', + 'administrator/components/com_akeeba/Master/Installers/abi.jpa', + 'administrator/components/com_akeeba/Master/Installers/angie-generic.ini', + 'administrator/components/com_akeeba/Master/Installers/angie-generic.jpa', + ), + 'folders' => array( + // Pro component features + 'administrator/components/com_akeeba/Alice', + 'administrator/components/com_akeeba/BackupPlatform/Joomla3x/Config/Pro', + 'administrator/components/com_akeeba/View/Alice', + 'administrator/components/com_akeeba/View/Discover', + 'administrator/components/com_akeeba/View/IncludeFolders', + 'administrator/components/com_akeeba/View/MultipleDatabases', + 'administrator/components/com_akeeba/View/RegExDatabaseFilters', + 'administrator/components/com_akeeba/View/RegExFileFilter', + 'administrator/components/com_akeeba/View/RemoteFiles', + 'administrator/components/com_akeeba/View/S3Import', + 'administrator/components/com_akeeba/View/Upload', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Connector', + ) + ); + + /** + * Obsolete files and folders to remove from both paid and free releases. This is used when you refactor code and + * some files inevitably become obsolete and need to be removed. + * + * @var array + */ + protected $removeFilesAllVersions = array( + 'files' => array( + // Outdated CLI scripts + 'cli/akeeba-update.php', + + // Outdated media files + 'media/com_akeeba/icons/akeeba-48.png', + 'media/com_akeeba/icons/akeeba-warning-48.png', + 'media/com_akeeba/icons/arrow_small.png', + 'media/com_akeeba/icons/error_small.png', + 'media/com_akeeba/icons/ok_small.png', + 'media/com_akeeba/icons/reload.png', + 'media/com_akeeba/icons/scheduling-32.png', + 'media/com_akeeba/icons/update.png', + + 'media/com_akeeba/js/akeebajq.js', + 'media/com_akeeba/js/akeebajqui.js', + 'media/com_akeeba/js/akeebaui.js', + 'media/com_akeeba/js/akeebauipro.js', + 'media/com_akeeba/js/alice.js', + 'media/com_akeeba/js/backup.js', + 'media/com_akeeba/js/configuration.js', + 'media/com_akeeba/js/confwiz.js', + 'media/com_akeeba/js/dbef.js', + 'media/com_akeeba/js/eff.js', + 'media/com_akeeba/js/encryption.js', + 'media/com_akeeba/js/fsfilter.js', + 'media/com_akeeba/js/gui-helpers.js', + 'media/com_akeeba/js/jquery.js', + 'media/com_akeeba/js/jquery-ui.js', + 'media/com_akeeba/js/multidb.js', + 'media/com_akeeba/js/regexdbfilter.js', + 'media/com_akeeba/js/regexfsfilter.js', + 'media/com_akeeba/js/restore.js', + 'media/com_akeeba/js/stepper.js', + 'media/com_akeeba/js/system.js', + 'media/com_akeeba/js/transfer.js', + + // Old CLI backup scripts, obsolete since 3.5.0, removed in 4.0.0 + 'administrator/components/com_akeeba/backup.php', + 'administrator/components/com_akeeba/altbackup.php', + + // Files used in version 4.2, but before 5.0 + // -- Back-end + 'administrator/components/com_akeeba/dispatcher.php', + 'administrator/components/com_akeeba/toolbar.php', + 'administrator/components/com_akeeba/views/backup/view.html.php', + 'administrator/components/com_akeeba/views/backup/tmpl/default.php', + 'administrator/components/com_akeeba/views/restore/view.html.php', + 'administrator/components/com_akeeba/views/restore/tmpl/default.php', + 'administrator/components/com_akeeba/views/restore/tmpl/restore.php', + 'administrator/components/com_akeeba/views/transfer/view.html.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default_dialogs.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default_manualtransfer.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default_prerequisites.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default_remoteconnection.php', + 'administrator/components/com_akeeba/views/transfer/tmpl/default_upload.php', + + // -- Front-end + 'components/com_akeeba/dispatcher.php', + + // Integrity check (obsolete) + 'administrator/components/com_akeeba/fileslist.php', + + // Blade files (replaced by regular PHP view files) + "administrator/components/com_akeeba/View/Alice/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Backup/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Backup/tmpl/script.blade.php", + "administrator/components/com_akeeba/View/Browser/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/ErrorModal.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/FolderBrowser.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPBrowser.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/FTPConnectionTest.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/ProfileName.blade.php", + "administrator/components/com_akeeba/View/CommonTemplates/tmpl/SFTPBrowser.blade.php", + "administrator/components/com_akeeba/View/Configuration/tmpl/confwiz_modal.blade.php", + "administrator/components/com_akeeba/View/Configuration/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/ConfigurationWizard/tmpl/wizard.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/footer.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_advanced.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_basic.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_includeexclude.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/icons_troubleshooting.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/oneclick.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/profile.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_backup.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/sidebar_status.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/warning_phpversion.blade.php", + "administrator/components/com_akeeba/View/ControlPanel/tmpl/warnings.blade.php", + "administrator/components/com_akeeba/View/DatabaseFilters/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/DatabaseFilters/tmpl/tabular.blade.php", + "administrator/components/com_akeeba/View/Discover/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Discover/tmpl/discover.blade.php", + "administrator/components/com_akeeba/View/FileFilters/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/FileFilters/tmpl/tabular.blade.php", + "administrator/components/com_akeeba/View/IncludeFolders/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Log/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Manage/tmpl/comment.blade.php", + "administrator/components/com_akeeba/View/Manage/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Manage/tmpl/howtorestore_modal.blade.php", + "administrator/components/com_akeeba/View/Manage/tmpl/manage_column.blade.php", + "administrator/components/com_akeeba/View/MultipleDatabases/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Profiles/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Profiles/tmpl/form.blade.php", + "administrator/components/com_akeeba/View/RegExDatabaseFilters/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/RegExFileFilter/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/RemoteFiles/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/RemoteFiles/tmpl/dlprogress.blade.php", + "administrator/components/com_akeeba/View/Restore/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Restore/tmpl/restore.blade.php", + "administrator/components/com_akeeba/View/S3Import/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/S3Import/tmpl/downloading.blade.php", + "administrator/components/com_akeeba/View/Schedule/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Schedule/tmpl/default_checkbackups.blade.php", + "administrator/components/com_akeeba/View/Schedule/tmpl/default_runbackups.blade.php", + "administrator/components/com_akeeba/View/Transfer/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Transfer/tmpl/default_manualtransfer.blade.php", + "administrator/components/com_akeeba/View/Transfer/tmpl/default_prerequisites.blade.php", + "administrator/components/com_akeeba/View/Transfer/tmpl/default_remoteconnection.blade.php", + "administrator/components/com_akeeba/View/Transfer/tmpl/default_upload.blade.php", + "administrator/components/com_akeeba/View/Upload/tmpl/default.blade.php", + "administrator/components/com_akeeba/View/Upload/tmpl/done.blade.php", + "administrator/components/com_akeeba/View/Upload/tmpl/error.blade.php", + "administrator/components/com_akeeba/View/Upload/tmpl/uploading.blade.php", + + // Dropbox v1 integration + 'administrator/components/com_akeeba/BackupEngine/Postproc/dropbox.ini', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Dropbox.php', + 'administrator/components/com_akeeba/BackupEngine/Postproc/Connector/Dropbox.php', + + // Obsolete Azure files + 'administrator/components/com_akeeba/BackupEngine/Postproc/Connector/Azure/Credentials/Sharedsignature.php', + + // Obsolete AES-128 CTR implementation in Javascript + 'media/com_akeeba/js/Encryption.min.js', + 'media/com_akeeba/js/Encryption.min.map', + + // PHP 7.2 compatibility + 'administrator/components/com_akeeba/BackupEngine/Base/Object.php', + + // Obsolete media files + 'media/com_akeeba/icons/akeeba-ui-32.png', + 'media/com_akeeba/changelog.png', + ), + 'folders' => array( + // Directories used up to version 4.1 (inclusive) + // -- Back-end + 'administrator/components/com_akeeba/akeeba', + 'administrator/components/com_akeeba/plugins', + // -- Front-end + 'components/com_akeeba/views', + + // Directories used in version 4.2, but before 5.0 + // -- Back-end + 'administrator/components/com_akeeba/alice', + 'administrator/components/com_akeeba/assets', + 'administrator/components/com_akeeba/controllers', + 'administrator/components/com_akeeba/engine', + 'administrator/components/com_akeeba/helpers', + 'administrator/components/com_akeeba/models', + 'administrator/components/com_akeeba/platform', + 'administrator/components/com_akeeba/tables', + 'administrator/components/com_akeeba/views/alices', + 'administrator/components/com_akeeba/views/browser', + 'administrator/components/com_akeeba/views/buadmin', + 'administrator/components/com_akeeba/views/config', + 'administrator/components/com_akeeba/views/confwiz', + 'administrator/components/com_akeeba/views/cpanel', + 'administrator/components/com_akeeba/views/dbef', + 'administrator/components/com_akeeba/views/discover', + 'administrator/components/com_akeeba/views/eff', + 'administrator/components/com_akeeba/views/fsfilter', + 'administrator/components/com_akeeba/views/log', + 'administrator/components/com_akeeba/views/multidb', + 'administrator/components/com_akeeba/views/profiles', + 'administrator/components/com_akeeba/views/regexdbfilter', + 'administrator/components/com_akeeba/views/regexfsfilter', + 'administrator/components/com_akeeba/views/remotefiles', + 'administrator/components/com_akeeba/views/s3import', + 'administrator/components/com_akeeba/views/schedule', + 'administrator/components/com_akeeba/views/updates', + 'administrator/components/com_akeeba/views/upload', + // -- Front-end + 'components/com_akeeba/controllers', + 'components/com_akeeba/models', + + // Outdated media directories + 'media/com_akeeba/theme', + + // Obsolete Plugins + 'plugins/system/aklazy', + 'plugins/system/srp', + + // Obsolete Modules + 'administrator/modules/mod_akadmin', + + // Dropbox v1 integration + 'administrator/components/com_akeeba/BackupEngine/Postproc/Connector/Dropbox', + ) + ); + + /** + * Runs on installation + * + * @param JInstallerAdapterComponent $parent The parent object + * + * @return void + */ + public function install($parent) + { + if (!defined('AKEEBA_THIS_IS_INSTALLATION_FROM_SCRATCH')) + { + define('AKEEBA_THIS_IS_INSTALLATION_FROM_SCRATCH', 1); + } + } + + /** + * Joomla! pre-flight event. This runs before Joomla! installs or updates the component. This is our last chance to + * tell Joomla! if it should abort the installation. + * + * @param string $type Installation type (install, update, discover_install) + * @param JInstallerAdapterComponent $parent Parent object + * + * @return boolean True to let the installation proceed, false to halt the installation + */ + public function preflight($type, $parent) + { + $this->isPaid = is_dir($parent->getParent()->getPath('source') . '/backend/AliceEngine'); + + $result = parent::preflight($type, $parent); + + if (!$result) + { + return $result; + } + + // Move the server key file from /akeeba or /engine to /BackupEngine + $componentPath = JPATH_ADMINISTRATOR . '/components/com_akeeba'; + $fromFile = $componentPath . '/akeeba/serverkey.php'; + $toFile = $componentPath . '/BackupEngine/serverkey.php'; + + if (!file_exists($fromFile)) + { + $fromFile = $componentPath . '/engine/serverkey.php'; + } + + if (@file_exists($fromFile) && !@file_exists($toFile)) + { + $toPath = $componentPath . '/BackupEngine'; + + if (class_exists('JLoader') && method_exists('JLoader', 'import')) + { + JLoader::import('joomla.filesystem.folder'); + JLoader::import('joomla.filesystem.file'); + } + + if (@is_dir($componentPath) && !@is_dir($toPath)) + { + JFolder::create($toPath); + } + + if (@is_dir($toPath)) + { + JFile::copy($fromFile, $toFile); + } + } + + return $result; + } + + /** + * Runs after install, update or discover_update. In other words, it executes after Joomla! has finished installing + * or updating your component. This is the last chance you've got to perform any additional installations, clean-up, + * database updates and similar housekeeping functions. + * + * @param string $type install, update or discover_update + * @param JInstallerAdapterComponent $parent Parent object + */ + function postflight($type, $parent) + { + // Let's install common tables + $container = null; + $model = null; + + if (class_exists('FOF30\\Container\\Container')) + { + try + { + $container = \FOF30\Container\Container::getInstance('com_akeeba'); + } + catch (\Exception $e) + { + $container = null; + } + } + + if (is_object($container) && class_exists('FOF30\\Container\\Container') && ($container instanceof \FOF30\Container\Container)) + { + /** @var \Akeeba\Backup\Admin\Model\UsageStatistics $model */ + try + { + $model = $container->factory->model('UsageStatistics')->tmpInstance(); + } + catch (\Exception $e) + { + $model = null; + } + } + + if (is_object($model) && class_exists('Akeeba\\Backup\\Admin\\Model\\UsageStatistics') + && ($model instanceof Akeeba\Backup\Admin\Model\UsageStatistics) + && method_exists($model, 'checkAndFixCommonTables')) + { + try + { + $model->checkAndFixCommonTables(); + } + catch (Exception $e) + { + // Do nothing if that failed. + } + } + + // Parent method + parent::postflight($type, $parent); + + // Add ourselves to the list of extensions depending on Akeeba FEF + $this->addDependency('file_fef', $this->componentName); + + // Uninstall post-installation messages we are no longer using + $this->uninstallObsoletePostinstallMessages(); + + // Remove the update sites for this component on installation. The update sites are now handled at the package + // level. + $this->removeObsoleteUpdateSites($parent); + + // Remove the FOF 2.x update sites (annoying leftovers) + $this->removeFOFUpdateSites(); + + // If this is a new installation tell it to NOT mark the backup profiles as configured. + if (defined('AKEEBA_THIS_IS_INSTALLATION_FROM_SCRATCH')) + { + $this->markProfilesAsNotConfiguredYet(); + } + + // This is an update of an existing installation + if (!defined('AKEEBA_THIS_IS_INSTALLATION_FROM_SCRATCH')) + { + // Migrate profiles if necessary + $this->migrateProfiles(); + } + } + + /** + * Override this method to display a custom component installation message if you so wish + * + * @param \JInstallerAdapterComponent $parent Parent class calling us + */ + protected function renderPostInstallation($parent) + { + try + { + $this->warnAboutJSNPowerAdmin(); + } + catch (Exception $e) + { + // Don't sweat if the site's db croaks while I'm checking for 3PD software that causes trouble + } + + // Load the version file + if (!defined('AKEEBA_PRO')) + { + @include_once JPATH_ADMINISTRATOR . '/components/com_akeeba/version.php'; + } + + if (!defined('AKEEBA_PRO')) + { + define('AKEEBA_PRO', '0'); + } + + $videoTutorialURL = 'https://www.akeebabackup.com/videos/1212-akeeba-backup-core.html'; + + if (AKEEBA_PRO) + { + $videoTutorialURL = 'https://www.akeebabackup.com/videos/1213-akeeba-backup-for-joomla-pro.html'; + } + + ?> + Akeeba Backup + +

        Welcome to Akeeba Backup!

        + +
        + You can download translation files directly + from our CDN page. +
        + +
        +

        + We strongly recommend watching our + video + tutorials before using this component. +

        + +

        + If this is the first time you install Akeeba Backup on your site please run the + Configuration Wizard. Akeeba Backup will + configure itself optimally for your site. +

        + +

        + By installing this component you are implicitly accepting + its license (GNU GPLv3) and our + Terms of Service, + including our Support Policy. +

        +
        + factory->model('UsageStatistics')->tmpInstance(); + } + catch (\Exception $e) + { + $model = null; + } + } + + /** @var \Akeeba\Backup\Admin\Model\UsageStatistics $model */ + try + { + if (is_object($model) && class_exists('Akeeba\\Backup\\Admin\\Model\\UsageStatistics') + && ($model instanceof Akeeba\Backup\Admin\Model\UsageStatistics) + && method_exists($model, 'collectStatistics')) + { + $iframe = $model->collectStatistics(true); + + if ($iframe) + { + echo $iframe; + } + } + } + catch (\Exception $e) + { + } + } + + /** + * Override this method to display a custom component uninstallation message if you so wish + * + * @param \JInstallerAdapterComponent $parent Parent class calling us + */ + protected function renderPostUninstallation($parent) + { + ?> +

        Akeeba Backup Uninstallation Status

        +

        We are sorry that you decided to uninstall Akeeba Backup. Please let us know why by using the Contact Us form on our site. We + appreciate your feedback; it helps us develop better software!

        + getQuery(true) + ->delete($db->qn('#__postinstall_messages')) + ->where($db->qn('title_key') . ' = ' . $db->q($obsoleteKey)); + try + { + $db->setQuery($query)->execute(); + } + catch (Exception $e) + { + // Do nothing + } + } + } + + /** + * The PowerAdmin extension makes menu items disappear. People assume it's our fault. JSN PowerAdmin authors don't + * own up to their software's issue. I have no choice but to warn our users about the faulty third party software. + */ + private function warnAboutJSNPowerAdmin() + { + $db = JFactory::getDbo(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->qn('#__extensions')) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('element') . ' = ' . $db->q('com_poweradmin')) + ->where($db->qn('enabled') . ' = ' . $db->q('1')); + $hasPowerAdmin = $db->setQuery($query)->loadResult(); + + if (!$hasPowerAdmin) + { + return; + } + + $query = $db->getQuery(true) + ->select('manifest_cache') + ->from($db->qn('#__extensions')) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('element') . ' = ' . $db->q('com_poweradmin')) + ->where($db->qn('enabled') . ' = ' . $db->q('1')); + $paramsJson = $db->setQuery($query)->loadResult(); + + $className = class_exists('JRegistry') ? 'JRegistry' : '\Joomla\Registry\Registry'; + + /** @var \Joomla\Registry\Registry $jsnPAManifest */ + $jsnPAManifest = new $className(); + $jsnPAManifest->loadString($paramsJson, 'JSON'); + $version = $jsnPAManifest->get('version', '0.0.0'); + + if (version_compare($version, '2.1.2', 'ge')) + { + return; + } + + echo <<< HTML +
        +

        WARNING: Menu items for {$this->componentName} might not be displayed on your site.

        +

        + We have detected that you are using JSN PowerAdmin on your site. This software ignores Joomla! standards and + hides the Component menu items to {$this->componentName} in the administrator backend of your site. Unfortunately we + can't provide support for third party software. Please contact the developers of JSN PowerAdmin for support + regarding this issue. +

        +

        + Tip: You can disable JSN PowerAdmin to see the menu items to Akeeba Backup. +

        +
        + +HTML; + + } + + /** + * Loads the Akeeba Engine if it's not already loaded + */ + private function loadAkeebaEngine() + { + if (class_exists('\\Akeeba\\Engine\\Platform')) + { + return; + } + + // Load the language files + $paths = array(JPATH_ADMINISTRATOR, JPATH_ROOT); + $jlang = JFactory::getLanguage(); + $jlang->load('com_akeeba', $paths[0], 'en-GB', true); + $jlang->load('com_akeeba', $paths[1], 'en-GB', true); + $jlang->load('com_akeeba' . '.override', $paths[0], 'en-GB', true); + $jlang->load('com_akeeba' . '.override', $paths[1], 'en-GB', true); + + // Load the version file + @include_once JPATH_ADMINISTRATOR . '/components/com_akeeba/version.php'; + + if (!defined('AKEEBA_PRO')) + { + define('AKEEBA_PRO', '0'); + } + + // Enable Akeeba Engine + if (!defined('AKEEBAENGINE')) + { + define('AKEEBAENGINE', 1); + } + + // Load the engine + $factoryPath = JPATH_ADMINISTRATOR . '/components/com_akeeba/BackupEngine/Factory.php'; + define('AKEEBAROOT', JPATH_ADMINISTRATOR . '/components/com_akeeba/BackupEngine'); + + require_once $factoryPath; + + // Assign the correct platform + \Akeeba\Engine\Platform::addPlatform('joomla3x', JPATH_ADMINISTRATOR . '/components/com_akeeba/BackupPlatform/Joomla3x'); + } + + /** + * Migrates existing backup profiles. The changes currently made are: + * – Change post-processing from "s3" (legacy) to "amazons3" (current version) + * – Fix profiles with invalid embedded installer settings + * + * @return void + */ + private function migrateProfiles() + { + $this->loadAkeebaEngine(); + + // Get a list of backup profiles + $db = JFactory::getDbo(); + + try + { + $query = $db->getQuery(true) + ->select($db->qn('id')) + ->from($db->qn('#__ak_profiles')); + $profiles = $db->setQuery($query)->loadColumn(); + } + catch (Exception $e) + { + // Eh, we couldn't load the profiles. Something's broken in the database. It will be fixed when the + // installation continues but for now we have to just return without doing anything. + return; + } + + // Normally this should never happen as we're supposed to have at least profile #1 + if (empty($profiles)) + { + return; + } + + // Migrate each profile + foreach ($profiles as $profile) + { + // Initialization + $dirty = false; + + // Load the profile configuration + try + { + \Akeeba\Engine\Platform::getInstance()->load_configuration($profile); + $config = \Akeeba\Engine\Factory::getConfiguration(); + } + catch (Exception $e) + { + // Your database is broken :( + continue; + } + + // -- Migrate obsolete "s3" engine to "amazons3" + $postProcType = $config->get('akeeba.advanced.postproc_engine', ''); + + if ($postProcType == 's3') + { + $config->setKeyProtection('akeeba.advanced.postproc_engine', false); + $config->setKeyProtection('engine.postproc.amazons3.signature', false); + $config->setKeyProtection('engine.postproc.amazons3.accesskey', false); + $config->setKeyProtection('engine.postproc.amazons3.secretkey', false); + $config->setKeyProtection('engine.postproc.amazons3.usessl', false); + $config->setKeyProtection('engine.postproc.amazons3.bucket', false); + $config->setKeyProtection('engine.postproc.amazons3.directory', false); + $config->setKeyProtection('engine.postproc.amazons3.rrs', false); + $config->setKeyProtection('engine.postproc.amazons3.customendpoint', false); + $config->setKeyProtection('engine.postproc.amazons3.legacy', false); + + $config->set('akeeba.advanced.postproc_engine', 'amazons3'); + $config->set('engine.postproc.amazons3.signature', 's3'); + $config->set('engine.postproc.amazons3.accesskey', $config->get('engine.postproc.s3.accesskey')); + $config->set('engine.postproc.amazons3.secretkey', $config->get('engine.postproc.s3.secretkey')); + $config->set('engine.postproc.amazons3.usessl', $config->get('engine.postproc.s3.usessl')); + $config->set('engine.postproc.amazons3.bucket', $config->get('engine.postproc.s3.bucket')); + $config->set('engine.postproc.amazons3.directory', $config->get('engine.postproc.s3.directory')); + $config->set('engine.postproc.amazons3.rrs', $config->get('engine.postproc.s3.rrs')); + $config->set('engine.postproc.amazons3.customendpoint', $config->get('engine.postproc.s3.customendpoint')); + $config->set('engine.postproc.amazons3.legacy', $config->get('engine.postproc.s3.legacy')); + + $dirty = true; + } + + // Fix profiles with invalid embedded installer settings + $embeddedInstaller = $config->get('akeeba.advanced.embedded_installer'); + + if (empty($embeddedInstaller) || ($embeddedInstaller == 'angie-joomla') || ( + (substr($embeddedInstaller, 0, 5) != 'angie') && ($embeddedInstaller != 'none') + )) + { + $config->setKeyProtection('akeeba.advanced.embedded_installer', false); + $config->set('akeeba.advanced.embedded_installer', 'angie'); + $dirty = true; + } + + // Save dirty records + if ($dirty) + { + try + { + \Akeeba\Engine\Platform::getInstance()->save_configuration($profile); + } + catch (Exception $e) + { + // Your database is broken! + continue; + } + } + } + } + + /** + * Remove FOF 2.x update sites + */ + private function removeFOFUpdateSites() + { + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->delete($db->qn('#__update_sites')) + ->where($db->qn('location') . ' = ' . $db->q('http://cdn.akeebabackup.com/updates/fof.xml')); + try + { + $db->setQuery($query)->execute(); + } + catch (\Exception $e) + { + // Do nothing on failure + } + + } + + private function markProfilesAsNotConfiguredYet() + { + try + { + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select($db->qn('params')) + ->from($db->qn('#__extensions')) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('element') . ' = ' . $db->q('com_akeeba')); + + $jsonData = $db->setQuery($query)->loadResult(); + + if (class_exists('JRegistry')) + { + $reg = new JRegistry($jsonData); + } + else + { + $reg = new \Joomla\Registry\Registry($jsonData); + } + + $reg->set('confwiz_upgrade', 1); + $jsonData = $reg->toString('JSON'); + + $query = $db->getQuery() + ->update($db->qn('#__extensions')) + ->set($db->qn('params') . ' = ' . $db->q($jsonData)) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('element') . ' = ' . $db->q('com_akeeba')); + $db->setQuery($query)->execute(); + } + catch (Exception $e) + { + // If that fails it's not the end of the world. The component is still usable, so just swallow any + // exception. + } + } + + /** + * Removes obsolete update sites created for the component (we are now using an update site for the package, not the + * component). + * + * @param JInstallerAdapterComponent $parent The parent installer + */ + protected function removeObsoleteUpdateSites($parent) + { + $db = $parent->getParent()->getDbo(); + + $query = $db->getQuery(true) + ->select($db->qn('extension_id')) + ->from($db->qn('#__extensions')) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('name') . ' = ' . $db->q($this->componentName)); + + try + { + $extensionId = $db->setQuery($query)->loadResult(); + } + catch (Exception $e) + { + // Your database is broken. + return; + } + + if (!$extensionId) + { + return; + } + + $query = $db->getQuery(true) + ->select($db->qn('update_site_id')) + ->from($db->qn('#__update_sites_extensions')) + ->where($db->qn('extension_id') . ' = ' . $db->q($extensionId)); + + try + { + $ids = $db->setQuery($query)->loadColumn(0); + } + catch (Exception $e) + { + // Your database is broken. + return; + } + + if (!is_array($ids) && empty($ids)) + { + return; + } + + foreach ($ids as $id) + { + $query = $db->getQuery(true) + ->delete($db->qn('#__update_sites')) + ->where($db->qn('update_site_id') . ' = ' . $db->q($id)); + $db->setQuery($query); + + try + { + $db->execute(); + } + catch (\Exception $e) + { + // Do not fail in this case + } + } + } +} diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/common/mysql.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/common/mysql.xml new file mode 100644 index 00000000..32e805ab --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/common/mysql.xml @@ -0,0 +1,40 @@ + + + + + mysql + mysqli + pdomysql + + + false + + + + + + + + + + + + + + + + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/common/postgresql.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/common/postgresql.xml new file mode 100644 index 00000000..4c4a8648 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/common/postgresql.xml @@ -0,0 +1,22 @@ + + + + + postgres + postgresql + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/common/sqlsrv.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/common/sqlsrv.xml new file mode 100644 index 00000000..36912748 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/common/sqlsrv.xml @@ -0,0 +1,25 @@ + + + + + sqlsrv + sqlazure + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/index.html b/deployed/akeeba/administrator/components/com_akeeba/sql/index.html new file mode 100644 index 00000000..0b551329 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/xml/mysql.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/mysql.xml new file mode 100644 index 00000000..af04cb18 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/mysql.xml @@ -0,0 +1,117 @@ + + + + + + + mysql + mysqli + pdomysql + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/xml/postgresql.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/postgresql.xml new file mode 100644 index 00000000..ba9d0574 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/postgresql.xml @@ -0,0 +1,106 @@ + + + + + + + postgres + postgresql + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/sql/xml/sqlsrv.xml b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/sqlsrv.xml new file mode 100644 index 00000000..544da8aa --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/sql/xml/sqlsrv.xml @@ -0,0 +1,123 @@ + + + + + + + sqlsrv + sqlazure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployed/akeeba/administrator/components/com_akeeba/version.php b/deployed/akeeba/administrator/components/com_akeeba/version.php new file mode 100644 index 00000000..8a575c0d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/version.php @@ -0,0 +1,13 @@ + + + + + + + + + +
        + + + + + + + + + + + + + +
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/views/Configuration/tmpl/default.xml b/deployed/akeeba/administrator/components/com_akeeba/views/Configuration/tmpl/default.xml new file mode 100644 index 00000000..5d659c7d --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/views/Configuration/tmpl/default.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/views/ControlPanel/tmpl/default.xml b/deployed/akeeba/administrator/components/com_akeeba/views/ControlPanel/tmpl/default.xml new file mode 100644 index 00000000..930886aa --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/views/ControlPanel/tmpl/default.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/views/Manage/tmpl/default.xml b/deployed/akeeba/administrator/components/com_akeeba/views/Manage/tmpl/default.xml new file mode 100644 index 00000000..c3eec638 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/views/Manage/tmpl/default.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/deployed/akeeba/administrator/components/com_akeeba/views/Restore/tmpl/default.xml b/deployed/akeeba/administrator/components/com_akeeba/views/Restore/tmpl/default.xml new file mode 100644 index 00000000..2f4aeff3 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/views/Restore/tmpl/default.xml @@ -0,0 +1,23 @@ + + + + + + + + + +
        + +
        +
        +
        diff --git a/deployed/akeeba/administrator/components/com_akeeba/views/Transfer/tmpl/default.xml b/deployed/akeeba/administrator/components/com_akeeba/views/Transfer/tmpl/default.xml new file mode 100644 index 00000000..ffe06ae9 --- /dev/null +++ b/deployed/akeeba/administrator/components/com_akeeba/views/Transfer/tmpl/default.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/deployed/akeeba/libraries/fof/autoloader/component.php b/deployed/akeeba/libraries/fof/autoloader/component.php new file mode 100644 index 00000000..f9a2a8e5 --- /dev/null +++ b/deployed/akeeba/libraries/fof/autoloader/component.php @@ -0,0 +1,780 @@ +getComponentBaseDirs($component); + $fofComponents[$component] = file_exists($componentPaths['admin'] . '/fof.xml'); + } + + return $fofComponents[$component]; + } + + /** + * Creates class aliases. On systems where eval() is enabled it creates a + * real class. On other systems it merely creates an alias. The eval() + * method is preferred as class_aliases result in the name of the class + * being instanciated not being available, making it impossible to create + * a class instance without passing a $config array :( + * + * @param string $original The name of the original (existing) class + * @param string $alias The name of the new (aliased) class + * @param boolean $autoload Should I try to autoload the $original class? + * + * @return void + */ + private function class_alias($original, $alias, $autoload = true) + { + static $hasEval = null; + + if (is_null($hasEval)) + { + $hasEval = false; + + if (function_exists('ini_get')) + { + $disabled_functions = ini_get('disabled_functions'); + + if (!is_string($disabled_functions)) + { + $hasEval = true; + } + else + { + $disabled_functions = explode(',', $disabled_functions); + $hasEval = !in_array('eval', $disabled_functions); + } + } + } + + if (!class_exists($original, $autoload)) + { + return; + } + + if ($hasEval) + { + $phpCode = "class $alias extends $original {}"; + eval($phpCode); + } + else + { + class_alias($original, $alias, $autoload); + } + } + + /** + * Autoload Controllers + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_controller($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'Controller') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need three parts in the name + if (count($parts) != 3) + { + return; + } + + // We need the second part to be "controller" + if ($parts[1] != 'controller') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + $view = $parts[2]; + + // Is this an FOF 2.1 or later component? + if (!$this->isFOFComponent($component)) + { + return; + } + + // Get the alternate view and class name (opposite singular/plural name) + $alt_view = FOFInflector::isSingular($view) ? FOFInflector::pluralize($view) : FOFInflector::singularize($view); + $alt_class = FOFInflector::camelize($component_raw . '_controller_' . $alt_view); + + // Get the component's paths + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + + // Get the proper and alternate paths and file names + $file = "/controllers/$view.php"; + $altFile = "/controllers/$alt_view.php"; + $path = $componentPaths['main']; + $altPath = $componentPaths['alt']; + + // Try to find the proper class in the proper path + if (file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the proper class in the alternate path + if (!class_exists($class_name) && file_exists($altPath . $file)) + { + @include_once $altPath . $file; + } + + // Try to find the alternate class in the proper path + if (!class_exists($alt_class) && file_exists($path . $altFile)) + { + @include_once $path . $altFile; + } + + // Try to find the alternate class in the alternate path + if (!class_exists($alt_class) && file_exists($altPath . $altFile)) + { + @include_once $altPath . $altFile; + } + + // If the alternate class exists just map the class to the alternate + if (!class_exists($class_name) && class_exists($alt_class)) + { + $this->class_alias($alt_class, $class_name); + } + + // No class found? Map to FOFController + elseif (!class_exists($class_name)) + { + if ($view != 'default') + { + $defaultClass = FOFInflector::camelize($component_raw . '_controller_default'); + $this->class_alias($defaultClass, $class_name); + } + else + { + $this->class_alias('FOFController', $class_name); + } + } + } + + /** + * Autoload Models + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_model($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'Model') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need three parts in the name + if (count($parts) != 3) + { + return; + } + + // We need the second part to be "model" + if ($parts[1] != 'model') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + $view = $parts[2]; + + // Is this an FOF 2.1 or later component? + if (!$this->isFOFComponent($component)) + { + return; + } + + // Get the alternate view and class name (opposite singular/plural name) + $alt_view = FOFInflector::isSingular($view) ? FOFInflector::pluralize($view) : FOFInflector::singularize($view); + $alt_class = FOFInflector::camelize($component_raw . '_model_' . $alt_view); + + // Get the proper and alternate paths and file names + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + + $file = "/models/$view.php"; + $altFile = "/models/$alt_view.php"; + $path = $componentPaths['main']; + $altPath = $componentPaths['alt']; + + // Try to find the proper class in the proper path + if (file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the proper class in the alternate path + if (!class_exists($class_name) && file_exists($altPath . $file)) + { + @include_once $altPath . $file; + } + + // Try to find the alternate class in the proper path + if (!class_exists($alt_class) && file_exists($path . $altFile)) + { + @include_once $path . $altFile; + } + + // Try to find the alternate class in the alternate path + if (!class_exists($alt_class) && file_exists($altPath . $altFile)) + { + @include_once $altPath . $altFile; + } + + // If the alternate class exists just map the class to the alternate + if (!class_exists($class_name) && class_exists($alt_class)) + { + $this->class_alias($alt_class, $class_name); + } + + // No class found? Map to FOFModel + elseif (!class_exists($class_name)) + { + if ($view != 'default') + { + $defaultClass = FOFInflector::camelize($component_raw . '_model_default'); + $this->class_alias($defaultClass, $class_name); + } + else + { + $this->class_alias('FOFModel', $class_name, true); + } + } + } + + /** + * Autoload Views + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_view($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'View') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need at least three parts in the name + + if (count($parts) < 3) + { + return; + } + + // We need the second part to be "view" + + if ($parts[1] != 'view') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + $view = $parts[2]; + + if (count($parts) > 3) + { + $format = $parts[3]; + } + else + { + $input = new FOFInput; + $format = $input->getCmd('format', 'html', 'cmd'); + } + + // Is this an FOF 2.1 or later component? + if (!$this->isFOFComponent($component)) + { + return; + } + + // Get the alternate view and class name (opposite singular/plural name) + $alt_view = FOFInflector::isSingular($view) ? FOFInflector::pluralize($view) : FOFInflector::singularize($view); + $alt_class = FOFInflector::camelize($component_raw . '_view_' . $alt_view); + + // Get the proper and alternate paths and file names + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + + $protoFile = "/models/$view"; + $protoAltFile = "/models/$alt_view"; + $path = $componentPaths['main']; + $altPath = $componentPaths['alt']; + + $formats = array($format); + + if ($format != 'html') + { + $formats[] = 'raw'; + } + + foreach ($formats as $currentFormat) + { + $file = $protoFile . '.' . $currentFormat . '.php'; + $altFile = $protoAltFile . '.' . $currentFormat . '.php'; + + // Try to find the proper class in the proper path + if (!class_exists($class_name) && file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the proper class in the alternate path + if (!class_exists($class_name) && file_exists($altPath . $file)) + { + @include_once $altPath . $file; + } + + // Try to find the alternate class in the proper path + if (!class_exists($alt_class) && file_exists($path . $altFile)) + { + @include_once $path . $altFile; + } + + // Try to find the alternate class in the alternate path + if (!class_exists($alt_class) && file_exists($altPath . $altFile)) + { + @include_once $altPath . $altFile; + } + } + + // If the alternate class exists just map the class to the alternate + if (!class_exists($class_name) && class_exists($alt_class)) + { + $this->class_alias($alt_class, $class_name); + } + + // No class found? Map to FOFModel + elseif (!class_exists($class_name)) + { + if ($view != 'default') + { + $defaultClass = FOFInflector::camelize($component_raw . '_view_default'); + $this->class_alias($defaultClass, $class_name); + } + else + { + if (!file_exists(self::$fofPath . '/view/' . $format . '.php')) + { + $default_class = 'FOFView'; + } + else + { + $default_class = 'FOFView' . ucfirst($format); + } + + $this->class_alias($default_class, $class_name, true); + } + } + } + + /** + * Autoload Tables + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_table($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'Table') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need three parts in the name + + if (count($parts) != 3) + { + return; + } + + // We need the second part to be "model" + if ($parts[1] != 'table') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + $view = $parts[2]; + + // Is this an FOF 2.1 or later component? + if (!$this->isFOFComponent($component)) + { + return; + } + + // Get the alternate view and class name (opposite singular/plural name) + $alt_view = FOFInflector::isSingular($view) ? FOFInflector::pluralize($view) : FOFInflector::singularize($view); + $alt_class = FOFInflector::camelize($component_raw . '_table_' . $alt_view); + + // Get the proper and alternate paths and file names + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + + $file = "/tables/$view.php"; + $altFile = "/tables/$alt_view.php"; + $path = $componentPaths['admin']; + + // Try to find the proper class in the proper path + if (file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the alternate class in the proper path + if (!class_exists($alt_class) && file_exists($path . $altFile)) + { + @include_once $path . $altFile; + } + + // If the alternate class exists just map the class to the alternate + if (!class_exists($class_name) && class_exists($alt_class)) + { + $this->class_alias($alt_class, $class_name); + } + + // No class found? Map to FOFModel + elseif (!class_exists($class_name)) + { + if ($view != 'default') + { + $defaultClass = FOFInflector::camelize($component_raw . '_table_default'); + $this->class_alias($defaultClass, $class_name); + } + else + { + $this->class_alias('FOFTable', $class_name, true); + } + } + } + + /** + * Autoload Helpers + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_helper($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'Helper') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need three parts in the name + if (count($parts) != 3) + { + return; + } + + // We need the second part to be "model" + if ($parts[1] != 'helper') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + $view = $parts[2]; + + // Is this an FOF 2.1 or later component? + if (!$this->isFOFComponent($component)) + { + return; + } + + // Get the alternate view and class name (opposite singular/plural name) + $alt_view = FOFInflector::isSingular($view) ? FOFInflector::pluralize($view) : FOFInflector::singularize($view); + $alt_class = FOFInflector::camelize($component_raw . '_helper_' . $alt_view); + + // Get the proper and alternate paths and file names + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + + $file = "/helpers/$view.php"; + $altFile = "/helpers/$alt_view.php"; + $path = $componentPaths['main']; + $altPath = $componentPaths['alt']; + + // Try to find the proper class in the proper path + if (file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the proper class in the alternate path + if (!class_exists($class_name) && file_exists($altPath . $file)) + { + @include_once $altPath . $file; + } + + // Try to find the alternate class in the proper path + if (!class_exists($alt_class) && file_exists($path . $altFile)) + { + @include_once $path . $altFile; + } + + // Try to find the alternate class in the alternate path + if (!class_exists($alt_class) && file_exists($altPath . $altFile)) + { + @include_once $altPath . $altFile; + } + + // If the alternate class exists just map the class to the alternate + if (!class_exists($class_name) && class_exists($alt_class)) + { + $this->class_alias($alt_class, $class_name); + } + } + + /** + * Autoload Toolbars + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_toolbar($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + static $isCli = null, $isAdmin = null; + + if (is_null($isCli) && is_null($isAdmin)) + { + list($isCli, $isAdmin) = FOFDispatcher::isCliAdmin(); + } + + if (strpos($class_name, 'Toolbar') === false) + { + return; + } + + // Change from camel cased into a lowercase array + $class_modified = preg_replace('/(\s)+/', '_', $class_name); + $class_modified = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $class_modified)); + $parts = explode('_', $class_modified); + + // We need two parts in the name + if (count($parts) != 2) + { + return; + } + + // We need the second part to be "model" + if ($parts[1] != 'toolbar') + { + return; + } + + // Get the information about this class + $component_raw = $parts[0]; + $component = 'com_' . $parts[0]; + + $platformDirs = FOFPlatform::getInstance()->getPlatformBaseDirs(); + + // Get the proper and alternate paths and file names + $file = "/components/$component/toolbar.php"; + $path = ($isAdmin || $isCli) ? $platformDirs['admin'] : $platformDirs['public']; + $altPath = ($isAdmin || $isCli) ? $platformDirs['public'] : $platformDirs['admin']; + + // Try to find the proper class in the proper path + + if (file_exists($path . $file)) + { + @include_once $path . $file; + } + + // Try to find the proper class in the alternate path + if (!class_exists($class_name) && file_exists($altPath . $file)) + { + @include_once $altPath . $file; + } + + // No class found? Map to FOFToolbar + if (!class_exists($class_name)) + { + $this->class_alias('FOFToolbar', $class_name, true); + } + } + + /** + * Autoload Fields + * + * @param string $class_name The name of the class to load + * + * @return void + */ + public function autoload_fof_field($class_name) + { + FOFPlatform::getInstance()->logDebug(__METHOD__ . "() autoloading $class_name"); + + // @todo + } +} diff --git a/deployed/akeeba/libraries/fof/autoloader/fof.php b/deployed/akeeba/libraries/fof/autoloader/fof.php new file mode 100644 index 00000000..602c9192 --- /dev/null +++ b/deployed/akeeba/libraries/fof/autoloader/fof.php @@ -0,0 +1,116 @@ +dispatcher; + + // Sanity check + + if (empty($dispatcherData)) + { + return; + } + + $options = $xml->xpath('dispatcher/option'); + + if (!empty($options)) + { + foreach ($options as $option) + { + $key = (string) $option['name']; + $ret['dispatcher'][$key] = (string) $option; + } + } + } + + /** + * Return a configuration variable + * + * @param string &$configuration Configuration variables (hashed array) + * @param string $var The variable we want to fetch + * @param mixed $default Default value + * + * @return mixed The variable's value + */ + public function get(&$configuration, $var, $default) + { + if (isset($configuration['dispatcher'][$var])) + { + return $configuration['dispatcher'][$var]; + } + else + { + return $default; + } + } +} diff --git a/deployed/akeeba/libraries/fof/config/domain/interface.php b/deployed/akeeba/libraries/fof/config/domain/interface.php new file mode 100644 index 00000000..d2901ab9 --- /dev/null +++ b/deployed/akeeba/libraries/fof/config/domain/interface.php @@ -0,0 +1,41 @@ +xpath('table'); + + // Sanity check + if (empty($tableData)) + { + return; + } + + foreach ($tableData as $aTable) + { + $key = (string) $aTable['name']; + + $ret['tables'][$key]['behaviors'] = (string) $aTable->behaviors; + $ret['tables'][$key]['tablealias'] = $aTable->xpath('tablealias'); + $ret['tables'][$key]['fields'] = array(); + $ret['tables'][$key]['relations'] = array(); + + $fieldData = $aTable->xpath('field'); + + if (!empty($fieldData)) + { + foreach ($fieldData as $field) + { + $k = (string) $field['name']; + $ret['tables'][$key]['fields'][$k] = (string) $field; + } + } + + $relationsData = $aTable->xpath('relation'); + + if (!empty($relationsData)) + { + foreach ($relationsData as $relationData) + { + $type = (string)$relationData['type']; + $itemName = (string)$relationData['name']; + + if (empty($type) || empty($itemName)) + { + continue; + } + + $tableClass = (string)$relationData['tableClass']; + $localKey = (string)$relationData['localKey']; + $remoteKey = (string)$relationData['remoteKey']; + $ourPivotKey = (string)$relationData['ourPivotKey']; + $theirPivotKey = (string)$relationData['theirPivotKey']; + $pivotTable = (string)$relationData['pivotTable']; + $default = (string)$relationData['default']; + + $default = !in_array($default, array('no', 'false', 0)); + + $relation = array( + 'type' => $type, + 'itemName' => $itemName, + 'tableClass' => empty($tableClass) ? null : $tableClass, + 'localKey' => empty($localKey) ? null : $localKey, + 'remoteKey' => empty($remoteKey) ? null : $remoteKey, + 'default' => $default, + ); + + if (!empty($ourPivotKey) || !empty($theirPivotKey) || !empty($pivotTable)) + { + $relation['ourPivotKey'] = empty($ourPivotKey) ? null : $ourPivotKey; + $relation['theirPivotKey'] = empty($theirPivotKey) ? null : $theirPivotKey; + $relation['pivotTable'] = empty($pivotTable) ? null : $pivotTable; + } + + $ret['tables'][$key]['relations'][] = $relation; + } + } + } + } + + /** + * Return a configuration variable + * + * @param string &$configuration Configuration variables (hashed array) + * @param string $var The variable we want to fetch + * @param mixed $default Default value + * + * @return mixed The variable's value + */ + public function get(&$configuration, $var, $default) + { + $parts = explode('.', $var); + + $view = $parts[0]; + $method = 'get' . ucfirst($parts[1]); + + if (!method_exists($this, $method)) + { + return $default; + } + + array_shift($parts); + array_shift($parts); + + $ret = $this->$method($view, $configuration, $parts, $default); + + return $ret; + } + + /** + * Internal method to return the magic field mapping + * + * @param string $table The table for which we will be fetching a field map + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the table we want to fetch + * @param string $default Default magic field mapping; empty if not defined + * + * @return array Field map + */ + protected function getField($table, &$configuration, $params, $default = '') + { + $fieldmap = array(); + + if (isset($configuration['tables']['*']) && isset($configuration['tables']['*']['fields'])) + { + $fieldmap = $configuration['tables']['*']['fields']; + } + + if (isset($configuration['tables'][$table]) && isset($configuration['tables'][$table]['fields'])) + { + $fieldmap = array_merge($fieldmap, $configuration['tables'][$table]['fields']); + } + + $map = $default; + + if (empty($params[0])) + { + $map = $fieldmap; + } + elseif (isset($fieldmap[$params[0]])) + { + $map = $fieldmap[$params[0]]; + } + + return $map; + } + + /** + * Internal method to get table alias + * + * @param string $table The table for which we will be fetching table alias + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the table we want to fetch + * @param string $default Default table alias + * + * @return string Table alias + */ + protected function getTablealias($table, &$configuration, $params, $default = '') + { + $tablealias = $default; + + if (isset($configuration['tables']['*']) + && isset($configuration['tables']['*']['tablealias']) + && isset($configuration['tables']['*']['tablealias'][0])) + { + $tablealias = (string) $configuration['tables']['*']['tablealias'][0]; + } + + if (isset($configuration['tables'][$table]) + && isset($configuration['tables'][$table]['tablealias']) + && isset($configuration['tables'][$table]['tablealias'][0])) + { + $tablealias = (string) $configuration['tables'][$table]['tablealias'][0]; + } + + return $tablealias; + } + + /** + * Internal method to get table behaviours + * + * @param string $table The table for which we will be fetching table alias + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the table we want to fetch + * @param string $default Default table alias + * + * @return string Table behaviours + */ + protected function getBehaviors($table, &$configuration, $params, $default = '') + { + $behaviors = $default; + + if (isset($configuration['tables']['*']) + && isset($configuration['tables']['*']['behaviors'])) + { + $behaviors = (string) $configuration['tables']['*']['behaviors']; + } + + if (isset($configuration['tables'][$table]) + && isset($configuration['tables'][$table]['behaviors'])) + { + $behaviors = (string) $configuration['tables'][$table]['behaviors']; + } + + return $behaviors; + } + + /** + * Internal method to get table relations + * + * @param string $table The table for which we will be fetching table alias + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the table we want to fetch + * @param string $default Default table alias + * + * @return array Table relations + */ + protected function getRelations($table, &$configuration, $params, $default = '') + { + $relations = $default; + + if (isset($configuration['tables']['*']) + && isset($configuration['tables']['*']['relations'])) + { + $relations = $configuration['tables']['*']['relations']; + } + + if (isset($configuration['tables'][$table]) + && isset($configuration['tables'][$table]['relations'])) + { + $relations = $configuration['tables'][$table]['relations']; + } + + return $relations; + } +} diff --git a/deployed/akeeba/libraries/fof/config/domain/views.php b/deployed/akeeba/libraries/fof/config/domain/views.php new file mode 100644 index 00000000..1c18b62e --- /dev/null +++ b/deployed/akeeba/libraries/fof/config/domain/views.php @@ -0,0 +1,302 @@ +xpath('view'); + + // Sanity check + + if (empty($viewData)) + { + return; + } + + foreach ($viewData as $aView) + { + $key = (string) $aView['name']; + + // Parse ACL options + $ret['views'][$key]['acl'] = array(); + $aclData = $aView->xpath('acl/task'); + + if (!empty($aclData)) + { + foreach ($aclData as $acl) + { + $k = (string) $acl['name']; + $ret['views'][$key]['acl'][$k] = (string) $acl; + } + } + + // Parse taskmap + $ret['views'][$key]['taskmap'] = array(); + $taskmapData = $aView->xpath('taskmap/task'); + + if (!empty($taskmapData)) + { + foreach ($taskmapData as $map) + { + $k = (string) $map['name']; + $ret['views'][$key]['taskmap'][$k] = (string) $map; + } + } + + // Parse controller configuration + $ret['views'][$key]['config'] = array(); + $optionData = $aView->xpath('config/option'); + + if (!empty($optionData)) + { + foreach ($optionData as $option) + { + $k = (string) $option['name']; + $ret['views'][$key]['config'][$k] = (string) $option; + } + } + + // Parse the toolbar + $ret['views'][$key]['toolbar'] = array(); + $toolBars = $aView->xpath('toolbar'); + + if (!empty($toolBars)) + { + foreach ($toolBars as $toolBar) + { + $taskName = isset($toolBar['task']) ? (string) $toolBar['task'] : '*'; + + // If a toolbar title is specified, create a title element. + if (isset($toolBar['title'])) + { + $ret['views'][$key]['toolbar'][$taskName]['title'] = array( + 'value' => (string) $toolBar['title'] + ); + } + + // Parse the toolbar buttons data + $toolbarData = $toolBar->xpath('button'); + + if (!empty($toolbarData)) + { + foreach ($toolbarData as $button) + { + $k = (string) $button['type']; + $ret['views'][$key]['toolbar'][$taskName][$k] = current($button->attributes()); + $ret['views'][$key]['toolbar'][$taskName][$k]['value'] = (string) $button; + } + } + } + } + } + } + + /** + * Return a configuration variable + * + * @param string &$configuration Configuration variables (hashed array) + * @param string $var The variable we want to fetch + * @param mixed $default Default value + * + * @return mixed The variable's value + */ + public function get(&$configuration, $var, $default) + { + $parts = explode('.', $var); + + $view = $parts[0]; + $method = 'get' . ucfirst($parts[1]); + + if (!method_exists($this, $method)) + { + return $default; + } + + array_shift($parts); + array_shift($parts); + + $ret = $this->$method($view, $configuration, $parts, $default); + + return $ret; + } + + /** + * Internal function to return the task map for a view + * + * @param string $view The view for which we will be fetching a task map + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options (not used) + * @param array $default ßDefault task map; empty array if not provided + * + * @return array The task map as a hash array in the format task => method + */ + protected function getTaskmap($view, &$configuration, $params, $default = array()) + { + $taskmap = array(); + + if (isset($configuration['views']['*']) && isset($configuration['views']['*']['taskmap'])) + { + $taskmap = $configuration['views']['*']['taskmap']; + } + + if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['taskmap'])) + { + $taskmap = array_merge($taskmap, $configuration['views'][$view]['taskmap']); + } + + if (empty($taskmap)) + { + return $default; + } + + return $taskmap; + } + + /** + * Internal method to return the ACL mapping (privilege required to access + * a specific task) for the given view's tasks + * + * @param string $view The view for which we will be fetching a task map + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the task we want to fetch + * @param string $default Default ACL option; empty (no ACL check) if not defined + * + * @return string The privilege required to access this view + */ + protected function getAcl($view, &$configuration, $params, $default = '') + { + $aclmap = array(); + + if (isset($configuration['views']['*']) && isset($configuration['views']['*']['acl'])) + { + $aclmap = $configuration['views']['*']['acl']; + } + + if (isset($configuration['views'][$view]) && isset($configuration['views'][$view]['acl'])) + { + $aclmap = array_merge($aclmap, $configuration['views'][$view]['acl']); + } + + $acl = $default; + + if (isset($aclmap['*'])) + { + $acl = $aclmap['*']; + } + + if (isset($aclmap[$params[0]])) + { + $acl = $aclmap[$params[0]]; + } + + return $acl; + } + + /** + * Internal method to return the a configuration option for the view. These + * are equivalent to $config array options passed to the Controller + * + * @param string $view The view for which we will be fetching a task map + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options; key 0 defines the option variable we want to fetch + * @param mixed $default Default option; null if not defined + * + * @return string The setting for the requested option + */ + protected function getConfig($view, &$configuration, $params, $default = null) + { + $ret = $default; + + if (isset($configuration['views']['*']) + && isset($configuration['views']['*']['config']) + && isset($configuration['views']['*']['config'][$params[0]])) + { + $ret = $configuration['views']['*']['config'][$params[0]]; + } + + if (isset($configuration['views'][$view]) + && isset($configuration['views'][$view]['config']) + && isset($configuration['views'][$view]['config'][$params[0]])) + { + $ret = $configuration['views'][$view]['config'][$params[0]]; + } + + return $ret; + } + + /** + * Internal method to return the toolbar infos. + * + * @param string $view The view for which we will be fetching buttons + * @param array &$configuration The configuration parameters hash array + * @param array $params Extra options + * @param string $default Default option + * + * @return string The toolbar data for this view + */ + protected function getToolbar($view, &$configuration, $params, $default = '') + { + $toolbar = array(); + + if (isset($configuration['views']['*']) + && isset($configuration['views']['*']['toolbar']) + && isset($configuration['views']['*']['toolbar']['*'])) + { + $toolbar = $configuration['views']['*']['toolbar']['*']; + } + + if (isset($configuration['views']['*']) + && isset($configuration['views']['*']['toolbar']) + && isset($configuration['views']['*']['toolbar'][$params[0]])) + { + $toolbar = array_merge($toolbar, $configuration['views']['*']['toolbar'][$params[0]]); + } + + if (isset($configuration['views'][$view]) + && isset($configuration['views'][$view]['toolbar']) + && isset($configuration['views'][$view]['toolbar']['*'])) + { + $toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar']['*']); + } + + if (isset($configuration['views'][$view]) + && isset($configuration['views'][$view]['toolbar']) + && isset($configuration['views'][$view]['toolbar'][$params[0]])) + { + $toolbar = array_merge($toolbar, $configuration['views'][$view]['toolbar'][$params[0]]); + } + + if (empty($toolbar)) + { + return $default; + } + + return $toolbar; + } +} diff --git a/deployed/akeeba/libraries/fof/config/provider.php b/deployed/akeeba/libraries/fof/config/provider.php new file mode 100644 index 00000000..4b335371 --- /dev/null +++ b/deployed/akeeba/libraries/fof/config/provider.php @@ -0,0 +1,212 @@ +isCli()) + { + $order = array('cli', 'backend'); + } + elseif (FOFPlatform::getInstance()->isBackend()) + { + $order = array('backend'); + } + else + { + $order = array('frontend'); + } + + $order[] = 'common'; + + $order = array_reverse($order); + self::$configurations[$component] = array(); + + foreach ($order as $area) + { + $config = $this->parseComponentArea($component, $area); + self::$configurations[$component] = array_merge_recursive(self::$configurations[$component], $config); + } + } + + /** + * Returns the value of a variable. Variables use a dot notation, e.g. + * view.config.whatever where the first part is the domain, the rest of the + * parts specify the path to the variable. + * + * @param string $variable The variable name + * @param mixed $default The default value, or null if not specified + * + * @return mixed The value of the variable + */ + public function get($variable, $default = null) + { + static $domains = null; + + if (is_null($domains)) + { + $domains = $this->getDomains(); + } + + list($component, $domain, $var) = explode('.', $variable, 3); + + if (!isset(self::$configurations[$component])) + { + $this->parseComponent($component); + } + + if (!in_array($domain, $domains)) + { + return $default; + } + + $class = 'FOFConfigDomain' . ucfirst($domain); + $o = new $class; + + return $o->get(self::$configurations[$component], $var, $default); + } + + /** + * Parses the configuration options of a specific component area + * + * @param string $component Which component's cionfiguration to parse + * @param string $area Which area to parse (frontend, backend, cli) + * + * @return array A hash array with the configuration data + */ + protected function parseComponentArea($component, $area) + { + // Initialise the return array + $ret = array(); + + // Get the folders of the component + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($component); + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + // Check that the path exists + $path = $componentPaths['admin']; + $path = $filesystem->pathCheck($path); + + if (!$filesystem->folderExists($path)) + { + return $ret; + } + + // Read the filename if it exists + $filename = $path . '/fof.xml'; + + if (!$filesystem->fileExists($filename)) + { + return $ret; + } + + $data = file_get_contents($filename); + + // Load the XML data in a SimpleXMLElement object + $xml = simplexml_load_string($data); + + if (!($xml instanceof SimpleXMLElement)) + { + return $ret; + } + + // Get this area's data + $areaData = $xml->xpath('//' . $area); + + if (empty($areaData)) + { + return $ret; + } + + $xml = array_shift($areaData); + + // Parse individual configuration domains + $domains = $this->getDomains(); + + foreach ($domains as $dom) + { + $class = 'FOFConfigDomain' . ucfirst($dom); + + if (class_exists($class, true)) + { + $o = new $class; + $o->parseDomain($xml, $ret); + } + } + + // Finally, return the result + return $ret; + } + + /** + * Gets a list of the available configuration domain adapters + * + * @return array A list of the available domains + */ + protected function getDomains() + { + static $domains = array(); + + if (empty($domains)) + { + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + $files = $filesystem->folderFiles(__DIR__ . '/domain', '.php'); + + if (!empty($files)) + { + foreach ($files as $file) + { + $domain = basename($file, '.php'); + + if ($domain == 'interface') + { + continue; + } + + $domain = preg_replace('/[^A-Za-z0-9]/', '', $domain); + $domains[] = $domain; + } + + $domains = array_unique($domains); + } + } + + return $domains; + } +} diff --git a/deployed/akeeba/libraries/fof/controller/controller.php b/deployed/akeeba/libraries/fof/controller/controller.php new file mode 100644 index 00000000..d972f077 --- /dev/null +++ b/deployed/akeeba/libraries/fof/controller/controller.php @@ -0,0 +1,3413 @@ +getCmd('option', 'com_foobar'); + $config['view'] = !is_null($view) ? $view : $input->getCmd('view', 'cpanel'); + + // Get the class base name, e.g. FoobarController + $classBaseName = ucfirst(str_replace('com_', '', $config['option'])) . 'Controller'; + + // Get the class name suffixes, in the order to be searched for: plural, singular, 'default' + $classSuffixes = array( + FOFInflector::pluralize($config['view']), + FOFInflector::singularize($config['view']), + 'default' + ); + + // Get the path names for the component + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($config['option']); + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + // Look for the best classname match + foreach ($classSuffixes as $suffix) + { + $className = $classBaseName . ucfirst($suffix); + + if (class_exists($className)) + { + // The class is already loaded. We have a match! + break; + } + + // The class is not already loaded. Try to find and load it. + $searchPaths = array( + $componentPaths['main'] . '/controllers', + $componentPaths['admin'] . '/controllers' + ); + + // If we have a searchpath in the configuration please search it first + + if (array_key_exists('searchpath', $config)) + { + array_unshift($searchPaths, $config['searchpath']); + } + else + { + $configProvider = new FOFConfigProvider; + $searchPath = $configProvider->get($config['option'] . '.views.' . FOFInflector::singularize($config['view']) . '.config.searchpath', null); + + if ($searchPath) + { + array_unshift($searchPaths, $componentPaths['admin'] . '/' . $searchPath); + array_unshift($searchPaths, $componentPaths['main'] . '/' . $searchPath); + } + } + + /** + * Try to find the path to this file. First try to find the + * format-specific controller file, e.g. foobar.json.php for + * format=json, then the regular one-size-fits-all controller + */ + + $format = $input->getCmd('format', 'html'); + $path = null; + + if (!empty($format)) + { + $path = $filesystem->pathFind( + $searchPaths, strtolower($suffix) . '.' . strtolower($format) . '.php' + ); + } + + if (!$path) + { + $path = $filesystem->pathFind( + $searchPaths, strtolower($suffix) . '.php' + ); + } + + // The path is found. Load the file and make sure the expected class name exists. + + if ($path) + { + require_once $path; + + if (class_exists($className)) + { + // The class was loaded successfully. We have a match! + break; + } + } + } + + if (!class_exists($className)) + { + // If no specialised class is found, instantiate the generic FOFController + $className = 'FOFController'; + } + + $instance = new $className($config); + + return $instance; + } + + /** + * Public constructor of the Controller class + * + * @param array $config Optional configuration parameters + */ + public function __construct($config = array()) + { + // Make sure $config is an array + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config)) + { + $config = array(); + } + + $this->methods = array(); + $this->message = null; + $this->messageType = 'message'; + $this->paths = array(); + $this->redirect = null; + $this->taskMap = array(); + + // Cache the config + $this->config = $config; + + // Get the input for this MVC triad + + if (array_key_exists('input', $config)) + { + $input = $config['input']; + } + else + { + $input = null; + } + + if (array_key_exists('input_options', $config)) + { + $input_options = $config['input_options']; + } + else + { + $input_options = array(); + } + + if ($input instanceof FOFInput) + { + $this->input = $input; + } + else + { + $this->input = new FOFInput($input, $input_options); + } + + // Load the configuration provider + $this->configProvider = new FOFConfigProvider; + + // Determine the methods to exclude from the base class. + $xMethods = get_class_methods('FOFController'); + + // Some methods must always be considered valid tasks + $iMethods = array('accesspublic', 'accessregistered', 'accessspecial', + 'add', 'apply', 'browse', 'cancel', 'copy', 'edit', 'orderdown', + 'orderup', 'publish', 'read', 'remove', 'save', 'savenew', + 'saveorder', 'unpublish', 'display', 'archive', 'trash', 'loadhistory'); + + // Get the public methods in this class using reflection. + $r = new ReflectionClass($this); + $rMethods = $r->getMethods(ReflectionMethod::IS_PUBLIC); + + foreach ($rMethods as $rMethod) + { + $mName = $rMethod->getName(); + + // If the developer screwed up and declared one of the helper method public do NOT make them available as + // tasks. + if ((substr($mName, 0, 8) == 'onBefore') || (substr($mName, 0, 7) == 'onAfter') || substr($mName, 0, 1) == '_') + { + continue; + } + + // Add default display method if not explicitly declared. + if (!in_array($mName, $xMethods) || in_array($mName, $iMethods)) + { + $this->methods[] = strtolower($mName); + + // Auto register the methods as tasks. + $this->taskMap[strtolower($mName)] = $mName; + } + } + + // Get the default values for the component and view names + $classNameParts = FOFInflector::explode(get_class($this)); + + if (count($classNameParts) == 3) + { + $defComponent = "com_" . $classNameParts[0]; + $defView = $classNameParts[2]; + } + else + { + $defComponent = 'com_foobar'; + $defView = 'cpanel'; + } + + $this->component = $this->input->get('option', $defComponent, 'cmd'); + $this->view = $this->input->get('view', $defView, 'cmd'); + $this->layout = $this->input->get('layout', null, 'cmd'); + + // Overrides from the config + if (array_key_exists('option', $config)) + { + $this->component = $config['option']; + } + + if (array_key_exists('view', $config)) + { + $this->view = $config['view']; + } + + if (array_key_exists('layout', $config)) + { + $this->layout = $config['layout']; + } + + $this->layout = $this->configProvider->get($this->component . '.views.' . FOFInflector::singularize($this->view) . '.config.layout', $this->layout); + + $this->input->set('option', $this->component); + + // Set the bareComponent variable + $this->bareComponent = str_replace('com_', '', strtolower($this->component)); + + // Set the $name variable + $this->name = $this->bareComponent; + + // Set the basePath variable + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($this->component); + $basePath = $componentPaths['main']; + + if (array_key_exists('base_path', $config)) + { + $basePath = $config['base_path']; + } + + $altBasePath = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.base_path', null + ); + + if (!is_null($altBasePath)) + { + $platformDirs = FOFPlatform::getInstance()->getPlatformBaseDirs(); + $basePath = $platformDirs['public'] . '/' . $altBasePath; + } + + $this->basePath = $basePath; + + // If the default task is set, register it as such + $defaultTask = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.default_task', 'display' + ); + + if (array_key_exists('default_task', $config)) + { + $this->registerDefaultTask($config['default_task']); + } + else + { + $this->registerDefaultTask($defaultTask); + } + + // Set the models prefix + + if (empty($this->model_prefix)) + { + if (array_key_exists('model_prefix', $config)) + { + // User-defined prefix + $this->model_prefix = $config['model_prefix']; + } + else + { + $this->model_prefix = $this->name . 'Model'; + $this->model_prefix = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.model_prefix', $this->model_prefix + ); + } + } + + // Set the default model search path + + if (array_key_exists('model_path', $config)) + { + // User-defined dirs + $this->addModelPath($config['model_path'], $this->model_prefix); + } + else + { + $modelPath = $this->basePath . '/models'; + $altModelPath = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.model_path', null + ); + + if (!is_null($altModelPath)) + { + $modelPath = $this->basePath . '/' . $altModelPath; + } + + $this->addModelPath($modelPath, $this->model_prefix); + } + + // Set the default view search path + if (array_key_exists('view_path', $config)) + { + // User-defined dirs + $this->setPath('view', $config['view_path']); + } + else + { + $viewPath = $this->basePath . '/views'; + $altViewPath = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.view_path', null + ); + + if (!is_null($altViewPath)) + { + $viewPath = $this->basePath . '/' . $altViewPath; + } + + $this->setPath('view', $viewPath); + } + + // Set the default view. + + if (array_key_exists('default_view', $config)) + { + $this->default_view = $config['default_view']; + } + else + { + if (empty($this->default_view)) + { + $this->default_view = $this->getName(); + } + + $this->default_view = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.default_view', $this->default_view + ); + } + + // Set the CSRF protection + if (array_key_exists('csrf_protection', $config)) + { + $this->csrfProtection = $config['csrf_protection']; + } + + $this->csrfProtection = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.csrf_protection', $this->csrfProtection + ); + + // Set any model/view name overrides + if (array_key_exists('viewName', $config)) + { + $this->setThisViewName($config['viewName']); + } + else + { + $overrideViewName = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.viewName', null + ); + + if ($overrideViewName) + { + $this->setThisViewName($overrideViewName); + } + } + + if (array_key_exists('modelName', $config)) + { + $this->setThisModelName($config['modelName']); + } + else + { + $overrideModelName = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.modelName', null + ); + + if ($overrideModelName) + { + $this->setThisModelName($overrideModelName); + } + } + + // Caching + if (array_key_exists('cacheableTasks', $config)) + { + if (is_array($config['cacheableTasks'])) + { + $this->cacheableTasks = $config['cacheableTasks']; + } + } + else + { + $cacheableTasks = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.cacheableTasks', null + ); + + if ($cacheableTasks) + { + $cacheableTasks = explode(',', $cacheableTasks); + + if (count($cacheableTasks)) + { + $temp = array(); + + foreach ($cacheableTasks as $t) + { + $temp[] = trim($t); + } + + $temp = array_unique($temp); + $this->cacheableTasks = $temp; + } + } + } + + // Bit mask for auto routing on setRedirect + $this->autoRouting = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.config.autoRouting', $this->autoRouting + ); + + if (array_key_exists('autoRouting', $config)) + { + $this->autoRouting = $config['autoRouting']; + } + + // Apply task map + $taskmap = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.taskmap' + ); + + if (is_array($taskmap) && !empty($taskmap)) + { + foreach ($taskmap as $aliasedtask => $realmethod) + { + $this->registerTask($aliasedtask, $realmethod); + } + } + } + + /** + * Adds to the stack of model paths in LIFO order. + * + * @param mixed $path The directory (string) , or list of directories (array) to add. + * @param string $prefix A prefix for models + * + * @return void + */ + public static function addModelPath($path, $prefix = '') + { + FOFModel::addIncludePath($path, $prefix); + } + + /** + * Adds to the search path for templates and resources. + * + * @param string $type The path type (e.g. 'model', 'view'). + * @param mixed $path The directory string or stream array to search. + * + * @return FOFController A FOFController object to support chaining. + */ + protected function addPath($type, $path) + { + // Just force path to array + settype($path, 'array'); + + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + if (!isset($this->paths[$type])) + { + $this->paths[$type] = array(); + } + + // Loop through the path directories + foreach ($path as $dir) + { + // No surrounding spaces allowed! + $dir = rtrim($filesystem->pathCheck($dir, '/'), '/') . '/'; + + // Add to the top of the search dirs + array_unshift($this->paths[$type], $dir); + } + + return $this; + } + + /** + * Add one or more view paths to the controller's stack, in LIFO order. + * + * @param mixed $path The directory (string) or list of directories (array) to add. + * + * @return FOFController This object to support chaining. + */ + public function addViewPath($path) + { + $this->addPath('view', $path); + + return $this; + } + + /** + * Authorisation check + * + * @param string $task The ACO Section Value to check access on. + * + * @return boolean True if authorised + * + * @deprecated 2.0 Use JAccess instead. + */ + public function authorise($task) + { + FOFPlatform::getInstance()->logDeprecated(__CLASS__ . '::' .__METHOD__ . ' is deprecated. Use checkACL() instead.'); + + return true; + } + + /** + * Create the filename for a resource. + * + * @param string $type The resource type to create the filename for. + * @param array $parts An associative array of filename information. Optional. + * + * @return string The filename. + */ + protected static function createFileName($type, $parts = array()) + { + $filename = ''; + + switch ($type) + { + case 'controller': + if (!empty($parts['format'])) + { + if ($parts['format'] == 'html') + { + $parts['format'] = ''; + } + else + { + $parts['format'] = '.' . $parts['format']; + } + } + else + { + $parts['format'] = ''; + } + + $filename = strtolower($parts['name'] . $parts['format'] . '.php'); + break; + + case 'view': + if (!empty($parts['type'])) + { + $parts['type'] = '.' . $parts['type']; + } + else + { + $parts['type'] = ''; + } + + $filename = strtolower($parts['name'] . '/view' . $parts['type'] . '.php'); + break; + } + + return $filename; + } + + /** + * Executes a given controller task. The onBefore and onAfter + * methods are called automatically if they exist. + * + * @param string $task The task to execute, e.g. "browse" + * + * @throws Exception Exception thrown if the onBefore returns false + * + * @return null|bool False on execution failure + */ + public function execute($task) + { + $this->task = $task; + + $method_name = 'onBefore' . ucfirst($task); + + if (!method_exists($this, $method_name)) + { + $result = $this->onBeforeGenericTask($task); + } + elseif (method_exists($this, $method_name)) + { + $result = $this->$method_name(); + } + else + { + $result = true; + } + + if ($result) + { + $plugin_event = FOFInflector::camelize('on before ' . $this->bareComponent . ' controller ' . $this->view . ' ' . $task); + $plugin_result = FOFPlatform::getInstance()->runPlugins($plugin_event, array(&$this, &$this->input)); + + if (in_array(false, $plugin_result, true)) + { + $result = false; + } + } + + if (!$result) + { + throw new Exception(JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Do not allow the display task to be directly called + $task = strtolower($task); + + if (isset($this->taskMap[$task])) + { + $doTask = $this->taskMap[$task]; + } + elseif (isset($this->taskMap['__default'])) + { + $doTask = $this->taskMap['__default']; + } + else + { + $doTask = null; + } + + if ($doTask == 'display') + { + FOFPlatform::getInstance()->setHeader('Status', '400 Bad Request', true); + + throw new Exception('Bad Request', 400); + } + + $this->doTask = $doTask; + + $ret = $this->$doTask(); + + $method_name = 'onAfter' . ucfirst($task); + + if (method_exists($this, $method_name)) + { + $result = $this->$method_name(); + } + else + { + $result = true; + } + + if ($result) + { + $plugin_event = FOFInflector::camelize('on after ' . $this->bareComponent . ' controller ' . $this->view . ' ' . $task); + $plugin_result = FOFPlatform::getInstance()->runPlugins($plugin_event, array(&$this, &$this->input, &$ret)); + + if (in_array(false, $plugin_result, true)) + { + $result = false; + } + } + + if (!$result) + { + throw new Exception(JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + return $ret; + } + + /** + * Default task. Assigns a model to the view and asks the view to render + * itself. + * + * YOU MUST NOT USETHIS TASK DIRECTLY IN A URL. It is supposed to be + * used ONLY inside your code. In the URL, use task=browse instead. + * + * @param bool $cachable Is this view cacheable? + * @param bool $urlparams Add your safe URL parameters (see further down in the code) + * @param string $tpl The name of the template file to parse + * + * @return bool + */ + public function display($cachable = false, $urlparams = false, $tpl = null) + { + $document = FOFPlatform::getInstance()->getDocument(); + + if ($document instanceof JDocument) + { + $viewType = $document->getType(); + } + else + { + $viewType = $this->input->getCmd('format', 'html'); + } + + $view = $this->getThisView(); + + // Get/Create the model + + if ($model = $this->getThisModel()) + { + // Push the model into the view (as default) + $view->setModel($model, true); + } + + // Set the layout + $view->setLayout(is_null($this->layout) ? 'default' : $this->layout); + + // Display the view + $conf = FOFPlatform::getInstance()->getConfig(); + + if (FOFPlatform::getInstance()->isFrontend() && $cachable && ($viewType != 'feed') && $conf->get('caching') >= 1) + { + // Get a JCache object + $option = $this->input->get('option', 'com_foobar', 'cmd'); + $cache = JFactory::getCache($option, 'view'); + + // Set up a cache ID based on component, view, task and user group assignment + $user = FOFPlatform::getInstance()->getUser(); + + if ($user->guest) + { + $groups = array(); + } + else + { + $groups = $user->groups; + } + + $importantParameters = array(); + + // Set up safe URL parameters + if (!is_array($urlparams)) + { + $urlparams = array( + 'option' => 'CMD', + 'view' => 'CMD', + 'task' => 'CMD', + 'format' => 'CMD', + 'layout' => 'CMD', + 'id' => 'INT', + ); + } + + if (is_array($urlparams)) + { + $app = JFactory::getApplication(); + + $registeredurlparams = null; + + if (version_compare(JVERSION, '3.0', 'ge')) + { + if (property_exists($app, 'registeredurlparams')) + { + $registeredurlparams = $app->registeredurlparams; + } + } + else + { + $registeredurlparams = $app->get('registeredurlparams'); + } + + if (empty($registeredurlparams)) + { + $registeredurlparams = new stdClass; + } + + foreach ($urlparams AS $key => $value) + { + // Add your safe url parameters with variable type as value {@see JFilterInput::clean()}. + $registeredurlparams->$key = $value; + + // Add the URL-important parameters into the array + $importantParameters[$key] = $this->input->get($key, null, $value); + } + + if (version_compare(JVERSION, '3.0', 'ge')) + { + $app->registeredurlparams = $registeredurlparams; + } + else + { + $app->set('registeredurlparams', $registeredurlparams); + } + } + + // Create the cache ID after setting the registered URL params, as they are used to generate the ID + $cacheId = md5(serialize(array(JCache::makeId(), $view->getName(), $this->doTask, $groups, $importantParameters))); + + // Get the cached view or cache the current view + $cache->get($view, 'display', $cacheId); + } + else + { + // Display without caching + $view->display($tpl); + } + + return true; + } + + /** + * Implements a default browse task, i.e. read a bunch of records and send + * them to the browser. + * + * @return boolean + */ + public function browse() + { + if ($this->input->get('savestate', -999, 'int') == -999) + { + $this->input->set('savestate', true); + } + + // Do I have a form? + $model = $this->getThisModel(); + + if (empty($this->layout)) + { + $formname = 'form.default'; + } + else + { + $formname = 'form.' . $this->layout; + } + + $model->setState('form_name', $formname); + + $form = $model->getForm(); + + if ($form !== false) + { + $this->hasForm = true; + } + + $this->display(in_array('browse', $this->cacheableTasks)); + + return true; + } + + /** + * Single record read. The id set in the request is passed to the model and + * then the item layout is used to render the result. + * + * @return bool + */ + public function read() + { + // Load the model + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + // Set the layout to item, if it's not set in the URL + if (is_null($this->layout)) + { + $this->layout = 'item'; + } + + // Do I have a form? + $model->setState('form_name', 'form.' . $this->layout); + + $item = $model->getItem(); + + if (!($item instanceof FOFTable)) + { + return false; + } + + $itemKey = $item->getKeyName(); + + if ($item->$itemKey != $model->getId()) + { + return false; + } + + $formData = is_object($item) ? $item->getData() : array(); + $form = $model->getForm($formData); + + if ($form !== false) + { + $this->hasForm = true; + } + + // Display + $this->display(in_array('read', $this->cacheableTasks)); + + return true; + } + + /** + * Single record add. The form layout is used to present a blank page. + * + * @return false|void + */ + public function add() + { + // Load and reset the model + $model = $this->getThisModel(); + $model->reset(); + + // Set the layout to form, if it's not set in the URL + + if (!$this->layout) + { + $this->layout = 'form'; + } + + // Do I have a form? + $model->setState('form_name', 'form.' . $this->layout); + + $item = $model->getItem(); + + if (!($item instanceof FOFTable)) + { + return false; + } + + $formData = is_object($item) ? $item->getData() : array(); + $form = $model->getForm($formData); + + if ($form !== false) + { + $this->hasForm = true; + } + + // Display + $this->display(in_array('add', $this->cacheableTasks)); + } + + /** + * Single record edit. The ID set in the request is passed to the model, + * then the form layout is used to edit the result. + * + * @return bool + */ + public function edit() + { + // Load the model + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->checkout(); + + if (!$status) + { + // Redirect on error + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url, $model->getError(), 'error'); + + return false; + } + + // Set the layout to form, if it's not set in the URL + + if (is_null($this->layout)) + { + $this->layout = 'form'; + } + + // Do I have a form? + $model->setState('form_name', 'form.' . $this->layout); + + $item = $model->getItem(); + + if (!($item instanceof FOFTable)) + { + return false; + } + + $itemKey = $item->getKeyName(); + + if ($item->$itemKey != $model->getId()) + { + return false; + } + + $formData = is_object($item) ? $item->getData() : array(); + $form = $model->getForm($formData); + + if ($form !== false) + { + $this->hasForm = true; + } + + // Display + $this->display(in_array('edit', $this->cacheableTasks)); + + return true; + } + + /** + * Save the incoming data and then return to the Edit task + * + * @return bool + */ + public function apply() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + $result = $this->applySave(); + + // Redirect to the edit task + if ($result) + { + $id = $this->input->get('id', 0, 'int'); + $textkey = strtoupper($this->component) . '_LBL_' . strtoupper($this->view) . '_SAVED'; + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . $this->view . '&task=edit&id=' . $id . $this->getItemidURLSuffix(); + $this->setRedirect($url, JText::_($textkey)); + } + + return $result; + } + + /** + * Duplicates selected items + * + * @return bool + */ + public function copy() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->copy(); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + + return false; + } + else + { + if(!FOFPlatform::getInstance()->isCli()) + { + FOFPlatform::getInstance()->setHeader('Status', '201 Created', true); + } + + $this->setRedirect($url); + + return true; + } + } + + /** + * Save the incoming data and then return to the Browse task + * + * @return bool + */ + public function save() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $result = $this->applySave(); + + // Redirect to the display task + if ($result) + { + $textkey = strtoupper($this->component) . '_LBL_' . strtoupper($this->view) . '_SAVED'; + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url, JText::_($textkey)); + } + + return $result; + } + + /** + * Save the incoming data and then return to the Add task + * + * @return bool + */ + public function savenew() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $result = $this->applySave(); + + // Redirect to the display task + + if ($result) + { + $textkey = strtoupper($this->component) . '_LBL_' . strtoupper($this->view) . '_SAVED'; + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . $this->view . '&task=add' . $this->getItemidURLSuffix(); + $this->setRedirect($url, JText::_($textkey)); + } + + return $result; + } + + /** + * Cancel the edit, check in the record and return to the Browse task + * + * @return bool + */ + public function cancel() + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $model->checkin(); + + // Remove any saved data + JFactory::getSession()->set($model->getHash() . 'savedata', null); + + // Redirect to the display task + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url); + + return true; + } + + /** + * Method to load a row from version history + * + * @return boolean True if the content history is reverted, false otherwise + * + * @since 2.2 + */ + public function loadhistory() + { + $app = JFactory::getApplication(); + $lang = JFactory::getLanguage(); + $model = $this->getThisModel(); + $table = $model->getTable(); + $historyId = $app->input->get('version_id', null, 'integer'); + $status = $model->checkout(); + $alias = $this->component . '.' . $this->view; + + if (!$model->loadhistory($historyId, $table, $alias)) + { + $this->setMessage($model->getError(), 'error'); + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url); + + return false; + } + + // Determine the name of the primary key for the data. + if (empty($key)) + { + $key = $table->getKeyName(); + } + + $recordId = $table->$key; + + // To avoid data collisions the urlVar may be different from the primary key. + $urlVar = empty($this->urlVar) ? $key : $this->urlVar; + + // Access check. + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.edit', 'core.edit' + ); + + if (!$this->checkACL($privilege)) + { + $this->setError(JText::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + $this->setMessage($this->getError(), 'error'); + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url); + $table->checkin(); + + return false; + } + + $table->store(); + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url); + + $this->setMessage(JText::sprintf('JLIB_APPLICATION_SUCCESS_LOAD_HISTORY', $model->getState('save_date'), $model->getState('version_note'))); + + return true; + } + + /** + * Sets the access to public. Joomla! 1.5 compatibility. + * + * @return bool + * + * @deprecated since 2.0 + */ + public function accesspublic() + { + // CSRF prevention + + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setaccess(0); + } + + /** + * Sets the access to registered. Joomla! 1.5 compatibility. + * + * @return bool + * + * @deprecated since 2.0 + */ + public function accessregistered() + { + // CSRF prevention + + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setaccess(1); + } + + /** + * Sets the access to special. Joomla! 1.5 compatibility. + * + * @return bool + * + * @deprecated since 2.0 + */ + public function accessspecial() + { + // CSRF prevention + + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setaccess(2); + } + + /** + * Publish (set enabled = 1) an item. + * + * @return bool + */ + public function publish() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setstate(1); + } + + /** + * Unpublish (set enabled = 0) an item. + * + * @return bool + */ + public function unpublish() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setstate(0); + } + + /** + * Archive (set enabled = 2) an item. + * + * @return bool + */ + public function archive() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setstate(2); + } + + /** + * Trash (set enabled = -2) an item. + * + * @return bool + */ + public function trash() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + return $this->setstate(-2); + } + + /** + * Saves the order of the items + * + * @return bool + */ + public function saveorder() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $ordering = $model->getTable()->getColumnAlias('ordering'); + $ids = $model->getIds(); + $orders = $this->input->get('order', array(), 'array'); + + if ($n = count($ids)) + { + for ($i = 0; $i < $n; $i++) + { + $model->setId($ids[$i]); + $neworder = (int) $orders[$i]; + + $item = $model->getItem(); + + if (!($item instanceof FOFTable)) + { + return false; + } + + $key = $item->getKeyName(); + + if ($item->$key == $ids[$i]) + { + $item->$ordering = $neworder; + $model->save($item); + } + } + } + + $status = $model->reorder(); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + $this->setRedirect($url); + + return $status; + } + + /** + * Moves selected items one position down the ordering list + * + * @return bool + */ + public function orderdown() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->move(1); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + } + else + { + $this->setRedirect($url); + } + + return $status; + } + + /** + * Moves selected items one position up the ordering list + * + * @return bool + */ + public function orderup() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->move(-1); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + } + else + { + $this->setRedirect($url); + } + + return $status; + } + + /** + * Delete selected item(s) + * + * @return bool + */ + public function remove() + { + // CSRF prevention + if ($this->csrfProtection) + { + $this->_csrfProtection(); + } + + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->delete(); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + } + else + { + $this->setRedirect($url); + } + + return $status; + } + + /** + * Redirects the browser or returns false if no redirect is set. + * + * @return boolean False if no redirect exists. + */ + public function redirect() + { + if ($this->redirect) + { + $app = JFactory::getApplication(); + $app->enqueueMessage($this->message, $this->messageType); + $app->redirect($this->redirect); + + return true; + } + + return false; + } + + /** + * Returns true if there is a redirect set in the controller + * + * @return boolean + */ + public function hasRedirect() + { + return !empty($this->redirect); + } + + /** + * Register the default task to perform if a mapping is not found. + * + * @param string $method The name of the method in the derived class to perform if a named task is not found. + * + * @return FOFController A FOFController object to support chaining. + */ + public function registerDefaultTask($method) + { + $this->registerTask('__default', $method); + + return $this; + } + + /** + * Register (map) a task to a method in the class. + * + * @param string $task The task. + * @param string $method The name of the method in the derived class to perform for this task. + * + * @return FOFController A FOFController object to support chaining. + */ + public function registerTask($task, $method) + { + if (in_array(strtolower($method), $this->methods)) + { + $this->taskMap[strtolower($task)] = $method; + } + + return $this; + } + + /** + * Unregister (unmap) a task in the class. + * + * @param string $task The task. + * + * @return FOFController This object to support chaining. + */ + public function unregisterTask($task) + { + unset($this->taskMap[strtolower($task)]); + + return $this; + } + + /** + * Sets the internal message that is passed with a redirect + * + * @param string $text Message to display on redirect. + * @param string $type Message type. Optional, defaults to 'message'. + * + * @return string Previous message + */ + public function setMessage($text, $type = 'message') + { + $previous = $this->message; + $this->message = $text; + $this->messageType = $type; + + return $previous; + } + + /** + * Sets an entire array of search paths for resources. + * + * @param string $type The type of path to set, typically 'view' or 'model'. + * @param string $path The new set of search paths. If null or false, resets to the current directory only. + * + * @return void + */ + protected function setPath($type, $path) + { + // Clear out the prior search dirs + $this->paths[$type] = array(); + + // Actually add the user-specified directories + $this->addPath($type, $path); + } + + /** + * Registers a redirection with an optional message. The redirection is + * carried out when you use the redirect method. + * + * @param string $url The URL to redirect to + * @param string $msg The message to be pushed to the application + * @param string $type The message type to be pushed to the application, e.g. 'error' + * + * @return FOFController This object to support chaining + */ + public function setRedirect($url, $msg = null, $type = null) + { + // Do the logic only if we're parsing a raw url (index.php?foo=bar&etc=etc) + if (strpos($url, 'index.php') === 0) + { + $isAdmin = FOFPlatform::getInstance()->isBackend(); + $auto = false; + + if (($this->autoRouting == 2 || $this->autoRouting == 3) && $isAdmin) + { + $auto = true; + } + elseif (($this->autoRouting == 1 || $this->autoRouting == 3) && !$isAdmin) + { + $auto = true; + } + + if ($auto) + { + $url = JRoute::_($url, false); + } + } + + $this->redirect = $url; + + if ($msg !== null) + { + // Controller may have set this directly + $this->message = $msg; + } + + // Ensure the type is not overwritten by a previous call to setMessage. + if (empty($type)) + { + if (empty($this->messageType)) + { + $this->messageType = 'message'; + } + } + // If the type is explicitly set, set it. + else + { + $this->messageType = $type; + } + + return $this; + } + + /** + * Sets the published state (the enabled field) of the selected item(s) + * + * @param integer $state The desired state. 0 is unpublished, 1 is published. + * + * @return bool + */ + protected function setstate($state = 0) + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $status = $model->publish($state); + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + } + else + { + $this->setRedirect($url); + } + + return $status; + } + + /** + * Sets the access level of the selected item(s). + * + * @param integer $level The desired viewing access level ID + * + * @return bool + */ + protected function setaccess($level = 0) + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $id = $model->getId(); + $item = $model->getItem(); + + if (!($item instanceof FOFTable)) + { + return false; + } + + $accessField = $item->getColumnAlias('access'); + $key = $item->getKeyName(); + $loadedid = $item->$key; + + if ($id == $loadedid) + { + $item->$accessField = $level; + $status = $model->save($item); + } + else + { + $status = false; + } + + // Redirect + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + $url = !empty($customURL) ? $customURL : 'index.php?option=' . $this->component . '&view=' . FOFInflector::pluralize($this->view) . $this->getItemidURLSuffix(); + + if (!$status) + { + $this->setRedirect($url, $model->getError(), 'error'); + } + else + { + $this->setRedirect($url); + } + + return $status; + } + + /** + * Common method to handle apply and save tasks + * + * @return boolean Returns true on success + */ + final private function applySave() + { + // Load the model + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $id = $model->getId(); + + $data = $this->input->getData(); + + if (!$this->onBeforeApplySave($data)) + { + return false; + } + + // Set the layout to form, if it's not set in the URL + + if (is_null($this->layout)) + { + $this->layout = 'form'; + } + + // Do I have a form? + $model->setState('form_name', 'form.' . $this->layout); + + $status = $model->save($data); + + if ($status && ($id != 0)) + { + FOFPlatform::getInstance()->setHeader('Status', '201 Created', true); + + // Try to check-in the record if it's not a new one + $status = $model->checkin(); + } + + if ($status) + { + $status = $this->onAfterApplySave(); + } + + $this->input->set('id', $model->getId()); + + if (!$status) + { + // Redirect on error + $id = $model->getId(); + + if ($customURL = $this->input->get('returnurl', '', 'string')) + { + $customURL = base64_decode($customURL); + } + + if (!empty($customURL)) + { + $url = $customURL; + } + elseif ($id != 0) + { + $url = 'index.php?option=' . $this->component . '&view=' . $this->view . '&task=edit&id=' . $id . $this->getItemidURLSuffix(); + } + else + { + $url = 'index.php?option=' . $this->component . '&view=' . $this->view . '&task=add' . $this->getItemidURLSuffix(); + } + + $this->setRedirect($url, '
      • ' . implode('
      • ', $model->getErrors()) . '
      • ', 'error'); + + return false; + } + else + { + $session = JFactory::getSession(); + $session->set($model->getHash() . 'savedata', null); + + return true; + } + } + + /** + * Returns the default model associated with the current view + * + * @param array $config Configuration variables for the model + * + * @return FOFModel The global instance of the model (singleton) + */ + final public function getThisModel($config = array()) + { + if (!is_object($this->_modelObject)) + { + // Make sure $config is an array + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config)) + { + $config = array(); + } + + if (!empty($this->modelName)) + { + $parts = FOFInflector::explode($this->modelName); + $modelName = ucfirst(array_pop($parts)); + $prefix = FOFInflector::implode($parts); + } + else + { + $prefix = ucfirst($this->bareComponent) . 'Model'; + $modelName = ucfirst(FOFInflector::pluralize($this->view)); + } + + if (!array_key_exists('input', $config) || !($config['input'] instanceof FOFInput)) + { + $config['input'] = $this->input; + } + + $this->_modelObject = $this->getModel($modelName, $prefix, $config); + } + + return $this->_modelObject; + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + */ + public function getModel($name = '', $prefix = '', $config = array()) + { + // Make sure $config is an array + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config) || empty($config)) + { + // array_merge is required to create a copy instead of assigning by reference + $config = array_merge($this->config); + } + + if (empty($name)) + { + $name = $this->getName(); + } + + if (empty($prefix)) + { + $prefix = $this->model_prefix; + } + + if ($model = $this->createModel($name, $prefix, $config)) + { + // Task is a reserved state + $model->setState('task', $this->task); + + // Let's get the application object and set menu information if it's available + if (!FOFPlatform::getInstance()->isCli()) + { + $app = JFactory::getApplication(); + $menu = $app->getMenu(); + + if (is_object($menu)) + { + if ($item = $menu->getActive()) + { + $params = $menu->getParams($item->id); + + // Set default state data + $model->setState('parameters.menu', $params); + } + } + } + } + + return $model; + } + + /** + * Returns current view object + * + * @param array $config Configuration variables for the model + * + * @return FOFView The global instance of the view object (singleton) + */ + final public function getThisView($config = array()) + { + if (!is_object($this->_viewObject)) + { + // Make sure $config is an array + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config) || empty($config)) + { + // array_merge is required to create a copy instead of assigning by reference + $config = array_merge($this->config); + } + + $prefix = null; + $viewName = null; + $viewType = null; + + if (!empty($this->viewName)) + { + $parts = FOFInflector::explode($this->viewName); + $viewName = ucfirst(array_pop($parts)); + $prefix = FOFInflector::implode($parts); + } + else + { + $prefix = ucfirst($this->bareComponent) . 'View'; + $viewName = ucfirst($this->view); + } + + $document = FOFPlatform::getInstance()->getDocument(); + + if ($document instanceof JDocument) + { + $viewType = $document->getType(); + } + else + { + $viewType = $this->input->getCmd('format', 'html'); + } + + if (($viewType == 'html') && $this->hasForm) + { + $viewType = 'form'; + } + + if (!array_key_exists('input', $config) || !($config['input'] instanceof FOFInput)) + { + $config['input'] = $this->input; + } + + $config['input']->set('base_path', $this->basePath); + + $this->_viewObject = $this->getView($viewName, $viewType, $prefix, $config); + } + + return $this->_viewObject; + } + + /** + * Method to get the controller name + * + * The dispatcher name is set by default parsed using the classname, or it can be set + * by passing a $config['name'] in the class constructor + * + * @throws Exception + * + * @return string The name of the dispatcher + */ + public function getName() + { + if (empty($this->name)) + { + if (empty($this->bareComponent)) + { + $r = null; + + if (!preg_match('/(.*)Controller/i', get_class($this), $r)) + { + throw new Exception(JText::_('JLIB_APPLICATION_ERROR_CONTROLLER_GET_NAME'), 500); + } + + $this->name = strtolower($r[1]); + } + else + { + $this->name = $this->bareComponent; + } + } + + return $this->name; + } + + /** + * Get the last task that is being performed or was most recently performed. + * + * @return string The task that is being performed or was most recently performed. + */ + public function getTask() + { + return $this->task; + } + + /** + * Gets the available tasks in the controller. + * + * @return array Array[i] of task names. + */ + public function getTasks() + { + return $this->methods; + } + + /** + * Method to get a reference to the current view and load it if necessary. + * + * @param string $name The view name. Optional, defaults to the controller name. + * @param string $type The view type. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for view. Optional. + * + * @throws Exception + * + * @return FOFView Reference to the view or an error. + */ + public function getView($name = '', $type = '', $prefix = '', $config = array()) + { + // Make sure $config is an array + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config)) + { + $config = array(); + } + + if (empty($name)) + { + $name = $this->getName(); + } + + if (empty($prefix)) + { + $prefix = $this->getName() . 'View'; + } + + $signature = md5($name . $type . $prefix . serialize($config)); + + if (empty($this->viewsCache[$signature])) + { + if ($view = $this->createView($name, $prefix, $type, $config)) + { + $this->viewsCache[$signature] = & $view; + } + else + { + throw new Exception(JText::sprintf('JLIB_APPLICATION_ERROR_VIEW_NOT_FOUND', $name, $type, $prefix), 500); + } + } + + return $this->viewsCache[$signature]; + } + + /** + * Creates a new model object + * + * @param string $name The name of the model class, e.g. Items + * @param string $prefix The prefix of the model class, e.g. FoobarModel + * @param array $config The configuration parameters for the model class + * + * @return FOFModel The model object + */ + protected function createModel($name, $prefix = '', $config = array()) + { + // Make sure $config is an array + + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config)) + { + $config = array(); + } + + $result = null; + + // Clean the model name + $modelName = preg_replace('/[^A-Z0-9_]/i', '', $name); + $classPrefix = preg_replace('/[^A-Z0-9_]/i', '', $prefix); + + $result = FOFModel::getAnInstance($modelName, $classPrefix, $config); + + return $result; + } + + /** + * Method to load and return a model object. + * + * @param string $name The name of the model. + * @param string $prefix Optional model prefix. + * @param array $config Configuration array for the model. Optional. + * + * @return mixed Model object on success; otherwise null + */ + protected function &_createModel($name, $prefix = '', $config = array()) + { + FOFPlatform::getInstance()->logDeprecated(__CLASS__ . '::' .__METHOD__ . ' is deprecated. Use createModel() instead.'); + + return $this->createModel($name, $prefix, $config); + } + + /** + * Creates a View object instance and returns it + * + * @param string $name The name of the view, e.g. Items + * @param string $prefix The prefix of the view, e.g. FoobarView + * @param string $type The type of the view, usually one of Html, Raw, Json or Csv + * @param array $config The configuration variables to use for creating the view + * + * @return FOFView + */ + protected function createView($name, $prefix = '', $type = '', $config = array()) + { + // Make sure $config is an array + + if (is_object($config)) + { + $config = (array) $config; + } + elseif (!is_array($config)) + { + $config = array(); + } + + $result = null; + + // Clean the view name + $viewName = preg_replace('/[^A-Z0-9_]/i', '', $name); + $classPrefix = preg_replace('/[^A-Z0-9_]/i', '', $prefix); + $viewType = preg_replace('/[^A-Z0-9_]/i', '', $type); + + if (!isset($config['input'])) + { + $config['input'] = $this->input; + } + + if (($config['input'] instanceof FOFInput)) + { + $tmpInput = $config['input']; + } + else + { + $tmpInput = new FOFInput($config['input']); + } + + // Guess the component name and view + + if (!empty($prefix)) + { + preg_match('/(.*)View$/', $prefix, $m); + $component = 'com_' . strtolower($m[1]); + } + else + { + $component = ''; + } + + if (empty($component) && array_key_exists('input', $config)) + { + $component = $tmpInput->get('option', $component, 'cmd'); + } + + if (array_key_exists('option', $config)) + { + if ($config['option']) + { + $component = $config['option']; + } + } + + $config['option'] = $component; + + $view = strtolower($viewName); + + if (empty($view) && array_key_exists('input', $config)) + { + $view = $tmpInput->get('view', $view, 'cmd'); + } + + if (array_key_exists('view', $config)) + { + if ($config['view']) + { + $view = $config['view']; + } + } + + $config['view'] = $view; + + if (array_key_exists('input', $config)) + { + $tmpInput->set('option', $config['option']); + $tmpInput->set('view', $config['view']); + $config['input'] = $tmpInput; + } + + // Get the component directories + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($config['option']); + + // Get the base paths where the view class files are expected to live + $basePaths = array( + $componentPaths['main'], + $componentPaths['alt'] + ); + $basePaths = array_merge($this->paths['view']); + + // Get the alternate (singular/plural) view name + $altViewName = FOFInflector::isPlural($viewName) ? FOFInflector::singularize($viewName) : FOFInflector::pluralize($viewName); + + $suffixes = array( + $viewName, + $altViewName, + 'default' + ); + + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + foreach ($suffixes as $suffix) + { + // Build the view class name + $viewClass = $classPrefix . ucfirst($suffix); + + if (class_exists($viewClass)) + { + // The class is already loaded + break; + } + + // The class is not loaded. Let's load it! + $viewPath = $this->createFileName('view', array('name' => $suffix, 'type' => $viewType)); + $path = $filesystem->pathFind($basePaths, $viewPath); + + if ($path) + { + require_once $path; + } + + if (class_exists($viewClass)) + { + // The class was loaded successfully + break; + } + } + + if (!class_exists($viewClass)) + { + $viewClass = 'FOFView' . ucfirst($type); + } + + $templateOverridePath = FOFPlatform::getInstance()->getTemplateOverridePath($config['option']); + + // Setup View configuration options + + if (!array_key_exists('template_path', $config)) + { + $config['template_path'][] = $componentPaths['main'] . '/views/' . FOFInflector::pluralize($config['view']) . '/tmpl'; + + if ($templateOverridePath) + { + $config['template_path'][] = $templateOverridePath . '/' . FOFInflector::pluralize($config['view']); + } + + $config['template_path'][] = $componentPaths['main'] . '/views/' . FOFInflector::singularize($config['view']) . '/tmpl'; + + if ($templateOverridePath) + { + $config['template_path'][] = $templateOverridePath . '/' . FOFInflector::singularize($config['view']); + } + + $config['template_path'][] = $componentPaths['main'] . '/views/' . $config['view'] . '/tmpl'; + + if ($templateOverridePath) + { + $config['template_path'][] = $templateOverridePath . '/' . $config['view']; + } + } + + $extraTemplatePath = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.template_path', null); + + if ($extraTemplatePath) + { + array_unshift($config['template_path'], $componentPaths['main'] . '/' . $extraTemplatePath); + } + + if (!array_key_exists('helper_path', $config)) + { + $config['helper_path'] = array( + $componentPaths['main'] . '/helpers', + $componentPaths['admin'] . '/helpers' + ); + } + + $extraHelperPath = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.helper_path', null); + + if ($extraHelperPath) + { + $config['helper_path'][] = $componentPaths['main'] . '/' . $extraHelperPath; + } + + // Set up the page title + $setFrontendPageTitle = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.setFrontendPageTitle', null); + + if ($setFrontendPageTitle) + { + $setFrontendPageTitle = strtolower($setFrontendPageTitle); + $config['setFrontendPageTitle'][] = in_array($setFrontendPageTitle, array('1', 'yes', 'true', 'on')); + } + + $defaultPageTitle = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.defaultPageTitle', null); + + if ($defaultPageTitle) + { + $config['defaultPageTitle'][] = in_array($defaultPageTitle, array('1', 'yes', 'true', 'on')); + } + + // Set the use_hypermedia flag in $config if it's not already set + if (!isset($config['use_hypermedia'])) + { + $config['use_hypermedia'] = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.use_hypermedia', false); + } + + // Set also the linkbar_style + if (!isset($config['linkbar_style'])) + { + $style = $this->configProvider->get($config['option'] . '.views.' . $config['view'] . '.config.linkbar_style', false); + + if ($style) { + $config['linkbar_style'] = $style; + } + } + + /** + * Some administrative templates force format=utf (yeah, I know, what the heck, right?) when a format + * URL parameter does not exist in the URL. Of course there is no such thing as FOFViewUtf (why the heck would + * it be, there is no such thing as a format=utf in Joomla! for crying out loud) which causes a Fatal Error. So + * we have to detect that and force $type='html'... + */ + if (!class_exists($viewClass) && ($type != 'html')) + { + $type = 'html'; + $result = $this->createView($name, $prefix, $type, $config); + } + else + { + $result = new $viewClass($config); + } + + return $result; + } + + /** + * Deprecated function to create a View object instance + * + * @param string $name The name of the view, e.g. 'Items' + * @param string $prefix The prefix of the view, e.g. 'FoobarView' + * @param string $type The view type, e.g. 'html' + * @param array $config The configuration array for the view + * + * @return FOFView + * + * @see FOFController::createView + * + * @deprecated since version 2.0 + */ + protected function &_createView($name, $prefix = '', $type = '', $config = array()) + { + FOFPlatform::getInstance()->logDeprecated(__CLASS__ . '::' . __METHOD__ . ' is deprecated. Use createView() instead.'); + + return $this->createView($name, $prefix, $type, $config); + } + + /** + * Set the name of the view to be used by this Controller + * + * @param string $viewName The name of the view + * + * @return void + */ + public function setThisViewName($viewName) + { + $this->viewName = $viewName; + } + + /** + * Set the name of the model to be used by this Controller + * + * @param string $modelName The name of the model + * + * @return void + */ + public function setThisModelName($modelName) + { + $this->modelName = $modelName; + } + + /** + * Checks if the current user has enough privileges for the requested ACL + * area. + * + * @param string $area The ACL area, e.g. core.manage. + * + * @return boolean True if the user has the ACL privilege specified + */ + protected function checkACL($area) + { + if (in_array(strtolower($area), array('false','0','no','403'))) + { + return false; + } + + if (in_array(strtolower($area), array('true','1','yes'))) + { + return true; + } + elseif (empty($area)) + { + return true; + } + else + { + // Check if we're dealing with ids + $ids = null; + + // First, check if there is an asset for this record + $table = $this->getThisModel()->getTable(); + + if ($table && $table->isAssetsTracked()) + { + $ids = $this->getThisModel()->getId() ? $this->getThisModel()->getId() : null; + } + + // Generic or Asset tracking + + if (empty($ids)) + { + return FOFPlatform::getInstance()->authorise($area, $this->component); + } + else + { + if (!is_array($ids)) + { + $ids = array($ids); + } + + $resource = FOFInflector::singularize($this->view); + $isEditState = ($area == 'core.edit.state'); + + foreach ($ids as $id) + { + $asset = $this->component . '.' . $resource . '.' . $id; + + // Dedicated permission found, check it! + + if (FOFPlatform::getInstance()->authorise($area, $asset) ) + { + return true; + } + + // Fallback on edit.own, if not edit.state. First test if the permission is available. + + if ((!$isEditState) && (FOFPlatform::getInstance()->authorise('core.edit.own', $asset))) + { + $table = $this->getThisModel()->getTable(); + $table->load($id); + + $created_by = $table->getColumnAlias('created_by'); + + if ($table && isset($table->$created_by)) + { + // Now test the owner is the user. + $owner_id = (int) $table->$created_by; + + // If the owner matches 'me' then do the test. + if ($owner_id == FOFPlatform::getInstance()->getUser()->id) + { + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + } + } + } + + return false; + } + + /** + * A catch-all method for all tasks without a corresponding onBefore + * method. Applies the ACL preferences defined in fof.xml. + * + * @param string $task The task being executed + * + * @return boolean True to allow execution of the task + */ + protected function onBeforeGenericTask($task) + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.' . $task, '' + ); + + return $this->checkACL($privilege); + } + + /** + * Execute something before applySave is called. Return false to prevent + * applySave from executing. + * + * @param array &$data The data upon which applySave will act + * + * @return boolean True to allow applySave to run + */ + protected function onBeforeApplySave(&$data) + { + return true; + } + + /** + * Execute something after applySave has run. + * + * @return boolean True to allow normal return, false to cause a 403 error + */ + protected function onAfterApplySave() + { + return true; + } + + /** + * ACL check before changing the access level; override to customise + * + * @return boolean True to allow accesspublic() to run + */ + protected function onBeforeAccesspublic() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.accesspublic', 'core.edit.state'); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the access level; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeAccessregistered() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.accessregistered', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the access level; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeAccessspecial() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.accessspecial', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before adding a new record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeAdd() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.add', 'core.create' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before saving a new/modified record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeApply() + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $id = $model->getId(); + + if(!$id) + { + $defaultPrivilege = 'core.create'; + } + else + { + $defaultPrivilege = 'core.edit'; + } + + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.apply', $defaultPrivilege + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before allowing someone to browse + * + * @return boolean True to allow the method to run + */ + protected function onBeforeBrowse() + { + $defaultPrivilege = ''; + + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.browse', $defaultPrivilege + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before cancelling an edit + * + * @return boolean True to allow the method to run + */ + protected function onBeforeCancel() + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $id = $model->getId(); + + if(!$id) + { + $defaultPrivilege = 'core.create'; + } + else + { + $defaultPrivilege = 'core.edit'; + } + + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.cancel', $defaultPrivilege + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before editing a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeEdit() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.edit', 'core.edit' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the ordering of a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeOrderdown() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.orderdown', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the ordering of a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeOrderup() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.orderup', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the publish status of a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforePublish() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.publish', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before removing a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeRemove() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.remove', 'core.delete' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before saving a new/modified record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeSave() + { + $model = $this->getThisModel(); + + if (!$model->getId()) + { + $model->setIDsFromRequest(); + } + + $id = $model->getId(); + + if(!$id) + { + $defaultPrivilege = 'core.create'; + } + else + { + $defaultPrivilege = 'core.edit'; + } + + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.save', $defaultPrivilege + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before saving a new/modified record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeSavenew() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.savenew', 'core.create' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the ordering of a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeSaveorder() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.saveorder', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * ACL check before changing the publish status of a record; override to customise + * + * @return boolean True to allow the method to run + */ + protected function onBeforeUnpublish() + { + $privilege = $this->configProvider->get( + $this->component . '.views.' . + FOFInflector::singularize($this->view) . '.acl.unpublish', 'core.edit.state' + ); + + return $this->checkACL($privilege); + } + + /** + * Gets a URL suffix with the Itemid parameter. If it's not the front-end of the site, or if + * there is no Itemid set it returns an empty string. + * + * @return string The &Itemid=123 URL suffix, or an empty string if Itemid is not applicable + */ + public function getItemidURLSuffix() + { + if (FOFPlatform::getInstance()->isFrontend() && ($this->input->getCmd('Itemid', 0) != 0)) + { + return '&Itemid=' . $this->input->getInt('Itemid', 0); + } + else + { + return ''; + } + } + + /** + * Applies CSRF protection by means of a standard Joomla! token (nonce) check. + * Raises a 403 Access Forbidden error through the platform if the check fails. + * + * TODO Move this check inside the platform + * + * @return boolean True if the CSRF check is successful + * + * @throws Exception + */ + protected function _csrfProtection() + { + static $isCli = null, $isAdmin = null; + + if (is_null($isCli)) + { + $isCli = FOFPlatform::getInstance()->isCli(); + $isAdmin = FOFPlatform::getInstance()->isBackend(); + } + + switch ($this->csrfProtection) + { + // Never + case 0: + return true; + break; + + // Always + case 1: + break; + + // Only back-end and HTML format + case 2: + if ($isCli) + { + return true; + } + elseif (!$isAdmin && ($this->input->get('format', 'html', 'cmd') != 'html')) + { + return true; + } + break; + + // Only back-end + case 3: + if (!$isAdmin) + { + return true; + } + break; + } + + $hasToken = false; + $session = JFactory::getSession(); + + // Joomla! 1.5/1.6/1.7/2.5 (classic Joomla! API) method + if (method_exists('JUtility', 'getToken')) + { + $token = JUtility::getToken(); + $hasToken = $this->input->get($token, false, 'none') == 1; + + if (!$hasToken) + { + $hasToken = $this->input->get('_token', null, 'none') == $token; + } + } + + // Joomla! 2.5+ (Platform 12.1+) method + if (!$hasToken) + { + if (method_exists($session, 'getToken')) + { + $token = $session->getToken(); + $hasToken = $this->input->get($token, false, 'none') == 1; + + if (!$hasToken) + { + $hasToken = $this->input->get('_token', null, 'none') == $token; + } + } + } + + // Joomla! 2.5+ formToken method + if (!$hasToken) + { + if (method_exists($session, 'getFormToken')) + { + $token = $session->getFormToken(); + $hasToken = $this->input->get($token, false, 'none') == 1; + + if (!$hasToken) + { + $hasToken = $this->input->get('_token', null, 'none') == $token; + } + } + } + + if (!$hasToken) + { + FOFPlatform::getInstance()->raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + + return false; + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/database.php b/deployed/akeeba/libraries/fof/database/database.php new file mode 100644 index 00000000..dc5a7188 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/database.php @@ -0,0 +1,199 @@ +execute(); + } + + /** + * Get a list of available database connectors. The list will only be populated with connectors that both + * the class exists and the static test method returns true. This gives us the ability to have a multitude + * of connector classes that are self-aware as to whether or not they are able to be used on a given system. + * + * @return array An array of available database connectors. + * + * @since 11.1 + * @deprecated 13.1 (Platform) & 4.0 (CMS) + */ + public static function getConnectors() + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::getConnectors() is deprecated, use FOFDatabaseDriver::getConnectors() instead.', JLog::WARNING, 'deprecated'); + } + + return FOFDatabaseDriver::getConnectors(); + } + + /** + * Gets the error message from the database connection. + * + * @param boolean $escaped True to escape the message string for use in JavaScript. + * + * @return string The error message for the most recent query. + * + * @deprecated 13.3 (Platform) & 4.0 (CMS) + * @since 11.1 + */ + public function getErrorMsg($escaped = false) + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::getErrorMsg() is deprecated, use exception handling instead.', JLog::WARNING, 'deprecated'); + } + + if ($escaped) + { + return addslashes($this->errorMsg); + } + else + { + return $this->errorMsg; + } + } + + /** + * Gets the error number from the database connection. + * + * @return integer The error number for the most recent query. + * + * @since 11.1 + * @deprecated 13.3 (Platform) & 4.0 (CMS) + */ + public function getErrorNum() + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::getErrorNum() is deprecated, use exception handling instead.', JLog::WARNING, 'deprecated'); + } + + return $this->errorNum; + } + + /** + * Method to return a FOFDatabaseDriver instance based on the given options. There are three global options and then + * the rest are specific to the database driver. The 'driver' option defines which FOFDatabaseDriver class is + * used for the connection -- the default is 'mysqli'. The 'database' option determines which database is to + * be used for the connection. The 'select' option determines whether the connector should automatically select + * the chosen database. + * + * Instances are unique to the given options and new objects are only created when a unique options array is + * passed into the method. This ensures that we don't end up with unnecessary database connection resources. + * + * @param array $options Parameters to be passed to the database driver. + * + * @return FOFDatabaseDriver A database object. + * + * @since 11.1 + * @deprecated 13.1 (Platform) & 4.0 (CMS) + */ + public static function getInstance($options = array()) + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::getInstance() is deprecated, use FOFDatabaseDriver::getInstance() instead.', JLog::WARNING, 'deprecated'); + } + + return FOFDatabaseDriver::getInstance($options); + } + + /** + * Splits a string of multiple queries into an array of individual queries. + * + * @param string $query Input SQL string with which to split into individual queries. + * + * @return array The queries from the input string separated into an array. + * + * @since 11.1 + * @deprecated 13.1 (Platform) & 4.0 (CMS) + */ + public static function splitSql($query) + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::splitSql() is deprecated, use FOFDatabaseDriver::splitSql() instead.', JLog::WARNING, 'deprecated'); + } + + return FOFDatabaseDriver::splitSql($query); + } + + /** + * Return the most recent error message for the database connector. + * + * @param boolean $showSQL True to display the SQL statement sent to the database as well as the error. + * + * @return string The error message for the most recent query. + * + * @since 11.1 + * @deprecated 13.3 (Platform) & 4.0 (CMS) + */ + public function stderr($showSQL = false) + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::stderr() is deprecated.', JLog::WARNING, 'deprecated'); + } + + if ($this->errorNum != 0) + { + return JText::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $this->errorNum, $this->errorMsg) + . ($showSQL ? "
        SQL =
        $this->sql
        " : ''); + } + else + { + return JText::_('JLIB_DATABASE_FUNCTION_NOERROR'); + } + } + + /** + * Test to see if the connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 11.1 + * @deprecated 12.3 (Platform) & 4.0 (CMS) - Use FOFDatabaseDriver::isSupported() instead. + */ + public static function test() + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabase::test() is deprecated. Use FOFDatabaseDriver::isSupported() instead.', JLog::WARNING, 'deprecated'); + } + + return static::isSupported(); + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver.php b/deployed/akeeba/libraries/fof/database/driver.php new file mode 100644 index 00000000..27b17efc --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver.php @@ -0,0 +1,2255 @@ +getFilename(); + + // Only load for php files. + if (!$file->isFile() || $file->getExtension() != 'php') + { + continue; + } + + // Block the ext/mysql driver for PHP 7 + if ($fileName === 'mysql.php' && PHP_MAJOR_VERSION >= 7) + { + continue; + } + + // Derive the class name from the type. + $class = str_ireplace('.php', '', 'FOFDatabaseDriver' . ucfirst(trim($fileName))); + + // If the class doesn't exist we have nothing left to do but look at the next type. We did our best. + if (!class_exists($class)) + { + continue; + } + + // Sweet! Our class exists, so now we just need to know if it passes its test method. + if ($class::isSupported()) + { + // Connector names should not have file extensions. + $connectors[] = str_ireplace('.php', '', $fileName); + } + } + + return $connectors; + } + + /** + * Method to return a FOFDatabaseDriver instance based on the given options. There are three global options and then + * the rest are specific to the database driver. The 'driver' option defines which FOFDatabaseDriver class is + * used for the connection -- the default is 'mysqli'. The 'database' option determines which database is to + * be used for the connection. The 'select' option determines whether the connector should automatically select + * the chosen database. + * + * Instances are unique to the given options and new objects are only created when a unique options array is + * passed into the method. This ensures that we don't end up with unnecessary database connection resources. + * + * @param array $options Parameters to be passed to the database driver. + * + * @return FOFDatabaseDriver A database object. + * + * @since 11.1 + * @throws RuntimeException + */ + public static function getInstance($options = array()) + { + // Sanitize the database connector options. + $options['driver'] = (isset($options['driver'])) ? preg_replace('/[^A-Z0-9_\.-]/i', '', $options['driver']) : 'mysqli'; + $options['database'] = (isset($options['database'])) ? $options['database'] : null; + $options['select'] = (isset($options['select'])) ? $options['select'] : true; + + // If the selected driver is `mysql` and we are on PHP 7 or greater, switch to the `mysqli` driver. + if ($options['driver'] === 'mysql' && PHP_MAJOR_VERSION >= 7) + { + // Check if we have support for the other MySQL drivers + $mysqliSupported = FOFDatabaseDriverMysqli::isSupported(); + $pdoMysqlSupported = FOFDatabaseDriverPdomysql::isSupported(); + + // If neither is supported, then the user cannot use MySQL; throw an exception + if (!$mysqliSupported && !$pdoMysqlSupported) + { + throw new RuntimeException( + 'The PHP `ext/mysql` extension is removed in PHP 7, cannot use the `mysql` driver.' + . ' Also, this system does not support MySQLi or PDO MySQL. Cannot instantiate database driver.' + ); + } + + // Prefer MySQLi as it is a closer replacement for the removed MySQL driver, otherwise use the PDO driver + if ($mysqliSupported) + { + if (class_exists('JLog')) + { + JLog::add( + 'The PHP `ext/mysql` extension is removed in PHP 7, cannot use the `mysql` driver. Trying `mysqli` instead.', + JLog::WARNING, + 'deprecated' + ); + } + + $options['driver'] = 'mysqli'; + } + else + { + if (class_exists('JLog')) + { + JLog::add( + 'The PHP `ext/mysql` extension is removed in PHP 7, cannot use the `mysql` driver. Trying `pdomysql` instead.', + JLog::WARNING, + 'deprecated' + ); + } + + $options['driver'] = 'pdomysql'; + } + } + + // Get the options signature for the database connector. + $signature = md5(serialize($options)); + + // If we already have a database connector instance for these options then just use that. + if (empty(self::$instances[$signature])) + { + // Derive the class name from the driver. + $class = 'FOFDatabaseDriver' . ucfirst(strtolower($options['driver'])); + + // If the class still doesn't exist we have nothing left to do but throw an exception. We did our best. + if (!class_exists($class)) + { + throw new RuntimeException(sprintf('Unable to load Database Driver: %s', $options['driver'])); + } + + // Create our new FOFDatabaseDriver connector based on the options given. + try + { + $instance = new $class($options); + } + catch (RuntimeException $e) + { + throw new RuntimeException(sprintf('Unable to connect to the Database: %s', $e->getMessage()), $e->getCode(), $e); + } + + // Set the new connector to the global instances based on signature. + self::$instances[$signature] = $instance; + } + + return self::$instances[$signature]; + } + + /** + * Splits a string of multiple queries into an array of individual queries. + * + * @param string $sql Input SQL string with which to split into individual queries. + * + * @return array The queries from the input string separated into an array. + * + * @since 11.1 + */ + public static function splitSql($sql) + { + $start = 0; + $open = false; + $char = ''; + $end = strlen($sql); + $queries = array(); + + for ($i = 0; $i < $end; $i++) + { + $current = substr($sql, $i, 1); + + if (($current == '"' || $current == '\'')) + { + $n = 2; + + while (substr($sql, $i - $n + 1, 1) == '\\' && $n < $i) + { + $n++; + } + + if ($n % 2 == 0) + { + if ($open) + { + if ($current == $char) + { + $open = false; + $char = ''; + } + } + else + { + $open = true; + $char = $current; + } + } + } + + if (($current == ';' && !$open) || $i == $end - 1) + { + $queries[] = substr($sql, $start, ($i - $start + 1)); + $start = $i + 1; + } + } + + return $queries; + } + + /** + * Magic method to provide method alias support for quote() and quoteName(). + * + * @param string $method The called method. + * @param array $args The array of arguments passed to the method. + * + * @return mixed The aliased method's return value or null. + * + * @since 11.1 + */ + public function __call($method, $args) + { + if (empty($args)) + { + return; + } + + switch ($method) + { + case 'q': + return $this->quote($args[0], isset($args[1]) ? $args[1] : true); + break; + case 'qn': + return $this->quoteName($args[0], isset($args[1]) ? $args[1] : null); + break; + } + } + + /** + * Constructor. + * + * @param array $options List of options used to configure the connection + * + * @since 11.1 + */ + public function __construct($options) + { + // Initialise object variables. + $this->_database = (isset($options['database'])) ? $options['database'] : ''; + $this->tablePrefix = (isset($options['prefix'])) ? $options['prefix'] : 'jos_'; + $this->connection = array_key_exists('connection', $options) ? $options['connection'] : null; + + $this->count = 0; + $this->errorNum = 0; + $this->log = array(); + + // Set class options. + $this->options = $options; + } + + /** + * Alter database's character set, obtaining query string from protected member. + * + * @param string $dbName The database name that will be altered + * + * @return string The query that alter the database query string + * + * @since 12.2 + * @throws RuntimeException + */ + public function alterDbCharacterSet($dbName) + { + if (is_null($dbName)) + { + throw new RuntimeException('Database name must not be null.'); + } + + $this->setQuery($this->getAlterDbCharacterSet($dbName)); + + return $this->execute(); + } + + /** + * Alter a table's character set, obtaining an array of queries to do so from a protected method. The conversion is + * wrapped in a transaction, if supported by the database driver. Otherwise the table will be locked before the + * conversion. This prevents data corruption. + * + * @param string $tableName The name of the table to alter + * @param boolean $rethrow True to rethrow database exceptions. Default: false (exceptions are suppressed) + * + * @return boolean True if successful + * + * @since CMS 3.5.0 + * @throws RuntimeException If the table name is empty + * @throws Exception Relayed from the database layer if a database error occurs and $rethrow == true + */ + public function alterTableCharacterSet($tableName, $rethrow = false) + { + if (is_null($tableName)) + { + throw new RuntimeException('Table name must not be null.'); + } + + $queries = $this->getAlterTableCharacterSet($tableName); + + if (empty($queries)) + { + return false; + } + + $hasTransaction = true; + + try + { + $this->transactionStart(); + } + catch (Exception $e) + { + $hasTransaction = false; + $this->lockTable($tableName); + } + + foreach ($queries as $query) + { + try + { + $this->setQuery($query)->execute(); + } + catch (Exception $e) + { + if ($hasTransaction) + { + $this->transactionRollback(); + } + else + { + $this->unlockTables(); + } + + if ($rethrow) + { + throw $e; + } + + return false; + } + } + + if ($hasTransaction) + { + try + { + $this->transactionCommit(); + } + catch (Exception $e) + { + $this->transactionRollback(); + + if ($rethrow) + { + throw $e; + } + + return false; + } + } + else + { + $this->unlockTables(); + } + + return true; + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + abstract public function connect(); + + /** + * Determines if the connection to the server is active. + * + * @return boolean True if connected to the database engine. + * + * @since 11.1 + */ + abstract public function connected(); + + /** + * Create a new database using information from $options object, obtaining query string + * from protected member. + * + * @param stdClass $options Object used to pass user and database name to database driver. + * This object must have "db_name" and "db_user" set. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return string The query that creates database + * + * @since 12.2 + * @throws RuntimeException + */ + public function createDatabase($options, $utf = true) + { + if (is_null($options)) + { + throw new RuntimeException('$options object must not be null.'); + } + elseif (empty($options->db_name)) + { + throw new RuntimeException('$options object must have db_name set.'); + } + elseif (empty($options->db_user)) + { + throw new RuntimeException('$options object must have db_user set.'); + } + + $this->setQuery($this->getCreateDatabaseQuery($options, $utf)); + + return $this->execute(); + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + abstract public function disconnect(); + + /** + * Adds a function callable just before disconnecting the database. Parameter of the callable is $this FOFDatabaseDriver + * + * @param callable $callable Function to call in disconnect() method just before disconnecting from database + * + * @return void + * + * @since CMS 3.1.2 + */ + public function addDisconnectHandler($callable) + { + $this->disconnectHandlers[] = $callable; + } + + /** + * 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 FOFDatabaseDriver Returns this object to support chaining. + * + * @since 11.4 + * @throws RuntimeException + */ + public abstract function dropTable($table, $ifExists = true); + + /** + * Escapes 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. + * + * @since 11.1 + */ + abstract public function escape($text, $extra = false); + + /** + * 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. + * + * @since 11.1 + */ + abstract protected function fetchArray($cursor = null); + + /** + * 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. + * + * @since 11.1 + */ + abstract protected function fetchAssoc($cursor = null); + + /** + * 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. + * + * @since 11.1 + */ + abstract protected function fetchObject($cursor = null, $class = 'stdClass'); + + /** + * 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 + * + * @since 11.1 + */ + abstract protected function freeResult($cursor = null); + + /** + * Get the number of affected rows for the previous executed SQL statement. + * + * @return integer The number of affected rows. + * + * @since 11.1 + */ + abstract public function getAffectedRows(); + + /** + * Return the query string to alter the database character set. + * + * @param string $dbName The database name + * + * @return string The query that alter the database query string + * + * @since 12.2 + */ + public function getAlterDbCharacterSet($dbName) + { + $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8'; + + return 'ALTER DATABASE ' . $this->quoteName($dbName) . ' CHARACTER SET `' . $charset . '`'; + } + + /** + * Get the query strings to alter the character set and collation of a table. + * + * @param string $tableName The name of the table + * + * @return string[] The queries required to alter the table's character set and collation + * + * @since CMS 3.5.0 + */ + public function getAlterTableCharacterSet($tableName) + { + $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8'; + $collation = $charset . '_general_ci'; + + $quotedTableName = $this->quoteName($tableName); + + $queries = array(); + $queries[] = "ALTER TABLE $quotedTableName CONVERT TO CHARACTER SET $charset COLLATE $collation"; + + /** + * We also need to convert each text column, modifying their character set and collation. This allows us to + * change, for example, a utf8_bin collated column to a utf8mb4_bin collated column. + */ + $sql = "SHOW FULL COLUMNS FROM $quotedTableName"; + $this->setQuery($sql); + $columns = $this->loadAssocList(); + $columnMods = array(); + + if (is_array($columns)) + { + foreach ($columns as $column) + { + // Make sure we are redefining only columns which do support a collation + $col = (object) $column; + + if (empty($col->Collation)) + { + continue; + } + + // Default new collation: utf8_general_ci or utf8mb4_general_ci + $newCollation = $charset . '_general_ci'; + $collationParts = explode('_', $col->Collation); + + /** + * If the collation is in the form charset_collationType_ci or charset_collationType we have to change + * the charset but leave the collationType intact (e.g. utf8_bin must become utf8mb4_bin, NOT + * utf8mb4_general_ci). + */ + if (count($collationParts) >= 2) + { + $ci = array_pop($collationParts); + $collationType = array_pop($collationParts); + $newCollation = $charset . '_' . $collationType . '_' . $ci; + + /** + * When the last part of the old collation is not _ci we have a charset_collationType format, + * something like utf8_bin. Therefore the new collation only has *two* parts. + */ + if ($ci != 'ci') + { + $newCollation = $charset . '_' . $ci; + } + } + + // If the old and new collation is the same we don't have to change the collation type + if (strtolower($newCollation) == strtolower($col->Collation)) + { + continue; + } + + $null = $col->Null == 'YES' ? 'NULL' : 'NOT NULL'; + $default = is_null($col->Default) ? '' : "DEFAULT '" . $this->q($col->Default) . "'"; + $columnMods[] = "MODIFY COLUMN `{$col->Field}` {$col->Type} CHARACTER SET $charset COLLATE $newCollation $null $default"; + } + } + + if (count($columnMods)) + { + $queries[] = "ALTER TABLE $quotedTableName " . + implode(',', $columnMods) . + " CHARACTER SET $charset COLLATE $collation"; + } + + return $queries; + } + + /** + * Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8. Used + * when the server doesn't support UTF-8 Multibyte. + * + * @param string $query The query to convert + * + * @return string The converted query + */ + public function convertUtf8mb4QueryToUtf8($query) + { + if ($this->hasUTF8mb4Support()) + { + return $query; + } + + // If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert + $beginningOfQuery = substr($query, 0, 12); + $beginningOfQuery = strtoupper($beginningOfQuery); + + if (!in_array($beginningOfQuery, array('ALTER TABLE ', 'CREATE TABLE'))) + { + return $query; + } + + // Replace utf8mb4 with utf8 + return str_replace('utf8mb4', 'utf8', $query); + } + + /** + * Return the query string to create new Database. + * Each database driver, other than MySQL, need to override this member to return correct string. + * + * @param stdClass $options Object used to pass user and database name to database driver. + * This object must have "db_name" and "db_user" set. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return string The query that creates database + * + * @since 12.2 + */ + protected function getCreateDatabaseQuery($options, $utf) + { + if ($utf) + { + $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8'; + + return 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' CHARACTER SET `' . $charset . '`'; + } + + return 'CREATE DATABASE ' . $this->quoteName($options->db_name); + } + + /** + * 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 11.1 + */ + abstract public function getCollation(); + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + return ''; + } + + /** + * Method that provides access to the underlying database connection. Useful for when you need to call a + * proprietary method such as postgresql's lo_* methods. + * + * @return resource The underlying database connection resource. + * + * @since 11.1 + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Get the total number of SQL statements executed by the database driver. + * + * @return integer + * + * @since 11.1 + */ + public function getCount() + { + return $this->count; + } + + /** + * Gets the name of the database used by this conneciton. + * + * @return string + * + * @since 11.4 + */ + protected function getDatabase() + { + return $this->_database; + } + + /** + * Returns a PHP date() function compliant date format for the database driver. + * + * @return string The format string. + * + * @since 11.1 + */ + public function getDateFormat() + { + return 'Y-m-d H:i:s'; + } + + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since 11.1 + */ + public function getLog() + { + return $this->log; + } + + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since CMS 3.1.2 + */ + public function getTimings() + { + return $this->timings; + } + + /** + * Get the database driver SQL statement log. + * + * @return array SQL statements executed by the database driver. + * + * @since CMS 3.1.2 + */ + public function getCallStacks() + { + return $this->callStacks; + } + + /** + * Get the minimum supported database version. + * + * @return string The minimum version number for the database driver. + * + * @since 12.1 + */ + public function getMinimum() + { + return static::$dbMinimum; + } + + /** + * Get the null or zero representation of a timestamp for the database driver. + * + * @return string Null or zero representation of a timestamp. + * + * @since 11.1 + */ + public function getNullDate() + { + return $this->nullDate; + } + + /** + * 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. + * + * @since 11.1 + */ + abstract public function getNumRows($cursor = null); + + /** + * Get the common table prefix for the database driver. + * + * @return string The common database table prefix. + * + * @since 11.1 + */ + public function getPrefix() + { + return $this->tablePrefix; + } + + /** + * Gets an exporter class object. + * + * @return FOFDatabaseExporter An exporter object. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getExporter() + { + // Derive the class name from the driver. + $class = 'FOFDatabaseExporter' . ucfirst($this->name); + + // Make sure we have an exporter class for this driver. + if (!class_exists($class)) + { + // If it doesn't exist we are at an impasse so throw an exception. + throw new RuntimeException('Database Exporter not found.'); + } + + $o = new $class; + $o->setDbo($this); + + return $o; + } + + /** + * Gets an importer class object. + * + * @return FOFDatabaseImporter An importer object. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getImporter() + { + // Derive the class name from the driver. + $class = 'FOFDatabaseImporter' . ucfirst($this->name); + + // Make sure we have an importer class for this driver. + if (!class_exists($class)) + { + // If it doesn't exist we are at an impasse so throw an exception. + throw new RuntimeException('Database Importer not found'); + } + + $o = new $class; + $o->setDbo($this); + + return $o; + } + + /** + * Get the name of the database driver. If $this->name is not set it will try guessing the driver name from the + * class name. + * + * @return string + * + * @since CMS 3.5.0 + */ + public function getName() + { + if (empty($this->name)) + { + $className = get_class($this); + $className = str_replace('FOFDatabaseDriver', '', $className); + $this->name = strtolower($className); + } + + return $this->name; + } + + /** + * Get the server family type, e.g. mysql, postgresql, oracle, sqlite, mssql. If $this->serverType is not set it + * will attempt guessing the server family type from the driver name. If this is not possible the driver name will + * be returned instead. + * + * @return string + * + * @since CMS 3.5.0 + */ + public function getServerType() + { + if (empty($this->serverType)) + { + $name = $this->getName(); + + if (stristr($name, 'mysql') !== false) + { + $this->serverType = 'mysql'; + } + elseif (stristr($name, 'postgre') !== false) + { + $this->serverType = 'postgresql'; + } + elseif (stristr($name, 'oracle') !== false) + { + $this->serverType = 'oracle'; + } + elseif (stristr($name, 'sqlite') !== false) + { + $this->serverType = 'sqlite'; + } + elseif (stristr($name, 'sqlsrv') !== false) + { + $this->serverType = 'mssql'; + } + elseif (stristr($name, 'mssql') !== false) + { + $this->serverType = 'mssql'; + } + else + { + $this->serverType = $name; + } + } + + return $this->serverType; + } + + /** + * Get the current query object or a new FOFDatabaseQuery object. + * + * @param boolean $new False to return the current query object, True to return a new FOFDatabaseQuery object. + * + * @return FOFDatabaseQuery The current query object or a new object extending the FOFDatabaseQuery class. + * + * @since 11.1 + * @throws RuntimeException + */ + public function getQuery($new = false) + { + if ($new) + { + // Derive the class name from the driver. + $class = 'FOFDatabaseQuery' . ucfirst($this->name); + + // Make sure we have a query class for this driver. + if (!class_exists($class)) + { + // If it doesn't exist we are at an impasse so throw an exception. + throw new RuntimeException('Database Query Class not found.'); + } + + return new $class($this); + } + else + { + return $this->sql; + } + } + + /** + * Get a new iterator on the current query. + * + * @param string $column An option column to use as the iterator key. + * @param string $class The class of object that is returned. + * + * @return FOFDatabaseIterator A new database iterator. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getIterator($column = null, $class = 'stdClass') + { + // Derive the class name from the driver. + $iteratorClass = 'FOFDatabaseIterator' . ucfirst($this->name); + + // Make sure we have an iterator class for this driver. + if (!class_exists($iteratorClass)) + { + // If it doesn't exist we are at an impasse so throw an exception. + throw new RuntimeException(sprintf('class *%s* is not defined', $iteratorClass)); + } + + // Return a new iterator + return new $iteratorClass($this->execute(), $column, $class); + } + + /** + * 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. + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function getTableColumns($table, $typeOnly = true); + + /** + * 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. + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function getTableCreate($tables); + + /** + * 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). + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function getTableKeys($tables); + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function getTableList(); + + /** + * Determine whether or not the database engine supports UTF-8 character encoding. + * + * @return boolean True if the database engine supports UTF-8 character encoding. + * + * @since 11.1 + * @deprecated 12.3 (Platform) & 4.0 (CMS) - Use hasUTFSupport() instead + */ + public function getUTFSupport() + { + if (class_exists('JLog')) + { + JLog::add('FOFDatabaseDriver::getUTFSupport() is deprecated. Use FOFDatabaseDriver::hasUTFSupport() instead.', JLog::WARNING, 'deprecated'); + } + + return $this->hasUTFSupport(); + } + + /** + * Determine whether or not the database engine supports UTF-8 character encoding. + * + * @return boolean True if the database engine supports UTF-8 character encoding. + * + * @since 12.1 + */ + public function hasUTFSupport() + { + return $this->utf; + } + + /** + * Determine whether the database engine support the UTF-8 Multibyte (utf8mb4) character encoding. This applies to + * MySQL databases. + * + * @return boolean True if the database engine supports UTF-8 Multibyte. + * + * @since CMS 3.5.0 + */ + public function hasUTF8mb4Support() + { + return $this->utf8mb4; + } + + /** + * Get the version of the database connector + * + * @return string The database connector version. + * + * @since 11.1 + */ + abstract public function getVersion(); + + /** + * Method to get the auto-incremented value from the last INSERT statement. + * + * @return mixed The value of the auto-increment field from the last inserted row. + * + * @since 11.1 + */ + abstract public function insertid(); + + /** + * 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. + * + * @since 11.1 + * @throws RuntimeException + */ + public function insertObject($table, &$object, $key = null) + { + $fields = array(); + $values = array(); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Only process non-null scalars. + if (is_array($v) or is_object($v) or $v === null) + { + continue; + } + + // Ignore any internal fields. + if ($k[0] == '_') + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->quote($v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + // Set the query and execute the insert. + $this->setQuery($query); + + if (!$this->execute()) + { + return false; + } + + // Update the primary key if it exists. + $id = $this->insertid(); + + if ($key && $id && is_string($key)) + { + $object->$key = $id; + } + + return true; + } + + /** + * Method to check whether the installed database version is supported by the database driver + * + * @return boolean True if the database version is supported + * + * @since 12.1 + */ + public function isMinimumVersion() + { + return version_compare($this->getVersion(), static::$dbMinimum) >= 0; + } + + /** + * Method to get the first row of the result set from the database query as an associative array + * of ['field_name' => 'row_value']. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadAssoc() + { + $this->connect(); + + $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 associative array. + if ($array = $this->fetchAssoc($cursor)) + { + $ret = $array; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an associative array + * of ['field_name' => 'row_value']. The array of rows can optionally be keyed by a field name, but defaults to + * a sequential numeric array. + * + * NOTE: Chosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * @param string $column An optional column name. Instead of the whole row, only this column value will be in + * the result array. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadAssocList($key = null, $column = null) + { + $this->connect(); + + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set. + while ($row = $this->fetchAssoc($cursor)) + { + $value = ($column) ? (isset($row[$column]) ? $row[$column] : $row) : $row; + + if ($key) + { + $array[$row[$key]] = $value; + } + else + { + $array[] = $value; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * Method to get an array of values from the $offset field in each row of the result set from + * the database query. + * + * @param integer $offset The row offset to use to build the result array. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadColumn($offset = 0) + { + $this->connect(); + + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as arrays. + while ($row = $this->fetchArray($cursor)) + { + $array[] = $row[$offset]; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * 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. + * + * @since 11.1 + * @throws RuntimeException + * @deprecated 12.3 (Platform) & 4.0 (CMS) - Use getIterator() instead + */ + public function loadNextObject($class = 'stdClass') + { + if (class_exists('JLog')) + { + JLog::add(__METHOD__ . '() is deprecated. Use FOFDatabaseDriver::getIterator() instead.', JLog::WARNING, 'deprecated'); + } + + $this->connect(); + + static $cursor = null; + + // Execute the query and get the result set cursor. + if ( is_null($cursor) ) + { + if (!($cursor = $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($cursor, $class)) + { + return $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + $cursor = null; + + 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. + * + * @since 11.1 + * @throws RuntimeException + * @deprecated 4.0 (CMS) Use FOFDatabaseDriver::getIterator() instead + */ + public function loadNextRow() + { + if (class_exists('JLog')) + { + JLog::add(__METHOD__ . '() is deprecated. Use FOFDatabaseDriver::getIterator() instead.', JLog::WARNING, 'deprecated'); + } + + $this->connect(); + + static $cursor = null; + + // Execute the query and get the result set cursor. + if ( is_null($cursor) ) + { + if (!($cursor = $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($cursor)) + { + return $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + $cursor = null; + + return false; + } + + /** + * Method to get the first row of 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 return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadObject($class = 'stdClass') + { + $this->connect(); + + $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 object of type $class. + if ($object = $this->fetchObject($cursor, $class)) + { + $ret = $object; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an object. The array + * of objects can optionally be keyed by a field name, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * @param string $class The class name to use for the returned row objects. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadObjectList($key = '', $class = 'stdClass') + { + $this->connect(); + + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as objects of type $class. + while ($row = $this->fetchObject($cursor, $class)) + { + if ($key) + { + $array[$row->$key] = $row; + } + else + { + $array[] = $row; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * 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. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadResult() + { + $this->connect(); + + $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 = $this->fetchArray($cursor)) + { + $ret = $row[0]; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get the first row of the result set from the database query as an array. Columns are indexed + * numerically so the first column in the result set would be accessible via $row[0], etc. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadRow() + { + $this->connect(); + + $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 = $this->fetchArray($cursor)) + { + $ret = $row; + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $ret; + } + + /** + * Method to get an array of the result set rows from the database query where each row is an array. The array + * of objects can optionally be keyed by a field offset, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadRowList($key = null) + { + $this->connect(); + + $array = array(); + + // Execute the query and get the result set cursor. + if (!($cursor = $this->execute())) + { + return null; + } + + // Get all of the rows from the result set as arrays. + while ($row = $this->fetchArray($cursor)) + { + if ($key !== null) + { + $array[$row[$key]] = $row; + } + else + { + $array[] = $row; + } + } + + // Free up system resources and return. + $this->freeResult($cursor); + + return $array; + } + + /** + * Locks a table in the database. + * + * @param string $tableName The name of the table to unlock. + * + * @return FOFDatabaseDriver Returns this object to support chaining. + * + * @since 11.4 + * @throws RuntimeException + */ + public abstract function lockTable($tableName); + + /** + * Quotes and optionally escapes a string to database requirements for use in database queries. + * + * @param mixed $text A string or an array of strings to quote. + * @param boolean $escape True (default) to escape the string, false to leave it unchanged. + * + * @return string The quoted input string. + * + * @note Accepting an array of strings was added in 12.3. + * @since 11.1 + */ + public function quote($text, $escape = true) + { + if (is_array($text)) + { + foreach ($text as $k => $v) + { + $text[$k] = $this->quote($v, $escape); + } + + return $text; + } + else + { + return '\'' . ($escape ? $this->escape($text) : $text) . '\''; + } + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * @param mixed $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes. + * Each type supports dot-notation name. + * @param mixed $as The AS query part associated to $name. It can be string or array, in latter case it has to be + * same length of $name; if is null there will not be any AS part for string or array element. + * + * @return mixed The quote wrapped name, same type of $name. + * + * @since 11.1 + */ + public function quoteName($name, $as = null) + { + if (is_string($name)) + { + $quotedName = $this->quoteNameStr(explode('.', $name)); + + $quotedAs = ''; + + if (!is_null($as)) + { + settype($as, 'array'); + $quotedAs .= ' AS ' . $this->quoteNameStr($as); + } + + return $quotedName . $quotedAs; + } + else + { + $fin = array(); + + if (is_null($as)) + { + foreach ($name as $str) + { + $fin[] = $this->quoteName($str); + } + } + elseif (is_array($name) && (count($name) == count($as))) + { + $count = count($name); + + for ($i = 0; $i < $count; $i++) + { + $fin[] = $this->quoteName($name[$i], $as[$i]); + } + } + + return $fin; + } + } + + /** + * Quote strings coming from quoteName call. + * + * @param array $strArr Array of strings coming from quoteName dot-explosion. + * + * @return string Dot-imploded string of quoted parts. + * + * @since 11.3 + */ + protected function quoteNameStr($strArr) + { + $parts = array(); + $q = $this->nameQuote; + + foreach ($strArr as $part) + { + if (is_null($part)) + { + continue; + } + + if (strlen($q) == 1) + { + $parts[] = $q . $part . $q; + } + else + { + $parts[] = $q{0} . $part . $q{1}; + } + } + + return implode('.', $parts); + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix 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($sql, $prefix = '#__') + { + $startPos = 0; + $literal = ''; + + $sql = trim($sql); + $n = strlen($sql); + + while ($startPos < $n) + { + $ip = strpos($sql, $prefix, $startPos); + + if ($ip === false) + { + break; + } + + $j = strpos($sql, "'", $startPos); + $k = strpos($sql, '"', $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($sql, $startPos, $j - $startPos)); + $startPos = $j; + + $j = $startPos + 1; + + if ($j >= $n) + { + break; + } + + // Quote comes first, find end of quote + while (true) + { + $k = strpos($sql, $quoteChar, $j); + $escaped = false; + + if ($k === false) + { + break; + } + + $l = $k - 1; + + while ($l >= 0 && $sql{$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($sql, $startPos, $k - $startPos + 1); + $startPos = $k + 1; + } + + if ($startPos < $n) + { + $literal .= substr($sql, $startPos, $n - $startPos); + } + + return $literal; + } + + /** + * 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 FOFDatabaseDriver Returns this object to support chaining. + * + * @since 11.4 + * @throws RuntimeException + */ + public abstract function renameTable($oldTable, $newTable, $backup = null, $prefix = null); + + /** + * 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 RuntimeException + */ + abstract public function select($database); + + /** + * Sets the database debugging state for the driver. + * + * @param boolean $level True to enable debugging. + * + * @return boolean The old debugging level. + * + * @since 11.1 + */ + public function setDebug($level) + { + $previous = $this->debug; + $this->debug = (bool) $level; + + return $previous; + } + + /** + * Sets the SQL statement string for later execution. + * + * @param mixed $query The SQL statement to set either as a FOFDatabaseQuery object or a string. + * @param integer $offset The affected row offset to set. + * @param integer $limit The maximum affected rows to set. + * + * @return FOFDatabaseDriver This object to support method chaining. + * + * @since 11.1 + */ + public function setQuery($query, $offset = 0, $limit = 0) + { + $this->sql = $query; + + if ($query instanceof FOFDatabaseQueryLimitable) + { + if (!$limit && $query->limit) + { + $limit = $query->limit; + } + + if (!$offset && $query->offset) + { + $offset = $query->offset; + } + + $query->setLimit($limit, $offset); + } + else + { + $this->limit = (int) max(0, $limit); + $this->offset = (int) max(0, $offset); + } + + return $this; + } + + /** + * Set the connection to use UTF-8 character encoding. + * + * @return boolean True on success. + * + * @since 11.1 + */ + abstract public function setUtf(); + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function transactionCommit($toSavepoint = false); + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function transactionRollback($toSavepoint = false); + + /** + * Method to initialize a transaction. + * + * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created. + * + * @return void + * + * @since 11.1 + * @throws RuntimeException + */ + abstract public function transactionStart($asSavepoint = false); + + /** + * Method to truncate a table. + * + * @param string $table The table to truncate + * + * @return void + * + * @since 11.3 + * @throws RuntimeException + */ + public function truncateTable($table) + { + $this->setQuery('TRUNCATE TABLE ' . $this->quoteName($table)); + $this->execute(); + } + + /** + * Updates a row in a table based on an object's properties. + * + * @param string $table The name of the database table to update. + * @param object &$object A reference to an object whose public properties match the table fields. + * @param array $key The name of the primary key. + * @param boolean $nulls True to update null fields or false to ignore them. + * + * @return boolean True on success. + * + * @since 11.1 + * @throws RuntimeException + */ + public function updateObject($table, &$object, $key, $nulls = false) + { + $fields = array(); + $where = array(); + + if (is_string($key)) + { + $key = array($key); + } + + if (is_object($key)) + { + $key = (array) $key; + } + + // Create the base update statement. + $statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s'; + + // Iterate over the object variables to build the query fields/value pairs. + foreach (get_object_vars($object) as $k => $v) + { + // Only process scalars that are not internal fields. + if (is_array($v) or is_object($v) or $k[0] == '_') + { + continue; + } + + // Set the primary key to the WHERE clause instead of a field to update. + if (in_array($k, $key)) + { + $where[] = $this->quoteName($k) . '=' . $this->quote($v); + continue; + } + + // Prepare and sanitize the fields and values for the database query. + if ($v === null) + { + // If the value is null and we want to update nulls then set it. + if ($nulls) + { + $val = 'NULL'; + } + // If the value is null and we do not want to update nulls then ignore this field. + else + { + continue; + } + } + // The field is not null so we prep it for update. + else + { + $val = $this->quote($v); + } + + // Add the field to be updated. + $fields[] = $this->quoteName($k) . '=' . $val; + } + + // We don't have any fields to update. + if (empty($fields)) + { + return true; + } + + // Set the query and execute the update. + $this->setQuery(sprintf($statement, implode(",", $fields), implode(' AND ', $where))); + + return $this->execute(); + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @since 12.1 + * @throws RuntimeException + */ + abstract public function execute(); + + /** + * Unlocks tables in the database. + * + * @return FOFDatabaseDriver Returns this object to support chaining. + * + * @since 11.4 + * @throws RuntimeException + */ + public abstract function unlockTables(); +} diff --git a/deployed/akeeba/libraries/fof/database/driver/joomla.php b/deployed/akeeba/libraries/fof/database/driver/joomla.php new file mode 100644 index 00000000..63b67627 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/joomla.php @@ -0,0 +1,778 @@ +dbo = JFactory::getDbo(); + + $reflection = new ReflectionClass($this->dbo); + + try + { + $refProp = $reflection->getProperty('nameQuote'); + $refProp->setAccessible(true); + $this->nameQuote = $refProp->getValue($this->dbo); + } + catch (Exception $e) + { + $this->nameQuote = '`'; + } + } + + public function close() + { + if (method_exists($this->dbo, 'close')) + { + $this->dbo->close(); + } + elseif (method_exists($this->dbo, 'disconnect')) + { + $this->dbo->disconnect(); + } + } + + public function disconnect() + { + $this->close(); + } + + public function open() + { + if (method_exists($this->dbo, 'open')) + { + $this->dbo->open(); + } + elseif (method_exists($this->dbo, 'connect')) + { + $this->dbo->connect(); + } + } + + public function connect() + { + $this->open(); + } + + public function connected() + { + if (method_exists($this->dbo, 'connected')) + { + return $this->dbo->connected(); + } + + return true; + } + + public function escape($text, $extra = false) + { + return $this->dbo->escape($text, $extra); + } + + public function execute() + { + if (method_exists($this->dbo, 'execute')) + { + return $this->dbo->execute(); + } + + return $this->dbo->query(); + } + + public function getAffectedRows() + { + if (method_exists($this->dbo, 'getAffectedRows')) + { + return $this->dbo->getAffectedRows(); + } + + return 0; + } + + public function getCollation() + { + if (method_exists($this->dbo, 'getCollation')) + { + return $this->dbo->getCollation(); + } + + return 'utf8_general_ci'; + } + + public function getConnection() + { + if (method_exists($this->dbo, 'getConnection')) + { + return $this->dbo->getConnection(); + } + + return null; + } + + public function getCount() + { + if (method_exists($this->dbo, 'getCount')) + { + return $this->dbo->getCount(); + } + + return 0; + } + + public function getDateFormat() + { + if (method_exists($this->dbo, 'getDateFormat')) + { + return $this->dbo->getDateFormat(); + } + + return 'Y-m-d H:i:s';; + } + + public function getMinimum() + { + if (method_exists($this->dbo, 'getMinimum')) + { + return $this->dbo->getMinimum(); + } + + return '5.0.40'; + } + + public function getNullDate() + { + if (method_exists($this->dbo, 'getNullDate')) + { + return $this->dbo->getNullDate(); + } + + return '0000-00-00 00:00:00'; + } + + public function getNumRows($cursor = null) + { + if (method_exists($this->dbo, 'getNumRows')) + { + return $this->dbo->getNumRows($cursor); + } + + return 0; + } + + public function getQuery($new = false) + { + if (method_exists($this->dbo, 'getQuery')) + { + return $this->dbo->getQuery($new); + } + + return null; + } + + public function getTableColumns($table, $typeOnly = true) + { + if (method_exists($this->dbo, 'getTableColumns')) + { + return $this->dbo->getTableColumns($table, $typeOnly); + } + + $result = $this->dbo->getTableFields(array($table), $typeOnly); + + return $result[$table]; + } + + public function getTableKeys($tables) + { + if (method_exists($this->dbo, 'getTableKeys')) + { + return $this->dbo->getTableKeys($tables); + } + + return array(); + } + + public function getTableList() + { + if (method_exists($this->dbo, 'getTableList')) + { + return $this->dbo->getTableList(); + } + + return array(); + } + + public function getVersion() + { + if (method_exists($this->dbo, 'getVersion')) + { + return $this->dbo->getVersion(); + } + + return '5.0.40'; + } + + public function insertid() + { + if (method_exists($this->dbo, 'insertid')) + { + return $this->dbo->insertid(); + } + + return null; + } + + public function insertObject($table, &$object, $key = null) + { + if (method_exists($this->dbo, 'insertObject')) + { + return $this->dbo->insertObject($table, $object, $key); + } + + return null; + } + + public function loadAssoc() + { + if (method_exists($this->dbo, 'loadAssoc')) + { + return $this->dbo->loadAssoc(); + } + + return null; + } + + public function loadAssocList($key = null, $column = null) + { + if (method_exists($this->dbo, 'loadAssocList')) + { + return $this->dbo->loadAssocList($key, $column); + } + + return null; + } + + public function loadObject($class = 'stdClass') + { + if (method_exists($this->dbo, 'loadObject')) + { + return $this->dbo->loadObject($class); + } + + return null; + } + + public function loadObjectList($key = '', $class = 'stdClass') + { + if (method_exists($this->dbo, 'loadObjectList')) + { + return $this->dbo->loadObjectList($key, $class); + } + + return null; + } + + public function loadResult() + { + if (method_exists($this->dbo, 'loadResult')) + { + return $this->dbo->loadResult(); + } + + return null; + } + + public function loadRow() + { + if (method_exists($this->dbo, 'loadRow')) + { + return $this->dbo->loadRow(); + } + + return null; + } + + public function loadRowList($key = null) + { + if (method_exists($this->dbo, 'loadRowList')) + { + return $this->dbo->loadRowList($key); + } + + return null; + } + + public function lockTable($tableName) + { + if (method_exists($this->dbo, 'lockTable')) + { + return $this->dbo->lockTable($this); + } + + return $this; + } + + public function quote($text, $escape = true) + { + if (method_exists($this->dbo, 'quote')) + { + return $this->dbo->quote($text, $escape); + } + + return $text; + } + + public function select($database) + { + if (method_exists($this->dbo, 'select')) + { + return $this->dbo->select($database); + } + + return false; + } + + public function setQuery($query, $offset = 0, $limit = 0) + { + if (method_exists($this->dbo, 'setQuery')) + { + return $this->dbo->setQuery($query, $offset, $limit); + } + + return false; + } + + public function transactionCommit($toSavepoint = false) + { + if (method_exists($this->dbo, 'transactionCommit')) + { + $this->dbo->transactionCommit($toSavepoint); + } + } + + public function transactionRollback($toSavepoint = false) + { + if (method_exists($this->dbo, 'transactionRollback')) + { + $this->dbo->transactionRollback($toSavepoint); + } + } + + public function transactionStart($asSavepoint = false) + { + if (method_exists($this->dbo, 'transactionStart')) + { + $this->dbo->transactionStart($asSavepoint); + } + } + + public function unlockTables() + { + if (method_exists($this->dbo, 'unlockTables')) + { + return $this->dbo->unlockTables(); + } + + return $this; + } + + public function updateObject($table, &$object, $key, $nulls = false) + { + if (method_exists($this->dbo, 'updateObject')) + { + return $this->dbo->updateObject($table, $object, $key, $nulls); + } + + return false; + } + + public function getLog() + { + if (method_exists($this->dbo, 'getLog')) + { + return $this->dbo->getLog(); + } + + return array(); + } + + public function dropTable($table, $ifExists = true) + { + if (method_exists($this->dbo, 'dropTable')) + { + return $this->dbo->dropTable($table, $ifExists); + } + + return $this; + } + + public function getTableCreate($tables) + { + if (method_exists($this->dbo, 'getTableCreate')) + { + return $this->dbo->getTableCreate($tables); + } + + return array(); + } + + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + if (method_exists($this->dbo, 'renameTable')) + { + return $this->dbo->renameTable($oldTable, $newTable, $backup, $prefix); + } + + return $this; + } + + public function setUtf() + { + if (method_exists($this->dbo, 'setUtf')) + { + return $this->dbo->setUtf(); + } + + return false; + } + + + protected function freeResult($cursor = null) + { + return false; + } + + /** + * Method to get an array of values from the $offset field in each row of the result set from + * the database query. + * + * @param integer $offset The row offset to use to build the result array. + * + * @return mixed The return value or null if the query failed. + * + * @since 11.1 + * @throws RuntimeException + */ + public function loadColumn($offset = 0) + { + if (method_exists($this->dbo, 'loadColumn')) + { + return $this->dbo->loadColumn($offset); + } + + return $this->dbo->loadResultArray($offset); + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * @param mixed $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes. + * Each type supports dot-notation name. + * @param mixed $as The AS query part associated to $name. It can be string or array, in latter case it has to be + * same length of $name; if is null there will not be any AS part for string or array element. + * + * @return mixed The quote wrapped name, same type of $name. + * + * @since 11.1 + */ + public function quoteName($name, $as = null) + { + if (is_string($name)) + { + $quotedName = $this->quoteNameStr(explode('.', $name)); + + $quotedAs = ''; + + if (!is_null($as)) + { + settype($as, 'array'); + $quotedAs .= ' AS ' . $this->quoteNameStr($as); + } + + return $quotedName . $quotedAs; + } + else + { + $fin = array(); + + if (is_null($as)) + { + foreach ($name as $str) + { + $fin[] = $this->quoteName($str); + } + } + elseif (is_array($name) && (count($name) == count($as))) + { + $count = count($name); + + for ($i = 0; $i < $count; $i++) + { + $fin[] = $this->quoteName($name[$i], $as[$i]); + } + } + + return $fin; + } + } + + /** + * Quote strings coming from quoteName call. + * + * @param array $strArr Array of strings coming from quoteName dot-explosion. + * + * @return string Dot-imploded string of quoted parts. + * + * @since 11.3 + */ + protected function quoteNameStr($strArr) + { + $parts = array(); + $q = $this->nameQuote; + + foreach ($strArr as $part) + { + if (is_null($part)) + { + continue; + } + + if (strlen($q) == 1) + { + $parts[] = $q . $part . $q; + } + else + { + $parts[] = $q{0} . $part . $q{1}; + } + } + + return implode('.', $parts); + } + + /** + * Gets the error message from the database connection. + * + * @param boolean $escaped True to escape the message string for use in JavaScript. + * + * @return string The error message for the most recent query. + * + * @since 11.1 + */ + public function getErrorMsg($escaped = false) + { + if (method_exists($this->dbo, 'getErrorMsg')) + { + $errorMessage = $this->dbo->getErrorMsg(); + } + else + { + $errorMessage = $this->errorMsg; + } + + if ($escaped) + { + return addslashes($errorMessage); + } + + return $errorMessage; + } + + /** + * Gets the error number from the database connection. + * + * @return integer The error number for the most recent query. + * + * @since 11.1 + * @deprecated 13.3 (Platform) & 4.0 (CMS) + */ + public function getErrorNum() + { + if (method_exists($this->dbo, 'getErrorNum')) + { + $errorNum = $this->dbo->getErrorNum(); + } + else + { + $errorNum = $this->getErrorNum; + } + + return $errorNum; + } + + /** + * Return the most recent error message for the database connector. + * + * @param boolean $showSQL True to display the SQL statement sent to the database as well as the error. + * + * @return string The error message for the most recent query. + */ + public function stderr($showSQL = false) + { + if (method_exists($this->dbo, 'stderr')) + { + return $this->dbo->stderr($showSQL); + } + + return parent::stderr($showSQL); + } + + /** + * Magic method to proxy all calls to the loaded database driver object + */ + public function __call($name, array $arguments) + { + if (is_null($this->dbo)) + { + throw new Exception('FOF database driver is not loaded'); + } + + if (method_exists($this->dbo, $name) || in_array($name, array('q', 'nq', 'qn', 'query'))) + { + switch ($name) + { + case 'execute': + $name = 'query'; + break; + + case 'q': + $name = 'quote'; + break; + + case 'qn': + case 'nq': + switch (count($arguments)) + { + case 0 : + $result = $this->quoteName(); + break; + case 1 : + $result = $this->quoteName($arguments[0]); + break; + case 2: + default: + $result = $this->quoteName($arguments[0], $arguments[1]); + break; + } + return $result; + + break; + } + + switch (count($arguments)) + { + case 0 : + $result = $this->dbo->$name(); + break; + case 1 : + $result = $this->dbo->$name($arguments[0]); + break; + case 2: + $result = $this->dbo->$name($arguments[0], $arguments[1]); + break; + case 3: + $result = $this->dbo->$name($arguments[0], $arguments[1], $arguments[2]); + break; + case 4: + $result = $this->dbo->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + break; + case 5: + $result = $this->dbo->$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($this->dbo, $name), $arguments); + } + + if (class_exists('JDatabase') && is_object($result) && ($result instanceof JDatabase)) + { + return $this; + } + + return $result; + } + else + { + throw new \Exception('Method ' . $name . ' not found in FOFDatabase'); + } + } + + public function __get($name) + { + if (isset($this->dbo->$name) || property_exists($this->dbo, $name)) + { + return $this->dbo->$name; + } + else + { + $this->dbo->$name = null; + user_error('Database driver does not support property ' . $name); + } + } + + public function __set($name, $value) + { + if (isset($this->dbo->name) || property_exists($this->dbo, $name)) + { + $this->dbo->$name = $value; + } + else + { + $this->dbo->$name = null; + user_error('Database driver not support property ' . $name); + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/mysql.php b/deployed/akeeba/libraries/fof/database/driver/mysql.php new file mode 100644 index 00000000..5e35879f --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/mysql.php @@ -0,0 +1,577 @@ += 7) + { + throw new RuntimeException( + 'This driver is unsupported in PHP 7, please use the MySQLi or PDO MySQL driver instead.' + ); + } + + // Get some basic values from the options. + $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost'; + $options['user'] = (isset($options['user'])) ? $options['user'] : 'root'; + $options['password'] = (isset($options['password'])) ? $options['password'] : ''; + $options['database'] = (isset($options['database'])) ? $options['database'] : ''; + $options['select'] = (isset($options['select'])) ? (bool) $options['select'] : true; + + // Finalize initialisation. + parent::__construct($options); + } + + /** + * Destructor. + * + * @since 12.1 + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + // Make sure the MySQL extension for PHP is installed and enabled. + if (!self::isSupported()) + { + throw new RuntimeException('Could not connect to MySQL.'); + } + + // Attempt to connect to the server. + if (!($this->connection = @ mysql_connect($this->options['host'], $this->options['user'], $this->options['password'], true))) + { + throw new RuntimeException('Could not connect to MySQL.'); + } + + // Set sql_mode to non_strict mode + mysql_query("SET @@SESSION.sql_mode = '';", $this->connection); + + // If auto-select is enabled select the given database. + if ($this->options['select'] && !empty($this->options['database'])) + { + $this->select($this->options['database']); + } + + // Pre-populate the UTF-8 Multibyte compatibility flag based on server version + $this->utf8mb4 = $this->serverClaimsUtf8mb4Support(); + + // Set the character set (needed for MySQL 4.1.2+). + $this->utf = $this->setUtf(); + + // Turn MySQL profiling ON in debug mode: + if ($this->debug && $this->hasProfiling()) + { + mysql_query("SET profiling = 1;", $this->connection); + } + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + // Close the connection. + if (is_resource($this->connection)) + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + mysql_close($this->connection); + } + + $this->connection = null; + } + + /** + * 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. + * + * @since 12.1 + */ + public function escape($text, $extra = false) + { + $this->connect(); + + $result = mysql_real_escape_string($text, $this->getConnection()); + + if ($extra) + { + $result = addcslashes($result, '%_'); + } + + return $result; + } + + /** + * Test to see if the MySQL connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function isSupported() + { + return (function_exists('mysql_connect')); + } + + /** + * Determines if the connection to the server is active. + * + * @return boolean True if connected to the database engine. + * + * @since 12.1 + */ + public function connected() + { + if (is_resource($this->connection)) + { + return @mysql_ping($this->connection); + } + + return false; + } + + /** + * Get the number of affected rows by the last INSERT, UPDATE, REPLACE or DELETE for the previous executed SQL statement. + * + * @return integer The number of affected rows. + * + * @since 12.1 + */ + public function getAffectedRows() + { + $this->connect(); + + return mysql_affected_rows($this->connection); + } + + /** + * Get the number of returned rows for the previous executed SQL statement. + * This command is only valid for statements like SELECT or SHOW that return an actual result set. + * To retrieve the number of rows affected by a INSERT, UPDATE, REPLACE or DELETE query, use getAffectedRows(). + * + * @param resource $cursor An optional database cursor resource to extract the row count from. + * + * @return integer The number of returned rows. + * + * @since 12.1 + */ + public function getNumRows($cursor = null) + { + $this->connect(); + + return mysql_num_rows($cursor ? $cursor : $this->cursor); + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + + return mysql_get_server_info($this->connection); + } + + /** + * 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. + * + * @since 12.1 + */ + public function insertid() + { + $this->connect(); + + return mysql_insert_id($this->connection); + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @since 12.1 + * @throws RuntimeException + */ + public function execute() + { + $this->connect(); + + if (!is_resource($this->connection)) + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database'); + } + 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->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0)) + { + $query .= ' LIMIT ' . $this->offset . ', ' . $this->limit; + } + + // Increment the query counter. + $this->count++; + + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + + // If debugging is enabled then let's log the query. + if ($this->debug) + { + // Add the query to the object queue. + $this->log[] = $query; + + if (class_exists('JLog')) + { + JLog::add($query, JLog::DEBUG, 'databasequery'); + } + + $this->timings[] = microtime(true); + } + + // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. + $this->cursor = @mysql_query($query, $this->connection); + + if ($this->debug) + { + $this->timings[] = microtime(true); + + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) + { + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + else + { + $this->callStacks[] = debug_backtrace(); + } + } + + // If an error occurred handle it. + if (!$this->cursor) + { + // Get the error number and message before we execute any more queries. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Check if the server was disconnected. + if (!$this->connected()) + { + try + { + // Attempt to reconnect. + $this->connection = null; + $this->connect(); + } + // If connect fails, ignore that exception and throw the normal exception. + catch (RuntimeException $e) + { + // Get the error number and message. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum, $e); + } + + // Since we were able to reconnect, run the query again. + return $this->execute(); + } + // The server was not disconnected. + else + { + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + if (!$database) + { + return false; + } + + if (!mysql_select_db($database, $this->connection)) + { + throw new RuntimeException('Could not connect to database'); + } + + return true; + } + + /** + * Set the connection to use UTF-8 character encoding. + * + * @return boolean True on success. + * + * @since 12.1 + */ + public function setUtf() + { + // If UTF is not supported return false immediately + if (!$this->utf) + { + return false; + } + + // Make sure we're connected to the server + $this->connect(); + + // Which charset should I use, plain utf8 or multibyte utf8mb4? + $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8'; + + $result = @mysql_set_charset($charset, $this->connection); + + /** + * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. + * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd + * masks the server version and reports only its own we can not be sure if the server actually does support + * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we + * catch the error and determine that utf8mb4 is not supported! + */ + if (!$result && $this->utf8mb4) + { + $this->utf8mb4 = false; + $result = @mysql_set_charset('utf8', $this->connection); + } + + 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. + * + * @since 12.1 + */ + protected function fetchArray($cursor = null) + { + return mysql_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. + * + * @since 12.1 + */ + protected function fetchAssoc($cursor = null) + { + return mysql_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. + * + * @since 12.1 + */ + protected function fetchObject($cursor = null, $class = 'stdClass') + { + return mysql_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 + * + * @since 12.1 + */ + protected function freeResult($cursor = null) + { + mysql_free_result($cursor ? $cursor : $this->cursor); + } + + /** + * Internal function to check if profiling is available + * + * @return boolean + * + * @since 3.1.3 + */ + private function hasProfiling() + { + try + { + $res = mysql_query("SHOW VARIABLES LIKE 'have_profiling'", $this->connection); + $row = mysql_fetch_assoc($res); + + return isset($row); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Does the database server claim to have support for UTF-8 Multibyte (utf8mb4) collation? + * + * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9. + * + * @return boolean + * + * @since CMS 3.5.0 + */ + private function serverClaimsUtf8mb4Support() + { + $client_version = mysql_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', '>='); + } + } + + /** + * Return the actual SQL Error number + * + * @return integer The SQL Error number + * + * @since 3.4.6 + */ + protected function getErrorNumber() + { + return (int) mysql_errno($this->connection); + } + + /** + * Return the actual SQL Error message + * + * @param string $query The SQL Query that fails + * + * @return string The SQL Error message + * + * @since 3.4.6 + */ + protected function getErrorMessage($query) + { + $errorMessage = (string) mysql_error($this->connection); + + // Replace the Databaseprefix with `#__` if we are not in Debug + if (!$this->debug) + { + $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage); + $query = str_replace($this->tablePrefix, '#__', $query); + } + + return $errorMessage . ' SQL=' . $query; + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/mysqli.php b/deployed/akeeba/libraries/fof/database/driver/mysqli.php new file mode 100644 index 00000000..bc271f1c --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/mysqli.php @@ -0,0 +1,1019 @@ +disconnect(); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + /* + * Unlike mysql_connect(), mysqli_connect() takes the port and socket as separate arguments. Therefore, we + * have to extract them from the host string. + */ + $port = isset($this->options['port']) ? $this->options['port'] : 3306; + $regex = '/^(?P((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.+))?$/'; + + if (preg_match($regex, $this->options['host'], $matches)) + { + // It's an IPv4 address with ot without port + $this->options['host'] = $matches['host']; + + if (!empty($matches['port'])) + { + $port = $matches['port']; + } + } + elseif (preg_match('/^(?P\[.*\])(:(?P.+))?$/', $this->options['host'], $matches)) + { + // We assume square-bracketed IPv6 address with or without port, e.g. [fe80:102::2%eth1]:3306 + $this->options['host'] = $matches['host']; + + if (!empty($matches['port'])) + { + $port = $matches['port']; + } + } + elseif (preg_match('/^(?P(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P[^:]+))?$/i', $this->options['host'], $matches)) + { + // Named host (e.g domain.com or localhost) with ot without port + $this->options['host'] = $matches['host']; + + if (!empty($matches['port'])) + { + $port = $matches['port']; + } + } + elseif (preg_match('/^:(?P[^:]+)$/', $this->options['host'], $matches)) + { + // Empty host, just port, e.g. ':3306' + $this->options['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)) + { + $this->options['port'] = (int) $port; + } + else + { + $this->options['socket'] = $port; + } + + // Make sure the MySQLi extension for PHP is installed and enabled. + if (!function_exists('mysqli_connect')) + { + throw new RuntimeException('The MySQL adapter mysqli is not available'); + } + + $this->connection = @mysqli_connect( + $this->options['host'], $this->options['user'], $this->options['password'], null, $this->options['port'], $this->options['socket'] + ); + + // Attempt to connect to the server. + if (!$this->connection) + { + throw new RuntimeException('Could not connect to MySQL.'); + } + + // Set sql_mode to non_strict mode + mysqli_query($this->connection, "SET @@SESSION.sql_mode = '';"); + + // If auto-select is enabled select the given database. + if ($this->options['select'] && !empty($this->options['database'])) + { + $this->select($this->options['database']); + } + + // Pre-populate the UTF-8 Multibyte compatibility flag based on server version + $this->utf8mb4 = $this->serverClaimsUtf8mb4Support(); + + // Set the character set (needed for MySQL 4.1.2+). + $this->utf = $this->setUtf(); + + // Turn MySQL profiling ON in debug mode: + if ($this->debug && $this->hasProfiling()) + { + mysqli_query($this->connection, "SET profiling_history_size = 100;"); + mysqli_query($this->connection, "SET profiling = 1;"); + } + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + // Close the connection. + if ($this->connection) + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + mysqli_close($this->connection); + } + + $this->connection = null; + } + + /** + * 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. + * + * @since 12.1 + */ + public function escape($text, $extra = false) + { + $this->connect(); + + $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. + * + * @since 12.1 + */ + 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. + * + * @since 12.1 + */ + public function connected() + { + if (is_object($this->connection)) + { + return mysqli_ping($this->connection); + } + + return false; + } + + /** + * 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 FOFDatabaseDriverMysqli Returns this object to support chaining. + * + * @since 12.2 + * @throws RuntimeException + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $query = $this->getQuery(true); + + $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($tableName)); + + $this->execute(); + + return $this; + } + + /** + * Get the number of affected rows by the last INSERT, UPDATE, REPLACE or DELETE for the previous executed SQL statement. + * + * @return integer The number of affected rows. + * + * @since 12.1 + */ + public function getAffectedRows() + { + $this->connect(); + + return mysqli_affected_rows($this->connection); + } + + /** + * Method to get the database collation. + * + * @return mixed The collation in use by the database (string) or boolean false if not supported. + * + * @since 12.2 + * @throws RuntimeException + */ + public function getCollation() + { + $this->connect(); + + // Attempt to get the database collation by accessing the server system variable. + $this->setQuery('SHOW VARIABLES LIKE "collation_database"'); + $result = $this->loadObject(); + + if (property_exists($result, 'Value')) + { + return $result->Value; + } + else + { + return false; + } + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + $this->connect(); + + // Attempt to get the database collation by accessing the server system variable. + $this->setQuery('SHOW VARIABLES LIKE "collation_connection"'); + $result = $this->loadObject(); + + if (property_exists($result, 'Value')) + { + return $result->Value; + } + else + { + return false; + } + } + + /** + * Get the number of returned rows for the previous executed SQL statement. + * This command is only valid for statements like SELECT or SHOW that return an actual result set. + * To retrieve the number of rows affected by a INSERT, UPDATE, REPLACE or DELETE query, use getAffectedRows(). + * + * @param resource $cursor An optional database cursor resource to extract the row count from. + * + * @return integer The number of returned rows. + * + * @since 12.1 + */ + public function getNumRows($cursor = null) + { + return mysqli_num_rows($cursor ? $cursor : $this->cursor); + } + + /** + * 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableCreate($tables) + { + $this->connect(); + + $result = array(); + + // Sanitize input to an array and iterate over the list. + settype($tables, 'array'); + + foreach ($tables as $table) + { + // Set the query to get the table CREATE statement. + $this->setQuery('SHOW CREATE table ' . $this->quoteName($this->escape($table))); + $row = $this->loadRow(); + + // Populate the result array based on the create statements. + $result[$table] = $row[1]; + } + + return $result; + } + + /** + * 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 12.2 + * @throws RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->connect(); + + $result = array(); + + // Set the query to get the table fields statement. + $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table))); + $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; + } + + /** + * 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 12.2 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + // Get the details columns information. + $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table)); + $keys = $this->loadObjectList(); + + return $keys; + } + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + * + * @since 12.2 + * @throws RuntimeException + */ + public function getTableList() + { + $this->connect(); + + // Set the query to get the tables statement. + $this->setQuery('SHOW TABLES'); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + + return mysqli_get_server_info($this->connection); + } + + /** + * Method to get the auto-incremented value from the last INSERT statement. + * + * @return mixed The value of the auto-increment field from the last inserted row. + * If the value is greater than maximal int value, it will return a string. + * + * @since 12.1 + */ + public function insertid() + { + $this->connect(); + + return mysqli_insert_id($this->connection); + } + + /** + * Locks a table in the database. + * + * @param string $table The name of the table to unlock. + * + * @return FOFDatabaseDriverMysqli Returns this object to support chaining. + * + * @since 12.2 + * @throws RuntimeException + */ + public function lockTable($table) + { + $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->execute(); + + return $this; + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @since 12.1 + * @throws RuntimeException + */ + public function execute() + { + $this->connect(); + + if (!is_object($this->connection)) + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database'); + } + 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->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0)) + { + $query .= ' LIMIT ' . $this->offset . ', ' . $this->limit; + } + + // Increment the query counter. + $this->count++; + + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + $memoryBefore = null; + + // If debugging is enabled then let's log the query. + if ($this->debug) + { + // Add the query to the object queue. + $this->log[] = $query; + + if (class_exists('JLog')) + { + JLog::add($query, JLog::DEBUG, 'databasequery'); + } + + $this->timings[] = microtime(true); + + if (is_object($this->cursor)) + { + // Avoid warning if result already freed by third-party library + @$this->freeResult(); + } + + $memoryBefore = memory_get_usage(); + } + + // 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 ($this->debug) + { + $this->timings[] = microtime(true); + + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) + { + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + else + { + $this->callStacks[] = debug_backtrace(); + } + + $this->callStacks[count($this->callStacks) - 1][0]['memory'] = array( + $memoryBefore, + memory_get_usage(), + is_object($this->cursor) ? $this->getNumRows() : null + ); + } + + // If an error occurred handle it. + if (!$this->cursor) + { + // Get the error number and message before we execute any more queries. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Check if the server was disconnected. + if (!$this->connected()) + { + try + { + // Attempt to reconnect. + $this->connection = null; + $this->connect(); + } + // If connect fails, ignore that exception and throw the normal exception. + catch (RuntimeException $e) + { + // Get the error number and message. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum, $e); + } + + // Since we were able to reconnect, run the query again. + return $this->execute(); + } + // The server was not disconnected. + else + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum); + } + } + + return $this->cursor; + } + + /** + * 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 MySQL. + * @param string $prefix Not used by MySQL. + * + * @return FOFDatabaseDriverMysqli Returns this object to support chaining. + * + * @since 12.2 + * @throws RuntimeException + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->setQuery('RENAME TABLE ' . $oldTable . ' TO ' . $newTable)->execute(); + + 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + if (!$database) + { + return false; + } + + if (!mysqli_select_db($this->connection, $database)) + { + throw new RuntimeException('Could not connect to database.'); + } + + return true; + } + + /** + * Set the connection to use UTF-8 character encoding. + * + * @return boolean True on success. + * + * @since 12.1 + */ + public function setUtf() + { + // If UTF is not supported return false immediately + if (!$this->utf) + { + return false; + } + + // Make sure we're connected to the server + $this->connect(); + + // Which charset should I use, plain utf8 or multibyte utf8mb4? + $charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8'; + + $result = @$this->connection->set_charset($charset); + + /** + * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. + * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd + * masks the server version and reports only its own we can not be sure if the server actually does support + * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we + * catch the error and determine that utf8mb4 is not supported! + */ + if (!$result && $this->utf8mb4) + { + $this->utf8mb4 = false; + $result = @$this->connection->set_charset('utf8'); + } + + return $result; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.2 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('COMMIT')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $this->transactionDepth--; + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.2 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('ROLLBACK')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $savepoint = 'SP_' . ($this->transactionDepth - 1); + $this->setQuery('ROLLBACK TO SAVEPOINT ' . $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 12.2 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + if ($this->setQuery('START TRANSACTION')->execute()) + { + $this->transactionDepth = 1; + } + + return; + } + + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } + + /** + * 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. + * + * @since 12.1 + */ + 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. + * + * @since 12.1 + */ + protected 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. + * + * @since 12.1 + */ + 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 + * + * @since 12.1 + */ + protected function freeResult($cursor = null) + { + mysqli_free_result($cursor ? $cursor : $this->cursor); + + if ((! $cursor) || ($cursor === $this->cursor)) + { + $this->cursor = null; + } + } + + /** + * Unlocks tables in the database. + * + * @return FOFDatabaseDriverMysqli Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function unlockTables() + { + $this->setQuery('UNLOCK TABLES')->execute(); + + return $this; + } + + /** + * Internal function to check if profiling is available + * + * @return boolean + * + * @since 3.1.3 + */ + private function hasProfiling() + { + try + { + $res = mysqli_query($this->connection, "SHOW VARIABLES LIKE 'have_profiling'"); + $row = mysqli_fetch_assoc($res); + + return isset($row); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Does the database server claim to have support for UTF-8 Multibyte (utf8mb4) collation? + * + * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9. + * + * @return boolean + * + * @since CMS 3.5.0 + */ + private function serverClaimsUtf8mb4Support() + { + $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', '>='); + } + } + + /** + * Return the actual SQL Error number + * + * @return integer The SQL Error number + * + * @since 3.4.6 + */ + protected function getErrorNumber() + { + return (int) mysqli_errno($this->connection); + } + + /** + * Return the actual SQL Error message + * + * @param string $query The SQL Query that fails + * + * @return string The SQL Error message + * + * @since 3.4.6 + */ + protected function getErrorMessage($query) + { + $errorMessage = (string) mysqli_error($this->connection); + + // Replace the Databaseprefix with `#__` if we are not in Debug + if (!$this->debug) + { + $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage); + $query = str_replace($this->tablePrefix, '#__', $query); + } + + return $errorMessage . ' SQL=' . $query; + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/oracle.php b/deployed/akeeba/libraries/fof/database/driver/oracle.php new file mode 100644 index 00000000..155246e5 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/oracle.php @@ -0,0 +1,712 @@ +charset = $options['charset']; + $this->dateformat = $options['dateformat']; + + // Finalize initialisation + parent::__construct($options); + } + + /** + * Destructor. + * + * @since 12.1 + */ + public function __destruct() + { + $this->freeResult(); + unset($this->connection); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + parent::connect(); + + if (isset($this->options['schema'])) + { + $this->setQuery('ALTER SESSION SET CURRENT_SCHEMA = ' . $this->quoteName($this->options['schema']))->execute(); + } + + $this->setDateFormat($this->dateformat); + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + // Close the connection. + $this->freeResult(); + unset($this->connection); + } + + /** + * Drops a table from the database. + * + * Note: The IF EXISTS flag is unused in the Oracle driver. + * + * @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 FOFDatabaseDriverOracle Returns this object to support chaining. + * + * @since 12.1 + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $query = $this->getQuery(true) + ->setQuery('DROP TABLE :tableName'); + $query->bind(':tableName', $tableName); + + $this->setQuery($query); + + $this->execute(); + + return $this; + } + + /** + * 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 12.1 + */ + public function getCollation() + { + return $this->charset; + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + return $this->charset; + } + + /** + * Get a query to run and verify the database is operational. + * + * @return string The query to check the health of the DB. + * + * @since 12.2 + */ + public function getConnectedQuery() + { + return 'SELECT 1 FROM dual'; + } + + /** + * Returns the current date format + * This method should be useful in the case that + * somebody actually wants to use a different + * date format and needs to check what the current + * one is to see if it needs to be changed. + * + * @return string The current date format + * + * @since 12.1 + */ + public function getDateFormat() + { + return $this->dateformat; + } + + /** + * Shows the table CREATE statement that creates the given tables. + * + * Note: You must have the correct privileges before this method + * will return usable results! + * + * @param mixed $tables A table name or a list of table names. + * + * @return array A list of the create SQL for the tables. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableCreate($tables) + { + $this->connect(); + + $result = array(); + $query = $this->getQuery(true) + ->select('dbms_metadata.get_ddl(:type, :tableName)') + ->from('dual') + ->bind(':type', 'TABLE'); + + // Sanitize input to an array and iterate over the list. + settype($tables, 'array'); + + foreach ($tables as $table) + { + $query->bind(':tableName', $table); + $this->setQuery($query); + $statement = (string) $this->loadResult(); + $result[$table] = $statement; + } + + return $result; + } + + /** + * 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 12.1 + * @throws RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->connect(); + + $columns = array(); + $query = $this->getQuery(true); + + $fieldCasing = $this->getOption(PDO::ATTR_CASE); + + $this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER); + + $table = strtoupper($table); + + $query->select('*'); + $query->from('ALL_TAB_COLUMNS'); + $query->where('table_name = :tableName'); + + $prefixedTable = str_replace('#__', strtoupper($this->tablePrefix), $table); + $query->bind(':tableName', $prefixedTable); + $this->setQuery($query); + $fields = $this->loadObjectList(); + + if ($typeOnly) + { + foreach ($fields as $field) + { + $columns[$field->COLUMN_NAME] = $field->DATA_TYPE; + } + } + else + { + foreach ($fields as $field) + { + $columns[$field->COLUMN_NAME] = $field; + $columns[$field->COLUMN_NAME]->Default = null; + } + } + + $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 12.1 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + $query = $this->getQuery(true); + + $fieldCasing = $this->getOption(PDO::ATTR_CASE); + + $this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER); + + $table = strtoupper($table); + $query->select('*') + ->from('ALL_CONSTRAINTS') + ->where('table_name = :tableName') + ->bind(':tableName', $table); + + $this->setQuery($query); + $keys = $this->loadObjectList(); + + $this->setOption(PDO::ATTR_CASE, $fieldCasing); + + return $keys; + } + + /** + * Method to get an array of all tables in the database (schema). + * + * @param string $databaseName The database (schema) name + * @param boolean $includeDatabaseName Whether to include the schema name in the results + * + * @return array An array of all the tables in the database. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableList($databaseName = null, $includeDatabaseName = false) + { + $this->connect(); + + $query = $this->getQuery(true); + + if ($includeDatabaseName) + { + $query->select('owner, table_name'); + } + else + { + $query->select('table_name'); + } + + $query->from('all_tables'); + + if ($databaseName) + { + $query->where('owner = :database') + ->bind(':database', $databaseName); + } + + $query->order('table_name'); + + $this->setQuery($query); + + if ($includeDatabaseName) + { + $tables = $this->loadAssocList(); + } + else + { + $tables = $this->loadColumn(); + } + + return $tables; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + + $this->setQuery("select value from nls_database_parameters where parameter = 'NLS_RDBMS_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 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + return true; + } + + /** + * Sets the Oracle Date Format for the session + * Default date format for Oracle is = DD-MON-RR + * The default date format for this driver is: + * 'RRRR-MM-DD HH24:MI:SS' since it is the format + * that matches the MySQL one used within most Joomla + * tables. + * + * @param string $dateFormat Oracle Date Format String + * + * @return boolean + * + * @since 12.1 + */ + public function setDateFormat($dateFormat = 'DD-MON-RR') + { + $this->connect(); + + $this->setQuery("ALTER SESSION SET NLS_DATE_FORMAT = '$dateFormat'"); + + if (!$this->execute()) + { + return false; + } + + $this->setQuery("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = '$dateFormat'"); + + if (!$this->execute()) + { + return false; + } + + $this->dateformat = $dateFormat; + + 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 12.1 + */ + public function setUtf() + { + return false; + } + + /** + * Locks a table in the database. + * + * @param string $table The name of the table to unlock. + * + * @return FOFDatabaseDriverOracle Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function lockTable($table) + { + $this->setQuery('LOCK TABLE ' . $this->quoteName($table) . ' IN EXCLUSIVE MODE')->execute(); + + 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 Oracle. + * @param string $prefix Not used by Oracle. + * + * @return FOFDatabaseDriverOracle Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->setQuery('RENAME ' . $oldTable . ' TO ' . $newTable)->execute(); + + return $this; + } + + /** + * Unlocks tables in the database. + * + * @return FOFDatabaseDriverOracle Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function unlockTables() + { + $this->setQuery('COMMIT')->execute(); + + return $this; + } + + /** + * Test to see if the PDO ODBC connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function isSupported() + { + return class_exists('PDO') && in_array('oci', PDO::getAvailableDrivers()); + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix class variable. + * + * @param string $query 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 = '#__') + { + $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, "'", $startPos); + + 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; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.3 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionCommit($toSavepoint); + } + else + { + $this->transactionDepth--; + } + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.3 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionRollback($toSavepoint); + } + else + { + $savepoint = 'SP_' . ($this->transactionDepth - 1); + $this->setQuery('ROLLBACK TO SAVEPOINT ' . $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 12.3 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + return parent::transactionStart($asSavepoint); + } + + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/pdo.php b/deployed/akeeba/libraries/fof/database/driver/pdo.php new file mode 100644 index 00000000..ebc59eff --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/pdo.php @@ -0,0 +1,1106 @@ +disconnect(); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + // Make sure the PDO extension for PHP is installed and enabled. + if (!self::isSupported()) + { + throw new RuntimeException('PDO Extension is not available.', 1); + } + + $replace = array(); + $with = array(); + + // Find the correct PDO DSN Format to use: + switch ($this->options['driver']) + { + case 'cubrid': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 33000; + + $format = 'cubrid:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + + break; + + case 'dblib': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 1433; + + $format = 'dblib:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + + break; + + case 'firebird': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 3050; + + $format = 'firebird:dbname=#DBNAME#'; + + $replace = array('#DBNAME#'); + $with = array($this->options['database']); + + break; + + case 'ibm': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 56789; + + if (!empty($this->options['dsn'])) + { + $format = 'ibm:DSN=#DSN#'; + + $replace = array('#DSN#'); + $with = array($this->options['dsn']); + } + else + { + $format = 'ibm:hostname=#HOST#;port=#PORT#;database=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + } + + break; + + case 'informix': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 1526; + $this->options['protocol'] = (isset($this->options['protocol'])) ? $this->options['protocol'] : 'onsoctcp'; + + if (!empty($this->options['dsn'])) + { + $format = 'informix:DSN=#DSN#'; + + $replace = array('#DSN#'); + $with = array($this->options['dsn']); + } + else + { + $format = 'informix:host=#HOST#;service=#PORT#;database=#DBNAME#;server=#SERVER#;protocol=#PROTOCOL#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#', '#SERVER#', '#PROTOCOL#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database'], $this->options['server'], $this->options['protocol']); + } + + break; + + case 'mssql': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 1433; + + $format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + + break; + + // The pdomysql case is a special case within the CMS environment + case 'pdomysql': + case 'mysql': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 3306; + + $format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#', '#CHARSET#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database'], $this->options['charset']); + + break; + + case 'oci': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 1521; + $this->options['charset'] = (isset($this->options['charset'])) ? $this->options['charset'] : 'AL32UTF8'; + + if (!empty($this->options['dsn'])) + { + $format = 'oci:dbname=#DSN#'; + + $replace = array('#DSN#'); + $with = array($this->options['dsn']); + } + else + { + $format = 'oci:dbname=//#HOST#:#PORT#/#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + } + + $format .= ';charset=' . $this->options['charset']; + + break; + + case 'odbc': + $format = 'odbc:DSN=#DSN#;UID:#USER#;PWD=#PASSWORD#'; + + $replace = array('#DSN#', '#USER#', '#PASSWORD#'); + $with = array($this->options['dsn'], $this->options['user'], $this->options['password']); + + break; + + case 'pgsql': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 5432; + + $format = 'pgsql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + + break; + + case 'sqlite': + if (isset($this->options['version']) && $this->options['version'] == 2) + { + $format = 'sqlite2:#DBNAME#'; + } + else + { + $format = 'sqlite:#DBNAME#'; + } + + $replace = array('#DBNAME#'); + $with = array($this->options['database']); + + break; + + case 'sybase': + $this->options['port'] = (isset($this->options['port'])) ? $this->options['port'] : 1433; + + $format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; + + $replace = array('#HOST#', '#PORT#', '#DBNAME#'); + $with = array($this->options['host'], $this->options['port'], $this->options['database']); + + break; + } + + // Create the connection string: + $connectionString = str_replace($replace, $with, $format); + + try + { + $this->connection = new PDO( + $connectionString, + $this->options['user'], + $this->options['password'], + $this->options['driverOptions'] + ); + } + catch (PDOException $e) + { + throw new RuntimeException('Could not connect to PDO: ' . $e->getMessage(), 2, $e); + } + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + $this->freeResult(); + unset($this->connection); + } + + /** + * Method to escape a string for usage in an SQL statement. + * + * Oracle escaping reference: + * http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F + * + * SQLite escaping notes: + * http://www.sqlite.org/faq.html#q14 + * + * Method body is as implemented by the Zend Framework + * + * 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 12.1 + */ + public function escape($text, $extra = false) + { + if (is_int($text) || is_float($text)) + { + return $text; + } + + $text = str_replace("'", "''", $text); + + return addcslashes($text, "\000\n\r\\\032"); + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @since 12.1 + * @throws RuntimeException + * @throws Exception + */ + public function execute() + { + $this->connect(); + + if (!is_object($this->connection)) + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database'); + } + 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->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0)) + { + // @TODO + $query .= ' LIMIT ' . $this->offset . ', ' . $this->limit; + } + + // Increment the query counter. + $this->count++; + + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + + // If debugging is enabled then let's log the query. + if ($this->debug) + { + // Add the query to the object queue. + $this->log[] = $query; + + if (class_exists('JLog')) + { + JLog::add($query, JLog::DEBUG, 'databasequery'); + } + + $this->timings[] = microtime(true); + } + + // Execute the query. + $this->executed = false; + + if ($this->prepared instanceof PDOStatement) + { + // Bind the variables: + if ($this->sql instanceof FOFDatabaseQueryPreparable) + { + $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 ($this->debug) + { + $this->timings[] = microtime(true); + + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) + { + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + else + { + $this->callStacks[] = debug_backtrace(); + } + } + + // If an error occurred handle it. + if (!$this->executed) + { + // Get the error number and message before we execute any more queries. + $errorNum = $this->getErrorNumber(); + $errorMsg = $this->getErrorMessage($query); + + // Check if the server was disconnected. + if (!$this->connected()) + { + try + { + // Attempt to reconnect. + $this->connection = null; + $this->connect(); + } + // If connect fails, ignore that exception and throw the normal exception. + catch (RuntimeException $e) + { + // Get the error number and message. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum, $e); + } + + // Since we were able to reconnect, run the query again. + return $this->execute(); + } + // The server was not disconnected. + else + { + // Get the error number and message from before we tried to reconnect. + $this->errorNum = $errorNum; + $this->errorMsg = $errorMsg; + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + 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 12.1 + */ + public function getOption($key) + { + $this->connect(); + + return $this->connection->getAttribute($key); + } + + /** + * Get a query to run and verify the database is operational. + * + * @return string The query to check the health of the DB. + * + * @since 12.2 + */ + public function getConnectedQuery() + { + return 'SELECT 1'; + } + + /** + * 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 12.1 + */ + public function setOption($key, $value) + { + $this->connect(); + + return $this->connection->setAttribute($key, $value); + } + + /** + * Test to see if the PDO extension is available. + * Override as needed to check for specific PDO Drivers. + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function isSupported() + { + return defined('PDO::ATTR_DRIVER_NAME'); + } + + /** + * Determines if the connection to the server is active. + * + * @return boolean True if connected to the database engine. + * + * @since 12.1 + */ + public function connected() + { + // Flag to prevent recursion into this function. + static $checkingConnected = false; + + if ($checkingConnected) + { + // Reset this flag and throw an exception. + $checkingConnected = true; + die('Recursion trying to check if connected.'); + } + + // Backup the query state. + $query = $this->sql; + $limit = $this->limit; + $offset = $this->offset; + $prepared = $this->prepared; + + try + { + // Set the checking connection flag. + $checkingConnected = true; + + // Run a simple query to check the connection. + $this->setQuery($this->getConnectedQuery()); + $status = (bool) $this->loadResult(); + } + // If we catch an exception here, we must not be connected. + catch (Exception $e) + { + $status = false; + } + + // Restore the query state. + $this->sql = $query; + $this->limit = $limit; + $this->offset = $offset; + $this->prepared = $prepared; + $checkingConnected = false; + + return $status; + } + + /** + * Get the number of affected rows for the previous executed SQL statement. + * Only applicable for DELETE, INSERT, or UPDATE statements. + * + * @return integer The number of affected rows. + * + * @since 12.1 + */ + public function getAffectedRows() + { + $this->connect(); + + if ($this->prepared instanceof PDOStatement) + { + return $this->prepared->rowCount(); + } + else + { + return 0; + } + } + + /** + * Get the number of returned rows for the previous executed SQL statement. + * Only applicable for DELETE, INSERT, or UPDATE statements. + * + * @param resource $cursor An optional database cursor resource to extract the row count from. + * + * @return integer The number of returned rows. + * + * @since 12.1 + */ + public function getNumRows($cursor = null) + { + $this->connect(); + + if ($cursor instanceof PDOStatement) + { + return $cursor->rowCount(); + } + elseif ($this->prepared instanceof PDOStatement) + { + return $this->prepared->rowCount(); + } + else + { + return 0; + } + } + + /** + * Method to get the auto-incremented value from the last INSERT statement. + * + * @return string The value of the auto-increment field from the last inserted row. + * + * @since 12.1 + */ + public function insertid() + { + $this->connect(); + + // 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + return true; + } + + /** + * Sets the SQL statement string for later execution. + * + * @param mixed $query The SQL statement to set either as a FOFDatabaseQuery 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 FOFDatabaseDriver This object to support method chaining. + * + * @since 12.1 + */ + public function setQuery($query, $offset = null, $limit = null, $driverOptions = array()) + { + $this->connect(); + + $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 FOFDatabaseQueryLimitable && !is_null($offset) && !is_null($limit)) + { + $query = $query->processLimit($query, $limit, $offset); + } + + // Create a stringified version of the query (with prefixes replaced): + $sql = $this->replacePrefix((string) $query); + + // Use the stringified version in the prepare call: + $this->prepared = $this->connection->prepare($sql, $driverOptions); + + // Store reference to the original FOFDatabaseQuery instance within the class. + // This is important since binding variables depends on it within execute(): + parent::setQuery($query, $offset, $limit); + + return $this; + } + + /** + * Set the connection to use UTF-8 character encoding. + * + * @return boolean True on success. + * + * @since 12.1 + */ + public function setUtf() + { + return false; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth == 1) + { + $this->connection->commit(); + } + + $this->transactionDepth--; + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth == 1) + { + $this->connection->rollBack(); + } + + $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 12.1 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + $this->connection->beginTransaction(); + } + + $this->transactionDepth++; + } + + /** + * 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. + * + * @since 12.1 + */ + 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); + } + } + + /** + * 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. + * + * @since 12.1 + */ + protected 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); + } + } + + /** + * 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 Unused, only necessary so method signature will be the same as parent. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + * + * @since 12.1 + */ + 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); + } + } + + /** + * 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 + * + * @since 12.1 + */ + protected 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; + } + } + + /** + * 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. + * + * @since 12.1 + * @throws RuntimeException + * @deprecated 4.0 (CMS) Use getIterator() instead + */ + public function loadNextObject($class = 'stdClass') + { + if (class_exists('JLog')) + { + JLog::add(__METHOD__ . '() is deprecated. Use FOFDatabaseDriver::getIterator() instead.', JLog::WARNING, 'deprecated'); + } + $this->connect(); + + // Execute the query and get the result set cursor. + if (!$this->executed) + { + 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function loadNextAssoc() + { + $this->connect(); + + // Execute the query and get the result set cursor. + if (!$this->executed) + { + 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->fetchAssoc()) + { + 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. + * + * @since 12.1 + * @throws RuntimeException + * @deprecated 4.0 (CMS) Use getIterator() instead + */ + public function loadNextRow() + { + if (class_exists('JLog')) + { + JLog::add(__METHOD__ . '() is deprecated. Use FOFDatabaseDriver::getIterator() instead.', JLog::WARNING, 'deprecated'); + } + $this->connect(); + + // Execute the query and get the result set cursor. + if (!$this->executed) + { + 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 + * + * @since 12.3 + */ + 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 + * + * @since 12.3 + */ + public function __wakeup() + { + // Get connection back + $this->__construct($this->options); + } + + /** + * Return the actual SQL Error number + * + * @return integer The SQL Error number + * + * @since 3.4.6 + */ + protected function getErrorNumber() + { + return (int) $this->connection->errorCode(); + } + + /** + * Return the actual SQL Error message + * + * @param string $query The SQL Query that fails + * + * @return string The SQL Error message + * + * @since 3.4.6 + */ + protected function getErrorMessage($query) + { + // Note we ignoring $query here as it not used in the original code. + + // The SQL Error Information + $errorInfo = implode(", ", $this->connection->errorInfo()); + + // Replace the Databaseprefix with `#__` if we are not in Debug + if (!$this->debug) + { + $errorInfo = str_replace($this->tablePrefix, '#__', $errorInfo); + } + + return 'SQL: ' . $errorInfo; + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/pdomysql.php b/deployed/akeeba/libraries/fof/database/driver/pdomysql.php new file mode 100644 index 00000000..8e975747 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/pdomysql.php @@ -0,0 +1,559 @@ +utf8mb4 = true; + + // Get some basic values from the options. + $options['driver'] = 'mysql'; + $options['charset'] = (isset($options['charset'])) ? $options['charset'] : 'utf8'; + + if ($this->utf8mb4 && ($options['charset'] == 'utf8')) + { + $options['charset'] = 'utf8mb4'; + } + + $this->charset = $options['charset']; + + // Finalize initialisation. + parent::__construct($options); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 3.4 + * @throws RuntimeException + */ + public function connect() + { + try + { + // Try to connect to MySQL + parent::connect(); + } + catch (\RuntimeException $e) + { + // If the connection failed but not because of the wrong character set bubble up the exception + if (!$this->utf8mb4 || ($this->options['charset'] != 'utf8mb4')) + { + throw $e; + } + + /** + * If the connection failed and I was trying to use the utf8mb4 charset then it is likely that the server + * doesn't support utf8mb4 despite claiming otherwise. + * + * This happens on old MySQL server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd + * masks the server version and reports only its own we can not be sure if the server actually does support + * UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is undefined in this case we + * catch the error and determine that utf8mb4 is not supported! + */ + $this->utf8mb4 = false; + $this->options['charset'] = 'utf8'; + + parent::connect(); + } + + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + } + + /** + * Test to see if the MySQL connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 3.4 + */ + public static function isSupported() + { + return class_exists('PDO') && in_array('mysql', PDO::getAvailableDrivers()); + } + + /** + * 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 FOFDatabaseDriverPdomysql Returns this object to support chaining. + * + * @since 3.4 + * @throws RuntimeException + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $query = $this->getQuery(true); + + $query->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName)); + + $this->setQuery($query); + + $this->execute(); + + 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. + * + * @since 3.4 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + $this->setQuery('USE ' . $this->quoteName($database)); + + $this->execute(); + + return $this; + } + + /** + * 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 (string) or boolean false if not supported. + * + * @since 3.4 + * @throws RuntimeException + */ + public function getCollation() + { + $this->connect(); + + // Attempt to get the database collation by accessing the server system variable. + $this->setQuery('SHOW VARIABLES LIKE "collation_database"'); + $result = $this->loadObject(); + + if (property_exists($result, 'Value')) + { + return $result->Value; + } + else + { + return false; + } + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + $this->connect(); + + // Attempt to get the database collation by accessing the server system variable. + $this->setQuery('SHOW VARIABLES LIKE "collation_connection"'); + $result = $this->loadObject(); + + if (property_exists($result, 'Value')) + { + return $result->Value; + } + else + { + return false; + } + } + + /** + * 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. + * + * @since 3.4 + * @throws RuntimeException + */ + public function getTableCreate($tables) + { + $this->connect(); + + // Initialise variables. + $result = array(); + + // Sanitize input to an array and iterate over the list. + settype($tables, 'array'); + + foreach ($tables as $table) + { + $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table)); + + $row = $this->loadRow(); + + // Populate the result array based on the create statements. + $result[$table] = $row[1]; + } + + return $result; + } + + /** + * 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 3.4 + * @throws RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->connect(); + + $result = array(); + + // Set the query to get the table fields statement. + $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table)); + + $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; + } + + /** + * 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 3.4 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + // Get the details columns information. + $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table)); + + $keys = $this->loadObjectList(); + + return $keys; + } + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + * + * @since 3.4 + * @throws RuntimeException + */ + public function getTableList() + { + $this->connect(); + + // Set the query to get the tables statement. + $this->setQuery('SHOW TABLES'); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 3.4 + */ + public function getVersion() + { + $this->connect(); + + return $this->getOption(PDO::ATTR_SERVER_VERSION); + } + + /** + * Locks a table in the database. + * + * @param string $table The name of the table to unlock. + * + * @return FOFDatabaseDriverPdomysql Returns this object to support chaining. + * + * @since 3.4 + * @throws RuntimeException + */ + public function lockTable($table) + { + $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->execute(); + + 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 MySQL. + * @param string $prefix Not used by MySQL. + * + * @return FOFDatabaseDriverPdomysql Returns this object to support chaining. + * + * @since 3.4 + * @throws RuntimeException + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->setQuery('RENAME TABLE ' . $this->quoteName($oldTable) . ' TO ' . $this->quoteName($newTable)); + + $this->execute(); + + return $this; + } + + /** + * Method to escape a string for usage in an SQL statement. + * + * Oracle escaping reference: + * http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F + * + * SQLite escaping notes: + * http://www.sqlite.org/faq.html#q14 + * + * Method body is as implemented by the Zend Framework + * + * 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 3.4 + */ + public function escape($text, $extra = false) + { + $this->connect(); + + if (is_int($text) || is_float($text)) + { + return $text; + } + + $result = substr($this->connection->quote($text), 1, -1); + + if ($extra) + { + $result = addcslashes($result, '%_'); + } + + return $result; + } + + /** + * Unlocks tables in the database. + * + * @return FOFDatabaseDriverPdomysql Returns this object to support chaining. + * + * @since 3.4 + * @throws RuntimeException + */ + public function unlockTables() + { + $this->setQuery('UNLOCK TABLES')->execute(); + + return $this; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 3.4 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionCommit($toSavepoint); + } + else + { + $this->transactionDepth--; + } + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 3.4 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionRollback($toSavepoint); + } + else + { + $savepoint = 'SP_' . ($this->transactionDepth - 1); + $this->setQuery('ROLLBACK TO SAVEPOINT ' . $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 3.4 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + parent::transactionStart($asSavepoint); + } + else + { + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/postgresql.php b/deployed/akeeba/libraries/fof/database/driver/postgresql.php new file mode 100644 index 00000000..4908377f --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/postgresql.php @@ -0,0 +1,1529 @@ +disconnect(); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + // Make sure the postgresql extension for PHP is installed and enabled. + if (!function_exists('pg_connect')) + { + throw new RuntimeException('PHP extension pg_connect is not available.'); + } + + // Build the DSN for the connection. + $dsn = ''; + + if (!empty($this->options['host'])) + { + $dsn .= "host={$this->options['host']} "; + } + + $dsn .= "dbname={$this->options['database']} user={$this->options['user']} password={$this->options['password']}"; + + // Attempt to connect to the server. + if (!($this->connection = @pg_connect($dsn))) + { + throw new RuntimeException('Error connecting to PGSQL database.'); + } + + pg_set_error_verbosity($this->connection, PGSQL_ERRORS_DEFAULT); + pg_query('SET standard_conforming_strings=off'); + pg_query('SET escape_string_warning=off'); + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + // Close the connection. + if (is_resource($this->connection)) + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + pg_close($this->connection); + } + + $this->connection = null; + } + + /** + * 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. + * + * @since 12.1 + */ + public function escape($text, $extra = false) + { + $this->connect(); + + $result = pg_escape_string($this->connection, $text); + + if ($extra) + { + $result = addcslashes($result, '%_'); + } + + return $result; + } + + /** + * Test to see if the PostgreSQL connector is available + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function test() + { + return (function_exists('pg_connect')); + } + + /** + * Determines if the connection to the server is active. + * + * @return boolean + * + * @since 12.1 + */ + public function connected() + { + $this->connect(); + + if (is_resource($this->connection)) + { + return pg_ping($this->connection); + } + + return false; + } + + /** + * 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 boolean + * + * @since 12.1 + * @throws RuntimeException + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName)); + $this->execute(); + + return true; + } + + /** + * Get the number of affected rows by the last INSERT, UPDATE, REPLACE or DELETE for the previous executed SQL statement. + * + * @return integer The number of affected rows in the previous operation + * + * @since 12.1 + */ + public function getAffectedRows() + { + $this->connect(); + + return pg_affected_rows($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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getCollation() + { + $this->connect(); + + $this->setQuery('SHOW LC_COLLATE'); + $array = $this->loadAssocList(); + + return $array[0]['lc_collate']; + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + return pg_client_encoding($this->connection); + } + + /** + * Get the number of returned rows for the previous executed SQL statement. + * This command is only valid for statements like SELECT or SHOW that return an actual result set. + * To retrieve the number of rows affected by a INSERT, UPDATE, REPLACE or DELETE query, use getAffectedRows(). + * + * @param resource $cur An optional database cursor resource to extract the row count from. + * + * @return integer The number of returned rows. + * + * @since 12.1 + */ + public function getNumRows($cur = null) + { + $this->connect(); + + return pg_num_rows((int) $cur ? $cur : $this->cursor); + } + + /** + * Get the current or query, or new FOFDatabaseQuery object. + * + * @param boolean $new False to return the last query set, True to return a new FOFDatabaseQuery object. + * @param boolean $asObj False to return last query as string, true to get FOFDatabaseQueryPostgresql object. + * + * @return FOFDatabaseQuery The current query object or a new object extending the FOFDatabaseQuery class. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getQuery($new = false, $asObj = false) + { + if ($new) + { + // Make sure we have a query class for this driver. + if (!class_exists('FOFDatabaseQueryPostgresql')) + { + throw new RuntimeException('FOFDatabaseQueryPostgresql Class not found.'); + } + + $this->queryObject = new FOFDatabaseQueryPostgresql($this); + + return $this->queryObject; + } + else + { + if ($asObj) + { + return $this->queryObject; + } + else + { + return $this->sql; + } + } + } + + /** + * Shows the table CREATE statement that creates the given tables. + * + * This is unsuported by PostgreSQL. + * + * @param mixed $tables A table name or a list of table names. + * + * @return string An empty char because this function is not supported by PostgreSQL. + * + * @since 12.1 + */ + public function getTableCreate($tables) + { + return ''; + } + + /** + * 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 12.1 + * @throws RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->connect(); + + $result = array(); + + $tableSub = $this->replacePrefix($table); + + $this->setQuery(' + SELECT a.attname AS "column_name", + pg_catalog.format_type(a.atttypid, a.atttypmod) as "type", + CASE WHEN a.attnotnull IS TRUE + THEN \'NO\' + ELSE \'YES\' + END AS "null", + CASE WHEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) IS NOT NULL + THEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) + END as "Default", + CASE WHEN pg_catalog.col_description(a.attrelid, a.attnum) IS NULL + THEN \'\' + ELSE pg_catalog.col_description(a.attrelid, a.attnum) + END AS "comments" + FROM pg_catalog.pg_attribute a + LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum + LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid + WHERE a.attrelid = + (SELECT oid FROM pg_catalog.pg_class WHERE relname=' . $this->quote($tableSub) . ' + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE + nspname = \'public\') + ) + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum' + ); + + $fields = $this->loadObjectList(); + + if ($typeOnly) + { + foreach ($fields as $field) + { + $result[$field->column_name] = preg_replace("/[(0-9)]/", '', $field->type); + } + } + else + { + foreach ($fields as $field) + { + if (stristr(strtolower($field->type), "character varying")) + { + $field->Default = ""; + } + if (stristr(strtolower($field->type), "text")) + { + $field->Default = ""; + } + // Do some dirty translation to MySQL output. + // TODO: Come up with and implement a standard across databases. + $result[$field->column_name] = (object) array( + 'column_name' => $field->column_name, + 'type' => $field->type, + 'null' => $field->null, + 'Default' => $field->Default, + 'comments' => '', + 'Field' => $field->column_name, + 'Type' => $field->type, + 'Null' => $field->null, + // TODO: Improve query above to return primary key info as well + // 'Key' => ($field->PK == '1' ? 'PRI' : '') + ); + } + } + + /* Change Postgresql's NULL::* type with PHP's null one */ + foreach ($fields as $field) + { + if (preg_match("/^NULL::*/", $field->Default)) + { + $field->Default = null; + } + } + + return $result; + } + + /** + * 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 12.1 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + if (in_array($table, $tableList)) + { + // Get the details columns information. + $this->setQuery(' + SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique AS "isUnique", + CASE WHEN indisprimary = true THEN + ( SELECT \'ALTER TABLE \' || tablename || \' ADD \' || pg_catalog.pg_get_constraintdef(const.oid, true) + FROM pg_constraint AS const WHERE const.conname= pgClassFirst.relname ) + ELSE pg_catalog.pg_get_indexdef(indexrelid, 0, true) + END AS "Query" + FROM pg_indexes + LEFT JOIN pg_class AS pgClassFirst ON indexname=pgClassFirst.relname + LEFT JOIN pg_index AS pgIndex ON pgClassFirst.oid=pgIndex.indexrelid + WHERE tablename=' . $this->quote($table) . ' ORDER BY indkey' + ); + + $keys = $this->loadObjectList(); + + return $keys; + } + + return false; + } + + /** + * Method to get an array of all tables in the database. + * + * @return array An array of all the tables in the database. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableList() + { + $this->connect(); + + $query = $this->getQuery(true) + ->select('table_name') + ->from('information_schema.tables') + ->where('table_type=' . $this->quote('BASE TABLE')) + ->where('table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ')') + ->order('table_name ASC'); + + $this->setQuery($query); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the details list of sequences for a table. + * + * @param string $table The name of the table. + * + * @return array An array of sequences specification for the table. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableSequences($table) + { + $this->connect(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + if (in_array($table, $tableList)) + { + $name = array( + 's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type', 'info.minimum_value', 'info.maximum_value', + 'info.increment', 'info.cycle_option' + ); + $as = array('sequence', 'schema', 'table', 'column', 'data_type', 'minimum_value', 'maximum_value', 'increment', 'cycle_option'); + + if (version_compare($this->getVersion(), '9.1.0') >= 0) + { + $name[] .= 'info.start_value'; + $as[] .= 'start_value'; + } + + // Get the details columns information. + $query = $this->getQuery(true) + ->select($this->quoteName($name, $as)) + ->from('pg_class AS s') + ->join('LEFT', "pg_depend d ON d.objid=s.oid AND d.classid='pg_class'::regclass AND d.refclassid='pg_class'::regclass") + ->join('LEFT', 'pg_class t ON t.oid=d.refobjid') + ->join('LEFT', 'pg_namespace n ON n.oid=t.relnamespace') + ->join('LEFT', 'pg_attribute a ON a.attrelid=t.oid AND a.attnum=d.refobjsubid') + ->join('LEFT', 'information_schema.sequences AS info ON info.sequence_name=s.relname') + ->where("s.relkind='S' AND d.deptype='a' AND t.relname=" . $this->quote($table)); + $this->setQuery($query); + $seq = $this->loadObjectList(); + + return $seq; + } + + return false; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + $version = pg_version($this->connection); + + return $version['server']; + } + + /** + * Method to get the auto-incremented value from the last INSERT statement. + * To be called after the INSERT statement, it's MANDATORY to have a sequence on + * every primary key table. + * + * To get the auto incremented value it's possible to call this function after + * INSERT INTO query, or use INSERT INTO with RETURNING clause. + * + * @example with insertid() call: + * $query = $this->getQuery(true) + * ->insert('jos_dbtest') + * ->columns('title,start_date,description') + * ->values("'testTitle2nd','1971-01-01','testDescription2nd'"); + * $this->setQuery($query); + * $this->execute(); + * $id = $this->insertid(); + * + * @example with RETURNING clause: + * $query = $this->getQuery(true) + * ->insert('jos_dbtest') + * ->columns('title,start_date,description') + * ->values("'testTitle2nd','1971-01-01','testDescription2nd'") + * ->returning('id'); + * $this->setQuery($query); + * $id = $this->loadResult(); + * + * @return integer The value of the auto-increment field from the last inserted row. + * + * @since 12.1 + */ + public function insertid() + { + $this->connect(); + $insertQuery = $this->getQuery(false, true); + $table = $insertQuery->__get('insert')->getElements(); + + /* find sequence column name */ + $colNameQuery = $this->getQuery(true); + $colNameQuery->select('column_default') + ->from('information_schema.columns') + ->where("table_name=" . $this->quote($this->replacePrefix(str_replace('"', '', $table[0]))), 'AND') + ->where("column_default LIKE '%nextval%'"); + + $this->setQuery($colNameQuery); + $colName = $this->loadRow(); + $changedColName = str_replace('nextval', 'currval', $colName); + + $insertidQuery = $this->getQuery(true); + $insertidQuery->select($changedColName); + $this->setQuery($insertidQuery); + $insertVal = $this->loadRow(); + + return $insertVal[0]; + } + + /** + * Locks a table in the database. + * + * @param string $tableName The name of the table to unlock. + * + * @return FOFDatabaseDriverPostgresql Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function lockTable($tableName) + { + $this->transactionStart(); + $this->setQuery('LOCK TABLE ' . $this->quoteName($tableName) . ' IN ACCESS EXCLUSIVE MODE')->execute(); + + return $this; + } + + /** + * Execute the SQL statement. + * + * @return mixed A database cursor resource on success, boolean false on failure. + * + * @since 12.1 + * @throws RuntimeException + */ + public function execute() + { + $this->connect(); + + if (!is_resource($this->connection)) + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database'); + } + 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->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0)) + { + $query .= ' LIMIT ' . $this->limit . ' OFFSET ' . $this->offset; + } + + // Increment the query counter. + $this->count++; + + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + + // If debugging is enabled then let's log the query. + if ($this->debug) + { + // Add the query to the object queue. + $this->log[] = $query; + + if (class_exists('JLog')) + { + JLog::add($query, JLog::DEBUG, 'databasequery'); + } + + $this->timings[] = microtime(true); + } + + // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. + $this->cursor = @pg_query($this->connection, $query); + + if ($this->debug) + { + $this->timings[] = microtime(true); + + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) + { + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + else + { + $this->callStacks[] = debug_backtrace(); + } + } + + // If an error occurred handle it. + if (!$this->cursor) + { + // Get the error number and message before we execute any more queries. + $errorNum = $this->getErrorNumber(); + $errorMsg = $this->getErrorMessage($query); + + // Check if the server was disconnected. + if (!$this->connected()) + { + try + { + // Attempt to reconnect. + $this->connection = null; + $this->connect(); + } + // If connect fails, ignore that exception and throw the normal exception. + catch (RuntimeException $e) + { + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, null, $e); + } + + // Since we were able to reconnect, run the query again. + return $this->execute(); + } + // The server was not disconnected. + else + { + // Get the error number and message from before we tried to reconnect. + $this->errorNum = $errorNum; + $this->errorMsg = $errorMsg; + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg); + } + } + + return $this->cursor; + } + + /** + * 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 PostgreSQL. + * @param string $prefix Not used by PostgreSQL. + * + * @return FOFDatabaseDriverPostgresql Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->connect(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + // Origin Table does not exist + if (!in_array($oldTable, $tableList)) + { + // Origin Table not found + throw new RuntimeException('Table not found in Postgresql database.'); + } + else + { + /* Rename indexes */ + $this->setQuery( + 'SELECT relname + FROM pg_class + WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname=' . $this->quote($oldTable, true) . ' + AND pg_class.oid=pg_index.indrelid );' + ); + + $oldIndexes = $this->loadColumn(); + + foreach ($oldIndexes as $oldIndex) + { + $changedIdxName = str_replace($oldTable, $newTable, $oldIndex); + $this->setQuery('ALTER INDEX ' . $this->escape($oldIndex) . ' RENAME TO ' . $this->escape($changedIdxName)); + $this->execute(); + } + + /* Rename sequence */ + $this->setQuery( + 'SELECT relname + FROM pg_class + WHERE relkind = \'S\' + AND relnamespace IN ( + SELECT oid + FROM pg_namespace + WHERE nspname NOT LIKE \'pg_%\' + AND nspname != \'information_schema\' + ) + AND relname LIKE \'%' . $oldTable . '%\' ;' + ); + + $oldSequences = $this->loadColumn(); + + foreach ($oldSequences as $oldSequence) + { + $changedSequenceName = str_replace($oldTable, $newTable, $oldSequence); + $this->setQuery('ALTER SEQUENCE ' . $this->escape($oldSequence) . ' RENAME TO ' . $this->escape($changedSequenceName)); + $this->execute(); + } + + /* Rename table */ + $this->setQuery('ALTER TABLE ' . $this->escape($oldTable) . ' RENAME TO ' . $this->escape($newTable)); + $this->execute(); + } + + return true; + } + + /** + * Selects the database, but redundant for PostgreSQL + * + * @param string $database Database name to select. + * + * @return boolean Always true + * + * @since 12.1 + */ + public function select($database) + { + return true; + } + + /** + * Custom settings for UTF support + * + * @return integer Zero on success, -1 on failure + * + * @since 12.1 + */ + public function setUtf() + { + $this->connect(); + + return pg_set_client_encoding($this->connection, 'UTF8'); + } + + /** + * This function return a field value as a prepared string to be used in a SQL statement. + * + * @param array $columns Array of table's column returned by ::getTableColumns. + * @param string $field_name The table field's name. + * @param string $field_value The variable value to quote and return. + * + * @return string The quoted string. + * + * @since 12.1 + */ + public function sqlValue($columns, $field_name, $field_value) + { + switch ($columns[$field_name]) + { + case 'boolean': + $val = 'NULL'; + + if ($field_value == 't') + { + $val = 'TRUE'; + } + elseif ($field_value == 'f') + { + $val = 'FALSE'; + } + + break; + + case 'bigint': + case 'bigserial': + case 'integer': + case 'money': + case 'numeric': + case 'real': + case 'smallint': + case 'serial': + case 'numeric,': + $val = strlen($field_value) == 0 ? 'NULL' : $field_value; + break; + + case 'date': + case 'timestamp without time zone': + if (empty($field_value)) + { + $field_value = $this->getNullDate(); + } + + $val = $this->quote($field_value); + break; + + default: + $val = $this->quote($field_value); + break; + } + + return $val; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('COMMIT')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $this->transactionDepth--; + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('ROLLBACK')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $savepoint = 'SP_' . ($this->transactionDepth - 1); + $this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth--; + $this->setQuery('RELEASE SAVEPOINT ' . $this->quoteName($savepoint))->execute(); + } + } + + /** + * Method to initialize a transaction. + * + * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + if ($this->setQuery('START TRANSACTION')->execute()) + { + $this->transactionDepth = 1; + } + + return; + } + + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } + + /** + * 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. + * + * @since 12.1 + */ + protected function fetchArray($cursor = null) + { + return pg_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. + * + * @since 12.1 + */ + protected function fetchAssoc($cursor = null) + { + return pg_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. + * + * @since 12.1 + */ + protected function fetchObject($cursor = null, $class = 'stdClass') + { + return pg_fetch_object(is_null($cursor) ? $this->cursor : $cursor, null, $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 + * + * @since 12.1 + */ + protected function freeResult($cursor = null) + { + pg_free_result($cursor ? $cursor : $this->cursor); + } + + /** + * 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function insertObject($table, &$object, $key = null) + { + $columns = $this->getTableColumns($table); + + $fields = array(); + $values = array(); + + // Iterate over the object variables to build the query fields and values. + foreach (get_object_vars($object) as $k => $v) + { + // Only process non-null scalars. + if (is_array($v) or is_object($v) or $v === null) + { + continue; + } + + // Ignore any internal fields or primary keys with value 0. + if (($k[0] == "_") || ($k == $key && (($v === 0) || ($v === '0')))) + { + continue; + } + + // Prepare and sanitize the fields and values for the database query. + $fields[] = $this->quoteName($k); + $values[] = $this->sqlValue($columns, $k, $v); + } + + // Create the base insert statement. + $query = $this->getQuery(true) + ->insert($this->quoteName($table)) + ->columns($fields) + ->values(implode(',', $values)); + + $retVal = false; + + if ($key) + { + $query->returning($key); + + // Set the query and execute the insert. + $this->setQuery($query); + + $id = $this->loadResult(); + + if ($id) + { + $object->$key = $id; + $retVal = true; + } + } + else + { + // Set the query and execute the insert. + $this->setQuery($query); + + if ($this->execute()) + { + $retVal = true; + } + } + + return $retVal; + } + + /** + * Test to see if the PostgreSQL connector is available. + * + * @return boolean True on success, false otherwise. + * + * @since 12.1 + */ + public static function isSupported() + { + return (function_exists('pg_connect')); + } + + /** + * Returns an array containing database's table list. + * + * @return array The database's table list. + * + * @since 12.1 + */ + public function showTables() + { + $this->connect(); + + $query = $this->getQuery(true) + ->select('table_name') + ->from('information_schema.tables') + ->where('table_type = ' . $this->quote('BASE TABLE')) + ->where('table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ' )'); + + $this->setQuery($query); + $tableList = $this->loadColumn(); + + return $tableList; + } + + /** + * Get the substring position inside a string + * + * @param string $substring The string being sought + * @param string $string The string/column being searched + * + * @return integer The position of $substring in $string + * + * @since 12.1 + */ + public function getStringPositionSql( $substring, $string ) + { + $this->connect(); + + $query = "SELECT POSITION( $substring IN $string )"; + $this->setQuery($query); + $position = $this->loadRow(); + + return $position['position']; + } + + /** + * Generate a random value + * + * @return float The random generated number + * + * @since 12.1 + */ + public function getRandom() + { + $this->connect(); + + $this->setQuery('SELECT RANDOM()'); + $random = $this->loadAssoc(); + + return $random['random']; + } + + /** + * Get the query string to alter the database character set. + * + * @param string $dbName The database name + * + * @return string The query that alter the database query string + * + * @since 12.1 + */ + public function getAlterDbCharacterSet( $dbName ) + { + $query = 'ALTER DATABASE ' . $this->quoteName($dbName) . ' SET CLIENT_ENCODING TO ' . $this->quote('UTF8'); + + return $query; + } + + /** + * Get the query string to create new Database in correct PostgreSQL syntax. + * + * @param object $options object coming from "initialise" function to pass user and database name to database driver. + * @param boolean $utf True if the database supports the UTF-8 character set, not used in PostgreSQL "CREATE DATABASE" query. + * + * @return string The query that creates database, owned by $options['user'] + * + * @since 12.1 + */ + public function getCreateDbQuery($options, $utf) + { + $query = 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' OWNER ' . $this->quoteName($options->db_user); + + if ($utf) + { + $query .= ' ENCODING ' . $this->quote('UTF-8'); + } + + return $query; + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix class variable. + * + * @param string $query The SQL statement to prepare. + * @param string $prefix The common table prefix. + * + * @return string The processed SQL statement. + * + * @since 12.1 + */ + public function replacePrefix($query, $prefix = '#__') + { + $query = trim($query); + + if (strpos($query, '\'')) + { + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'currval')) + { + $query = explode('currval', $query); + + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + + $query = implode('currval', $query); + } + + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'nextval')) + { + $query = explode('nextval', $query); + + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + + $query = implode('nextval', $query); + } + + // Sequence name quoted with ' ' but need to be replaced + if (strpos($query, 'setval')) + { + $query = explode('setval', $query); + + for ($nIndex = 1; $nIndex < count($query); $nIndex = $nIndex + 2) + { + $query[$nIndex] = str_replace($prefix, $this->tablePrefix, $query[$nIndex]); + } + + $query = implode('setval', $query); + } + + $explodedQuery = explode('\'', $query); + + for ($nIndex = 0; $nIndex < count($explodedQuery); $nIndex = $nIndex + 2) + { + if (strpos($explodedQuery[$nIndex], $prefix)) + { + $explodedQuery[$nIndex] = str_replace($prefix, $this->tablePrefix, $explodedQuery[$nIndex]); + } + } + + $replacedQuery = implode('\'', $explodedQuery); + } + else + { + $replacedQuery = str_replace($prefix, $this->tablePrefix, $query); + } + + return $replacedQuery; + } + + /** + * Method to release a savepoint. + * + * @param string $savepointName Savepoint's name to release + * + * @return void + * + * @since 12.1 + */ + public function releaseTransactionSavepoint( $savepointName ) + { + $this->connect(); + $this->setQuery('RELEASE SAVEPOINT ' . $this->quoteName($this->escape($savepointName))); + $this->execute(); + } + + /** + * Method to create a savepoint. + * + * @param string $savepointName Savepoint's name to create + * + * @return void + * + * @since 12.1 + */ + public function transactionSavepoint( $savepointName ) + { + $this->connect(); + $this->setQuery('SAVEPOINT ' . $this->quoteName($this->escape($savepointName))); + $this->execute(); + } + + /** + * Unlocks tables in the database, this command does not exist in PostgreSQL, + * it is automatically done on commit or rollback. + * + * @return FOFDatabaseDriverPostgresql Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function unlockTables() + { + $this->transactionCommit(); + + return $this; + } + + /** + * Updates a row in a table based on an object's properties. + * + * @param string $table The name of the database table to update. + * @param object &$object A reference to an object whose public properties match the table fields. + * @param array $key The name of the primary key. + * @param boolean $nulls True to update null fields or false to ignore them. + * + * @return boolean True on success. + * + * @since 12.1 + * @throws RuntimeException + */ + public function updateObject($table, &$object, $key, $nulls = false) + { + $columns = $this->getTableColumns($table); + $fields = array(); + $where = array(); + + if (is_string($key)) + { + $key = array($key); + } + + if (is_object($key)) + { + $key = (array) $key; + } + + // Create the base update statement. + $statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s'; + + // Iterate over the object variables to build the query fields/value pairs. + foreach (get_object_vars($object) as $k => $v) + { + // Only process scalars that are not internal fields. + if (is_array($v) or is_object($v) or $k[0] == '_') + { + continue; + } + + // Set the primary key to the WHERE clause instead of a field to update. + if (in_array($k, $key)) + { + $key_val = $this->sqlValue($columns, $k, $v); + $where[] = $this->quoteName($k) . '=' . $key_val; + continue; + } + + // Prepare and sanitize the fields and values for the database query. + if ($v === null) + { + // If the value is null and we want to update nulls then set it. + if ($nulls) + { + $val = 'NULL'; + } + // If the value is null and we do not want to update nulls then ignore this field. + else + { + continue; + } + } + // The field is not null so we prep it for update. + else + { + $val = $this->sqlValue($columns, $k, $v); + } + + // Add the field to be updated. + $fields[] = $this->quoteName($k) . '=' . $val; + } + + // We don't have any fields to update. + if (empty($fields)) + { + return true; + } + + // Set the query and execute the update. + $this->setQuery(sprintf($statement, implode(",", $fields), implode(' AND ', $where))); + + return $this->execute(); + } + + /** + * Return the actual SQL Error number + * + * @return integer The SQL Error number + * + * @since 3.4.6 + */ + protected function getErrorNumber() + { + return (int) pg_result_error_field($this->cursor, PGSQL_DIAG_SQLSTATE) . ' '; + } + + /** + * Return the actual SQL Error message + * + * @param string $query The SQL Query that fails + * + * @return string The SQL Error message + * + * @since 3.4.6 + */ + protected function getErrorMessage($query) + { + $errorMessage = (string) pg_last_error($this->connection); + + // Replace the Databaseprefix with `#__` if we are not in Debug + if (!$this->debug) + { + $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage); + $query = str_replace($this->tablePrefix, '#__', $query); + } + + return $errorMessage . "SQL=" . $query; + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/sqlazure.php b/deployed/akeeba/libraries/fof/database/driver/sqlazure.php new file mode 100644 index 00000000..90e0a4f8 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/sqlazure.php @@ -0,0 +1,30 @@ +freeResult(); + unset($this->connection); + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + 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 FOFDatabaseDriverSqlite Returns this object to support chaining. + * + * @since 12.1 + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $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 12.1 + */ + 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 12.1 + */ + public function getCollation() + { + return $this->charset; + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + 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 12.1 + * @throws RuntimeException + */ + public function getTableCreate($tables) + { + $this->connect(); + + // 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 12.1 + * @throws RuntimeException + */ + public function getTableColumns($table, $typeOnly = true) + { + $this->connect(); + + $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. + // TODO: Come up with and implement a standard across databases. + $columns[$field->NAME] = (object) array( + 'Field' => $field->NAME, + 'Type' => $field->TYPE, + 'Null' => ($field->NOTNULL == '1' ? 'NO' : 'YES'), + 'Default' => $field->DFLT_VALUE, + 'Key' => ($field->PK != '0' ? '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 12.1 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + $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 12.1 + * @throws RuntimeException + */ + public function getTableList() + { + $this->connect(); + + $type = 'table'; + + $query = $this->getQuery(true) + ->select('name') + ->from('sqlite_master') + ->where('type = :type') + ->bind(':type', $type) + ->order('name'); + + $this->setQuery($query); + + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Get the version of the database connector. + * + * @return string The database connector version. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + + $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 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + 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 12.1 + */ + public function setUtf() + { + $this->connect(); + + return false; + } + + /** + * Locks a table in the database. + * + * @param string $table The name of the table to unlock. + * + * @return FOFDatabaseDriverSqlite Returns this object to support chaining. + * + * @since 12.1 + * @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 FOFDatabaseDriverSqlite Returns this object to support chaining. + * + * @since 12.1 + * @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 FOFDatabaseDriverSqlite Returns this object to support chaining. + * + * @since 12.1 + * @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 12.1 + */ + public static function isSupported() + { + return class_exists('PDO') && in_array('sqlite', PDO::getAvailableDrivers()); + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.3 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionCommit($toSavepoint); + } + else + { + $this->transactionDepth--; + } + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.3 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + parent::transactionRollback($toSavepoint); + } + 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 12.3 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + parent::transactionStart($asSavepoint); + } + + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/driver/sqlsrv.php b/deployed/akeeba/libraries/fof/database/driver/sqlsrv.php new file mode 100644 index 00000000..cf47e2b8 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/driver/sqlsrv.php @@ -0,0 +1,1171 @@ +disconnect(); + } + + /** + * Connects to the database if needed. + * + * @return void Returns void if the database connected successfully. + * + * @since 12.1 + * @throws RuntimeException + */ + public function connect() + { + if ($this->connection) + { + return; + } + + // Build the connection configuration array. + $config = array( + 'Database' => $this->options['database'], + 'uid' => $this->options['user'], + 'pwd' => $this->options['password'], + 'CharacterSet' => 'UTF-8', + 'ReturnDatesAsStrings' => true); + + // Make sure the SQLSRV extension for PHP is installed and enabled. + if (!function_exists('sqlsrv_connect')) + { + throw new RuntimeException('PHP extension sqlsrv_connect is not available.'); + } + + // Attempt to connect to the server. + if (!($this->connection = @ sqlsrv_connect($this->options['host'], $config))) + { + throw new RuntimeException('Database sqlsrv_connect failed'); + } + + // 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->options['select'] && !empty($this->options['database'])) + { + $this->select($this->options['database']); + } + + // Set charactersets. + $this->utf = $this->setUtf(); + } + + /** + * Disconnects the database. + * + * @return void + * + * @since 12.1 + */ + public function disconnect() + { + // Close the connection. + if (is_resource($this->connection)) + { + foreach ($this->disconnectHandlers as $h) + { + call_user_func_array($h, array( &$this)); + } + + sqlsrv_close($this->connection); + } + + $this->connection = null; + } + + /** + * Get table constraints + * + * @param string $tableName The name of the database table. + * + * @return array Any constraints available for the table. + * + * @since 12.1 + */ + protected function getTableConstraints($tableName) + { + $this->connect(); + + $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 + * + * @since 12.1 + */ + protected function renameConstraints($constraints = array(), $prefix = null, $backup = null) + { + $this->connect(); + + foreach ($constraints as $constraint) + { + $this->setQuery('sp_rename ' . $constraint . ',' . str_replace($prefix, $backup, $constraint)); + $this->execute(); + } + } + + /** + * 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. + * + * @since 12.1 + */ + 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. + * + * @since 12.1 + */ + 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 FOFDatabaseDriverSqlsrv Returns this object to support chaining. + * + * @since 12.1 + */ + public function dropTable($tableName, $ifExists = true) + { + $this->connect(); + + $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. + * + * @since 12.1 + */ + public function getAffectedRows() + { + $this->connect(); + + 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. + * + * @since 12.1 + */ + public function getCollation() + { + // TODO: Not fake this + return 'MSSQL UTF-8 (UCS2)'; + } + + /** + * Method to get the database connection collation, as reported by the driver. If the connector doesn't support + * reporting this value please return an empty string. + * + * @return string + */ + public function getConnectionCollation() + { + // 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. + * + * @since 12.1 + */ + public function getNumRows($cursor = null) + { + $this->connect(); + + return sqlsrv_num_rows($cursor ? $cursor : $this->cursor); + } + + /** + * 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. + * + * @since 12.1 + * @throws RuntimeException + */ + 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) + { + if (stristr(strtolower($field->Type), "nvarchar")) + { + $field->Default = ""; + } + $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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableCreate($tables) + { + $this->connect(); + + 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableKeys($table) + { + $this->connect(); + + // 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. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getTableList() + { + $this->connect(); + + // 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. + * + * @since 12.1 + */ + public function getVersion() + { + $this->connect(); + + $version = sqlsrv_server_info($this->connection); + + return $version['SQLServerVersion']; + } + + /** + * 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. + * + * @since 12.1 + * @throws RuntimeException + */ + 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) + { + // Only process non-null scalars. + 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. + * + * @since 12.1 + */ + public function insertid() + { + $this->connect(); + + // TODO: SELECT IDENTITY + $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. + * + * @since 12.1 + * @throws RuntimeException + */ + 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 12.1 + * @throws RuntimeException + * @throws Exception + */ + public function execute() + { + $this->connect(); + + if (!is_resource($this->connection)) + { + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database'); + } + 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->sql instanceof FOFDatabaseQuery) && ($this->limit > 0 || $this->offset > 0)) + { + $query = $this->limit($query, $this->limit, $this->offset); + } + + // Increment the query counter. + $this->count++; + + // Reset the error values. + $this->errorNum = 0; + $this->errorMsg = ''; + + // If debugging is enabled then let's log the query. + if ($this->debug) + { + // Add the query to the object queue. + $this->log[] = $query; + + if (class_exists('JLog')) + { + JLog::add($query, JLog::DEBUG, 'databasequery'); + } + + $this->timings[] = microtime(true); + } + + // 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 ($this->debug) + { + $this->timings[] = microtime(true); + + if (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) + { + $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + else + { + $this->callStacks[] = debug_backtrace(); + } + } + + // If an error occurred handle it. + if (!$this->cursor) + { + // Get the error number and message before we execute any more queries. + $errorNum = $this->getErrorNumber(); + $errorMsg = $this->getErrorMessage($query); + + // Check if the server was disconnected. + if (!$this->connected()) + { + try + { + // Attempt to reconnect. + $this->connection = null; + $this->connect(); + } + // If connect fails, ignore that exception and throw the normal exception. + catch (RuntimeException $e) + { + // Get the error number and message. + $this->errorNum = $this->getErrorNumber(); + $this->errorMsg = $this->getErrorMessage($query); + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum, $e); + } + + // Since we were able to reconnect, run the query again. + return $this->execute(); + } + // The server was not disconnected. + else + { + // Get the error number and message from before we tried to reconnect. + $this->errorNum = $errorNum; + $this->errorMsg = $errorMsg; + + // Throw the normal query exception. + if (class_exists('JLog')) + { + JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error'); + } + + throw new RuntimeException($this->errorMsg, $this->errorNum); + } + } + + return $this->cursor; + } + + /** + * This function replaces a string identifier $prefix with the string held is the + * tablePrefix class variable. + * + * @param string $query The SQL statement to prepare. + * @param string $prefix The common table prefix. + * + * @return string The processed SQL statement. + * + * @since 12.1 + */ + public function replacePrefix($query, $prefix = '#__') + { + $startPos = 0; + $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 12.1 + * @throws RuntimeException + */ + public function select($database) + { + $this->connect(); + + 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. + * + * @since 12.1 + */ + public function setUtf() + { + return false; + } + + /** + * Method to commit a transaction. + * + * @param boolean $toSavepoint If true, commit to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionCommit($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('COMMIT TRANSACTION')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $this->transactionDepth--; + } + + /** + * Method to roll back a transaction. + * + * @param boolean $toSavepoint If true, rollback to the last savepoint. + * + * @return void + * + * @since 12.1 + * @throws RuntimeException + */ + public function transactionRollback($toSavepoint = false) + { + $this->connect(); + + if (!$toSavepoint || $this->transactionDepth <= 1) + { + if ($this->setQuery('ROLLBACK TRANSACTION')->execute()) + { + $this->transactionDepth = 0; + } + + return; + } + + $savepoint = 'SP_' . ($this->transactionDepth - 1); + $this->setQuery('ROLLBACK TRANSACTION ' . $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 12.1 + * @throws RuntimeException + */ + public function transactionStart($asSavepoint = false) + { + $this->connect(); + + if (!$asSavepoint || !$this->transactionDepth) + { + if ($this->setQuery('BEGIN TRANSACTION')->execute()) + { + $this->transactionDepth = 1; + } + + return; + } + + $savepoint = 'SP_' . $this->transactionDepth; + $this->setQuery('BEGIN TRANSACTION ' . $this->quoteName($savepoint)); + + if ($this->execute()) + { + $this->transactionDepth++; + } + } + + /** + * 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. + * + * @since 12.1 + */ + 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. + * + * @since 12.1 + */ + protected 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. + * + * @since 12.1 + */ + 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 + * + * @since 12.1 + */ + protected 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. + * + * @since 12.1 + */ + protected function checkFieldExists($table, $field) + { + $this->connect(); + + $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. + * + * @since 12.1 + */ + protected function limit($query, $limit, $offset) + { + if ($limit == 0 && $offset == 0) + { + return $query; + } + + $start = $offset + 1; + $end = $offset + $limit; + + $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('/\sFROM\s/i', $rowNumberText, $query, 1); + + 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 FOFDatabaseDriverSqlsrv Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + 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 FOFDatabaseDriverSqlsrv Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function lockTable($tableName) + { + return $this; + } + + /** + * Unlocks tables in the database. + * + * @return FOFDatabaseDriverSqlsrv Returns this object to support chaining. + * + * @since 12.1 + * @throws RuntimeException + */ + public function unlockTables() + { + return $this; + } + + /** + * Return the actual SQL Error number + * + * @return integer The SQL Error number + * + * @since 3.4.6 + */ + protected function getErrorNumber() + { + $errors = sqlsrv_errors(); + + return $errors[0]['SQLSTATE']; + } + + /** + * Return the actual SQL Error message + * + * @param string $query The SQL Query that fails + * + * @return string The SQL Error message + * + * @since 3.4.6 + */ + protected function getErrorMessage($query) + { + $errors = sqlsrv_errors(); + $errorMessage = (string) $errors[0]['message']; + + // Replace the Databaseprefix with `#__` if we are not in Debug + if (!$this->debug) + { + $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage); + $query = str_replace($this->tablePrefix, '#__', $query); + } + + return $errorMessage . ' SQL=' . $query; + } +} diff --git a/deployed/akeeba/libraries/fof/database/factory.php b/deployed/akeeba/libraries/fof/database/factory.php new file mode 100644 index 00000000..681247ad --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/factory.php @@ -0,0 +1,127 @@ +getMessage()), $e->getCode(), $e); + } + + return $instance; + } + + /** + * Gets an instance of the factory object. + * + * @return FOFDatabaseFactory + * + * @since 12.1 + */ + public static function getInstance() + { + return self::$_instance ? self::$_instance : new FOFDatabaseFactory; + } + + /** + * Get the current query object or a new FOFDatabaseQuery object. + * + * @param string $name Name of the driver you want an query object for. + * @param FOFDatabaseDriver $db Optional FOFDatabaseDriver instance + * + * @return FOFDatabaseQuery The current query object or a new object extending the FOFDatabaseQuery class. + * + * @since 12.1 + * @throws RuntimeException + */ + public function getQuery($name, FOFDatabaseDriver $db = null) + { + // Derive the class name from the driver. + $class = 'FOFDatabaseQuery' . ucfirst(strtolower($name)); + + // Make sure we have a query class for this driver. + if (!class_exists($class)) + { + // If it doesn't exist we are at an impasse so throw an exception. + throw new RuntimeException('Database Query class not found'); + } + + return new $class($db); + } + + /** + * Gets an instance of a factory object to return on subsequent calls of getInstance. + * + * @param FOFDatabaseFactory $instance A FOFDatabaseFactory object. + * + * @return void + * + * @since 12.1 + */ + public static function setInstance(FOFDatabaseFactory $instance = null) + { + self::$_instance = $instance; + } +} diff --git a/deployed/akeeba/libraries/fof/database/installer.php b/deployed/akeeba/libraries/fof/database/installer.php new file mode 100644 index 00000000..c7aec16e --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/installer.php @@ -0,0 +1,1020 @@ +input = $config['input']; + } + else + { + $this->input = new FOFInput($config['input']); + } + } + else + { + $this->input = new FOFInput; + } + + // Set the database object + if (array_key_exists('dbo', $config)) + { + $this->db = $config['dbo']; + } + else + { + $this->db = FOFPlatform::getInstance()->getDbo(); + } + + // Set the $name/$_name variable + $component = $this->input->getCmd('option', 'com_foobar'); + + if (array_key_exists('option', $config)) + { + $component = $config['option']; + } + + // Figure out where the XML schema files are stored + if (array_key_exists('dbinstaller_directory', $config)) + { + $this->xmlDirectory = $config['dbinstaller_directory']; + } + else + { + // Nothing is defined, assume the files are stored in the sql/xml directory inside the component's administrator section + $directories = FOFPlatform::getInstance()->getComponentBaseDirs($component); + $this->setXmlDirectory($directories['admin'] . '/sql/xml'); + } + + // Do we have a set of XML files to look for? + if (array_key_exists('dbinstaller_files', $config)) + { + $files = $config['dbinstaller_files']; + + if (!is_array($files)) + { + $files = explode(',', $files); + } + + $this->xmlFiles = $files; + } + + } + + /** + * Sets the directory where XML schema files are stored + * + * @param string $xmlDirectory + */ + public function setXmlDirectory($xmlDirectory) + { + $this->xmlDirectory = $xmlDirectory; + } + + /** + * Returns the directory where XML schema files are stored + * + * @return string + */ + public function getXmlDirectory() + { + return $this->xmlDirectory; + } + + /** + * Creates or updates the database schema + * + * @return void + * + * @throws Exception When a database query fails and it doesn't have the canfail flag + */ + public function updateSchema() + { + // Get the schema XML file + $xml = $this->findSchemaXml(); + + if (empty($xml)) + { + return; + } + + // Make sure there are SQL commands in this file + if (!$xml->sql) + { + return; + } + + // Walk the sql > action tags to find all tables + /** @var SimpleXMLElement $actions */ + $actions = $xml->sql->children(); + + /** + * The meta/autocollation node defines if I should automatically apply the correct collation (utf8 or utf8mb4) + * to the database tables managed by the schema updater. When enabled (default) the queries are automatically + * converted to the correct collation (utf8mb4_unicode_ci or utf8_general_ci) depending on whether your Joomla! + * and MySQL server support Multibyte UTF-8 (UTF8MB4). Moreover, if UTF8MB4 is supported, all CREATE TABLE + * queries are analyzed and the tables referenced in them are auto-converted to the proper utf8mb4 collation. + */ + $autoCollationConversion = true; + + if ($xml->meta->autocollation) + { + $value = (string) $xml->meta->autocollation; + $value = trim($value); + $value = strtolower($value); + + $autoCollationConversion = in_array($value, array('true', '1', 'on', 'yes')); + } + + try + { + $hasUtf8mb4Support = $this->db->hasUTF8mb4Support(); + } + catch (\Exception $e) + { + $hasUtf8mb4Support = false; + } + + $tablesToConvert = array(); + + /** @var SimpleXMLElement $action */ + foreach ($actions as $action) + { + // Get the attributes + $attributes = $action->attributes(); + + // Get the table / view name + $table = $attributes->table ? (string)$attributes->table : ''; + + if (empty($table)) + { + continue; + } + + // Am I allowed to let this action fail? + $canFailAction = $attributes->canfail ? $attributes->canfail : 0; + + // Evaluate conditions + $shouldExecute = true; + + /** @var SimpleXMLElement $node */ + foreach ($action->children() as $node) + { + if ($node->getName() == 'condition') + { + // Get the operator + $operator = $node->attributes()->operator ? (string)$node->attributes()->operator : 'and'; + $operator = empty($operator) ? 'and' : $operator; + + $condition = $this->conditionMet($table, $node); + + switch ($operator) + { + case 'not': + $shouldExecute = $shouldExecute && !$condition; + break; + + case 'or': + $shouldExecute = $shouldExecute || $condition; + break; + + case 'nor': + $shouldExecute = !$shouldExecute && !$condition; + break; + + case 'xor': + $shouldExecute = ($shouldExecute xor $condition); + break; + + case 'maybe': + $shouldExecute = $condition ? true : $shouldExecute; + break; + + default: + $shouldExecute = $shouldExecute && $condition; + break; + } + } + + // DO NOT USE BOOLEAN SHORT CIRCUIT EVALUATION! + // if (!$shouldExecute) break; + } + + // Do I have to only collect the tables from CREATE TABLE queries? + $onlyCollectTables = !$shouldExecute && $autoCollationConversion && $hasUtf8mb4Support; + + // Make sure all conditions are met OR I have to collect tables from CREATE TABLE queries. + if (!$shouldExecute && !$onlyCollectTables) + { + continue; + } + + // Execute queries + foreach ($action->children() as $node) + { + if ($node->getName() == 'query') + { + $query = (string) $node; + + if ($autoCollationConversion && $hasUtf8mb4Support) + { + $this->extractTablesToConvert($query, $tablesToConvert); + } + + // If we're only collecting tables do not run the queries + if ($onlyCollectTables) + { + continue; + } + + $canFail = $node->attributes->canfail ? (string)$node->attributes->canfail : $canFailAction; + + if (is_string($canFail)) + { + $canFail = strtoupper($canFail); + } + + $canFail = (in_array($canFail, array(true, 1, 'YES', 'TRUE'))); + + // Do I need to automatically convert the collation of all CREATE / ALTER queries? + if ($autoCollationConversion) + { + if ($hasUtf8mb4Support) + { + // We have UTF8MB4 support. Convert all queries to UTF8MB4. + $query = $this->convertUtf8QueryToUtf8mb4($query); + } + else + { + // We do not have UTF8MB4 support. Convert all queries to plain old UTF8. + $query = $this->convertUtf8mb4QueryToUtf8($query); + } + } + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (Exception $e) + { + // If we are not allowed to fail, throw back the exception we caught + if (!$canFail) + { + throw $e; + } + } + } + } + } + + // Auto-convert the collation of tables if we are told to do so, have utf8mb4 support and a list of tables. + if ($autoCollationConversion && $hasUtf8mb4Support && !empty($tablesToConvert)) + { + $this->convertTablesToUtf8mb4($tablesToConvert); + } + } + + /** + * Uninstalls the database schema + * + * @return void + */ + public function removeSchema() + { + // Get the schema XML file + $xml = $this->findSchemaXml(); + + if (empty($xml)) + { + return; + } + + // Make sure there are SQL commands in this file + if (!$xml->sql) + { + return; + } + + // Walk the sql > action tags to find all tables + $tables = array(); + /** @var SimpleXMLElement $actions */ + $actions = $xml->sql->children(); + + /** @var SimpleXMLElement $action */ + foreach ($actions as $action) + { + $attributes = $action->attributes(); + $tables[] = (string)$attributes->table; + } + + // Simplify the tables list + $tables = array_unique($tables); + + // Start dropping tables + foreach ($tables as $table) + { + try + { + $this->db->dropTable($table); + } + catch (Exception $e) + { + // Do not fail if I can't drop the table + } + } + } + + /** + * Find an suitable schema XML file for this database type and return the SimpleXMLElement holding its information + * + * @return null|SimpleXMLElement Null if no suitable schema XML file is found + */ + protected function findSchemaXml() + { + $driverType = $this->db->name; + $xml = null; + + // And now look for the file + foreach ($this->xmlFiles as $baseName) + { + // Remove any accidental whitespace + $baseName = trim($baseName); + + // Get the full path to the file + $fileName = $this->xmlDirectory . '/' . $baseName . '.xml'; + + // Make sure the file exists + if (!@file_exists($fileName)) + { + continue; + } + + // Make sure the file is a valid XML document + try + { + $xml = new SimpleXMLElement($fileName, LIBXML_NONET, true); + } + catch (Exception $e) + { + $xml = null; + continue; + } + + // Make sure the file is an XML schema file + if ($xml->getName() != 'schema') + { + $xml = null; + continue; + } + + if (!$xml->meta) + { + $xml = null; + continue; + } + + if (!$xml->meta->drivers) + { + $xml = null; + continue; + } + + /** @var SimpleXMLElement $drivers */ + $drivers = $xml->meta->drivers; + + // Strict driver name match + foreach ($drivers->children() as $driverTypeTag) + { + $thisDriverType = (string)$driverTypeTag; + + if ($thisDriverType == $driverType) + { + return $xml; + } + } + + // Some custom database drivers use a non-standard $name variable. Let try a relaxed match. + foreach ($drivers->children() as $driverTypeTag) + { + $thisDriverType = (string)$driverTypeTag; + + if ( + // e.g. $driverType = 'mysqlistupid', $thisDriverType = 'mysqli' => driver matched + strpos($driverType, $thisDriverType) === 0 + // e.g. $driverType = 'stupidmysqli', $thisDriverType = 'mysqli' => driver matched + || (substr($driverType, -strlen($thisDriverType)) == $thisDriverType) + ) + { + return $xml; + } + } + + $xml = null; + } + + return $xml; + } + + /** + * Checks if a condition is met + * + * @param string $table The table we're operating on + * @param SimpleXMLElement $node The condition definition node + * + * @return bool + */ + protected function conditionMet($table, SimpleXMLElement $node) + { + if (empty(static::$allTables)) + { + static::$allTables = $this->db->getTableList(); + } + + // Does the table exist? + $tableNormal = $this->db->replacePrefix($table); + $tableExists = in_array($tableNormal, static::$allTables); + + // Initialise + $condition = false; + + // Get the condition's attributes + $attributes = $node->attributes(); + $type = $attributes->type ? $attributes->type : null; + $value = $attributes->value ? (string) $attributes->value : null; + + switch ($type) + { + // Check if a table or column is missing + case 'missing': + $fieldName = (string)$value; + + if (empty($fieldName)) + { + $condition = !$tableExists; + } + else + { + try + { + $tableColumns = $this->db->getTableColumns($tableNormal, true); + } + catch (\Exception $e) + { + $tableColumns = array(); + } + + $condition = !array_key_exists($fieldName, $tableColumns); + } + + break; + + // Check if a column type matches the "coltype" attribute + case 'type': + try + { + $tableColumns = $this->db->getTableColumns($tableNormal, false); + } + catch (\Exception $e) + { + $tableColumns = array(); + } + + $condition = false; + + if (array_key_exists($value, $tableColumns)) + { + $coltype = $attributes->coltype ? $attributes->coltype : null; + + if (!empty($coltype)) + { + $coltype = strtolower($coltype); + $currentType = strtolower($tableColumns[$value]->Type); + + $condition = ($coltype == $currentType); + } + } + + break; + + // Check if a (named) index exists on the table. Currently only supported on MySQL. + case 'index': + $indexName = (string) $value; + $condition = true; + + if (!empty($indexName)) + { + $indexName = str_replace('#__', $this->db->getPrefix(), $indexName); + $condition = $this->hasIndex($tableNormal, $indexName); + } + + break; + + // Check if a table or column needs to be upgraded to utf8mb4 + case 'utf8mb4upgrade': + $condition = false; + + // Check if the driver and the database connection have UTF8MB4 support + try + { + $hasUtf8mb4Support = $this->db->hasUTF8mb4Support(); + } + catch (\Exception $e) + { + $hasUtf8mb4Support = false; + } + + if ($hasUtf8mb4Support) + { + $fieldName = (string)$value; + + if (empty($fieldName)) + { + $collation = $this->getTableCollation($tableNormal); + } + else + { + $collation = $this->getColumnCollation($tableNormal, $fieldName); + } + + $parts = explode('_', $collation, 3); + $encoding = empty($parts[0]) ? '' : strtolower($parts[0]); + + $condition = $encoding != 'utf8mb4'; + } + + break; + + // Check if the result of a query matches our expectation + case 'equals': + $query = (string)$node; + $this->db->setQuery($query); + + try + { + $result = $this->db->loadResult(); + $condition = ($result == $value); + } + catch (Exception $e) + { + return false; + } + + break; + + // Always returns true + case 'true': + return true; + break; + + default: + return false; + break; + } + + return $condition; + } + + /** + * Get the collation of a table. Uses an internal cache for efficiency. + * + * @param string $tableName The name of the table + * + * @return string The collation, e.g. "utf8_general_ci" + */ + private function getTableCollation($tableName) + { + static $cache = array(); + + $tableName = $this->db->replacePrefix($tableName); + + if (!isset($cache[$tableName])) + { + $cache[$tableName] = $this->realGetTableCollation($tableName); + } + + return $cache[$tableName]; + } + + /** + * Get the collation of a table. This is the internal method used by getTableCollation. + * + * @param string $tableName The name of the table + * + * @return string The collation, e.g. "utf8_general_ci" + */ + private function realGetTableCollation($tableName) + { + try + { + $utf8Support = $this->db->hasUTFSupport(); + } + catch (\Exception $e) + { + $utf8Support = false; + } + + try + { + $utf8mb4Support = $utf8Support && $this->db->hasUTF8mb4Support(); + } + catch (\Exception $e) + { + $utf8mb4Support = false; + } + + $collation = $utf8mb4Support ? 'utf8mb4_unicode_ci' : ($utf8Support ? 'utf_general_ci' : 'latin1_swedish_ci'); + + $query = 'SHOW TABLE STATUS LIKE ' . $this->db->q($tableName); + + try + { + $row = $this->db->setQuery($query)->loadAssoc(); + } + catch (\Exception $e) + { + return $collation; + } + + if (empty($row)) + { + return $collation; + } + + if (!isset($row['Collation'])) + { + return $collation; + } + + if (empty($row['Collation'])) + { + return $collation; + } + + return $row['Collation']; + } + + /** + * Get the collation of a column. Uses an internal cache for efficiency. + * + * @param string $tableName The name of the table + * @param string $columnName The name of the column + * + * @return string The collation, e.g. "utf8_general_ci" + */ + private function getColumnCollation($tableName, $columnName) + { + static $cache = array(); + + $tableName = $this->db->replacePrefix($tableName); + $columnName = $this->db->replacePrefix($columnName); + + if (!isset($cache[$tableName])) + { + $cache[$tableName] = array(); + } + + if (!isset($cache[$tableName][$columnName])) + { + $cache[$tableName][$columnName] = $this->realGetColumnCollation($tableName, $columnName); + } + + return $cache[$tableName][$columnName]; + } + + /** + * Get the collation of a column. This is the internal method used by getColumnCollation. + * + * @param string $tableName The name of the table + * @param string $columnName The name of the column + * + * @return string The collation, e.g. "utf8_general_ci" + */ + private function realGetColumnCollation($tableName, $columnName) + { + $collation = $this->getTableCollation($tableName); + + $query = 'SHOW FULL COLUMNS FROM ' . $this->db->qn($tableName) . ' LIKE ' . $this->db->q($columnName); + + try + { + $row = $this->db->setQuery($query)->loadAssoc(); + } + catch (\Exception $e) + { + return $collation; + } + + if (empty($row)) + { + return $collation; + } + + if (!isset($row['Collation'])) + { + return $collation; + } + + if (empty($row['Collation'])) + { + return $collation; + } + + return $row['Collation']; + } + + /** + * Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8. + * + * We use our own method so we can be site it works even on Joomla! 3.4 or earlier, where UTF8MB4 support is not + * implemented. + * + * @param string $query The query to convert + * + * @return string The converted query + */ + private function convertUtf8mb4QueryToUtf8($query) + { + // If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert + $beginningOfQuery = substr($query, 0, 12); + $beginningOfQuery = strtoupper($beginningOfQuery); + + if (!in_array($beginningOfQuery, array('ALTER TABLE ', 'CREATE TABLE'))) + { + return $query; + } + + // Replace utf8mb4 with utf8 + $from = array( + 'utf8mb4_unicode_ci', + 'utf8mb4_', + 'utf8mb4', + ); + + $to = array( + 'utf8_general_ci', // Yeah, we convert utf8mb4_unicode_ci to utf8_general_ci per Joomla!'s conventions + 'utf8_', + 'utf8', + ); + + return str_replace($from, $to, $query); + } + + /** + * Automatically upgrade a CREATE TABLE or ALTER TABLE query from plain utf8 to utf8mb4 (UTF-8 Multibyte). + * + * @param string $query The query to convert + * + * @return string The converted query + */ + private function convertUtf8QueryToUtf8mb4($query) + { + // If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert + $beginningOfQuery = substr($query, 0, 12); + $beginningOfQuery = strtoupper($beginningOfQuery); + + if (!in_array($beginningOfQuery, array('ALTER TABLE ', 'CREATE TABLE'))) + { + return $query; + } + + // Replace utf8 with utf8mb4 + $from = array( + 'utf8_general_ci', + 'utf8_', + 'utf8', + ); + + $to = array( + 'utf8mb4_unicode_ci', // Yeah, we convert utf8_general_ci to utf8mb4_unicode_ci per Joomla!'s conventions + 'utf8mb4_', + 'utf8mb4', + ); + + return str_replace($from, $to, $query); + } + + /** + * Analyzes a query. If it's a CREATE TABLE query the table is added to the $tables array. + * + * @param string $query The query to analyze + * @param string $tables The array where the name of the detected table is added + * + * @return void + */ + private function extractTablesToConvert($query, &$tables) + { + // Normalize the whitespace of the query + $query = trim($query); + $query = str_replace(array("\r\n", "\r", "\n"), ' ', $query); + + while (strstr($query, ' ') !== false) + { + $query = str_replace(' ', ' ', $query); + } + + // Is it a create table query? + $queryStart = substr($query, 0, 12); + $queryStart = strtoupper($queryStart); + + if ($queryStart != 'CREATE TABLE') + { + return; + } + + // Remove the CREATE TABLE keyword. Also, If there's an IF NOT EXISTS clause remove it. + $query = substr($query, 12); + $query = str_ireplace('IF NOT EXISTS', '', $query); + $query = trim($query); + + // Make sure there is a space between the table name and its definition, denoted by an open parenthesis + $query = str_replace('(', ' (', $query); + + // Now we should have the name of the table, a space and the rest of the query. Extract the table name. + $parts = explode(' ', $query, 2); + $tableName = $parts[0]; + + /** + * The table name may be quoted. Since UTF8MB4 is only supported in MySQL, the table name can only be + * quoted with surrounding backticks. Therefore we can trim backquotes from the table name to unquote it! + **/ + $tableName = trim($tableName, '`'); + + // Finally, add the table name to $tables if it doesn't already exist. + if (!in_array($tableName, $tables)) + { + $tables[] = $tableName; + } + } + + /** + * Converts the collation of tables listed in $tablesToConvert to utf8mb4_unicode_ci + * + * @param array $tablesToConvert The list of tables to convert + * + * @return void + */ + private function convertTablesToUtf8mb4($tablesToConvert) + { + try + { + $utf8mb4Support = $this->db->hasUTF8mb4Support(); + } + catch (\Exception $e) + { + $utf8mb4Support = false; + } + + // Make sure the database driver REALLY has support for converting character sets + if (!$utf8mb4Support) + { + return; + } + + asort($tablesToConvert); + + foreach ($tablesToConvert as $tableName) + { + $collation = $this->getTableCollation($tableName); + + $parts = explode('_', $collation, 3); + $encoding = empty($parts[0]) ? '' : strtolower($parts[0]); + + if ($encoding != 'utf8mb4') + { + $queries = $this->db->getAlterTableCharacterSet($tableName); + + try + { + foreach ($queries as $query) + { + $this->db->setQuery($query)->execute(); + } + } + catch (\Exception $e) + { + // We ignore failed conversions. Remember, you MUST change your indices MANUALLY. + } + } + } + } + + /** + * Returns true if table $tableName has an index named $indexName or if it's impossible to retrieve index names for + * the table (not enough privileges, not a MySQL database, ...) + * + * @param string $tableName The name of the table + * @param string $indexName The name of the index + * + * @return bool + */ + private function hasIndex($tableName, $indexName) + { + static $isMySQL = null; + static $cache = array(); + + if (is_null($isMySQL)) + { + $driverType = $this->db->name; + $driverType = strtolower($driverType); + $isMySQL = true; + + if ( + !strpos($driverType, 'mysql') === 0 + && !(substr($driverType, -5) == 'mysql') + && !(substr($driverType, -6) == 'mysqli') + ) + { + $isMySQL = false; + } + } + + // Not MySQL? Lie and return true. + if (!$isMySQL) + { + return true; + } + + if (!isset($cache[$tableName])) + { + $cache[$tableName] = array(); + } + + if (!isset($cache[$tableName][$indexName])) + { + $cache[$tableName][$indexName] = true; + + try + { + $indices = array(); + $query = 'SHOW INDEXES FROM ' . $this->db->qn($tableName); + $indexDefinitions = $this->db->setQuery($query)->loadAssocList(); + + if (!empty($indexDefinitions) && is_array($indexDefinitions)) + { + foreach ($indexDefinitions as $def) + { + $indices[] = $def['Key_name']; + } + + $indices = array_unique($indices); + } + + $cache[$tableName][$indexName] = in_array($indexName, $indices); + } + catch (\Exception $e) + { + // Ignore errors + } + } + + return $cache[$tableName][$indexName]; + } +} diff --git a/deployed/akeeba/libraries/fof/database/interface.php b/deployed/akeeba/libraries/fof/database/interface.php new file mode 100644 index 00000000..21be6112 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/interface.php @@ -0,0 +1,37 @@ +_tableObject = FOFTable::getInstance($parts[2], ucfirst($parts[0]) . ucfirst($parts[1]))->getClone(); + + $this->cursor = $cursor; + $this->class = 'stdClass'; + $this->_column = $column; + $this->_fetched = 0; + + $this->next(); + } + + /** + * Database iterator destructor. + */ + public function __destruct() + { + if ($this->cursor) + { + $this->freeResult($this->cursor); + } + } + + /** + * The current element in the iterator. + * + * @return object + * + * @see Iterator::current() + */ + public function current() + { + return $this->_currentTable; + } + + /** + * The key of the current element in the iterator. + * + * @return scalar + * + * @see Iterator::key() + */ + public function key() + { + return $this->_key; + } + + /** + * Moves forward to the next result from the SQL query. + * + * @return void + * + * @see Iterator::next() + */ + public function next() + { + // Set the default key as being the number of fetched object + $this->_key = $this->_fetched; + + // Try to get an object + $this->_current = $this->fetchObject(); + + // If an object has been found + if ($this->_current) + { + $this->_currentTable = $this->getTable(); + + // Set the key as being the indexed column (if it exists) + if (isset($this->_current->{$this->_column})) + { + $this->_key = $this->_current->{$this->_column}; + } + + // Update the number of fetched object + $this->_fetched++; + } + } + + /** + * Rewinds the iterator. + * + * This iterator cannot be rewound. + * + * @return void + * + * @see Iterator::rewind() + */ + public function rewind() + { + } + + /** + * Checks if the current position of the iterator is valid. + * + * @return boolean + * + * @see Iterator::valid() + */ + public function valid() + { + return (boolean) $this->_current; + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + abstract protected function fetchObject(); + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + abstract protected function freeResult(); + + /** + * Returns the data in $this->_current as a FOFTable instance + * + * @return FOFTable + * + * @throws OutOfBoundsException + */ + protected function getTable() + { + if (!$this->valid()) + { + throw new OutOfBoundsException('Cannot get item past iterator\'s bounds', 500); + } + + $this->_tableObject->bind($this->_current); + + return $this->_tableObject; + } +} diff --git a/deployed/akeeba/libraries/fof/database/iterator/azure.php b/deployed/akeeba/libraries/fof/database/iterator/azure.php new file mode 100644 index 00000000..384e1050 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/iterator/azure.php @@ -0,0 +1,20 @@ +cursor); + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + protected function fetchObject() + { + return @mysql_fetch_object($this->cursor, $this->class); + } + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + protected function freeResult() + { + @mysql_free_result($this->cursor); + } +} diff --git a/deployed/akeeba/libraries/fof/database/iterator/mysqli.php b/deployed/akeeba/libraries/fof/database/iterator/mysqli.php new file mode 100644 index 00000000..91de4c32 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/iterator/mysqli.php @@ -0,0 +1,51 @@ +cursor); + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + protected function fetchObject() + { + return @mysqli_fetch_object($this->cursor, $this->class); + } + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + protected function freeResult() + { + @mysqli_free_result($this->cursor); + } +} diff --git a/deployed/akeeba/libraries/fof/database/iterator/oracle.php b/deployed/akeeba/libraries/fof/database/iterator/oracle.php new file mode 100644 index 00000000..fc63e9e5 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/iterator/oracle.php @@ -0,0 +1,20 @@ +cursor) && $this->cursor instanceof PDOStatement) + { + return @$this->cursor->rowCount(); + } + else + { + return 0; + } + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + protected function fetchObject() + { + if (!empty($this->cursor) && $this->cursor instanceof PDOStatement) + { + return @$this->cursor->fetchObject($this->class); + } + else + { + return false; + } + } + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + protected function freeResult() + { + if (!empty($this->cursor) && $this->cursor instanceof PDOStatement) + { + @$this->cursor->closeCursor(); + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/iterator/pdomysql.php b/deployed/akeeba/libraries/fof/database/iterator/pdomysql.php new file mode 100644 index 00000000..558455a3 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/iterator/pdomysql.php @@ -0,0 +1,20 @@ +cursor); + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + protected function fetchObject() + { + return @pg_fetch_object($this->cursor, null, $this->class); + } + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + protected function freeResult() + { + @pg_free_result($this->cursor); + } +} diff --git a/deployed/akeeba/libraries/fof/database/iterator/sqlite.php b/deployed/akeeba/libraries/fof/database/iterator/sqlite.php new file mode 100644 index 00000000..02cffb82 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/iterator/sqlite.php @@ -0,0 +1,20 @@ +cursor); + } + + /** + * Method to fetch a row from the result set cursor as an object. + * + * @return mixed Either the next row from the result set or false if there are no more rows. + */ + protected function fetchObject() + { + return @sqlsrv_fetch_object($this->cursor, $this->class); + } + + /** + * Method to free up the memory used for the result set. + * + * @return void + */ + protected function freeResult() + { + @sqlsrv_free_stmt($this->cursor); + } +} diff --git a/deployed/akeeba/libraries/fof/database/query.php b/deployed/akeeba/libraries/fof/database/query.php new file mode 100644 index 00000000..cb610f08 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query.php @@ -0,0 +1,1732 @@ +quote($args[0], isset($args[1]) ? $args[1] : true); + break; + + case 'qn': + return $this->quoteName($args[0], isset($args[1]) ? $args[1] : null); + break; + + case 'e': + return $this->escape($args[0], isset($args[1]) ? $args[1] : false); + break; + } + } + + /** + * Class constructor. + * + * @param FOFDatabaseDriver $db The database driver. + * + * @since 11.1 + */ + public function __construct(FOFDatabaseDriver $db = null) + { + $this->db = $db; + } + + /** + * Magic function to convert the query to a string. + * + * @return string The completed query. + * + * @since 11.1 + */ + public function __toString() + { + $query = ''; + + if ($this->sql) + { + return $this->sql; + } + + switch ($this->type) + { + case 'element': + $query .= (string) $this->element; + break; + + 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->union) + { + $query .= (string) $this->union; + } + + break; + + case 'delete': + $query .= (string) $this->delete; + $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->order) + { + $query .= (string) $this->order; + } + + break; + + case 'update': + $query .= (string) $this->update; + + if ($this->join) + { + // Special case for joins + foreach ($this->join as $join) + { + $query .= (string) $join; + } + } + + $query .= (string) $this->set; + + if ($this->where) + { + $query .= (string) $this->where; + } + + if ($this->order) + { + $query .= (string) $this->order; + } + + break; + + 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->values->getElements(); + + if (!($elements[0] instanceof $this)) + { + $query .= ' VALUES '; + } + + $query .= (string) $this->values; + } + + break; + + case 'call': + $query .= (string) $this->call; + break; + + case 'exec': + $query .= (string) $this->exec; + break; + } + + if ($this instanceof FOFDatabaseQueryLimitable) + { + $query = $this->processLimit($query, $this->limit, $this->offset); + } + + return $query; + } + + /** + * Magic function to get protected variable value + * + * @param string $name The name of the variable. + * + * @return mixed + * + * @since 11.1 + */ + public function __get($name) + { + return isset($this->$name) ? $this->$name : null; + } + + /** + * Add a single column, or array of columns to the CALL clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The call method can, however, be called multiple times in the same query. + * + * Usage: + * $query->call('a.*')->call('b.id'); + * $query->call(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 12.1 + */ + public function call($columns) + { + $this->type = 'call'; + + if (is_null($this->call)) + { + $this->call = new FOFDatabaseQueryElement('CALL', $columns); + } + else + { + $this->call->append($columns); + } + + 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; + } + + /** + * 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 11.1 + */ + public function charLength($field, $operator = null, $condition = null) + { + return 'CHAR_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 FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function clear($clause = null) + { + $this->sql = null; + + switch ($clause) + { + case 'select': + $this->select = null; + $this->type = null; + break; + + case 'delete': + $this->delete = null; + $this->type = null; + break; + + case 'update': + $this->update = null; + $this->type = null; + break; + + case 'insert': + $this->insert = null; + $this->type = null; + $this->autoIncrementField = null; + break; + + case 'from': + $this->from = null; + break; + + case 'join': + $this->join = null; + break; + + case 'set': + $this->set = null; + break; + + case 'where': + $this->where = null; + break; + + case 'group': + $this->group = null; + break; + + case 'having': + $this->having = null; + break; + + case 'order': + $this->order = null; + break; + + case 'columns': + $this->columns = null; + break; + + case 'values': + $this->values = null; + break; + + case 'exec': + $this->exec = null; + $this->type = null; + break; + + case 'call': + $this->call = null; + $this->type = null; + break; + + case 'limit': + $this->offset = 0; + $this->limit = 0; + break; + + case 'offset': + $this->offset = 0; + break; + + case 'union': + $this->union = null; + break; + + case 'unionAll': + $this->unionAll = null; + break; + + default: + $this->type = null; + $this->select = null; + $this->delete = null; + $this->update = null; + $this->insert = null; + $this->from = null; + $this->join = null; + $this->set = null; + $this->where = null; + $this->group = null; + $this->having = null; + $this->order = null; + $this->columns = null; + $this->values = null; + $this->autoIncrementField = null; + $this->exec = null; + $this->call = null; + $this->union = null; + $this->unionAll = null; + $this->offset = 0; + $this->limit = 0; + break; + } + + return $this; + } + + /** + * Adds a column, or array of column names that would be used for an INSERT INTO statement. + * + * @param mixed $columns A column name, or array of column names. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function columns($columns) + { + if (is_null($this->columns)) + { + $this->columns = new FOFDatabaseQueryElement('()', $columns); + } + else + { + $this->columns->append($columns); + } + + return $this; + } + + /** + * 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.1 + */ + public function concatenate($values, $separator = null) + { + if ($separator) + { + return 'CONCATENATE(' . implode(' || ' . $this->quote($separator) . ' || ', $values) . ')'; + } + else + { + return 'CONCATENATE(' . implode(' || ', $values) . ')'; + } + } + + /** + * Gets the current date and time. + * + * Usage: + * $query->where('published_up < '.$query->currentTimestamp()); + * + * @return string + * + * @since 11.1 + */ + public function currentTimestamp() + { + return 'CURRENT_TIMESTAMP()'; + } + + /** + * Returns a PHP date() function compliant date format for the database driver. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the getDateFormat method directly. + * + * @return string The format string. + * + * @since 11.1 + */ + public function dateFormat() + { + if (!($this->db instanceof FOFDatabaseDriver)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_INVALID_DB_OBJECT'); + } + + return $this->db->getDateFormat(); + } + + /** + * Creates a formatted dump of the query for debugging purposes. + * + * Usage: + * echo $query->dump(); + * + * @return string + * + * @since 11.3 + */ + public function dump() + { + return '
        ' . str_replace('#__', $this->db->getPrefix(), $this) . '
        '; + } + + /** + * Add a table name to the DELETE clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->delete('#__a')->where('id = 1'); + * + * @param string $table The name of the table to delete from. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function delete($table = null) + { + $this->type = 'delete'; + $this->delete = new FOFDatabaseQueryElement('DELETE', null); + + if (!empty($table)) + { + $this->from($table); + } + + return $this; + } + + /** + * Method to escape a string for usage in an SQL statement. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the escape method directly. + * + * Note that 'e' is an alias for this method as it is in FOFDatabaseDriver. + * + * @param string $text The string to be escaped. + * @param boolean $extra Optional parameter to provide extra escaping. + * + * @return string The escaped string. + * + * @since 11.1 + * @throws RuntimeException if the internal db property is not a valid object. + */ + public function escape($text, $extra = false) + { + if (!($this->db instanceof FOFDatabaseDriver)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_INVALID_DB_OBJECT'); + } + + return $this->db->escape($text, $extra); + } + + /** + * Add a single column, or array of columns to the EXEC clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The exec method can, however, be called multiple times in the same query. + * + * Usage: + * $query->exec('a.*')->exec('b.id'); + * $query->exec(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 12.1 + */ + public function exec($columns) + { + $this->type = 'exec'; + + if (is_null($this->exec)) + { + $this->exec = new FOFDatabaseQueryElement('EXEC', $columns); + } + else + { + $this->exec->append($columns); + } + + return $this; + } + + /** + * Add a table to the FROM clause of the query. + * + * Note that while an array of tables can be provided, it is recommended you use explicit joins. + * + * Usage: + * $query->select('*')->from('#__a'); + * + * @param mixed $tables A string or array of table names. + * This can be a FOFDatabaseQuery object (or a child of it) when used + * as a subquery in FROM clause along with a value for $subQueryAlias. + * @param string $subQueryAlias Alias used when $tables is a FOFDatabaseQuery. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @throws RuntimeException + * + * @since 11.1 + */ + public function from($tables, $subQueryAlias = null) + { + if (is_null($this->from)) + { + if ($tables instanceof $this) + { + if (is_null($subQueryAlias)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_NULL_SUBQUERY_ALIAS'); + } + + $tables = '( ' . (string) $tables . ' ) AS ' . $this->quoteName($subQueryAlias); + } + + $this->from = new FOFDatabaseQueryElement('FROM', $tables); + } + else + { + $this->from->append($tables); + } + + 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 'YEAR(' . $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 'MONTH(' . $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 'DAY(' . $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 'HOUR(' . $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 'MINUTE(' . $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 'SECOND(' . $date . ')'; + } + + /** + * Add a grouping column to the GROUP clause of the query. + * + * Usage: + * $query->group('id'); + * + * @param mixed $columns A string or array of ordering columns. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function group($columns) + { + if (is_null($this->group)) + { + $this->group = new FOFDatabaseQueryElement('GROUP BY', $columns); + } + else + { + $this->group->append($columns); + } + + return $this; + } + + /** + * A conditions to the HAVING clause of the query. + * + * Usage: + * $query->group('id')->having('COUNT(id) > 5'); + * + * @param mixed $conditions A string or array of columns. + * @param string $glue The glue by which to join the conditions. Defaults to AND. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function having($conditions, $glue = 'AND') + { + if (is_null($this->having)) + { + $glue = strtoupper($glue); + $this->having = new FOFDatabaseQueryElement('HAVING', $conditions, " $glue "); + } + else + { + $this->having->append($conditions); + } + + return $this; + } + + /** + * Add an INNER JOIN clause to the query. + * + * Usage: + * $query->innerJoin('b ON b.id = a.id')->innerJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function innerJoin($condition) + { + $this->join('INNER', $condition); + + return $this; + } + + /** + * Add a table name to the INSERT clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->insert('#__a')->set('id = 1'); + * $query->insert('#__a')->columns('id, title')->values('1,2')->values('3,4'); + * $query->insert('#__a')->columns('id, title')->values(array('1,2', '3,4')); + * + * @param mixed $table The name of the table to insert data into. + * @param boolean $incrementField The name of the field to auto increment. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function insert($table, $incrementField=false) + { + $this->type = 'insert'; + $this->insert = new FOFDatabaseQueryElement('INSERT INTO', $table); + $this->autoIncrementField = $incrementField; + + return $this; + } + + /** + * Add a JOIN clause to the query. + * + * Usage: + * $query->join('INNER', 'b ON b.id = a.id); + * + * @param string $type The type of join. This string is prepended to the JOIN keyword. + * @param string $conditions A string or array of conditions. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function join($type, $conditions) + { + if (is_null($this->join)) + { + $this->join = array(); + } + + $this->join[] = new FOFDatabaseQueryElement(strtoupper($type) . ' JOIN', $conditions); + + return $this; + } + + /** + * Add a LEFT JOIN clause to the query. + * + * Usage: + * $query->leftJoin('b ON b.id = a.id')->leftJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function leftJoin($condition) + { + $this->join('LEFT', $condition); + + return $this; + } + + /** + * Get the length of a string in bytes. + * + * Note, use 'charLength' to find the number of characters in a string. + * + * Usage: + * query->where($query->length('a').' > 3'); + * + * @param string $value The string to measure. + * + * @return int + * + * @since 11.1 + */ + public function length($value) + { + return 'LENGTH(' . $value . ')'; + } + + /** + * Get the null or zero representation of a timestamp for the database driver. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the nullDate method directly. + * + * Usage: + * $query->where('modified_date <> '.$query->nullDate()); + * + * @param boolean $quoted Optionally wraps the null date in database quotes (true by default). + * + * @return string Null or zero representation of a timestamp. + * + * @since 11.1 + */ + public function nullDate($quoted = true) + { + if (!($this->db instanceof FOFDatabaseDriver)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_INVALID_DB_OBJECT'); + } + + $result = $this->db->getNullDate($quoted); + + if ($quoted) + { + return $this->db->quote($result); + } + + return $result; + } + + /** + * Add a ordering column to the ORDER clause of the query. + * + * Usage: + * $query->order('foo')->order('bar'); + * $query->order(array('foo','bar')); + * + * @param mixed $columns A string or array of ordering columns. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function order($columns) + { + if (is_null($this->order)) + { + $this->order = new FOFDatabaseQueryElement('ORDER BY', $columns); + } + else + { + $this->order->append($columns); + } + + return $this; + } + + /** + * Add an OUTER JOIN clause to the query. + * + * Usage: + * $query->outerJoin('b ON b.id = a.id')->outerJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function outerJoin($condition) + { + $this->join('OUTER', $condition); + + return $this; + } + + /** + * Method to quote and optionally escape a string to database requirements for insertion into the database. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the quote method directly. + * + * Note that 'q' is an alias for this method as it is in FOFDatabaseDriver. + * + * Usage: + * $query->quote('fulltext'); + * $query->q('fulltext'); + * $query->q(array('option', 'fulltext')); + * + * @param mixed $text A string or an array of strings to quote. + * @param boolean $escape True to escape the string, false to leave it unchanged. + * + * @return string The quoted input string. + * + * @since 11.1 + * @throws RuntimeException if the internal db property is not a valid object. + */ + public function quote($text, $escape = true) + { + if (!($this->db instanceof FOFDatabaseDriver)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_INVALID_DB_OBJECT'); + } + + return $this->db->quote($text, $escape); + } + + /** + * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection + * risks and reserved word conflicts. + * + * This method is provided for use where the query object is passed to a function for modification. + * If you have direct access to the database object, it is recommended you use the quoteName method directly. + * + * Note that 'qn' is an alias for this method as it is in FOFDatabaseDriver. + * + * Usage: + * $query->quoteName('#__a'); + * $query->qn('#__a'); + * + * @param mixed $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes. + * Each type supports dot-notation name. + * @param mixed $as The AS query part associated to $name. It can be string or array, in latter case it has to be + * same length of $name; if is null there will not be any AS part for string or array element. + * + * @return mixed The quote wrapped name, same type of $name. + * + * @since 11.1 + * @throws RuntimeException if the internal db property is not a valid object. + */ + public function quoteName($name, $as = null) + { + if (!($this->db instanceof FOFDatabaseDriver)) + { + throw new RuntimeException('JLIB_DATABASE_ERROR_INVALID_DB_OBJECT'); + } + + return $this->db->quoteName($name, $as); + } + + /** + * Add a RIGHT JOIN clause to the query. + * + * Usage: + * $query->rightJoin('b ON b.id = a.id')->rightJoin('c ON c.id = b.id'); + * + * @param string $condition The join condition. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function rightJoin($condition) + { + $this->join('RIGHT', $condition); + + return $this; + } + + /** + * Add a single column, or array of columns to the SELECT clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * The select method can, however, be called multiple times in the same query. + * + * Usage: + * $query->select('a.*')->select('b.id'); + * $query->select(array('a.*', 'b.id')); + * + * @param mixed $columns A string or an array of field names. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function select($columns) + { + $this->type = 'select'; + + if (is_null($this->select)) + { + $this->select = new FOFDatabaseQueryElement('SELECT', $columns); + } + else + { + $this->select->append($columns); + } + + return $this; + } + + /** + * Add a single condition string, or an array of strings to the SET clause of the query. + * + * Usage: + * $query->set('a = 1')->set('b = 2'); + * $query->set(array('a = 1', 'b = 2'); + * + * @param mixed $conditions A string or array of string conditions. + * @param string $glue The glue by which to join the condition strings. Defaults to ,. + * Note that the glue is set on first use and cannot be changed. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function set($conditions, $glue = ',') + { + if (is_null($this->set)) + { + $glue = strtoupper($glue); + $this->set = new FOFDatabaseQueryElement('SET', $conditions, "\n\t$glue "); + } + else + { + $this->set->append($conditions); + } + + return $this; + } + + /** + * Allows a direct query to be provided to the database + * driver's setQuery() method, but still allow queries + * to have bounded variables. + * + * Usage: + * $query->setQuery('select * from #__users'); + * + * @param mixed $sql An SQL Query + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 12.1 + */ + public function setQuery($sql) + { + $this->sql = $sql; + + return $this; + } + + /** + * Add a table name to the UPDATE clause of the query. + * + * Note that you must not mix insert, update, delete and select method calls when building a query. + * + * Usage: + * $query->update('#__foo')->set(...); + * + * @param string $table A table to update. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function update($table) + { + $this->type = 'update'; + $this->update = new FOFDatabaseQueryElement('UPDATE', $table); + + return $this; + } + + /** + * Adds a tuple, or array of tuples that would be used as values for an INSERT INTO statement. + * + * Usage: + * $query->values('1,2,3')->values('4,5,6'); + * $query->values(array('1,2,3', '4,5,6')); + * + * @param string $values A single tuple, or array of tuples. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function values($values) + { + if (is_null($this->values)) + { + $this->values = new FOFDatabaseQueryElement('()', $values, '),('); + } + else + { + $this->values->append($values); + } + + return $this; + } + + /** + * Add a single condition, or an array of conditions to the WHERE clause of the query. + * + * Usage: + * $query->where('a = 1')->where('b = 2'); + * $query->where(array('a = 1', 'b = 2')); + * + * @param mixed $conditions A string or array of where conditions. + * @param string $glue The glue by which to join the conditions. Defaults to AND. + * Note that the glue is set on first use and cannot be changed. + * + * @return FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 11.1 + */ + public function where($conditions, $glue = 'AND') + { + if (is_null($this->where)) + { + $glue = strtoupper($glue); + $this->where = new FOFDatabaseQueryElement('WHERE', $conditions, " $glue "); + } + else + { + $this->where->append($conditions); + } + + return $this; + } + + /** + * Method to provide deep copy support to nested objects and + * arrays when cloning. + * + * @return void + * + * @since 11.3 + */ + public function __clone() + { + foreach ($this as $k => $v) + { + if ($k === 'db') + { + continue; + } + + if (is_object($v) || is_array($v)) + { + $this->{$k} = unserialize(serialize($v)); + } + } + } + + /** + * Add a query to UNION with the current query. + * Multiple unions each require separate statements and create an array of unions. + * + * Usage (the $query base query MUST be a select query): + * $query->union('SELECT name FROM #__foo') + * $query->union('SELECT name FROM #__foo', true) + * $query->union(array('SELECT name FROM #__foo','SELECT name FROM #__bar')) + * $query->union($query2)->union($query3) + * $query->union(array($query2, $query3)) + * + * @param mixed $query The FOFDatabaseQuery object or string to union. + * @param boolean $distinct True to only return distinct rows from the union. + * @param string $glue The glue by which to join the conditions. + * + * @return mixed The FOFDatabaseQuery object on success or boolean false on failure. + * + * @see http://dev.mysql.com/doc/refman/5.0/en/union.html + * + * @since 12.1 + */ + public function union($query, $distinct = false, $glue = '') + { + // Set up the DISTINCT flag, the name with parentheses, and the glue. + if ($distinct) + { + $name = 'UNION DISTINCT ()'; + $glue = ')' . PHP_EOL . 'UNION DISTINCT ('; + } + else + { + $glue = ')' . PHP_EOL . 'UNION ('; + $name = 'UNION ()'; + } + + // Get the FOFDatabaseQueryElement if it does not exist + if (is_null($this->union)) + { + $this->union = new FOFDatabaseQueryElement($name, $query, "$glue"); + } + // Otherwise append the second UNION. + else + { + $this->union->append($query); + } + + return $this; + } + + /** + * Add a query to UNION DISTINCT with the current query. Simply a proxy to union with the DISTINCT keyword. + * + * Usage: + * $query->unionDistinct('SELECT name FROM #__foo') + * + * @param mixed $query The FOFDatabaseQuery object or string to union. + * @param string $glue The glue by which to join the conditions. + * + * @return mixed The FOFDatabaseQuery object on success or boolean false on failure. + * + * @see union + * + * @since 12.1 + */ + public function unionDistinct($query, $glue = '') + { + $distinct = true; + + // Apply the distinct flag to the union. + return $this->union($query, $distinct, $glue); + } + + /** + * Find and replace sprintf-like tokens in a format string. + * Each token takes one of the following forms: + * %% - A literal percent character. + * %[t] - Where [t] is a type specifier. + * %[n]$[x] - Where [n] is an argument specifier and [t] is a type specifier. + * + * Types: + * a - Numeric: Replacement text is coerced to a numeric type but not quoted or escaped. + * e - Escape: Replacement text is passed to $this->escape(). + * E - Escape (extra): Replacement text is passed to $this->escape() with true as the second argument. + * n - Name Quote: Replacement text is passed to $this->quoteName(). + * q - Quote: Replacement text is passed to $this->quote(). + * Q - Quote (no escape): Replacement text is passed to $this->quote() with false as the second argument. + * r - Raw: Replacement text is used as-is. (Be careful) + * + * Date Types: + * - Replacement text automatically quoted (use uppercase for Name Quote). + * - Replacement text should be a string in date format or name of a date column. + * y/Y - Year + * m/M - Month + * d/D - Day + * h/H - Hour + * i/I - Minute + * s/S - Second + * + * Invariable Types: + * - Takes no argument. + * - Argument index not incremented. + * t - Replacement text is the result of $this->currentTimestamp(). + * z - Replacement text is the result of $this->nullDate(false). + * Z - Replacement text is the result of $this->nullDate(true). + * + * Usage: + * $query->format('SELECT %1$n FROM %2$n WHERE %3$n = %4$a', 'foo', '#__foo', 'bar', 1); + * Returns: SELECT `foo` FROM `#__foo` WHERE `bar` = 1 + * + * Notes: + * The argument specifier is optional but recommended for clarity. + * The argument index used for unspecified tokens is incremented only when used. + * + * @param string $format The formatting string. + * + * @return string Returns a string produced according to the formatting string. + * + * @since 12.3 + */ + public function format($format) + { + $query = $this; + $args = array_slice(func_get_args(), 1); + array_unshift($args, null); + + $i = 1; + $func = function ($match) use ($query, $args, &$i) + { + if (isset($match[6]) && $match[6] == '%') + { + return '%'; + } + + // No argument required, do not increment the argument index. + switch ($match[5]) + { + case 't': + return $query->currentTimestamp(); + break; + + case 'z': + return $query->nullDate(false); + break; + + case 'Z': + return $query->nullDate(true); + break; + } + + // Increment the argument index only if argument specifier not provided. + $index = is_numeric($match[4]) ? (int) $match[4] : $i++; + + if (!$index || !isset($args[$index])) + { + // TODO - What to do? sprintf() throws a Warning in these cases. + $replacement = ''; + } + else + { + $replacement = $args[$index]; + } + + switch ($match[5]) + { + case 'a': + return 0 + $replacement; + break; + + case 'e': + return $query->escape($replacement); + break; + + case 'E': + return $query->escape($replacement, true); + break; + + case 'n': + return $query->quoteName($replacement); + break; + + case 'q': + return $query->quote($replacement); + break; + + case 'Q': + return $query->quote($replacement, false); + break; + + case 'r': + return $replacement; + break; + + // Dates + case 'y': + return $query->year($query->quote($replacement)); + break; + + case 'Y': + return $query->year($query->quoteName($replacement)); + break; + + case 'm': + return $query->month($query->quote($replacement)); + break; + + case 'M': + return $query->month($query->quoteName($replacement)); + break; + + case 'd': + return $query->day($query->quote($replacement)); + break; + + case 'D': + return $query->day($query->quoteName($replacement)); + break; + + case 'h': + return $query->hour($query->quote($replacement)); + break; + + case 'H': + return $query->hour($query->quoteName($replacement)); + break; + + case 'i': + return $query->minute($query->quote($replacement)); + break; + + case 'I': + return $query->minute($query->quoteName($replacement)); + break; + + case 's': + return $query->second($query->quote($replacement)); + break; + + case 'S': + return $query->second($query->quoteName($replacement)); + break; + } + + return ''; + }; + + /** + * Regexp to find an replace all tokens. + * Matched fields: + * 0: Full token + * 1: Everything following '%' + * 2: Everything following '%' unless '%' + * 3: Argument specifier and '$' + * 4: Argument specifier + * 5: Type specifier + * 6: '%' if full token is '%%' + */ + return preg_replace_callback('#%(((([\d]+)\$)?([aeEnqQryYmMdDhHiIsStzZ]))|(%))#', $func, $format); + } + + /** + * 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. + * Note: Not all drivers support all units. + * + * @param datetime $date The date to add to. May be date or 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 + * + * @link http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-add + * @since 13.1 + */ + public function dateAdd($date, $interval, $datePart) + { + return trim("DATE_ADD('" . $date . "', INTERVAL " . $interval . ' ' . $datePart . ')'); + } + + /** + * Add a query to UNION ALL with the current query. + * Multiple unions each require separate statements and create an array of unions. + * + * Usage: + * $query->union('SELECT name FROM #__foo') + * $query->union(array('SELECT name FROM #__foo','SELECT name FROM #__bar')) + * + * @param mixed $query The FOFDatabaseQuery object or string to union. + * @param boolean $distinct Not used - ignored. + * @param string $glue Not used - ignored. + * + * @return mixed The FOFDatabaseQuery object on success or boolean false on failure. + * + * @see union + * + * @since 13.1 + */ + public function unionAll($query, $distinct = false, $glue = '') + { + $glue = ')' . PHP_EOL . 'UNION ALL ('; + $name = 'UNION ALL ()'; + + // Get the FOFDatabaseQueryElement if it does not exist + if (is_null($this->unionAll)) + { + $this->unionAll = new FOFDatabaseQueryElement($name, $query, "$glue"); + } + + // Otherwise append the second UNION. + else + { + $this->unionAll->append($query); + } + + return $this; + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/element.php b/deployed/akeeba/libraries/fof/database/query/element.php new file mode 100644 index 00000000..f3a5bae8 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/element.php @@ -0,0 +1,132 @@ +elements = array(); + $this->name = $name; + $this->glue = $glue; + + $this->append($elements); + } + + /** + * Magic function to convert the query element to a string. + * + * @return string + * + * @since 11.1 + */ + 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 + * + * @since 11.1 + */ + 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 array + * + * @since 11.1 + */ + public function getElements() + { + return $this->elements; + } + + /** + * Method to provide deep copy support to nested objects and arrays + * when cloning. + * + * @return void + * + * @since 11.3 + */ + public function __clone() + { + foreach ($this as $k => $v) + { + if (is_object($v) || is_array($v)) + { + $this->{$k} = unserialize(serialize($v)); + } + } + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/limitable.php b/deployed/akeeba/libraries/fof/database/query/limitable.php new file mode 100644 index 00000000..5d450e5b --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/limitable.php @@ -0,0 +1,72 @@ +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 FOFDatabaseQuery Returns this object to allow chaining. + * + * @since 12.1 + */ + public function setLimit($limit = 0, $offset = 0); + } +} + +/** + * Joomla Database 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. + * + * @since 12.1 + */ +interface FOFDatabaseQueryLimitable extends JDatabaseQueryLimitable +{ +} diff --git a/deployed/akeeba/libraries/fof/database/query/mysql.php b/deployed/akeeba/libraries/fof/database/query/mysql.php new file mode 100644 index 00000000..73db5b31 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/mysql.php @@ -0,0 +1,23 @@ + 0 || $offset > 0) + { + $query .= ' LIMIT ' . $offset . ', ' . $limit; + } + + return $query; + } + + /** + * 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. + * + * @since 11.1 + */ + 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) . ')'; + } + } + + /** + * 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 FOFDatabaseQuery 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; + } + + /** + * Return correct regexp operator for mysqli. + * + * Ensure that the regexp operator is mysqli compatible. + * + * Usage: + * $query->where('field ' . $query->regexp($search)); + * + * @param string $value The regex pattern. + * + * @return string Returns the regex operator. + * + * @since 11.3 + */ + public function regexp($value) + { + return ' REGEXP ' . $value; + } + + /** + * Return correct rand() function for Mysql. + * + * Ensure that the rand() function is Mysql compatible. + * + * Usage: + * $query->Rand(); + * + * @return string The correct rand function. + * + * @since 3.5 + */ + public function Rand() + { + return ' RAND() '; + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/oracle.php b/deployed/akeeba/libraries/fof/database/query/oracle.php new file mode 100644 index 00000000..62206908 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/oracle.php @@ -0,0 +1,205 @@ +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 12.1 + */ + public function &getBounded($key = null) + { + if (empty($key)) + { + return $this->bounded; + } + else + { + if (isset($this->bounded[$key])) + { + return $this->bounded[$key]; + } + } + } + + /** + * 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 FOFDatabaseQueryOracle Returns this object to allow chaining. + * + * @since 12.1 + */ + public function clear($clause = null) + { + switch ($clause) + { + case null: + $this->bounded = array(); + break; + } + + parent::clear($clause); + + 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. This method is used + * automatically by the __toString() method if it detects that the + * query implements the FOFDatabaseQueryLimitable 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) + { + // Check if we need to mangle the query. + if ($limit || $offset) + { + $query = "SELECT joomla2.* + FROM ( + SELECT joomla1.*, ROWNUM AS joomla_db_rownum + FROM ( + " . $query . " + ) joomla1 + ) joomla2"; + + // Check if the limit value is greater than zero. + if ($limit > 0) + { + $query .= ' WHERE joomla2.joomla_db_rownum BETWEEN ' . ($offset + 1) . ' AND ' . ($offset + $limit); + } + else + { + // Check if there is an offset and then use this. + if ($offset) + { + $query .= ' WHERE joomla2.joomla_db_rownum > ' . ($offset + 1); + } + } + } + + 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 FOFDatabaseQueryOracle 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; + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/pdo.php b/deployed/akeeba/libraries/fof/database/query/pdo.php new file mode 100644 index 00000000..13d36198 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/pdo.php @@ -0,0 +1,22 @@ +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->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; + } + + if ($this instanceof FOFDatabaseQueryLimitable) + { + $query = $this->processLimit($query, $this->limit, $this->offset); + } + + 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 FOFDatabaseQueryPostgresql Returns this object to allow chaining. + * + * @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.3 + */ + 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 string $glue The glue by which to join the conditions. Defaults to ',' . + * + * @return FOFDatabaseQueryPostgresql 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 FOFDatabaseQueryElement('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 string $glue The glue by which to join the conditions. Defaults to ',' . + * + * @return FOFDatabaseQueryPostgresql 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 FOFDatabaseQueryElement('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 FOFDatabaseQueryPostgresql NO WAIT query element + * + * @since 11.3 + */ + public function noWait () + { + $this->type = 'noWait'; + + if (is_null($this->noWait)) + { + $this->noWait = new FOFDatabaseQueryElement('NOWAIT', null); + } + + return $this; + } + + /** + * Set the LIMIT clause to the query + * + * @param integer $limit An int of how many row will be returned + * + * @return FOFDatabaseQueryPostgresql Returns this object to allow chaining. + * + * @since 11.3 + */ + public function limit($limit = 0) + { + if (is_null($this->limit)) + { + $this->limit = new FOFDatabaseQueryElement('LIMIT', (int) $limit); + } + + return $this; + } + + /** + * Set the OFFSET clause to the query + * + * @param integer $offset An int for skipping row + * + * @return FOFDatabaseQueryPostgresql Returns this object to allow chaining. + * + * @since 11.3 + */ + public function offset($offset = 0) + { + if (is_null($this->offset)) + { + $this->offset = new FOFDatabaseQueryElement('OFFSET', (int) $offset); + } + + return $this; + } + + /** + * Add the RETURNING element to INSERT INTO statement. + * + * @param mixed $pkCol The name of the primary key column. + * + * @return FOFDatabaseQueryPostgresql Returns this object to allow chaining. + * + * @since 11.3 + */ + public function returning($pkCol) + { + if (is_null($this->returning)) + { + $this->returning = new FOFDatabaseQueryElement('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 FOFDatabaseQueryPostgresql 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 datetime $date The date to add to + * @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 . "'"; + } + } + + /** + * Return correct regexp operator for Postgresql. + * + * Ensure that the regexp operator is Postgresql compatible. + * + * Usage: + * $query->where('field ' . $query->regexp($search)); + * + * @param string $value The regex pattern. + * + * @return string Returns the regex operator. + * + * @since 11.3 + */ + public function regexp($value) + { + return ' ~* ' . $value; + } + + /** + * Return correct rand() function for Postgresql. + * + * Ensure that the rand() function is Postgresql compatible. + * + * Usage: + * $query->Rand(); + * + * @return string The correct rand function. + * + * @since 3.5 + */ + public function Rand() + { + return ' RANDOM() '; + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/preparable.php b/deployed/akeeba/libraries/fof/database/query/preparable.php new file mode 100644 index 00000000..8f58bfa8 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/preparable.php @@ -0,0 +1,62 @@ +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 12.1 + */ + 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 13.1 + */ + 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 FOFDatabaseQuerySqlite Returns this object to allow chaining. + * + * @since 12.1 + */ + public function clear($clause = null) + { + switch ($clause) + { + case null: + $this->bounded = array(); + break; + } + + parent::clear($clause); + + return $this; + } + + /** + * 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.1 + */ + 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 FOFDatabaseQueryLimitable 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) + { + 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 FOFDatabaseQuerySqlite 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; + } + + /** + * 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 datetime $date The date or datetime to add to + * @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 + * @link http://www.sqlite.org/lang_datefunc.html + */ + public function dateAdd($date, $interval, $datePart) + { + // SQLite does not support microseconds as a separate unit. Convert the interval to seconds + if (strcasecmp($datePart, 'microseconds') == 0) + { + $interval = .001 * $interval; + $datePart = 'seconds'; + } + + if (substr($interval, 0, 1) != '-') + { + return "datetime('" . $date . "', '+" . $interval . " " . $datePart . "')"; + } + else + { + return "datetime('" . $date . "', '" . $interval . " " . $datePart . "')"; + } + } + + /** + * Gets the current date and time. + * + * Usage: + * $query->where('published_up < '.$query->currentTimestamp()); + * + * @return string + * + * @since 3.4 + */ + public function currentTimestamp() + { + return 'CURRENT_TIMESTAMP'; + } +} diff --git a/deployed/akeeba/libraries/fof/database/query/sqlsrv.php b/deployed/akeeba/libraries/fof/database/query/sqlsrv.php new file mode 100644 index 00000000..bee0f807 --- /dev/null +++ b/deployed/akeeba/libraries/fof/database/query/sqlsrv.php @@ -0,0 +1,380 @@ +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->order) + { + $query .= (string) $this->order; + } + + if ($this->having) + { + $query .= (string) $this->having; + } + + if ($this instanceof FOFDatabaseQueryLimitable && ($this->limit > 0 || $this->offset > 0)) + { + $query = $this->processLimit($query, $this->limit, $this->offset); + } + + break; + + 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; + + case 'delete': + $query .= (string) $this->delete; + $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->order) + { + $query .= (string) $this->order; + } + + break; + + case 'update': + $query .= (string) $this->update; + + if ($this->join) + { + // Special case for joins + foreach ($this->join as $join) + { + $query .= (string) $join; + } + } + + $query .= (string) $this->set; + + if ($this->where) + { + $query .= (string) $this->where; + } + + if ($this->order) + { + $query .= (string) $this->order; + } + + 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. + * + * @since 11.1 + */ + public function castAsChar($value) + { + return 'CAST(' . $value . ' as NVARCHAR(10))'; + } + + /** + * Gets the function to determine the length of a character string. + * + * @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 11.1 + */ + public function charLength($field, $operator = null, $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. + * + * @since 11.1 + */ + 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 + * + * @since 11.1 + */ + public function currentTimestamp() + { + return 'GETDATE()'; + } + + /** + * Get the length of a string in bytes. + * + * @param string $value The string to measure. + * + * @return integer + * + * @since 11.1 + */ + 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 datetime $date The date to add to; type may be time or 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 + * + * @since 13.1 + * @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 . "'" . ')'; + } + + /** + * 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 && $offset == 0) + { + return $query; + } + + $start = $offset + 1; + $end = $offset + $limit; + + $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('/\sFROM\s/i', $rowNumberText, $query, 1); + $query = 'SELECT * FROM (' . $query . ') A WHERE A.RowNumber BETWEEN ' . $start . ' AND ' . $end; + + 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 FOFDatabaseQuery 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; + } + + /** + * Return correct rand() function for MSSQL. + * + * Ensure that the rand() function is MSSQL compatible. + * + * Usage: + * $query->Rand(); + * + * @return string The correct rand function. + * + * @since 3.5 + */ + public function Rand() + { + return ' NEWID() '; + } +} diff --git a/deployed/akeeba/libraries/fof/dispatcher/dispatcher.php b/deployed/akeeba/libraries/fof/dispatcher/dispatcher.php new file mode 100644 index 00000000..e253b15d --- /dev/null +++ b/deployed/akeeba/libraries/fof/dispatcher/dispatcher.php @@ -0,0 +1,718 @@ +getCmd('option', 'com_foobar'); + $config['view'] = !is_null($view) ? $view : $input->getCmd('view', ''); + + $input->set('option', $config['option']); + $input->set('view', $config['view']); + + $config['input'] = $input; + + $className = ucfirst(str_replace('com_', '', $config['option'])) . 'Dispatcher'; + + if (!class_exists($className)) + { + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($config['option']); + + $searchPaths = array( + $componentPaths['main'], + $componentPaths['main'] . '/dispatchers', + $componentPaths['admin'], + $componentPaths['admin'] . '/dispatchers' + ); + + if (array_key_exists('searchpath', $config)) + { + array_unshift($searchPaths, $config['searchpath']); + } + + $filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem'); + + $path = $filesystem->pathFind( + $searchPaths, 'dispatcher.php' + ); + + if ($path) + { + require_once $path; + } + } + + if (!class_exists($className)) + { + $className = 'FOFDispatcher'; + } + + $instance = new $className($config); + + return $instance; + } + + /** + * Public constructor + * + * @param array $config The configuration variables + */ + public function __construct($config = array()) + { + // Cache the config + $this->config = $config; + + // Get the input for this MVC triad + if (array_key_exists('input', $config)) + { + $this->input = $config['input']; + } + else + { + $this->input = new FOFInput; + } + + // Get the default values for the component name + $this->component = $this->input->getCmd('option', 'com_foobar'); + + // Load the component's fof.xml configuration file + $configProvider = new FOFConfigProvider; + $this->defaultView = $configProvider->get($this->component . '.dispatcher.default_view', $this->defaultView); + + // Get the default values for the view name + $this->view = $this->input->getCmd('view', null); + + if (empty($this->view)) + { + // Do we have a task formatted as controller.task? + $task = $this->input->getCmd('task', ''); + + if (!empty($task) && (strstr($task, '.') !== false)) + { + list($this->view, $task) = explode('.', $task, 2); + $this->input->set('task', $task); + } + } + + if (empty($this->view)) + { + $this->view = $this->defaultView; + } + + $this->layout = $this->input->getCmd('layout', null); + + // Overrides from the config + if (array_key_exists('option', $config)) + { + $this->component = $config['option']; + } + + if (array_key_exists('view', $config)) + { + $this->view = empty($config['view']) ? $this->view : $config['view']; + } + + if (array_key_exists('layout', $config)) + { + $this->layout = $config['layout']; + } + + $this->input->set('option', $this->component); + $this->input->set('view', $this->view); + $this->input->set('layout', $this->layout); + + if (array_key_exists('authTimeStep', $config)) + { + $this->fofAuth_timeStep = empty($config['authTimeStep']) ? 6 : $config['authTimeStep']; + } + } + + /** + * The main code of the Dispatcher. It spawns the necessary controller and + * runs it. + * + * @throws Exception + * + * @return void|Exception + */ + public function dispatch() + { + $platform = FOFPlatform::getInstance(); + + if (!$platform->authorizeAdmin($this->input->getCmd('option', 'com_foobar'))) + { + return $platform->raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + } + + $this->transparentAuthentication(); + + // Merge English and local translations + $platform->loadTranslations($this->component); + + $canDispatch = true; + + if ($platform->isCli()) + { + $canDispatch = $canDispatch && $this->onBeforeDispatchCLI(); + } + + $canDispatch = $canDispatch && $this->onBeforeDispatch(); + + if (!$canDispatch) + { + // We can set header only if we're not in CLI + if(!$platform->isCli()) + { + $platform->setHeader('Status', '403 Forbidden', true); + } + + return $platform->raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + } + + // Get and execute the controller + $option = $this->input->getCmd('option', 'com_foobar'); + $view = $this->input->getCmd('view', $this->defaultView); + $task = $this->input->getCmd('task', null); + + if (empty($task)) + { + $task = $this->getTask($view); + } + + // Pluralise/sungularise the view name for typical tasks + if (in_array($task, array('edit', 'add', 'read'))) + { + $view = FOFInflector::singularize($view); + } + elseif (in_array($task, array('browse'))) + { + $view = FOFInflector::pluralize($view); + } + + $this->input->set('view', $view); + $this->input->set('task', $task); + + $config = $this->config; + $config['input'] = $this->input; + + $controller = FOFController::getTmpInstance($option, $view, $config); + $status = $controller->execute($task); + + if (!$this->onAfterDispatch()) + { + // We can set header only if we're not in CLI + if(!$platform->isCli()) + { + $platform->setHeader('Status', '403 Forbidden', true); + } + + return $platform->raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + } + + $format = $this->input->get('format', 'html', 'cmd'); + $format = empty($format) ? 'html' : $format; + + if ($controller->hasRedirect()) + { + $controller->redirect(); + } + } + + /** + * Tries to guess the controller task to execute based on the view name and + * the HTTP request method. + * + * @param string $view The name of the view + * + * @return string The best guess of the task to execute + */ + protected function getTask($view) + { + // Get a default task based on plural/singular view + $request_task = $this->input->getCmd('task', null); + $task = FOFInflector::isPlural($view) ? 'browse' : 'edit'; + + // Get a potential ID, we might need it later + $id = $this->input->get('id', null, 'int'); + + if ($id == 0) + { + $ids = $this->input->get('ids', array(), 'array'); + + if (!empty($ids)) + { + $id = array_shift($ids); + } + } + + // Check the request method + + if (!isset($_SERVER['REQUEST_METHOD'])) + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + } + + $requestMethod = strtoupper($_SERVER['REQUEST_METHOD']); + + switch ($requestMethod) + { + case 'POST': + case 'PUT': + if (!is_null($id)) + { + $task = 'save'; + } + break; + + case 'DELETE': + if ($id != 0) + { + $task = 'delete'; + } + break; + + case 'GET': + default: + // If it's an edit without an ID or ID=0, it's really an add + if (($task == 'edit') && ($id == 0)) + { + $task = 'add'; + } + + // If it's an edit in the frontend, it's really a read + elseif (($task == 'edit') && FOFPlatform::getInstance()->isFrontend()) + { + $task = 'read'; + } + break; + } + + return $task; + } + + /** + * Executes right before the dispatcher tries to instantiate and run the + * controller. + * + * @return boolean Return false to abort + */ + public function onBeforeDispatch() + { + return true; + } + + /** + * Sets up some environment variables, so we can work as usually on CLI, too. + * + * @return boolean Return false to abort + */ + public function onBeforeDispatchCLI() + { + JLoader::import('joomla.environment.uri'); + JLoader::import('joomla.application.component.helper'); + + // Trick to create a valid url used by JURI + $this->_originalPhpScript = ''; + + // We have no Application Helper (there is no Application!), so I have to define these constants manually + $option = $this->input->get('option', '', 'cmd'); + + if ($option) + { + $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($option); + + if (!defined('JPATH_COMPONENT')) + { + define('JPATH_COMPONENT', $componentPaths['main']); + } + + if (!defined('JPATH_COMPONENT_SITE')) + { + define('JPATH_COMPONENT_SITE', $componentPaths['site']); + } + + if (!defined('JPATH_COMPONENT_ADMINISTRATOR')) + { + define('JPATH_COMPONENT_ADMINISTRATOR', $componentPaths['admin']); + } + } + + return true; + } + + /** + * Executes right after the dispatcher runs the controller. + * + * @return boolean Return false to abort + */ + public function onAfterDispatch() + { + // If we have to log out the user, please do so now + if ($this->fofAuth_LogoutOnReturn && $this->_fofAuth_isLoggedIn) + { + FOFPlatform::getInstance()->logoutUser(); + } + + return true; + } + + /** + * Transparently authenticates a user + * + * @return void + */ + public function transparentAuthentication() + { + // Only run when there is no logged in user + if (!FOFPlatform::getInstance()->getUser()->guest) + { + return; + } + + // @todo Check the format + $format = $this->input->getCmd('format', 'html'); + + if (!in_array($format, $this->fofAuth_Formats)) + { + return; + } + + foreach ($this->fofAuth_AuthMethods as $method) + { + // If we're already logged in, don't bother + if ($this->_fofAuth_isLoggedIn) + { + continue; + } + + // This will hold our authentication data array (username, password) + $authInfo = null; + + switch ($method) + { + case 'HTTPBasicAuth_TOTP': + + if (empty($this->fofAuth_Key)) + { + continue; + } + + if (!isset($_SERVER['PHP_AUTH_USER'])) + { + continue; + } + + if (!isset($_SERVER['PHP_AUTH_PW'])) + { + continue; + } + + if ($_SERVER['PHP_AUTH_USER'] != '_fof_auth') + { + continue; + } + + $encryptedData = $_SERVER['PHP_AUTH_PW']; + + $authInfo = $this->_decryptWithTOTP($encryptedData); + break; + + case 'QueryString_TOTP': + $encryptedData = $this->input->get('_fofauthentication', '', 'raw'); + + if (empty($encryptedData)) + { + continue; + } + + $authInfo = $this->_decryptWithTOTP($encryptedData); + break; + + case 'HTTPBasicAuth_Plaintext': + if (!isset($_SERVER['PHP_AUTH_USER'])) + { + continue; + } + + if (!isset($_SERVER['PHP_AUTH_PW'])) + { + continue; + } + + $authInfo = array( + 'username' => $_SERVER['PHP_AUTH_USER'], + 'password' => $_SERVER['PHP_AUTH_PW'] + ); + break; + + case 'QueryString_Plaintext': + $jsonencoded = $this->input->get('_fofauthentication', '', 'raw'); + + if (empty($jsonencoded)) + { + continue; + } + + $authInfo = json_decode($jsonencoded, true); + + if (!is_array($authInfo)) + { + $authInfo = null; + } + elseif (!array_key_exists('username', $authInfo) || !array_key_exists('password', $authInfo)) + { + $authInfo = null; + } + break; + + case 'SplitQueryString_Plaintext': + $authInfo = array( + 'username' => $this->input->get('_fofauthentication_username', '', 'raw'), + 'password' => $this->input->get('_fofauthentication_password', '', 'raw'), + ); + + if (empty($authInfo['username'])) + { + $authInfo = null; + } + + break; + + default: + continue; + + break; + } + + // No point trying unless we have a username and password + if (!is_array($authInfo)) + { + continue; + } + + $this->_fofAuth_isLoggedIn = FOFPlatform::getInstance()->loginUser($authInfo); + } + } + + /** + * Decrypts a transparent authentication message using a TOTP + * + * @param string $encryptedData The encrypted data + * + * @codeCoverageIgnore + * @return array The decrypted data + */ + private function _decryptWithTOTP($encryptedData) + { + if (empty($this->fofAuth_Key)) + { + $this->_fofAuth_CryptoKey = null; + + return null; + } + + $totp = new FOFEncryptTotp($this->fofAuth_timeStep); + $period = $totp->getPeriod(); + $period--; + + for ($i = 0; $i <= 2; $i++) + { + $time = ($period + $i) * $this->fofAuth_timeStep; + $otp = $totp->getCode($this->fofAuth_Key, $time); + $this->_fofAuth_CryptoKey = hash('sha256', $this->fofAuth_Key . $otp); + + $aes = new FOFEncryptAes($this->_fofAuth_CryptoKey); + $ret = $aes->decryptString($encryptedData); + $ret = rtrim($ret, "\000"); + + $ret = json_decode($ret, true); + + if (!is_array($ret)) + { + continue; + } + + if (!array_key_exists('username', $ret)) + { + continue; + } + + if (!array_key_exists('password', $ret)) + { + continue; + } + + // Successful decryption! + return $ret; + } + + // Obviously if we're here we could not decrypt anything. Bail out. + $this->_fofAuth_CryptoKey = null; + + return null; + } + + /** + * Creates a decryption key for use with the TOTP decryption method + * + * @param integer $time The timestamp used for TOTP calculation, leave empty to use current timestamp + * + * @codeCoverageIgnore + * @return string THe encryption key + */ + private function _createDecryptionKey($time = null) + { + $totp = new FOFEncryptTotp($this->fofAuth_timeStep); + $otp = $totp->getCode($this->fofAuth_Key, $time); + + $key = hash('sha256', $this->fofAuth_Key . $otp); + + return $key; + } + + /** + * Main function to detect if we're running in a CLI environment and we're admin + * + * @return array isCLI and isAdmin. It's not an associtive array, so we can use list. + */ + public static function isCliAdmin() + { + static $isCLI = null; + static $isAdmin = null; + + if (is_null($isCLI) && is_null($isAdmin)) + { + $isCLI = FOFPlatform::getInstance()->isCli(); + $isAdmin = FOFPlatform::getInstance()->isBackend(); + } + + return array($isCLI, $isAdmin); + } +} diff --git a/deployed/akeeba/libraries/fof/download/adapter/abstract.php b/deployed/akeeba/libraries/fof/download/adapter/abstract.php new file mode 100644 index 00000000..19f46f2f --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/adapter/abstract.php @@ -0,0 +1,113 @@ +supportsChunkDownload; + } + + /** + * Does this download adapter support reading the size of a remote file? + * + * @return boolean True if remote file size determination is supported + */ + public function supportsFileSize() + { + return $this->supportsFileSize; + } + + /** + * Is this download class supported in the current server environment? + * + * @return boolean True if this server environment supports this download class + */ + public function isSupported() + { + return $this->isSupported; + } + + /** + * Get the priority of this adapter. If multiple download adapters are + * supported on a site, the one with the highest priority will be + * used. + * + * @return boolean + */ + public function getPriority() + { + return $this->priority; + } + + /** + * Returns the name of this download adapter in use + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Download a part (or the whole) of a remote URL and return the downloaded + * data. You are supposed to check the size of the returned data. If it's + * smaller than what you expected you've reached end of file. If it's empty + * you have tried reading past EOF. If it's larger than what you expected + * the server doesn't support chunk downloads. + * + * If this class' supportsChunkDownload returns false you should assume + * that the $from and $to parameters will be ignored. + * + * @param string $url The remote file's URL + * @param integer $from Byte range to start downloading from. Use null for start of file. + * @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is ignored) + * @param array $params Additional params that will be added before performing the download + * + * @return string The raw file data retrieved from the remote URL. + * + * @throws Exception A generic exception is thrown on error + */ + public function downloadAndReturn($url, $from = null, $to = null, array $params = array()) + { + return ''; + } + + /** + * Get the size of a remote file in bytes + * + * @param string $url The remote file's URL + * + * @return integer The file size, or -1 if the remote server doesn't support this feature + */ + public function getFileSize($url) + { + return -1; + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/download/adapter/cacert.pem b/deployed/akeeba/libraries/fof/download/adapter/cacert.pem new file mode 100644 index 00000000..f9126f8f --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/adapter/cacert.pem @@ -0,0 +1,5104 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from CentOS as of Oct 3 2015 +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from CentOS /etc/pki/tls/certs/ca-bundle.crt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## + +-----BEGIN CERTIFICATE----- +MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD +VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv +bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv +b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV +UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU +cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds +b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH +iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS +r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 +04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r +GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 +3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P +lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm +MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx +MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT +DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 +dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl +cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 +DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 +yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX +L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj +EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG +7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e +QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ +qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL +EwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ +BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x +ETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg +bIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ +j9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV +Sn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG +SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx +JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI +RFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw +MjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5 +fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i ++DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG +SIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN +QseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+ +gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV +UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL +EwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ +BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x +ETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/ +k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso +LeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o +TQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG +SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx +JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI +RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3 +MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C +TShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5 +WzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG +SIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR +xdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL +B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do +lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc +AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK +VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm +Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J +h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul +uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 +DzFc6PLZ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns +YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y +aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe +Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj +IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx +KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM +HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw +DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC +AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji +nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX +rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn +jBJ7xUS0rg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy +NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y +LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ +TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y +TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 +LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW +I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw +nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy +NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY +dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 +WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS +v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v +UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu +IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC +W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 +IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz +BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y +aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG +9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy +NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y +azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs +YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw +Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl +cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD +cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs +2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY +JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE +Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ +n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A +PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 +nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO +8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV +ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb +PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 +6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr +n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a +qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 +wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 +ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs +pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 +E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy +aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp +Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV +BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp +Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g +Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU +J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO +JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY +wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o +koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN +qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E +Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe +xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u +7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU +sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI +sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP +cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 +GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ ++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd +U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm +NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY +ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ +ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 +CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq +g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c +2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ +bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT +ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw +MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj +dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l +c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC +UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc +58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ +o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr +aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA +A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA +Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv +8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc +MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT +ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw +MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j +LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo +RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu +WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw +Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK +eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM +zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ +WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN +/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTFOFkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy +MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp +eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg +/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl +wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh +AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 +PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu +AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR +MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc +HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ +Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ +f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO +rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch +6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 +7CAFYd4= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx +MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG +29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk +oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk +3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL +qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN +nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX +ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H +DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO +TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv +kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w +zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO +TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy +MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk +ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn +ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 +9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO +hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U +tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o +BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh +SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww +OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv +cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA +7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k +/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm +eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 +u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy +7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB +rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt +Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa +Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV +BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l +dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE +AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B +YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9 +hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l +L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm +SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM +1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws +6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw +Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50 +aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u +7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0 +xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ +rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim +eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk +USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEZjCCA06gAwIBAgIQRL4Mi1AAJLQR0zYt4LNfGzANBgkqhkiG9w0BAQUFADCB +lTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHTAbBgNVBAMTFFVUTi1VU0VSRmlyc3Qt +T2JqZWN0MB4XDTk5MDcwOTE4MzEyMFoXDTE5MDcwOTE4NDAzNlowgZUxCzAJBgNV +BAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAc +BgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3 +dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6qgT+jo2F4qjEAVZURnicP +HxzfOpuCaDDASmEd8S8O+r5596Uj71VRloTN2+O5bj4x2AogZ8f02b+U60cEPgLO +KqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQw5ujm9M89RKZd7G3CeBo +5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbHd2pBnqcP1/vulBe3/IW+ +pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh2JU022R5KP+6LhHC5ehb +kkj7RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzTbafc8H9vg2XiaquHhnUC +AwEAAaOBrzCBrDALBgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU2u1kdBScFDyr3ZmpvVsoTYs8ydgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov +L2NybC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDApBgNV +HSUEIjAgBggrBgEFBQcDAwYIKwYBBQUHAwgGCisGAQQBgjcKAwQwDQYJKoZIhvcN +AQEFBQADggEBAAgfUrE3RHjb/c652pWWmKpVZIC1WkDdIaXFwfNfLEzIR1pp6ujw +NTX00CXzyKakh0q9G7FzCL3Uw8q2NbtZhncxzaeAFK4T7/yxSPlrJSUtUbYsbUXB +mMiKVl0+7kNOPmsnjtA6S4ULX9Ptaqd1y9Fahy85dRNacrACgZ++8A+EVCBibGnU +4U3GDZlDAQ0Slox4nb9QorFEqmrPF3rPbw/U+CRVX/A0FklmPlBGyWNxODFiuGK5 +81OtbLUrohKqGU8J2l7nk8aOFAj+8DCAGKCGhU3IfdeLA/5u1fedFqySLKAj5ZyR +Uh+U3xeUc8OzwcFxBSAAeL0TUh2oPs0AH8g= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg +b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa +MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB +ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw +IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B +AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb +unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d +BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq +7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 +0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX +roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG +A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j +aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p +26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA +BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud +EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN +BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB +AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd +p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi +1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc +XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 +eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu +tGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo +YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 +MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy +NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G +A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA +A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 +Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s +QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV +eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 +B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh +z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T +AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i +ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w +TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH +MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD +VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE +VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B +AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM +bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi +ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG +VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c +ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ +AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD +EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz +aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w +MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G +A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh +Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l +dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh +bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq +eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe +r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5 +3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd +vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l +mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC +wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg +hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0 +TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh +biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg +ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg +dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6 +b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl +c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0 +ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3 +dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu +ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh +bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo +ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3 +Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u +ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA +A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ +MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+ +NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR +VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY +83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3 +macqaJVmlaut74nLYKkGEsaUR+ko +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV +MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe +TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 +dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB +KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 +N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC +dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu +MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL +b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD +zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi +3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 +WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY +Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi +NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC +ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 +QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 +YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz +aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm +ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg +ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs +amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv +IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 +Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 +ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 +YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg +dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs +b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G +CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO +xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP +0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ +QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk +f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK +8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD +EylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05 +OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G +A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh +Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l +dExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK +gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX +iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc +Q7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E +BAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G +SUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu +b3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh +bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv +Y2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln +aXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0 +IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh +c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph +biBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo +ZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP +UlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj +YXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo +dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA +bmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06 +sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa +n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS +NitjrFgBazMpUIaD8QFI +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx +ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 +b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD +EytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X +DTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw +DwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u +c2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr +TmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA +OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC +2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW +RMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P +AQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW +ggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0 +YWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz +b2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO +ZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB +IGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs +b2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs +ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s +YXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg +a2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g +SU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0 +aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg +YXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg +Y3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY +ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g +pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4 +Fp1hBWeAyNDYpQcCNJgEjTME1A== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE +AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x +CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW +MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF +RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 +09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 +XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P +Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK +t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb +X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 +MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU +fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI +2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH +K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae +ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw +RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv +bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm +fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 +gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe +I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i +5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi +ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn +MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ +o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 +zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN +GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt +r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK +Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsx +CzAJBgNVBAYTAkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRp +ZmljYWNpw7NuIERpZ2l0YWwgLSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwa +QUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4wHhcNMDYxMTI3MjA0NjI5WhcNMzAw +NDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2Ft +ZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMu +QS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeG +qentLhM0R7LQcNzJPNCNyu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzL +fDe3fezTf3MZsGqy2IiKLUV0qPezuMDU2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQ +Y5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU34ojC2I+GdV75LaeHM/J4 +Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP2yYe68yQ +54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+b +MMCm8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48j +ilSH5L887uvDdUhfHjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++Ej +YfDIJss2yKHzMI+ko6Kh3VOz3vCaMh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/zt +A/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK5lw1omdMEWux+IBkAC1vImHF +rEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1bczwmPS9KvqfJ +pxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCB +lTCBkgYEVR0gADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFy +YS5jb20vZHBjLzBaBggrBgEFBQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW50 +7WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2UgcHVlZGVuIGVuY29udHJhciBlbiBs +YSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEfAygPU3zmpFmps4p6 +xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuXEpBc +unvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/ +Jre7Ir5v/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dp +ezy4ydV/NgIlqmjCMRW3MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42 +gzmRkBDI8ck1fj+404HGIGQatlDCIaR43NAvO2STdPCWkPHv+wlaNECW8DYSwaN0 +jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wkeZBWN7PGKX6jD/EpOe9+ +XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f/RWmnkJD +W2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/ +RL5hRqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35r +MDOhYil/SrnhLecUIw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxk +BYn8eNZcLCZDqQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc +MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp +b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT +AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs +aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H +j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K +f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 +IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw +FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht +QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm +/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ +k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ +MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC +seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ +hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ +eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U +DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj +B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB +VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp +bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R +dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw +MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy +dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52 +ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM +EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj +lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ +znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH +2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1 +k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs +2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD +VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG +KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+ +8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R +FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS +mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE +DNuxUCAKGkq6ahq97BvIxYSazQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 +ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX +l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB +HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B +5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 +WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP +gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ +DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu +BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs +h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk +LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg +isRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z +NIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI ++MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R +hzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+ +mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP +Bdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s +EzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2 +mSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC +e/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow +dXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET +MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE +AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw +CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg +YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE +Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX +mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD +XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW +S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp +FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD +AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu +ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z +ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv +Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw +DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6 +yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq +EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ +CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB +EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN +PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy +MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk +D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o +OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A +fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe +IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n +oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK +/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj +rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD +3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE +7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC +yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd +qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI +hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR +xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA +SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo +HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB +emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC +AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb +7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x +DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk +F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF +a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT +Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk +BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 +Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl +cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 +aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY +F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N +8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe +rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K +/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu +7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC +28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 +lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E +nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB +0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 +5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj +WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN +jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s +ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM +OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q +619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn +2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj +o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v +nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG +5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq +pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb +dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 +BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC +Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g +Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 +aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa +Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg +SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo +aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp +ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z +7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// +DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx +zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 +hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs +4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u +gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY +NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 +j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG +52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB +echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws +ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI +zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy +wy39FCqQmbkHzJ8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD +TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 +MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF +Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh +IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 +dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO +V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC +GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN +v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB +AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB +Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO +76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK +OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH +ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi +yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL +buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj +2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0 +MRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG +EwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT +CkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK +8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2 +98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb +2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC +ejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi +Xd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB +o4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl +ZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD +AgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL +AZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd +foPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M +cXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq +8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp +hbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk +Res3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U +AGegcQCCSA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw +PDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu +MQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx +GzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL +MAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf +HZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh +gHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW +v+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue +Mv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr +9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt +6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7 +MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl +Y3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58 +ADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq +hkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p +iL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC +dsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL +kz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL +hfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz +OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx +ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w +MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD +VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx +FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu +ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 +gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH +fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a +ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT +ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk +c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto +dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt +aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI +hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk +QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ +h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR +rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 +9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV +BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt +ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4 +MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg +SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl +a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h +4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk +tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s +tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL +dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4 +c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um +TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z ++kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O +Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW +OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW +fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2 +l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw +FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+ +8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI +6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO +TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME +wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY +Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn +xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q +DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q +Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t +hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4 +7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7 +QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ +TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG +9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw +MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM +BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO +MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 +LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI +s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 +xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 +u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b +F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx +Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd +PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV +HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx +NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF +AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ +L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY +YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a +NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R +0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN +AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp +dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw +MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw +CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ +MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB +SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz +ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH +LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP +PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL +2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w +ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC +MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk +AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 +AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz +AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz +AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f +BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY +P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi +CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g +kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 +HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS +na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q +qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z +TbvGRNs2yyqcjg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw +cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy +b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z +ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 +NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN +TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p +Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u +uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ +LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA +vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 +Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx +62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB +AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw +LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP +BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB +AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov +MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 +ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT +AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh +ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo +AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa +AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln +bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p +Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP +PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv +Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB +EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu +w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj +cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV +HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI +VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS +BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS +b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS +8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds +ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl +7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR +hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ +MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s +YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz +dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 +aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh +IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ +KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw +MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy +b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx +KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG +A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u +aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 +7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 +BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G +ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 +JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 +PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 +0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH +0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ +6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m +v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 +K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev +bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw +MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w +MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD +gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 +b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh +bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 +cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp +ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg +ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq +hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD +AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w +MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag +RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t +UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl +cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v +Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG +AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN +AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS +1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB +3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv +Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh +HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm +pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz +sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE +qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb +mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 +opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H +YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvFOFBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF +UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ +R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN +MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw +JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ +WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj +SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl +u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy +A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk +Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 +MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr +aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC +IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A +cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA +YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA +bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA +bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA +aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA +ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA +YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA +ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA +LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 +Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y +eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw +CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G +A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu +Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn +lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt +b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg +9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF +ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC +IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz +MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N +IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 +bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE +RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO +zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 +bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF +MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 +VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC +OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW +tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ +q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb +EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ +Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O +VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIGGTCCBAGgAwIBAgIIPtVRGeZNzn4wDQYJKoZIhvcNAQELBQAwajEhMB8GA1UE +AxMYU0cgVFJVU1QgU0VSVklDRVMgUkFDSU5FMRwwGgYDVQQLExMwMDAyIDQzNTI1 +Mjg5NTAwMDIyMRowGAYDVQQKExFTRyBUUlVTVCBTRVJWSUNFUzELMAkGA1UEBhMC +RlIwHhcNMTAwOTA2MTI1MzQyWhcNMzAwOTA1MTI1MzQyWjBqMSEwHwYDVQQDExhT +RyBUUlVTVCBTRVJWSUNFUyBSQUNJTkUxHDAaBgNVBAsTEzAwMDIgNDM1MjUyODk1 +MDAwMjIxGjAYBgNVBAoTEVNHIFRSVVNUIFNFUlZJQ0VTMQswCQYDVQQGEwJGUjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoVgLsfJXwTukK0rcHoyKL +ULO5Lhk9V9sZqtIr5M5C4myh5F0lHjMdtkXRtPpZilZwyW0IdmlwmubHnAgwE/7m +0ZJoYT5MEfJu8rF7V1ZLCb3cD9lxDOiaN94iEByZXtaxFwfTpDktwhpz/cpLKQfC +eSnIyCauLMT8I8hL4oZWDyj9tocbaF85ZEX9aINsdSQePHWZYfrSFPipS7HYfad4 +0hNiZbXWvn5qA7y1svxkMMPQwpk9maTTzdGxxFOHe0wTE2Z/v9VlU2j5XB7ltP82 +mUWjn2LAfxGCAVTeD2WlOa6dSEyJoxA74OaD9bDaLB56HFwfAKzMq6dgZLPGxXvH +VUZ0PJCBDkqOWZ1UsEixUkw7mO6r2jS3U81J2i/rlb4MVxH2lkwEeVyZ1eXkvm/q +R+5RS+8iJq612BGqQ7t4vwt+tN3PdB0lqYljseI0gcSINTjiAg0PE8nVKoIV8IrE +QzJW5FMdHay2z32bll0eZOl0c8RW5BZKUm2SOdPhTQ4/YrnerbUdZbldUv5dCamc +tKQM2S9FdqXPjmqanqqwEaHrYcbrPx78ZrQSnUZ/MhaJvnFFr5Eh2f2Tv7QCkUL/ +SR/tixVo3R+OrJvdggWcRGkWZBdWX0EPSk8ED2VQhpOX7EW/XcIc3M/E2DrmeAXQ +xVVVqV7+qzohu+VyFPcLAgMBAAGjgcIwgb8wHQYDVR0OBBYEFCkgy/HDD9oGjhOT +h/5fYBopu/O2MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUKSDL8cMP2gaO +E5OH/l9gGim787YwEQYDVR0gBAowCDAGBgRVHSAAMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuc2d0cnVzdHNlcnZpY2VzLmNvbS9yYWNpbmUtR3JvdXBlU0cv +TGF0ZXN0Q1JMMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEATEZn +4ERQ9cW2urJRCiUTHbfHiC4fuStkoMuTiFJZqmD1zClSF/8E5ze0MRFGfisebKeL +PEeaXvSqXZA7RT2fSsmKe47A7j55i5KjyJRKuCgRa6YlX129x8j7g09VMeZc8BN8 +471/Kiw3N5RJr4QfFCeiWBCPCjk3GhIgQY8Z9qkfGe2yNLKtfTNEi18KB0PydkVF +La3kjQ4A/QQIqudr+xe9sAhWDjUqcvCz5006Tw3c82ASszhkjNv54SaNL+9O6CRH +PjY0imkPKGuLh8a9hSb50+tpIVZgkdb34GLCqHGuLt5mI7VSRqakSDcsfwEWVxH3 +Jw0O5Q/WkEXhHj8h3NL8FhgTPk1qsiZqQF4leP049KxYejcbmEAEx47J1MRnYbGY +rvDNDty5r2WDewoEij9hqvddQYbmxkzCTzpcVuooO6dEz8hKZPVyYC3jQ7hK4HU8 +MuSqFtcRucFF2ZtmY2blIrc07rrVdC8lZPOBVMt33lfUk+OsBzE6PlwDg1dTx/D+ +aNglUE0SyObhlY1nqzyTPxcCujjXnvcwpT09RAEzGpqfjtCf8e4wiHPvriQZupdz +FcHscQyEZLV77LxpPqRtCRY2yko5isune8YdfucziMm+MG2chZUh6Uc7Bn6B4upG +5nBYgOao8p0LadEziVkw82TTC/bOKwn7fRB2LhA= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB +rjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp +MRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz +c2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u +IGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa +Fw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t +V3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg +RGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV +U1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1 +toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo +TUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy +ggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1 +XgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF +hy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm +7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG +MCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV +HQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp +ttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD +pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo +LtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF +iXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y +h9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I +k63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID2DCCAsCgAwIBAgIQYFbFSyNAW2TU7SXa2dYeHjANBgkqhkiG9w0BAQsFADCB +hTELMAkGA1UEBhMCREUxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fzc2VuIFZl +cmxhZyBHbWJIMScwJQYDVQQLEx5TLVRSVVNUIENlcnRpZmljYXRpb24gU2Vydmlj +ZXMxIjAgBgNVBAMTGVMtVFJVU1QgVW5pdmVyc2FsIFJvb3QgQ0EwHhcNMTMxMDIy +MDAwMDAwWhcNMzgxMDIxMjM1OTU5WjCBhTELMAkGA1UEBhMCREUxKTAnBgNVBAoT +IERldXRzY2hlciBTcGFya2Fzc2VuIFZlcmxhZyBHbWJIMScwJQYDVQQLEx5TLVRS +VVNUIENlcnRpZmljYXRpb24gU2VydmljZXMxIjAgBgNVBAMTGVMtVFJVU1QgVW5p +dmVyc2FsIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo +4wvfETeFgpq1bGZ8YT/ARxodRuOwVWTluII5KAd+F//0m4rwkYHqOD8heGxI7Gsv +otOKcrKn19nqf7TASWswJYmM67fVQGGY4tw8IJLNZUpynxqOjPolFb/zIYMoDYuv +WRGCQ1ybTSVRf1gYY2A7s7WKi1hjN0hIkETCQN1d90NpKZhcEmVeq5CSS2bf1XUS +U1QYpt6K1rtXAzlZmRgFDPn9FcaQZEYXgtfCSkE9/QC+V3IYlHcbU1qJAfYzcg6T +OtzoHv0FBda8c+CI3KtP7LUYhk95hA5IKmYq3TLIeGXIC51YAQVx7YH1aBduyw20 +S9ih7K446xxYL6FlAzQvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBSafdfr639UmEUptCCrbQuWIxmkwjANBgkqhkiG +9w0BAQsFAAOCAQEATpYS2353XpInniEXGIJ22D+8pQkEZoiJrdtVszNqxmXEj03z +MjbceQSWqXcy0Zf1GGuMuu3OEdBEx5LxtESO7YhSSJ7V/Vn4ox5R+wFS5V/let2q +JE8ii912RvaloA812MoPmLkwXSBvwoEevb3A/hXTOCoJk5gnG5N70Cs0XmilFU/R +UsOgyqCDRR319bdZc11ZAY+qwkcvFHHVKeMQtUeTJcwjKdq3ctiR1OwbSIoi5MEq +9zpok59FGW5Dt8z+uJGaYRo2aWNkkijzb2GShROfyQcsi1fc65551cLeCNVUsldO +KjKNoeI60RAgIjl9NEVvcTvDHfz/sk+o4vYwHg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 +m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih +FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ +TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F +EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco +kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu +HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF +vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo +19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC +L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW +bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX +JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc +K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf +ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik +Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB +sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e +3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR +ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip +mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH +b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf +rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms +hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y +zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 +MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr +jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r +0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f +2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP +ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF +y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA +tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL +6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 +uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL +acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh +k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q +VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O +BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh +b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R +fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv +/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI +REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx +srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv +aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT +woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n +Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W +t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N +8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 +9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 +wSsSnqaeG8XmDtkx2Q== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw +ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp +dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 +IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD +VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy +dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg +MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx +UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD +1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH +oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR +HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ +5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv +idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL +OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC +NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f +46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB +UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth +7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G +A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED +MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB +bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x +XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T +PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 +Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 +WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL +Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm +7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S +nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN +vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB +WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI +fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb +I+2ksx0WckNLIOFZfsLorSa/ovc= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu +IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw +WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD +ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y +IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn +IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+ +6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob +jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw +izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl ++zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY +zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP +pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF +KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW +ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB +AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0 +ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW +IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA +A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0 +uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+ +FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7 +jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/ +u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D +YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1 +puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa +icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG +DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x +kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z +Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf +tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg +uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J +XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK +8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 +5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 +kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS +GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt +ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 +au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV +hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI +dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV +BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 +Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 +OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i +SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc +VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW +Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q +Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 +1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq +ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 +Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX +XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy +dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 +Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz +JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 +Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN +irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 +TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 +g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB +95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj +S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL +MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV +BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 +c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx +MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg +R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD +VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR +JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T +fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu +jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z +wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ +fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD +VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G +CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 +7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn +8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs +ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT +ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ +2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDNjCCAp+gAwIBAgIQNhIilsXjOKUgodJfTNcJVDANBgkqhkiG9w0BAQUFADCB +zjELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJ +Q2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UE +CxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhh +d3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNl +cnZlckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIxMDEwMTIzNTk1OVow +gc4xCzAJBgNVBAYTAlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcT +CUNhcGUgVG93bjEdMBsGA1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNV +BAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRo +YXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1z +ZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 +aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560 +ZXUCTe/LCaIhUdib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j ++ao6hnO2RlNYyIkFvYMRuHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/ +BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBlkKyID1bZ5jA01CbH0FDxkt5r1DmI +CSLGpmODA/eZd9iy5Ri4XWPz1HP7bJyZePFLeH0ZJMMrAoT4vCLZiiLXoPxx7JGH +IPG47LHlVYCsPVLIOQ7C8MAFT9aCdYy9X9LcdpoFEsmvcsPcJX6kTY4XpeCHf+Ga +WuFg3GQjPEIuTQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS +MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp +bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw +VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy +YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy +dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 +ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe +Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx +GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls +aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU +QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh +xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 +aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr +IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h +gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK +O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO +fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw +lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID +AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP +NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t +wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM +7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh +gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n +oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs +yZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg +MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz +MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy +dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD +VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg +xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu +xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7 +XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k +heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J +YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C +urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1 +JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51 +b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV +9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7 +kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh +fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy +B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA +aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS +RGQDJereW26fyfJOrN3H +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx +OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry +b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC +VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE +sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F +ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY +KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG ++7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG +HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P +IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M +733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk +Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW +AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I +aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 +mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa +XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ +qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 +WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv +bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU +UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw +bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe +LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef +J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh +R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ +Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX +JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p +zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S +Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq +ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz +gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH +uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS +y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f +zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi +TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW +NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV +Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx +IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs +cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 +MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl +bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD +DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r +WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU +Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs +HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj +z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf +SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl +AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG +KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P +AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j +BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC +VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX +ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB +ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd +/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB +A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn +k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 +iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv +2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV +BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw +MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX +b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN +rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U +fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc +f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 +ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M +x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR +aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch +zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar +uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K +mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA +Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv +HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H +EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ +MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e +JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN +g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp +dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab +R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ +PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce +xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ +J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl +OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT +ee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV +BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw +MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl +ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r +D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1 +9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf +v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk +UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L +NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb ++gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V +qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K +yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G +AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK +J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4 +WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj +/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6 +jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2 +ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX +X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n +FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D +u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l +O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le +ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1 +2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIEMDCCA5mgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBxzELMAkGA1UEBhMCVVMx +FzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMR8wHQYDVQQHExZSZXNlYXJjaCBUcmlh +bmdsZSBQYXJrMRYwFAYDVQQKEw1SZWQgSGF0LCBJbmMuMSEwHwYDVQQLExhSZWQg +SGF0IE5ldHdvcmsgU2VydmljZXMxIzAhBgNVBAMTGlJITlMgQ2VydGlmaWNhdGUg +QXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9yaG5zQHJlZGhhdC5jb20wHhcNMDAw +ODIzMjI0NTU1WhcNMDMwODI4MjI0NTU1WjCBxzELMAkGA1UEBhMCVVMxFzAVBgNV +BAgTDk5vcnRoIENhcm9saW5hMR8wHQYDVQQHExZSZXNlYXJjaCBUcmlhbmdsZSBQ +YXJrMRYwFAYDVQQKEw1SZWQgSGF0LCBJbmMuMSEwHwYDVQQLExhSZWQgSGF0IE5l +dHdvcmsgU2VydmljZXMxIzAhBgNVBAMTGlJITlMgQ2VydGlmaWNhdGUgQXV0aG9y +aXR5MR4wHAYJKoZIhvcNAQkBFg9yaG5zQHJlZGhhdC5jb20wgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAMBoKxIw4iEtIsZycVu/F6CTEOmb48mNOy2sxLuVO+DK +VTLclcIQswSyUfvohWEWNKW0HWdcp3f08JLatIuvlZNi82YprsCIt2SEDkiQYPhg +PgB/VN0XpqwY4ELefL6Qgff0BYUKCMzV8p/8JIt3pT3pSKnvDztjo/6mg0zo3At3 +AgMBAAGjggEoMIIBJDAdBgNVHQ4EFgQUVBXNnyz37A0f0qi+TAesiD77mwowgfQG +A1UdIwSB7DCB6YAUVBXNnyz37A0f0qi+TAesiD77mwqhgc2kgcowgccxCzAJBgNV +BAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEfMB0GA1UEBxMWUmVzZWFy +Y2ggVHJpYW5nbGUgUGFyazEWMBQGA1UEChMNUmVkIEhhdCwgSW5jLjEhMB8GA1UE +CxMYUmVkIEhhdCBOZXR3b3JrIFNlcnZpY2VzMSMwIQYDVQQDExpSSE5TIENlcnRp +ZmljYXRlIEF1dGhvcml0eTEeMBwGCSqGSIb3DQEJARYPcmhuc0ByZWRoYXQuY29t +ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAkwGIiGdnkYye0BIU +kHESh1UK8lIbrfLTBx2vcJm7sM2AI8ntK3PpY7HQs4xgxUJkpsGVVpDFNQYDWPWO +K9n5qaAQqZn3FUKSpVDXEQfxAtXgcORVbirOJfhdzQsvEGH49iBCzMOJ+IpPgiQS +zzl/IagsjVKXUsX3X0KlhwlmsMw= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID7jCCA1egAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsTELMAkGA1UEBhMCVVMx +FzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHEwdSYWxlaWdoMRYwFAYD +VQQKEw1SZWQgSGF0LCBJbmMuMRgwFgYDVQQLEw9SZWQgSGF0IE5ldHdvcmsxIjAg +BgNVBAMTGVJITiBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEW +EnJobi1ub2NAcmVkaGF0LmNvbTAeFw0wMjA5MDUyMDQ1MTZaFw0wNzA5MDkyMDQ1 +MTZaMIGxMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExEDAO +BgNVBAcTB1JhbGVpZ2gxFjAUBgNVBAoTDVJlZCBIYXQsIEluYy4xGDAWBgNVBAsT +D1JlZCBIYXQgTmV0d29yazEiMCAGA1UEAxMZUkhOIENlcnRpZmljYXRlIEF1dGhv +cml0eTEhMB8GCSqGSIb3DQEJARYScmhuLW5vY0ByZWRoYXQuY29tMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQCzFrfF9blpUR/NtD1wz2BXhaQqp10oIg7sGeKS +90iXpqYfUZWDEY+amKKQ4MtKJBmUqIpLiLQGbM531xU7PM1mg88jHQ28CgzLH8tA ++/PZ/iq0hSx7yaH+849oHfISsaQWGc4PuJqc2bxfSWKylZPOXS7deTzxW6a3orU5 +DY4SMQIDAQABo4IBEjCCAQ4wHQYDVR0OBBYEFH8bZKEuAsWofbjRsYsGnaOpUGOS +MIHeBgNVHSMEgdYwgdOAFH8bZKEuAsWofbjRsYsGnaOpUGOSoYG3pIG0MIGxMQsw +CQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcTB1Jh +bGVpZ2gxFjAUBgNVBAoTDVJlZCBIYXQsIEluYy4xGDAWBgNVBAsTD1JlZCBIYXQg +TmV0d29yazEiMCAGA1UEAxMZUkhOIENlcnRpZmljYXRlIEF1dGhvcml0eTEhMB8G +CSqGSIb3DQEJARYScmhuLW5vY0ByZWRoYXQuY29tggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEEBQADgYEAKE1C5TQi3caGYwR1UmcXRXLyOyErRVlyc/dZNp1X +Q8bclA8O/xNcT1A3hbLkwh81n3T051P7oQa4Oc7kCoZ7XyhdxxGeEqXWuWzpGAnV +8ELnVLWRniOtEnqqcnw5PIP4daR7A5L/KtTFdhkS+rQ7sIkslYwBkA3YugYFYQCs +ldo= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIID7jCCA1egAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsTELMAkGA1UEBhMCVVMx +FzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHEwdSYWxlaWdoMRYwFAYD +VQQKEw1SZWQgSGF0LCBJbmMuMRgwFgYDVQQLEw9SZWQgSGF0IE5ldHdvcmsxIjAg +BgNVBAMTGVJITiBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEW +EnJobi1ub2NAcmVkaGF0LmNvbTAeFw0wMzA4MjkwMjEwNTVaFw0xMzA4MjYwMjEw +NTVaMIGxMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExEDAO +BgNVBAcTB1JhbGVpZ2gxFjAUBgNVBAoTDVJlZCBIYXQsIEluYy4xGDAWBgNVBAsT +D1JlZCBIYXQgTmV0d29yazEiMCAGA1UEAxMZUkhOIENlcnRpZmljYXRlIEF1dGhv +cml0eTEhMB8GCSqGSIb3DQEJARYScmhuLW5vY0ByZWRoYXQuY29tMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQC/YWPrPYsrRUjmwvt80iEhuOyQk0EwfCyNedUU +6Q5+P+/WCpsKpgJSAS0mlqTtvameqggDwWEKQYDqrnTMYSbQBZFVPmYUoiCz1p1x +DKt3zPTwEbUlM4pOIpoQNmf6EW1Idjof0uNEe4lmvrSF+y+mqhP6mm3JuxjEBK9P +FWmJmwIDAQABo4IBEjCCAQ4wHQYDVR0OBBYEFGlEJwXcLu2l9IHE13hF50Rd+IdH +MIHeBgNVHSMEgdYwgdOAFGlEJwXcLu2l9IHE13hF50Rd+IdHoYG3pIG0MIGxMQsw +CQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcTB1Jh +bGVpZ2gxFjAUBgNVBAoTDVJlZCBIYXQsIEluYy4xGDAWBgNVBAsTD1JlZCBIYXQg +TmV0d29yazEiMCAGA1UEAxMZUkhOIENlcnRpZmljYXRlIEF1dGhvcml0eTEhMB8G +CSqGSIb3DQEJARYScmhuLW5vY0ByZWRoYXQuY29tggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEEBQADgYEAI8nKB59eljmD4E7a3UeEMMrU1TiG+d6Ig8osRyY2 +q/QUHigp3n0QSl6RPlqZBwypLuP7eERJxTLW6HqX/ynQM64munYGfnmXFwxPLSqL +iqxBWa7pxFUtuYjfm3tB+DIu7snAWeIwV143RynALXgz086jK9yE2r87Lku2s7ZO +noA= +-----END CERTIFICATE----- + diff --git a/deployed/akeeba/libraries/fof/download/adapter/curl.php b/deployed/akeeba/libraries/fof/download/adapter/curl.php new file mode 100644 index 00000000..70035bb6 --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/adapter/curl.php @@ -0,0 +1,226 @@ +priority = 110; + $this->supportsFileSize = true; + $this->supportsChunkDownload = true; + $this->name = 'curl'; + $this->isSupported = function_exists('curl_init') && function_exists('curl_exec') && function_exists('curl_close'); + } + + /** + * Download a part (or the whole) of a remote URL and return the downloaded + * data. You are supposed to check the size of the returned data. If it's + * smaller than what you expected you've reached end of file. If it's empty + * you have tried reading past EOF. If it's larger than what you expected + * the server doesn't support chunk downloads. + * + * If this class' supportsChunkDownload returns false you should assume + * that the $from and $to parameters will be ignored. + * + * @param string $url The remote file's URL + * @param integer $from Byte range to start downloading from. Use null for start of file. + * @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is ignored) + * @param array $params Additional params that will be added before performing the download + * + * @return string The raw file data retrieved from the remote URL. + * + * @throws Exception A generic exception is thrown on error + */ + public function downloadAndReturn($url, $from = null, $to = null, array $params = array()) + { + $ch = curl_init(); + + if (empty($from)) + { + $from = 0; + } + + if (empty($to)) + { + $to = 0; + } + + if ($to < $from) + { + $temp = $to; + $to = $from; + $from = $temp; + + unset($temp); + } + + // Default cURL options + $options = array( + CURLOPT_AUTOREFERER => 1, + CURLOPT_SSL_VERIFYPEER => 1, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSLVERSION => 0, + CURLOPT_AUTOREFERER => 1, + CURLOPT_URL => $url, + CURLOPT_BINARYTRANSFER => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FOLLOWLOCATION => 1, + CURLOPT_CAINFO => __DIR__ . '/cacert.pem', + CURLOPT_HEADERFUNCTION => array($this, 'reponseHeaderCallback') + ); + + if (!(empty($from) && empty($to))) + { + $options[CURLOPT_RANGE] = "$from-$to"; + } + + // Add any additional options: Since they are numeric, we must use the array operator. If the jey exists in both + // arrays, only the first one will be used while the second one will be ignored + $options = $params + $options; + + @curl_setopt_array($ch, $options); + + $this->headers = array(); + + $result = curl_exec($ch); + + $errno = curl_errno($ch); + $errmsg = curl_error($ch); + $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($result === false) + { + $error = JText::sprintf('LIB_FOF_DOWNLOAD_ERR_CURL_ERROR', $errno, $errmsg); + } + elseif (($http_status >= 300) && ($http_status <= 399) && isset($this->headers['Location']) && !empty($this->headers['Location'])) + { + return $this->downloadAndReturn($this->headers['Location'], $from, $to, $params); + } + elseif ($http_status > 399) + { + $result = false; + $errno = $http_status; + $error = JText::sprintf('LIB_FOF_DOWNLOAD_ERR_HTTPERROR', $http_status); + } + + curl_close($ch); + + if ($result === false) + { + throw new Exception($error, $errno); + } + else + { + return $result; + } + } + + /** + * Get the size of a remote file in bytes + * + * @param string $url The remote file's URL + * + * @return integer The file size, or -1 if the remote server doesn't support this feature + */ + public function getFileSize($url) + { + $result = -1; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_AUTOREFERER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_SSLVERSION, 0); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_NOBODY, true ); + curl_setopt($ch, CURLOPT_HEADER, true ); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true ); + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true ); + @curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem'); + + $data = curl_exec($ch); + curl_close($ch); + + if ($data) + { + $content_length = "unknown"; + $status = "unknown"; + $redirection = null; + + if (preg_match( "/^HTTP\/1\.[01] (\d\d\d)/", $data, $matches)) + { + $status = (int)$matches[1]; + } + + if (preg_match( "/Content-Length: (\d+)/", $data, $matches)) + { + $content_length = (int)$matches[1]; + } + + if (preg_match( "/Location: (.*)/", $data, $matches)) + { + $redirection = (int)$matches[1]; + } + + if ($status == 200) + { + $result = $content_length; + } + + if (($status > 300) && ($status <= 308)) + { + if (!empty($redirection)) + { + return $this->getFileSize($redirection); + } + + return -1; + } + } + + return $result; + } + + /** + * Handles the HTTP headers returned by cURL + * + * @param resource $ch cURL resource handle (unused) + * @param string $data Each header line, as returned by the server + * + * @return int The length of the $data string + */ + protected function reponseHeaderCallback(&$ch, &$data) + { + $strlen = strlen($data); + + if (($strlen) <= 2) + { + return $strlen; + } + + if (substr($data, 0, 4) == 'HTTP') + { + return $strlen; + } + + list($header, $value) = explode(': ', trim($data), 2); + + $this->headers[$header] = $value; + + return $strlen; + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/download/adapter/fopen.php b/deployed/akeeba/libraries/fof/download/adapter/fopen.php new file mode 100644 index 00000000..aa9cb3df --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/adapter/fopen.php @@ -0,0 +1,123 @@ +priority = 100; + $this->supportsFileSize = false; + $this->supportsChunkDownload = true; + $this->name = 'fopen'; + + // If we are not allowed to use ini_get, we assume that URL fopen is + // disabled. + if (!function_exists('ini_get')) + { + $this->isSupported = false; + } + else + { + $this->isSupported = ini_get('allow_url_fopen'); + } + } + + /** + * Download a part (or the whole) of a remote URL and return the downloaded + * data. You are supposed to check the size of the returned data. If it's + * smaller than what you expected you've reached end of file. If it's empty + * you have tried reading past EOF. If it's larger than what you expected + * the server doesn't support chunk downloads. + * + * If this class' supportsChunkDownload returns false you should assume + * that the $from and $to parameters will be ignored. + * + * @param string $url The remote file's URL + * @param integer $from Byte range to start downloading from. Use null for start of file. + * @param integer $to Byte range to stop downloading. Use null to download the entire file ($from is ignored) + * @param array $params Additional params that will be added before performing the download + * + * @return string The raw file data retrieved from the remote URL. + * + * @throws Exception A generic exception is thrown on error + */ + public function downloadAndReturn($url, $from = null, $to = null, array $params = array()) + { + if (empty($from)) + { + $from = 0; + } + + if (empty($to)) + { + $to = 0; + } + + if ($to < $from) + { + $temp = $to; + $to = $from; + $from = $temp; + + unset($temp); + } + + if (!(empty($from) && empty($to))) + { + $options = array( + 'http' => array( + 'method' => 'GET', + 'header' => "Range: bytes=$from-$to\r\n" + ), + 'ssl' => array( + 'verify_peer' => true, + 'cafile' => __DIR__ . '/cacert.pem', + 'verify_depth' => 5, + ) + ); + + $options = array_merge($options, $params); + + $context = stream_context_create($options); + $result = @file_get_contents($url, false, $context, $from - $to + 1); + } + else + { + $options = array( + 'http' => array( + 'method' => 'GET', + ), + 'ssl' => array( + 'verify_peer' => true, + 'cafile' => __DIR__ . '/cacert.pem', + 'verify_depth' => 5, + ) + ); + + $options = array_merge($options, $params); + + $context = stream_context_create($options); + $result = @file_get_contents($url, false, $context); + } + + if ($result === false) + { + $error = JText::sprintf('LIB_FOF_DOWNLOAD_ERR_HTTPERROR'); + throw new Exception($error, 1); + } + else + { + return $result; + } + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/download/download.php b/deployed/akeeba/libraries/fof/download/download.php new file mode 100644 index 00000000..3d346b95 --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/download.php @@ -0,0 +1,495 @@ +isSupported()) + { + continue; + } + + if ($adapter->priority > $priority) + { + $this->adapter = $adapter; + $priority = $adapter->priority; + } + } + + // Load the language strings + FOFPlatform::getInstance()->loadTranslations('lib_f0f'); + } + + /** + * Forces the use of a specific adapter + * + * @param string $className The name of the class or the name of the adapter, e.g. 'FOFDownloadAdapterCurl' or + * 'curl' + */ + public function setAdapter($className) + { + $adapter = null; + + if (class_exists($className, true)) + { + $adapter = new $className; + } + elseif (class_exists('FOFDownloadAdapter' . ucfirst($className))) + { + $className = 'FOFDownloadAdapter' . ucfirst($className); + $adapter = new $className; + } + + if (is_object($adapter) && ($adapter instanceof FOFDownloadInterface)) + { + $this->adapter = $adapter; + } + } + + /** + * Returns the name of the current adapter + * + * @return string + */ + public function getAdapterName() + { + if(is_object($this->adapter)) + { + $class = get_class($this->adapter); + + return strtolower(str_ireplace('FOFDownloadAdapter', '', $class)); + } + + return ''; + } + + /** + * Sets the additional options for the adapter + * + * @param array $options + */ + public function setAdapterOptions(array $options) + { + $this->adapterOptions = $options; + } + + /** + * Returns the additional options for the adapter + * + * @return array + */ + public function getAdapterOptions() + { + return $this->adapterOptions; + } + + /** + * Used to decode the $params array + * + * @param string $key The parameter key you want to retrieve the value for + * @param mixed $default The default value, if none is specified + * + * @return mixed The value for this parameter key + */ + private function getParam($key, $default = null) + { + if (array_key_exists($key, $this->params)) + { + return $this->params[$key]; + } + else + { + return $default; + } + } + + /** + * Download data from a URL and return it + * + * @param string $url The URL to download from + * + * @return bool|string The downloaded data or false on failure + */ + public function getFromURL($url) + { + try + { + return $this->adapter->downloadAndReturn($url, null, null, $this->adapterOptions); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Performs the staggered download of file. The downloaded file will be stored in Joomla!'s temp-path using the + * basename of the URL as a filename + * + * The $params array can have any of the following keys + * url The file being downloaded + * frag Rolling counter of the file fragment being downloaded + * totalSize The total size of the file being downloaded, in bytes + * doneSize How many bytes we have already downloaded + * maxExecTime Maximum execution time downloading file fragments, in seconds + * length How many bytes to download at once + * + * The array returned is in the following format: + * + * status True if there are no errors, false if there are errors + * error A string with the error message if there are errors + * frag The next file fragment to download + * totalSize The total size of the downloaded file in bytes, if the server supports HEAD requests + * doneSize How many bytes have already been downloaded + * percent % of the file already downloaded (if totalSize could be determined) + * localfile The name of the local file, without the path + * + * @param array $params A parameters array, as sent by the user interface + * + * @return array A return status array + */ + public function importFromURL($params) + { + $this->params = $params; + + // Fetch data + $url = $this->getParam('url'); + $localFilename = $this->getParam('localFilename'); + $frag = $this->getParam('frag', -1); + $totalSize = $this->getParam('totalSize', -1); + $doneSize = $this->getParam('doneSize', -1); + $maxExecTime = $this->getParam('maxExecTime', 5); + $runTimeBias = $this->getParam('runTimeBias', 75); + $length = $this->getParam('length', 1048576); + + if (empty($localFilename)) + { + $localFilename = basename($url); + + if (strpos($localFilename, '?') !== false) + { + $paramsPos = strpos($localFilename, '?'); + $localFilename = substr($localFilename, 0, $paramsPos - 1); + } + } + + $tmpDir = JFactory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp'); + $tmpDir = rtrim($tmpDir, '/\\'); + + // Init retArray + $retArray = array( + "status" => true, + "error" => '', + "frag" => $frag, + "totalSize" => $totalSize, + "doneSize" => $doneSize, + "percent" => 0, + "localfile" => $localFilename + ); + + try + { + $timer = new FOFUtilsTimer($maxExecTime, $runTimeBias); + $start = $timer->getRunningTime(); // Mark the start of this download + $break = false; // Don't break the step + + // Figure out where on Earth to put that file + $local_file = $tmpDir . '/' . $localFilename; + + while (($timer->getTimeLeft() > 0) && !$break) + { + // Do we have to initialize the file? + if ($frag == -1) + { + // Currently downloaded size + $doneSize = 0; + + if (@file_exists($local_file)) + { + @unlink($local_file); + } + + // Delete and touch the output file + $fp = @fopen($local_file, 'wb'); + + if ($fp !== false) + { + @fclose($fp); + } + + // Init + $frag = 0; + + //debugMsg("-- First frag, getting the file size"); + $retArray['totalSize'] = $this->adapter->getFileSize($url); + $totalSize = $retArray['totalSize']; + } + + // Calculate from and length + $from = $frag * $length; + $to = $length + $from - 1; + + // Try to download the first frag + $required_time = 1.0; + + try + { + $result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions); + + if ($result === false) + { + throw new Exception(JText::sprintf('LIB_FOF_DOWNLOAD_ERR_COULDNOTDOWNLOADFROMURL', $url), 500); + } + } + catch (Exception $e) + { + $result = false; + $error = $e->getMessage(); + } + + if ($result === false) + { + // Failed download + if ($frag == 0) + { + // Failure to download first frag = failure to download. Period. + $retArray['status'] = false; + $retArray['error'] = $error; + + //debugMsg("-- Download FAILED"); + + return $retArray; + } + else + { + // Since this is a staggered download, consider this normal and finish + $frag = -1; + //debugMsg("-- Import complete"); + $totalSize = $doneSize; + $break = true; + } + } + + // Add the currently downloaded frag to the total size of downloaded files + if ($result) + { + $filesize = strlen($result); + //debugMsg("-- Successful download of $filesize bytes"); + $doneSize += $filesize; + + // Append the file + $fp = @fopen($local_file, 'ab'); + + if ($fp === false) + { + //debugMsg("-- Can't open local file $local_file for writing"); + // Can't open the file for writing + $retArray['status'] = false; + $retArray['error'] = JText::sprintf('LIB_FOF_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $local_file); + + return $retArray; + } + + fwrite($fp, $result); + fclose($fp); + + //debugMsg("-- Appended data to local file $local_file"); + + $frag++; + + //debugMsg("-- Proceeding to next fragment, frag $frag"); + + if (($filesize < $length) || ($filesize > $length)) + { + // A partial download or a download larger than the frag size means we are done + $frag = -1; + //debugMsg("-- Import complete (partial download of last frag)"); + $totalSize = $doneSize; + $break = true; + } + } + + // Advance the frag pointer and mark the end + $end = $timer->getRunningTime(); + + // Do we predict that we have enough time? + $required_time = max(1.1 * ($end - $start), $required_time); + + if ($required_time > (10 - $end + $start)) + { + $break = true; + } + + $start = $end; + } + + if ($frag == -1) + { + $percent = 100; + } + elseif ($doneSize <= 0) + { + $percent = 0; + } + else + { + if ($totalSize > 0) + { + $percent = 100 * ($doneSize / $totalSize); + } + else + { + $percent = 0; + } + } + + // Update $retArray + $retArray = array( + "status" => true, + "error" => '', + "frag" => $frag, + "totalSize" => $totalSize, + "doneSize" => $doneSize, + "percent" => $percent, + ); + } + catch (Exception $e) + { + //debugMsg("EXCEPTION RAISED:"); + //debugMsg($e->getMessage()); + $retArray['status'] = false; + $retArray['error'] = $e->getMessage(); + } + + return $retArray; + } + + /** + * This method will crawl a starting directory and get all the valid files + * that will be analyzed by __construct. Then it organizes them into an + * associative array. + * + * @param string $path Folder where we should start looking + * @param array $ignoreFolders Folder ignore list + * @param array $ignoreFiles File ignore list + * + * @return array Associative array, where the `fullpath` key contains the path to the file, + * and the `classname` key contains the name of the class + */ + protected static function getFiles($path, array $ignoreFolders = array(), array $ignoreFiles = array()) + { + $return = array(); + + $files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles); + + // Ok, I got the files, now I have to organize them + foreach ($files as $file) + { + $clean = str_replace($path, '', $file); + $clean = trim(str_replace('\\', '/', $clean), '/'); + + $parts = explode('/', $clean); + + $return[] = array( + 'fullpath' => $file, + 'classname' => 'FOFDownloadAdapter' . ucfirst(basename($parts[0], '.php')) + ); + } + + return $return; + } + + /** + * Recursive function that will scan every directory unless it's in the + * ignore list. Files that aren't in the ignore list are returned. + * + * @param string $path Folder where we should start looking + * @param array $ignoreFolders Folder ignore list + * @param array $ignoreFiles File ignore list + * + * @return array List of all the files + */ + protected static function scanDirectory($path, array $ignoreFolders = array(), array $ignoreFiles = array()) + { + $return = array(); + + $handle = @opendir($path); + + if ( !$handle) + { + return $return; + } + + while (($file = readdir($handle)) !== false) + { + if ($file == '.' || $file == '..') + { + continue; + } + + $fullpath = $path . '/' . $file; + + if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles))) + { + continue; + } + + if (is_dir($fullpath)) + { + $return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return); + } + else + { + $return[] = $path . '/' . $file; + } + } + + return $return; + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/download/interface.php b/deployed/akeeba/libraries/fof/download/interface.php new file mode 100644 index 00000000..b5089fa5 --- /dev/null +++ b/deployed/akeeba/libraries/fof/download/interface.php @@ -0,0 +1,79 @@ +adapter = new FOFEncryptAesOpenssl(); + + if (!$this->adapter->isSupported($phpfunc)) + { + $this->adapter = new FOFEncryptAesMcrypt(); + } + } + else + { + $this->adapter = new FOFEncryptAesMcrypt(); + + if (!$this->adapter->isSupported($phpfunc)) + { + $this->adapter = new FOFEncryptAesOpenssl(); + } + } + + $this->adapter->setEncryptionMode($mode, $strength); + $this->setPassword($key, true); + } + + /** + * Sets the password for this instance. + * + * WARNING: Do not use the legacy mode, it's insecure + * + * @param string $password The password (either user-provided password or binary encryption key) to use + * @param bool $legacyMode True to use the legacy key expansion. We recommend against using it. + */ + public function setPassword($password, $legacyMode = false) + { + $this->key = $password; + + $passLength = strlen($password); + + if (function_exists('mb_strlen')) + { + $passLength = mb_strlen($password, 'ASCII'); + } + + // Legacy mode was doing something stupid, requiring a key of 32 bytes. DO NOT USE LEGACY MODE! + if ($legacyMode && ($passLength != 32)) + { + // Legacy mode: use the sha256 of the password + $this->key = hash('sha256', $password, true); + // We have to trim or zero pad the password (we end up throwing half of it away in Rijndael-128 / AES...) + $this->key = $this->adapter->resizeKey($this->key, $this->adapter->getBlockSize()); + } + } + + /** + * Encrypts a string using AES + * + * @param string $stringToEncrypt The plaintext to encrypt + * @param bool $base64encoded Should I Base64-encode the result? + * + * @return string The cryptotext. Please note that the first 16 bytes of + * the raw string is the IV (initialisation vector) which + * is necessary for decoding the string. + */ + public function encryptString($stringToEncrypt, $base64encoded = true) + { + $blockSize = $this->adapter->getBlockSize(); + $randVal = new FOFEncryptRandval(); + $iv = $randVal->generate($blockSize); + + $key = $this->getExpandedKey($blockSize, $iv); + $cipherText = $this->adapter->encrypt($stringToEncrypt, $key, $iv); + + // Optionally pass the result through Base64 encoding + if ($base64encoded) + { + $cipherText = base64_encode($cipherText); + } + + // Return the result + return $cipherText; + } + + /** + * Decrypts a ciphertext into a plaintext string using AES + * + * @param string $stringToDecrypt The ciphertext to decrypt. The first 16 bytes of the raw string must contain + * the IV (initialisation vector). + * @param bool $base64encoded Should I Base64-decode the data before decryption? + * + * @return string The plain text string + */ + public function decryptString($stringToDecrypt, $base64encoded = true) + { + if ($base64encoded) + { + $stringToDecrypt = base64_decode($stringToDecrypt); + } + + // Extract IV + $iv_size = $this->adapter->getBlockSize(); + $iv = substr($stringToDecrypt, 0, $iv_size); + $key = $this->getExpandedKey($iv_size, $iv); + + // Decrypt the data + $plainText = $this->adapter->decrypt($stringToDecrypt, $key); + + return $plainText; + } + + /** + * Is AES encryption supported by this PHP installation? + * + * @param FOFUtilsPhpfunc $phpfunc + * + * @return boolean + */ + public static function isSupported(FOFUtilsPhpfunc $phpfunc = null) + { + if (!is_object($phpfunc) || !($phpfunc instanceof $phpfunc)) + { + $phpfunc = new FOFUtilsPhpfunc(); + } + + $adapter = new FOFEncryptAesMcrypt(); + + if (!$adapter->isSupported($phpfunc)) + { + $adapter = new FOFEncryptAesOpenssl(); + } + + if (!$adapter->isSupported($phpfunc)) + { + return false; + } + + if (!$phpfunc->function_exists('base64_encode')) + { + return false; + } + + if (!$phpfunc->function_exists('base64_decode')) + { + return false; + } + + if (!$phpfunc->function_exists('hash_algos')) + { + return false; + } + + $algorightms = $phpfunc->hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + /** + * @param $blockSize + * @param $iv + * + * @return string + */ + public function getExpandedKey($blockSize, $iv) + { + $key = $this->key; + $passLength = strlen($key); + + if (function_exists('mb_strlen')) + { + $passLength = mb_strlen($key, 'ASCII'); + } + + if ($passLength != $blockSize) + { + $iterations = 1000; + $salt = $this->adapter->resizeKey($iv, 16); + $key = hash_pbkdf2('sha256', $this->key, $salt, $iterations, $blockSize, true); + } + + return $key; + } +} + +if (!function_exists('hash_pbkdf2')) +{ + function hash_pbkdf2($algo, $password, $salt, $count, $length = 0, $raw_output = false) + { + if (!in_array(strtolower($algo), hash_algos())) + { + trigger_error(__FUNCTION__ . '(): Unknown hashing algorithm: ' . $algo, E_USER_WARNING); + } + + if (!is_numeric($count)) + { + trigger_error(__FUNCTION__ . '(): expects parameter 4 to be long, ' . gettype($count) . ' given', E_USER_WARNING); + } + + if (!is_numeric($length)) + { + trigger_error(__FUNCTION__ . '(): expects parameter 5 to be long, ' . gettype($length) . ' given', E_USER_WARNING); + } + + if ($count <= 0) + { + trigger_error(__FUNCTION__ . '(): Iterations must be a positive integer: ' . $count, E_USER_WARNING); + } + + if ($length < 0) + { + trigger_error(__FUNCTION__ . '(): Length must be greater than or equal to 0: ' . $length, E_USER_WARNING); + } + + $output = ''; + $block_count = $length ? ceil($length / strlen(hash($algo, '', $raw_output))) : 1; + + for ($i = 1; $i <= $block_count; $i++) + { + $last = $xorsum = hash_hmac($algo, $salt . pack('N', $i), $password, true); + + for ($j = 1; $j < $count; $j++) + { + $xorsum ^= ($last = hash_hmac($algo, $last, $password, true)); + } + + $output .= $xorsum; + } + + if (!$raw_output) + { + $output = bin2hex($output); + } + + return $length ? substr($output, 0, $length) : $output; + } +} diff --git a/deployed/akeeba/libraries/fof/encrypt/aes/abstract.php b/deployed/akeeba/libraries/fof/encrypt/aes/abstract.php new file mode 100644 index 00000000..981f653b --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/aes/abstract.php @@ -0,0 +1,88 @@ + $size) + { + if (function_exists('mb_substr')) + { + return mb_substr($key, 0, $size, 'ASCII'); + } + + return substr($key, 0, $size); + } + + return $key . str_repeat("\0", ($size - $keyLength)); + } + + /** + * Returns null bytes to append to the string so that it's zero padded to the specified block size + * + * @param string $string The binary string which will be zero padded + * @param int $blockSize The block size + * + * @return string The zero bytes to append to the string to zero pad it to $blockSize + */ + protected function getZeroPadding($string, $blockSize) + { + $stringSize = strlen($string); + + if (function_exists('mb_strlen')) + { + $stringSize = mb_strlen($string, 'ASCII'); + } + + if ($stringSize == $blockSize) + { + return ''; + } + + if ($stringSize < $blockSize) + { + return str_repeat("\0", $blockSize - $stringSize); + } + + $paddingBytes = $stringSize % $blockSize; + + return str_repeat("\0", $blockSize - $paddingBytes); + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/encrypt/aes/interface.php b/deployed/akeeba/libraries/fof/encrypt/aes/interface.php new file mode 100644 index 00000000..e8edaebe --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/aes/interface.php @@ -0,0 +1,83 @@ +cipherType = MCRYPT_RIJNDAEL_128; + break; + + case '192': + $this->cipherType = MCRYPT_RIJNDAEL_192; + break; + + case '256': + $this->cipherType = MCRYPT_RIJNDAEL_256; + break; + } + + switch (strtolower($mode)) + { + case 'ecb': + $this->cipherMode = MCRYPT_MODE_ECB; + break; + + default: + case 'cbc': + $this->cipherMode = MCRYPT_MODE_CBC; + break; + } + + } + + public function encrypt($plainText, $key, $iv = null) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = $this->resizeKey($iv, $iv_size); + + if (empty($iv)) + { + $randVal = new FOFEncryptRandval(); + $iv = $randVal->generate($iv_size); + } + + $cipherText = mcrypt_encrypt($this->cipherType, $key, $plainText, $this->cipherMode, $iv); + $cipherText = $iv . $cipherText; + + return $cipherText; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv); + + return $plainText; + } + + public function isSupported(FOFUtilsPhpfunc $phpfunc = null) + { + if (!is_object($phpfunc) || !($phpfunc instanceof $phpfunc)) + { + $phpfunc = new FOFUtilsPhpfunc(); + } + + if (!$phpfunc->function_exists('mcrypt_get_key_size')) + { + return false; + } + + if (!$phpfunc->function_exists('mcrypt_get_iv_size')) + { + return false; + } + + if (!$phpfunc->function_exists('mcrypt_create_iv')) + { + return false; + } + + if (!$phpfunc->function_exists('mcrypt_encrypt')) + { + return false; + } + + if (!$phpfunc->function_exists('mcrypt_decrypt')) + { + return false; + } + + if (!$phpfunc->function_exists('mcrypt_list_algorithms')) + { + return false; + } + + if (!$phpfunc->function_exists('hash')) + { + return false; + } + + if (!$phpfunc->function_exists('hash_algos')) + { + return false; + } + + $algorightms = $phpfunc->mcrypt_list_algorithms(); + + if (!in_array('rijndael-128', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-192', $algorightms)) + { + return false; + } + + if (!in_array('rijndael-256', $algorightms)) + { + return false; + } + + $algorightms = $phpfunc->hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + public function getBlockSize() + { + return mcrypt_get_iv_size($this->cipherType, $this->cipherMode); + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/encrypt/aes/openssl.php b/deployed/akeeba/libraries/fof/encrypt/aes/openssl.php new file mode 100644 index 00000000..094b1e50 --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/aes/openssl.php @@ -0,0 +1,172 @@ +openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + + public function setEncryptionMode($mode = 'cbc', $strength = 128) + { + static $availableAlgorithms = null; + static $defaultAlgo = 'aes-128-cbc'; + + if (!is_array($availableAlgorithms)) + { + $availableAlgorithms = openssl_get_cipher_methods(); + + foreach (array('aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc', + 'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb') as $algo) + { + if (in_array($algo, $availableAlgorithms)) + { + $defaultAlgo = $algo; + break; + } + } + } + + $strength = (int) $strength; + $mode = strtolower($mode); + + if (!in_array($strength, array(128, 192, 256))) + { + $strength = 256; + } + + if (!in_array($mode, array('cbc', 'ebc'))) + { + $mode = 'cbc'; + } + + $algo = 'aes-' . $strength . '-' . $mode; + + if (!in_array($algo, $availableAlgorithms)) + { + $algo = $defaultAlgo; + } + + $this->method = $algo; + } + + public function encrypt($plainText, $key, $iv = null) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = $this->resizeKey($iv, $iv_size); + + if (empty($iv)) + { + $randVal = new FOFEncryptRandval(); + $iv = $randVal->generate($iv_size); + } + + $plainText .= $this->getZeroPadding($plainText, $iv_size); + $cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv); + $cipherText = $iv . $cipherText; + + return $cipherText; + } + + public function decrypt($cipherText, $key) + { + $iv_size = $this->getBlockSize(); + $key = $this->resizeKey($key, $iv_size); + $iv = substr($cipherText, 0, $iv_size); + $cipherText = substr($cipherText, $iv_size); + $plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv); + + return $plainText; + } + + public function isSupported(FOFUtilsPhpfunc $phpfunc = null) + { + if (!is_object($phpfunc) || !($phpfunc instanceof $phpfunc)) + { + $phpfunc = new FOFUtilsPhpfunc(); + } + + if (!$phpfunc->function_exists('openssl_get_cipher_methods')) + { + return false; + } + + if (!$phpfunc->function_exists('openssl_random_pseudo_bytes')) + { + return false; + } + + if (!$phpfunc->function_exists('openssl_cipher_iv_length')) + { + return false; + } + + if (!$phpfunc->function_exists('openssl_encrypt')) + { + return false; + } + + if (!$phpfunc->function_exists('openssl_decrypt')) + { + return false; + } + + if (!$phpfunc->function_exists('hash')) + { + return false; + } + + if (!$phpfunc->function_exists('hash_algos')) + { + return false; + } + + $algorightms = $phpfunc->openssl_get_cipher_methods(); + + if (!in_array('aes-128-cbc', $algorightms)) + { + return false; + } + + $algorightms = $phpfunc->hash_algos(); + + if (!in_array('sha256', $algorightms)) + { + return false; + } + + return true; + } + + /** + * @return int + */ + public function getBlockSize() + { + return openssl_cipher_iv_length($this->method); + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/encrypt/base32.php b/deployed/akeeba/libraries/fof/encrypt/base32.php new file mode 100644 index 00000000..768e064d --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/base32.php @@ -0,0 +1,222 @@ + 0) + { + throw new Exception('Length must be divisible by 8'); + } + + if (!preg_match('/^[01]+$/', $str)) + { + throw new Exception('Only 0\'s and 1\'s are permitted'); + } + + preg_match_all('/.{8}/', $str, $chrs); + $chrs = array_map('bindec', $chrs[0]); + + // I'm just being slack here + array_unshift($chrs, 'C*'); + + return call_user_func_array('pack', $chrs); + } + + /** + * fromBin + * + * Converts a correct binary string to base32 + * + * @param string $str The string of 0's and 1's you want to convert + * + * @return string String encoded as base32 + * + * @throws exception + */ + private function fromBin($str) + { + if (strlen($str) % 8 > 0) + { + throw new Exception('Length must be divisible by 8'); + } + + if (!preg_match('/^[01]+$/', $str)) + { + throw new Exception('Only 0\'s and 1\'s are permitted'); + } + + // Base32 works on the first 5 bits of a byte, so we insert blanks to pad it out + $str = preg_replace('/(.{5})/', '000$1', $str); + + // We need a string divisible by 5 + $length = strlen($str); + $rbits = $length & 7; + + if ($rbits > 0) + { + // Excessive bits need to be padded + $ebits = substr($str, $length - $rbits); + $str = substr($str, 0, $length - $rbits); + $str .= "000$ebits" . str_repeat('0', 5 - strlen($ebits)); + } + + preg_match_all('/.{8}/', $str, $chrs); + $chrs = array_map(array($this, '_mapcharset'), $chrs[0]); + + return join('', $chrs); + } + + /** + * toBin + * + * Accepts a base32 string and returns an ascii binary string + * + * @param string $str The base32 string to convert + * + * @return string Ascii binary string + * + * @throws Exception + */ + private function toBin($str) + { + if (!preg_match('/^[' . self::CSRFC3548 . ']+$/', $str)) + { + throw new Exception('Must match character set'); + } + + // Convert the base32 string back to a binary string + $str = join('', array_map(array($this, '_mapbin'), str_split($str))); + + // Remove the extra 0's we added + $str = preg_replace('/000(.{5})/', '$1', $str); + + // Unpad if nessicary + $length = strlen($str); + $rbits = $length & 7; + + if ($rbits > 0) + { + $str = substr($str, 0, $length - $rbits); + } + + return $str; + } + + /** + * fromString + * + * Convert any string to a base32 string + * This should be binary safe... + * + * @param string $str The string to convert + * + * @return string The converted base32 string + */ + public function encode($str) + { + return $this->fromBin($this->str2bin($str)); + } + + /** + * toString + * + * Convert any base32 string to a normal sctring + * This should be binary safe... + * + * @param string $str The base32 string to convert + * + * @return string The normal string + */ + public function decode($str) + { + $str = strtoupper($str); + + return $this->bin2str($this->tobin($str)); + } + + /** + * _mapcharset + * + * Used with array_map to map the bits from a binary string + * directly into a base32 character set + * + * @param string $str The string of 0's and 1's you want to convert + * + * @return string Resulting base32 character + * + * @access private + */ + private function _mapcharset($str) + { + // Huh! + $x = self::CSRFC3548; + + return $x[bindec($str)]; + } + + /** + * _mapbin + * + * Used with array_map to map the characters from a base32 + * character set directly into a binary string + * + * @param string $chr The caracter to map + * + * @return string String of 0's and 1's + * + * @access private + */ + private function _mapbin($chr) + { + return sprintf('%08b', strpos(self::CSRFC3548, $chr)); + } +} diff --git a/deployed/akeeba/libraries/fof/encrypt/randval.php b/deployed/akeeba/libraries/fof/encrypt/randval.php new file mode 100644 index 00000000..d6a51156 --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/randval.php @@ -0,0 +1,196 @@ +phpfunc = $phpfunc; + } + + /** + * + * Returns a cryptographically secure random value. + * + * @param integer $bytes How many bytes to return + * + * @return string + */ + public function generate($bytes = 32) + { + if ($this->phpfunc->extension_loaded('openssl') && (version_compare(PHP_VERSION, '5.3.4') >= 0 || IS_WIN)) + { + $strong = false; + $randBytes = openssl_random_pseudo_bytes($bytes, $strong); + + if ($strong) + { + return $randBytes; + } + } + + if ($this->phpfunc->extension_loaded('mcrypt')) + { + return $this->phpfunc->mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM); + } + + return $this->genRandomBytes($bytes); + } + + /** + * Generate random bytes. Adapted from Joomla! 3.2. + * + * @param integer $length Length of the random data to generate + * + * @return string Random binary data + */ + public function genRandomBytes($length = 32) + { + $length = (int) $length; + $sslStr = ''; + + /* + * Collect any entropy available in the system along with a number + * of time measurements of operating system randomness. + */ + $bitsPerRound = 2; + $maxTimeMicro = 400; + $shaHashLength = 20; + $randomStr = ''; + $total = $length; + + // Check if we can use /dev/urandom. + $urandom = false; + $handle = null; + + // This is PHP 5.3.3 and up + if ($this->phpfunc->function_exists('stream_set_read_buffer') && @is_readable('/dev/urandom')) + { + $handle = @fopen('/dev/urandom', 'rb'); + + if ($handle) + { + $urandom = true; + } + } + + while ($length > strlen($randomStr)) + { + $bytes = ($total > $shaHashLength)? $shaHashLength : $total; + $total -= $bytes; + + /* + * Collect any entropy available from the PHP system and filesystem. + * If we have ssl data that isn't strong, we use it once. + */ + $entropy = rand() . uniqid(mt_rand(), true) . $sslStr; + $entropy .= implode('', @fstat(fopen(__FILE__, 'r'))); + $entropy .= memory_get_usage(); + $sslStr = ''; + + if ($urandom) + { + stream_set_read_buffer($handle, 0); + $entropy .= @fread($handle, $bytes); + } + else + { + /* + * There is no external source of entropy so we repeat calls + * to mt_rand until we are assured there's real randomness in + * the result. + * + * Measure the time that the operations will take on average. + */ + $samples = 3; + $duration = 0; + + for ($pass = 0; $pass < $samples; ++$pass) + { + $microStart = microtime(true) * 1000000; + $hash = sha1(mt_rand(), true); + + for ($count = 0; $count < 50; ++$count) + { + $hash = sha1($hash, true); + } + + $microEnd = microtime(true) * 1000000; + $entropy .= $microStart . $microEnd; + + if ($microStart >= $microEnd) + { + $microEnd += 1000000; + } + + $duration += $microEnd - $microStart; + } + + $duration = $duration / $samples; + + /* + * Based on the average time, determine the total rounds so that + * the total running time is bounded to a reasonable number. + */ + $rounds = (int) (($maxTimeMicro / $duration) * 50); + + /* + * Take additional measurements. On average we can expect + * at least $bitsPerRound bits of entropy from each measurement. + */ + $iter = $bytes * (int) ceil(8 / $bitsPerRound); + + for ($pass = 0; $pass < $iter; ++$pass) + { + $microStart = microtime(true); + $hash = sha1(mt_rand(), true); + + for ($count = 0; $count < $rounds; ++$count) + { + $hash = sha1($hash, true); + } + + $entropy .= $microStart . microtime(true); + } + } + + $randomStr .= sha1($entropy, true); + } + + if ($urandom) + { + @fclose($handle); + } + + return substr($randomStr, 0, $length); + } +} diff --git a/deployed/akeeba/libraries/fof/encrypt/randvalinterface.php b/deployed/akeeba/libraries/fof/encrypt/randvalinterface.php new file mode 100644 index 00000000..85cd6bd7 --- /dev/null +++ b/deployed/akeeba/libraries/fof/encrypt/randvalinterface.php @@ -0,0 +1,22 @@ +_timeStep = $timeStep; + $this->_passCodeLength = $passCodeLength; + $this->_secretLength = $secretLength; + $this->_pinModulo = pow(10, $this->_passCodeLength); + + if (is_null($base32)) + { + $this->_base32 = new FOFEncryptBase32; + } + else + { + $this->_base32 = $base32; + } + } + + /** + * Get the time period based on the $time timestamp and the Time Step + * defined. If $time is skipped or set to null the current timestamp will + * be used. + * + * @param int|null $time Timestamp + * + * @return int The time period since the UNIX Epoch + */ + public function getPeriod($time = null) + { + if (is_null($time)) + { + $time = time(); + } + + $period = floor($time / $this->_timeStep); + + return $period; + } + + /** + * Check is the given passcode $code is a valid TOTP generated using secret + * key $secret + * + * @param string $secret The Base32-encoded secret key + * @param string $code The passcode to check + * + * @return boolean True if the code is valid + */ + public function checkCode($secret, $code) + { + $time = $this->getPeriod(); + + for ($i = -1; $i <= 1; $i++) + { + if ($this->getCode($secret, ($time + $i) * $this->_timeStep) == $code) + { + return true; + } + } + + return false; + } + + /** + * Gets the TOTP passcode for a given secret key $secret and a given UNIX + * timestamp $time + * + * @param string $secret The Base32-encoded secret key + * @param int $time UNIX timestamp + * + * @return string + */ + public function getCode($secret, $time = null) + { + $period = $this->getPeriod($time); + $secret = $this->_base32->decode($secret); + + $time = pack("N", $period); + $time = str_pad($time, 8, chr(0), STR_PAD_LEFT); + + $hash = hash_hmac('sha1', $time, $secret, true); + $offset = ord(substr($hash, -1)); + $offset = $offset & 0xF; + + $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF; + $pinValue = str_pad($truncatedHash % $this->_pinModulo, $this->_passCodeLength, "0", STR_PAD_LEFT); + + return $pinValue; + } + + /** + * Extracts a part of a hash as an integer + * + * @param string $bytes The hash + * @param string $start The char to start from (0 = first char) + * + * @return string + */ + protected function hashToInt($bytes, $start) + { + $input = substr($bytes, $start, strlen($bytes) - $start); + $val2 = unpack("N", substr($input, 0, 4)); + + return $val2[1]; + } + + /** + * Returns a QR code URL for easy setup of TOTP apps like Google Authenticator + * + * @param string $user User + * @param string $hostname Hostname + * @param string $secret Secret string + * + * @return string + */ + public function getUrl($user, $hostname, $secret) + { + $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret); + $encoder = "https://chart.googleapis.com/chart?chs=200x200&chld=Q|2&cht=qr&chl="; + $encoderURL = $encoder . urlencode($url); + + return $encoderURL; + } + + /** + * Generates a (semi-)random Secret Key for TOTP generation + * + * @return string + */ + public function generateSecret() + { + $secret = ""; + + for ($i = 1; $i <= $this->_secretLength; $i++) + { + $c = rand(0, 255); + $secret .= pack("c", $c); + } + $base32 = new FOFEncryptBase32; + + return $this->_base32->encode($secret); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field.php b/deployed/akeeba/libraries/fof/form/field.php new file mode 100644 index 00000000..3f6df7b4 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field.php @@ -0,0 +1,38 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + $params = $this->getOptions(); + + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true); + + $query->select('a.id AS value, a.title AS text'); + $query->from('#__viewlevels AS a'); + $query->group('a.id, a.title, a.ordering'); + $query->order('a.ordering ASC'); + $query->order($query->qn('title') . ' ASC'); + + // Get the options. + $db->setQuery($query); + $options = $db->loadObjectList(); + + // If params is an array, push these options to the array + if (is_array($params)) + { + $options = array_merge($params, $options); + } + + // If all levels is allowed, push it into the array. + elseif ($params) + { + array_unshift($options, JHtml::_('select.option', '', JText::_('JOPTION_ACCESS_SHOW_ALL_LEVELS'))); + } + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($options, $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $params = $this->getOptions(); + + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true); + + $query->select('a.id AS value, a.title AS text'); + $query->from('#__viewlevels AS a'); + $query->group('a.id, a.title, a.ordering'); + $query->order('a.ordering ASC'); + $query->order($query->qn('title') . ' ASC'); + + // Get the options. + $db->setQuery($query); + $options = $db->loadObjectList(); + + // If params is an array, push these options to the array + if (is_array($params)) + { + $options = array_merge($params, $options); + } + + // If all levels is allowed, push it into the array. + elseif ($params) + { + array_unshift($options, JHtml::_('select.option', '', JText::_('JOPTION_ACCESS_SHOW_ALL_LEVELS'))); + } + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($options, $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/actions.php b/deployed/akeeba/libraries/fof/form/field/actions.php new file mode 100644 index 00000000..fc232da7 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/actions.php @@ -0,0 +1,250 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the field configuration + * + * @return array + */ + protected function getConfig() + { + // If no custom options were defined let's figure out which ones of the + // defaults we shall use... + $config = array( + 'published' => 1, + 'unpublished' => 1, + 'archived' => 0, + 'trash' => 0, + 'all' => 0, + ); + + $stack = array(); + + if (isset($this->element['show_published'])) + { + $config['published'] = FOFStringUtils::toBool($this->element['show_published']); + } + + if (isset($this->element['show_unpublished'])) + { + $config['unpublished'] = FOFStringUtils::toBool($this->element['show_unpublished']); + } + + if (isset($this->element['show_archived'])) + { + $config['archived'] = FOFStringUtils::toBool($this->element['show_archived']); + } + + if (isset($this->element['show_trash'])) + { + $config['trash'] = FOFStringUtils::toBool($this->element['show_trash']); + } + + if (isset($this->element['show_all'])) + { + $config['all'] = FOFStringUtils::toBool($this->element['show_all']); + } + + return $config; + } + + /** + * Method to get the field options. + * + * @since 2.0 + * + * @return array The field option objects. + */ + protected function getOptions() + { + return null; + } + + /** + * Method to get a + * + * @param string $enabledFieldName Name of the enabled/published field + * + * @return FOFFormFieldPublished Field + */ + protected function getPublishedField($enabledFieldName) + { + $attributes = array( + 'name' => $enabledFieldName, + 'type' => 'published', + ); + + if ($this->element['publish_up']) + { + $attributes['publish_up'] = (string) $this->element['publish_up']; + } + + if ($this->element['publish_down']) + { + $attributes['publish_down'] = (string) $this->element['publish_down']; + } + + foreach ($attributes as $name => $value) + { + if (!is_null($value)) + { + $renderedAttributes[] = $name . '="' . $value . '"'; + } + } + + $publishedXml = new SimpleXMLElement(''); + + $publishedField = new FOFFormFieldPublished($this->form); + + // Pass required objects to the field + $publishedField->item = $this->item; + $publishedField->rowid = $this->rowid; + $publishedField->setup($publishedXml, $this->item->{$enabledFieldName}); + + return $publishedField; + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + throw new Exception(__CLASS__ . ' cannot be used in single item display forms'); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + if (!($this->item instanceof FOFTable)) + { + throw new Exception(__CLASS__ . ' needs a FOFTable to act upon'); + } + + $config = $this->getConfig(); + + // Initialise + $prefix = ''; + $checkbox = 'cb'; + $publish_up = null; + $publish_down = null; + $enabled = true; + + $html = '
        '; + + // Render a published field + if ($publishedFieldName = $this->item->getColumnAlias('enabled')) + { + if ($config['published'] || $config['unpublished']) + { + // Generate a FOFFormFieldPublished field + $publishedField = $this->getPublishedField($publishedFieldName); + + // Render the publish button + $html .= $publishedField->getRepeatable(); + } + + if ($config['archived']) + { + $archived = $this->item->{$publishedFieldName} == 2 ? true : false; + + // Create dropdown items + $action = $archived ? 'unarchive' : 'archive'; + JHtml::_('actionsdropdown.' . $action, 'cb' . $this->rowid, $prefix); + } + + if ($config['trash']) + { + $trashed = $this->item->{$publishedFieldName} == -2 ? true : false; + + $action = $trashed ? 'untrash' : 'trash'; + JHtml::_('actionsdropdown.' . $action, 'cb' . $this->rowid, $prefix); + } + + // Render dropdown list + if ($config['archived'] || $config['trash']) + { + $html .= JHtml::_('actionsdropdown.render', $this->item->title); + } + } + + $html .= '
        '; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/button.php b/deployed/akeeba/libraries/fof/form/field/button.php new file mode 100644 index 00000000..1f459dbb --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/button.php @@ -0,0 +1,137 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getInput() + { + $this->label = ''; + + $allowedElement = array('button', 'a'); + + if (in_array($this->element['htmlelement'], $allowedElement)) + $type = $this->element['htmlelement']; + else + $type = 'button'; + + $text = $this->element['text']; + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + $icon = $this->element['icon'] ? (string) $this->element['icon'] : ''; + $onclick = $this->element['onclick'] ? 'onclick="' . (string) $this->element['onclick'] . '"' : ''; + $url = $this->element['url'] ? 'href="' . $this->parseFieldTags((string) $this->element['url']) . '"' : ''; + $title = $this->element['title'] ? 'title="' . JText::_((string) $this->element['title']) . '"' : ''; + + $this->value = JText::_($text); + + if ($icon) + { + $icon = ''; + } + + return '<' . $type . ' id="' . $this->id . '" class="btn ' . $class . '" ' . + $onclick . $url . $title . '>' . + $icon . + htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Method to get the field title. + * + * @return string The field title. + */ + protected function getTitle() + { + return null; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/cachehandler.php b/deployed/akeeba/libraries/fof/form/field/cachehandler.php new file mode 100644 index 00000000..6b9816b5 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/cachehandler.php @@ -0,0 +1,102 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/calendar.php b/deployed/akeeba/libraries/fof/form/field/calendar.php new file mode 100644 index 00000000..40552e6d --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/calendar.php @@ -0,0 +1,211 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getCalendar('static'); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getCalendar('repeatable'); + } + + /** + * Method to get the calendar input markup. + * + * @param string $display The display to render ('static' or 'repeatable') + * + * @return string The field input markup. + * + * @since 2.1.rc4 + */ + protected function getCalendar($display) + { + // Initialize some field attributes. + $format = $this->element['format'] ? (string) $this->element['format'] : '%Y-%m-%d'; + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + $default = $this->element['default'] ? (string) $this->element['default'] : ''; + + // PHP date doesn't use percentages (%) for the format, but the calendar Javascript + // DOES use it (@see: calendar-uncompressed.js). Therefore we have to convert it. + $formatJS = $format; + $formatPHP = str_replace(array('%', 'H:M:S', 'B'), array('', 'H:i:s', 'F'), $formatJS); + + // Check for empty date values + if (empty($this->value) || $this->value == FOFPlatform::getInstance()->getDbo()->getNullDate() || $this->value == '0000-00-00') + { + $this->value = $default; + } + + // Get some system objects. + $config = FOFPlatform::getInstance()->getConfig(); + $user = JFactory::getUser(); + + // Format date if exists + if (!empty($this->value)) + { + $date = FOFPlatform::getInstance()->getDate($this->value, 'UTC'); + + // If a known filter is given use it. + switch (strtoupper((string) $this->element['filter'])) + { + case 'SERVER_UTC': + // Convert a date to UTC based on the server timezone. + if ((int) $this->value) + { + // Get a date object based on the correct timezone. + $date->setTimezone(new DateTimeZone($config->get('offset'))); + } + break; + + case 'USER_UTC': + // Convert a date to UTC based on the user timezone. + if ((int) $this->value) + { + // Get a date object based on the correct timezone. + $date->setTimezone($user->getTimezone()); + } + break; + + default: + break; + } + + // Transform the date string. + $this->value = $date->format($formatPHP, true, false); + } + + if ($display == 'static') + { + // Build the attributes array. + $attributes = array(); + + if ($this->element['size']) + { + $attributes['size'] = (int) $this->element['size']; + } + + if ($this->element['maxlength']) + { + $attributes['maxlength'] = (int) $this->element['maxlength']; + } + + if ($this->element['class']) + { + $attributes['class'] = (string) $this->element['class']; + } + + if ((string) $this->element['readonly'] == 'true') + { + $attributes['readonly'] = 'readonly'; + } + + if ((string) $this->element['disabled'] == 'true') + { + $attributes['disabled'] = 'disabled'; + } + + if ($this->element['onchange']) + { + $attributes['onchange'] = (string) $this->element['onchange']; + } + + if ($this->required) + { + $attributes['required'] = 'required'; + $attributes['aria-required'] = 'true'; + } + + return JHtml::_('calendar', $this->value, $this->name, $this->id, $formatJS, $attributes); + } + else + { + return '' . + htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8') . + ''; + } + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/captcha.php b/deployed/akeeba/libraries/fof/form/field/captcha.php new file mode 100644 index 00000000..4d55b3b9 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/captcha.php @@ -0,0 +1,93 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getInput(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/checkbox.php b/deployed/akeeba/libraries/fof/form/field/checkbox.php new file mode 100644 index 00000000..c8acc646 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/checkbox.php @@ -0,0 +1,129 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $value = $this->element['value'] ? (string) $this->element['value'] : '1'; + $disabled = ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : ''; + $onclick = $this->element['onclick'] ? ' onclick="' . (string) $this->element['onclick'] . '"' : ''; + $required = $this->required ? ' required="required" aria-required="true"' : ''; + + if (empty($this->value)) + { + $checked = (isset($this->element['checked'])) ? ' checked="checked"' : ''; + } + else + { + $checked = ' checked="checked"'; + } + + return '' . + '' . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + $value = $this->element['value'] ? (string) $this->element['value'] : '1'; + $disabled = ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : ''; + $onclick = $this->element['onclick'] ? ' onclick="' . (string) $this->element['onclick'] . '"' : ''; + $required = $this->required ? ' required="required" aria-required="true"' : ''; + + if (empty($this->value)) + { + $checked = (isset($this->element['checked'])) ? ' checked="checked"' : ''; + } + else + { + $checked = ' checked="checked"'; + } + + return '' . + '' . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/checkboxes.php b/deployed/akeeba/libraries/fof/form/field/checkboxes.php new file mode 100644 index 00000000..88023bc7 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/checkboxes.php @@ -0,0 +1,128 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getRepeatable(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : $this->id; + $translate = $this->element['translate'] ? (string) $this->element['translate'] : false; + + $html = ''; + foreach ($this->value as $value) { + + $html .= ''; + + if ($translate == true) + { + $html .= JText::_($value); + } + else + { + $html .= $value; + } + + $html .= ''; + } + $html .= ''; + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getInput() + { + // Used for J! 2.5 compatibility + $this->value = !is_array($this->value) ? explode(',', $this->value) : $this->value; + + return parent::getInput(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/components.php b/deployed/akeeba/libraries/fof/form/field/components.php new file mode 100644 index 00000000..f236632b --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/components.php @@ -0,0 +1,237 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.1 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.1 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get a list of all installed components and also translates them. + * + * The manifest_cache is used to get the extension names, since JInstaller is also + * translating those names in stead of the name column. Else some of the translations + * fails. + * + * @since 2.1 + * + * @return array An array of JHtml options. + */ + protected function getOptions() + { + $db = FOFPlatform::getInstance()->getDbo(); + + // Check for client_ids override + if ($this->client_ids !== null) + { + $client_ids = $this->client_ids; + } + else + { + $client_ids = $this->element['client_ids']; + } + + $client_ids = explode(',', $client_ids); + + // Calculate client_ids where clause + foreach ($client_ids as &$client_id) + { + $client_id = (int) trim($client_id); + $client_id = $db->q($client_id); + } + + $query = $db->getQuery(true) + ->select( + array( + $db->qn('name'), + $db->qn('element'), + $db->qn('client_id'), + $db->qn('manifest_cache'), + ) + ) + ->from($db->qn('#__extensions')) + ->where($db->qn('type') . ' = ' . $db->q('component')) + ->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')'); + $db->setQuery($query); + $components = $db->loadObjectList('element'); + + // Convert to array of objects, so we can use sortObjects() + // Also translate component names with JText::_() + $aComponents = array(); + $user = JFactory::getUser(); + + foreach ($components as $component) + { + // Don't show components in the list where the user doesn't have access for + // TODO: perhaps add an option for this + if (!$user->authorise('core.manage', $component->element)) + { + continue; + } + + $oData = (object) array( + 'value' => $component->element, + 'text' => $this->translate($component, 'component') + ); + $aComponents[$component->element] = $oData; + } + + // Reorder the components array, because the alphabetical + // ordering changed due to the JText::_() translation + uasort( + $aComponents, + function ($a, $b) { + return strcasecmp($a->text, $b->text); + } + ); + + return $aComponents; + } + + /** + * Translate a list of objects with JText::_(). + * + * @param array $item The array of objects + * @param string $type The extension type (e.g. component) + * + * @since 2.1 + * + * @return string $text The translated name of the extension + * + * @see administrator/com_installer/models/extension.php + */ + public function translate($item, $type) + { + $platform = FOFPlatform::getInstance(); + + // Map the manifest cache to $item. This is needed to get the name from the + // manifest_cache and NOT from the name column, else some JText::_() translations fails. + $mData = json_decode($item->manifest_cache); + + if ($mData) + { + foreach ($mData as $key => $value) + { + if ($key == 'type') + { + // Ignore the type field + continue; + } + + $item->$key = $value; + } + } + + $lang = $platform->getLanguage(); + + switch ($type) + { + case 'component': + $source = JPATH_ADMINISTRATOR . '/components/' . $item->element; + $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false) + || $lang->load("$item->element.sys", $source, null, false, false) + || $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) + || $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false); + break; + } + + $text = JText::_($item->name); + + return $text; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/editor.php b/deployed/akeeba/libraries/fof/form/field/editor.php new file mode 100644 index 00000000..8c01e26d --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/editor.php @@ -0,0 +1,97 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '
        ' . $this->value . '
        '; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '
        ' . $this->value . '
        '; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/email.php b/deployed/akeeba/libraries/fof/form/field/email.php new file mode 100644 index 00000000..e79ccca2 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/email.php @@ -0,0 +1,173 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $dolink = $this->element['show_link'] == 'true'; + $empty_replacement = ''; + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $innerHtml = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + if ($dolink) + { + $innerHtml = '' . + $innerHtml . ''; + } + + return '' . + $innerHtml . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + // Initialise + $class = ''; + $show_link = false; + $link_url = ''; + $empty_replacement = ''; + + // Get field parameters + if ($this->element['class']) + { + $class = (string) $this->element['class']; + } + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['url']) + { + $link_url = $this->element['url']; + } + else + { + $link_url = 'mailto:' . htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + } + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + // Get the (optionally formatted) value + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $value = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + // Create the HTML + $html = ''; + + if ($show_link) + { + $html .= ''; + } + + $html .= $value; + + if ($show_link) + { + $html .= ''; + } + + $html .= ''; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/groupedbutton.php b/deployed/akeeba/libraries/fof/form/field/groupedbutton.php new file mode 100644 index 00000000..c6d5b292 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/groupedbutton.php @@ -0,0 +1,134 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getInput() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $html = '
        '; + + foreach ($this->element->children() as $option) + { + $renderedAttributes = array(); + + foreach ($option->attributes() as $name => $value) + { + if (!is_null($value)) + { + $renderedAttributes[] = $name . '="' . htmlentities($value) . '"'; + } + } + + $buttonXML = new SimpleXMLElement(''); + $buttonField = new FOFFormFieldButton($this->form); + + // Pass required objects to the field + $buttonField->item = $this->item; + $buttonField->rowid = $this->rowid; + $buttonField->setup($buttonXML, null); + + $html .= $buttonField->getRepeatable(); + } + $html .= '
        '; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/groupedlist.php b/deployed/akeeba/libraries/fof/form/field/groupedlist.php new file mode 100644 index 00000000..dcdba418 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/groupedlist.php @@ -0,0 +1,185 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $selected = self::getOptionName($this->getGroups(), $this->value); + + if (is_null($selected)) + { + $selected = array( + 'group' => '', + 'item' => '' + ); + } + + return 'id . '-item" class="fof-groupedlist-item ' . $class . '>' . + htmlspecialchars($selected['item'], ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $selected = self::getOptionName($this->getGroups(), $this->value); + + if (is_null($selected)) + { + $selected = array( + 'group' => '', + 'item' => '' + ); + } + + return '' . + htmlspecialchars($selected['group'], ENT_COMPAT, 'UTF-8') . + '' . + '' . + htmlspecialchars($selected['item'], ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Gets the active option's label given an array of JHtml options + * + * @param array $data The JHtml options to parse + * @param mixed $selected The currently selected value + * @param string $groupKey Group name + * @param string $optKey Key name + * @param string $optText Value name + * + * @return mixed The label of the currently selected option + */ + public static function getOptionName($data, $selected = null, $groupKey = 'items', $optKey = 'value', $optText = 'text') + { + $ret = null; + + foreach ($data as $dataKey => $group) + { + $label = $dataKey; + $noGroup = is_int($dataKey); + + if (is_array($group)) + { + $subList = $group[$groupKey]; + $label = $group[$optText]; + $noGroup = false; + } + elseif (is_object($group)) + { + // Sub-list is in a property of an object + $subList = $group->$groupKey; + $label = $group->$optText; + $noGroup = false; + } + else + { + throw new RuntimeException('Invalid group contents.', 1); + } + + if ($noGroup) + { + $label = ''; + } + + $match = FOFFormFieldList::getOptionName($data, $selected, $optKey, $optText); + + if (!is_null($match)) + { + $ret = array( + 'group' => $label, + 'item' => $match + ); + break; + } + } + + return $ret; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/hidden.php b/deployed/akeeba/libraries/fof/form/field/hidden.php new file mode 100644 index 00000000..61c8e943 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/hidden.php @@ -0,0 +1,93 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getInput(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/image.php b/deployed/akeeba/libraries/fof/form/field/image.php new file mode 100644 index 00000000..c909453c --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/image.php @@ -0,0 +1,20 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $imgattr = array( + 'id' => $this->id + ); + + if ($this->element['class']) + { + $imgattr['class'] = (string) $this->element['class']; + } + + if ($this->element['style']) + { + $imgattr['style'] = (string) $this->element['style']; + } + + if ($this->element['width']) + { + $imgattr['width'] = (string) $this->element['width']; + } + + if ($this->element['height']) + { + $imgattr['height'] = (string) $this->element['height']; + } + + if ($this->element['align']) + { + $imgattr['align'] = (string) $this->element['align']; + } + + if ($this->element['rel']) + { + $imgattr['rel'] = (string) $this->element['rel']; + } + + if ($this->element['alt']) + { + $alt = JText::_((string) $this->element['alt']); + } + else + { + $alt = null; + } + + if ($this->element['title']) + { + $imgattr['title'] = JText::_((string) $this->element['title']); + } + + $path = (string) $this->element['directory']; + $path = trim($path, '/' . DIRECTORY_SEPARATOR); + + if ($this->value && file_exists(JPATH_ROOT . '/' . $path . '/' . $this->value)) + { + $src = FOFPlatform::getInstance()->URIroot() . '/' . $path . '/' . $this->value; + } + else + { + $src = ''; + } + + return JHtml::_('image', $src, $alt, $imgattr); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getStatic(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/integer.php b/deployed/akeeba/libraries/fof/form/field/integer.php new file mode 100644 index 00000000..a00e34b3 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/integer.php @@ -0,0 +1,101 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/language.php b/deployed/akeeba/libraries/fof/form/field/language.php new file mode 100644 index 00000000..4f822f93 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/language.php @@ -0,0 +1,122 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Method to get the field options. + * + * @since 2.0 + * + * @return array The field option objects. + */ + protected function getOptions() + { + $options = parent::getOptions(); + + $noneoption = $this->element['none'] ? $this->element['none'] : null; + + if ($noneoption) + { + array_unshift($options, JHtml::_('select.option', '*', JText::_($noneoption))); + } + + return $options; + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/list.php b/deployed/akeeba/libraries/fof/form/field/list.php new file mode 100644 index 00000000..1c9ee7bf --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/list.php @@ -0,0 +1,382 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(self::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $show_link = false; + $link_url = ''; + + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['url']) + { + $link_url = $this->element['url']; + } + else + { + $show_link = false; + } + + if ($show_link && ($this->item instanceof FOFTable)) + { + $link_url = $this->parseFieldTags($link_url); + } + else + { + $show_link = false; + } + + $html = ''; + + if ($show_link) + { + $html .= ''; + } + + $html .= htmlspecialchars(self::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8'); + + if ($show_link) + { + $html .= ''; + } + + $html .= ''; + + return $html; + } + + /** + * Gets the active option's label given an array of JHtml options + * + * @param array $data The JHtml options to parse + * @param mixed $selected The currently selected value + * @param string $optKey Key name + * @param string $optText Value name + * + * @return mixed The label of the currently selected option + */ + public static function getOptionName($data, $selected = null, $optKey = 'value', $optText = 'text') + { + $ret = null; + + foreach ($data as $elementKey => &$element) + { + if (is_array($element)) + { + $key = $optKey === null ? $elementKey : $element[$optKey]; + $text = $element[$optText]; + } + elseif (is_object($element)) + { + $key = $optKey === null ? $elementKey : $element->$optKey; + $text = $element->$optText; + } + else + { + // This is a simple associative array + $key = $elementKey; + $text = $element; + } + + if (is_null($ret)) + { + $ret = $text; + } + elseif ($selected == $key) + { + $ret = $text; + } + } + + return $ret; + } + + /** + * Method to get the field options. + * + * Ordering is disabled by default. You can enable ordering by setting the + * 'order' element in your form field. The other order values are optional. + * + * - order What to order. Possible values: 'name' or 'value' (default = false) + * - order_dir Order direction. Possible values: 'asc' = Ascending or 'desc' = Descending (default = 'asc') + * - order_case_sensitive Order case sensitive. Possible values: 'true' or 'false' (default = false) + * + * @return array The field option objects. + * + * @since Ordering is available since FOF 2.1.b2. + */ + protected function getOptions() + { + // Ordering is disabled by default for backward compatibility + $order = false; + + // Set default order direction + $order_dir = 'asc'; + + // Set default value for case sensitive sorting + $order_case_sensitive = false; + + if ($this->element['order'] && $this->element['order'] !== 'false') + { + $order = $this->element['order']; + } + + if ($this->element['order_dir']) + { + $order_dir = $this->element['order_dir']; + } + + if ($this->element['order_case_sensitive']) + { + // Override default setting when the form element value is 'true' + if ($this->element['order_case_sensitive'] == 'true') + { + $order_case_sensitive = true; + } + } + + // Create a $sortOptions array in order to apply sorting + $i = 0; + $sortOptions = array(); + + foreach ($this->element->children() as $option) + { + $name = JText::alt(trim((string) $option), preg_replace('/[^a-zA-Z0-9_\-]/', '_', $this->fieldname)); + + $sortOptions[$i] = new stdClass; + $sortOptions[$i]->option = $option; + $sortOptions[$i]->value = $option['value']; + $sortOptions[$i]->name = $name; + $i++; + } + + // Only order if it's set + if ($order) + { + jimport('joomla.utilities.arrayhelper'); + FOFUtilsArray::sortObjects($sortOptions, $order, $order_dir == 'asc' ? 1 : -1, $order_case_sensitive, false); + } + + // Initialise the options + $options = array(); + + // Get the field $options + foreach ($sortOptions as $sortOption) + { + $option = $sortOption->option; + $name = $sortOption->name; + + // Only add
      '; + if ($section == 'component' || $section == null) + { + $html[] = JText::_('JLIB_RULES_SETTING_NOTES'); + } + else + { + $html[] = JText::_('JLIB_RULES_SETTING_NOTES_ITEM'); + } + $html[] = '
      '; + + $js = "window.addEvent('domready', function(){ new Fx.Accordion($$('div#permissions-sliders.pane-sliders .panel h3.pane-toggler')," + . "$$('div#permissions-sliders.pane-sliders .panel div.pane-slider'), {onActive: function(toggler, i) {toggler.addClass('pane-toggler-down');" + . "toggler.removeClass('pane-toggler');i.addClass('pane-down');i.removeClass('pane-hide');Cookie.write('jpanesliders_permissions-sliders" + . $component + . "',$$('div#permissions-sliders.pane-sliders .panel h3').indexOf(toggler));}," + . "onBackground: function(toggler, i) {toggler.addClass('pane-toggler');toggler.removeClass('pane-toggler-down');i.addClass('pane-hide');" + . "i.removeClass('pane-down');}, duration: 300, display: " + . JRequest::getInt('jpanesliders_permissions-sliders' . $component, 0, 'cookie') . ", show: " + . JRequest::getInt('jpanesliders_permissions-sliders' . $component, 0, 'cookie') . ", alwaysHide:true, opacity: false}); });"; + + JFactory::getDocument()->addScriptDeclaration($js); + + return implode("\n", $html); + } + + protected function getInput3x() + { + JHtml::_('bootstrap.tooltip'); + + // Initialise some field attributes. + $section = $this->section; + $component = $this->component; + $assetField = $this->assetField; + + // Get the actions for the asset. + $actions = JAccess::getActions($component, $section); + + // Iterate over the children and add to the actions. + foreach ($this->element->children() as $el) + { + if ($el->getName() == 'action') + { + $actions[] = (object) array('name' => (string) $el['name'], 'title' => (string) $el['title'], + 'description' => (string) $el['description']); + } + } + + // Get the explicit rules for this asset. + if ($section == 'component') + { + // Need to find the asset id by the name of the component. + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = ' . $db->quote($component)); + + $assetId = (int) $db->setQuery($query)->loadResult(); + } + else + { + // Find the asset id of the content. + // Note that for global configuration, com_config injects asset_id = 1 into the form. + $assetId = $this->form->getValue($assetField); + + // ==== FOF Library fix - Start ==== + // If there is no assetId (let's say we are dealing with a new record), let's ask the table + // to give it to us. Here you should implement your logic (ie getting default permissions from + // the component or from the category) + if(!$assetId) + { + $table = $this->form->getModel()->getTable(); + $assetId = $table->getAssetParentId(); + } + // ==== FOF Library fix - End ==== + } + + // Full width format. + + // Get the rules for just this asset (non-recursive). + $assetRules = JAccess::getAssetRules($assetId); + + // Get the available user groups. + $groups = $this->getUserGroups(); + + // Prepare output + $html = array(); + + // Description + $html[] = '

      ' . JText::_('JLIB_RULES_SETTINGS_DESC') . '

      '; + + // Begin tabs + $html[] = '
      '; + + // Building tab nav + $html[] = ''; + + $html[] = '
      '; + + // Start a row for each user group. + foreach ($groups as $group) + { + // Initial Active Pane + $active = ""; + + if ($group->value == 1) + { + $active = " active"; + } + + $html[] = '
      '; + $html[] = ''; + $html[] = ''; + $html[] = ''; + + $html[] = ''; + + $html[] = ''; + + // The calculated setting is not shown for the root group of global configuration. + $canCalculateSettings = ($group->parent_id || !empty($component)); + + if ($canCalculateSettings) + { + $html[] = ''; + } + + $html[] = ''; + $html[] = ''; + $html[] = ''; + + foreach ($actions as $action) + { + $html[] = ''; + $html[] = ''; + + $html[] = ''; + + // Build the Calculated Settings column. + // The inherited settings column is not displayed for the root group in global configuration. + if ($canCalculateSettings) + { + $html[] = ''; + } + + $html[] = ''; + } + + $html[] = ''; + $html[] = '
      '; + $html[] = '' . JText::_('JLIB_RULES_ACTION') . ''; + $html[] = ''; + $html[] = '' . JText::_('JLIB_RULES_SELECT_SETTING') . ''; + $html[] = ''; + $html[] = '' . JText::_('JLIB_RULES_CALCULATED_SETTING') . ''; + $html[] = '
      '; + $html[] = ''; + $html[] = ''; + + $html[] = '  '; + + // If this asset's rule is allowed, but the inherited rule is deny, we have a conflict. + if (($assetRule === true) && ($inheritedRule === false)) + { + $html[] = JText::_('JLIB_RULES_CONFLICT'); + } + + $html[] = ''; + + // This is where we show the current effective settings considering currrent group, path and cascade. + // Check whether this is a component or global. Change the text slightly. + + if (JAccess::checkGroup($group->value, 'core.admin', $assetId) !== true) + { + if ($inheritedRule === null) + { + $html[] = '' . JText::_('JLIB_RULES_NOT_ALLOWED') . ''; + } + elseif ($inheritedRule === true) + { + $html[] = '' . JText::_('JLIB_RULES_ALLOWED') . ''; + } + elseif ($inheritedRule === false) + { + if ($assetRule === false) + { + $html[] = '' . JText::_('JLIB_RULES_NOT_ALLOWED') . ''; + } + else + { + $html[] = ' ' . JText::_('JLIB_RULES_NOT_ALLOWED_LOCKED') + . ''; + } + } + } + elseif (!empty($component)) + { + $html[] = ' ' . JText::_('JLIB_RULES_ALLOWED_ADMIN') + . ''; + } + else + { + // Special handling for groups that have global admin because they can't be denied. + // The admin rights can be changed. + if ($action->name === 'core.admin') + { + $html[] = '' . JText::_('JLIB_RULES_ALLOWED') . ''; + } + elseif ($inheritedRule === false) + { + // Other actions cannot be changed. + $html[] = ' ' + . JText::_('JLIB_RULES_NOT_ALLOWED_ADMIN_CONFLICT') . ''; + } + else + { + $html[] = ' ' . JText::_('JLIB_RULES_ALLOWED_ADMIN') + . ''; + } + } + + $html[] = '
      '; + } + + $html[] = '
      '; + + $html[] = '
      '; + + if ($section == 'component' || $section == null) + { + $html[] = JText::_('JLIB_RULES_SETTING_NOTES'); + } + else + { + $html[] = JText::_('JLIB_RULES_SETTING_NOTES_ITEM'); + } + + $html[] = '
      '; + + return implode("\n", $html); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/selectrow.php b/deployed/akeeba/libraries/fof/form/field/selectrow.php new file mode 100644 index 00000000..eec7cea8 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/selectrow.php @@ -0,0 +1,124 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Method to get the field input markup for this field type. + * + * @since 2.0 + * + * @return string The field input markup. + */ + protected function getInput() + { + throw new Exception(__CLASS__ . ' cannot be used in input forms'); + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + throw new Exception(__CLASS__ . ' cannot be used in single item display forms'); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + if (!($this->item instanceof FOFTable)) + { + throw new Exception(__CLASS__ . ' needs a FOFTable to act upon'); + } + + // Is this record checked out? + $checked_out = false; + $locked_by_field = $this->item->getColumnAlias('locked_by'); + $myId = JFactory::getUser()->get('id', 0); + + if (property_exists($this->item, $locked_by_field)) + { + $locked_by = $this->item->$locked_by_field; + $checked_out = ($locked_by != 0 && $locked_by != $myId); + } + + // Get the key id for this record + $key_field = $this->item->getKeyName(); + $key_id = $this->item->$key_field; + + // Get the HTML + return JHTML::_('grid.id', $this->rowid, $key_id, $checked_out); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/sessionhandler.php b/deployed/akeeba/libraries/fof/form/field/sessionhandler.php new file mode 100644 index 00000000..40082b76 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/sessionhandler.php @@ -0,0 +1,101 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/spacer.php b/deployed/akeeba/libraries/fof/form/field/spacer.php new file mode 100644 index 00000000..045dc53e --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/spacer.php @@ -0,0 +1,93 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + return $this->getInput(); + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getInput(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/sql.php b/deployed/akeeba/libraries/fof/form/field/sql.php new file mode 100644 index 00000000..a8214328 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/sql.php @@ -0,0 +1,101 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($this->getOptions(), $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/tag.php b/deployed/akeeba/libraries/fof/form/field/tag.php new file mode 100644 index 00000000..ef5d0b4a --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/tag.php @@ -0,0 +1,238 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Method to get a list of tags + * + * @return array The field option objects. + * + * @since 3.1 + */ + protected function getOptions() + { + $options = array(); + + $published = $this->element['published']? $this->element['published'] : array(0,1); + + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT a.id AS value, a.path, a.title AS text, a.level, a.published, a.lft') + ->from('#__tags AS a') + ->join('LEFT', $db->quoteName('#__tags') . ' AS b ON a.lft > b.lft AND a.rgt < b.rgt'); + + if ($this->item instanceof FOFTable) + { + $item = $this->item; + } + else + { + $item = $this->form->getModel()->getItem(); + } + + if ($item instanceof FOFTable) + { + // Fake value for selected tags + $keyfield = $item->getKeyName(); + $content_id = $item->$keyfield; + $type = $item->getContentType(); + + $selected_query = $db->getQuery(true); + $selected_query + ->select('tag_id') + ->from('#__contentitem_tag_map') + ->where('content_item_id = ' . (int) $content_id) + ->where('type_alias = ' . $db->quote($type)); + + $db->setQuery($selected_query); + + $this->value = $db->loadColumn(); + } + + // Filter language + if (!empty($this->element['language'])) + { + $query->where('a.language = ' . $db->quote($this->element['language'])); + } + + $query->where($db->qn('a.lft') . ' > 0'); + + // Filter to only load active items + + // Filter on the published state + if (is_numeric($published)) + { + $query->where('a.published = ' . (int) $published); + } + elseif (is_array($published)) + { + FOFUtilsArray::toInteger($published); + $query->where('a.published IN (' . implode(',', $published) . ')'); + } + + $query->order('a.lft ASC'); + + // Get the options. + $db->setQuery($query); + + try + { + $options = $db->loadObjectList(); + } + catch (RuntimeException $e) + { + return false; + } + + // Prepare nested data + if ($this->isNested()) + { + $this->prepareOptionsNested($options); + } + else + { + $options = JHelperTags::convertPathsToNames($options); + } + + return $options; + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + $translate = $this->element['translate'] ? (string) $this->element['translate'] : false; + + $options = $this->getOptions(); + + $html = ''; + + foreach ($options as $option) { + + $html .= ''; + + if ($translate == true) + { + $html .= JText::_($option->text); + } + else + { + $html .= $option->text; + } + + $html .= ''; + } + + return '' . + $html . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.1 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + $translate = $this->element['translate'] ? (string) $this->element['translate'] : false; + + $options = $this->getOptions(); + + $html = ''; + + foreach ($options as $option) { + + $html .= ''; + + if ($translate == true) + { + $html .= JText::_($option->text); + } + else + { + $html .= $option->text; + } + + $html .= ''; + } + + return '' . + $html . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/tel.php b/deployed/akeeba/libraries/fof/form/field/tel.php new file mode 100644 index 00000000..66f070a2 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/tel.php @@ -0,0 +1,165 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $dolink = $this->element['show_link'] == 'true'; + $empty_replacement = ''; + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $innerHtml = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + if ($dolink) + { + $innerHtml = '' . + $innerHtml . ''; + } + + return '' . + $innerHtml . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + // Initialise + $class = $this->id; + $show_link = false; + $empty_replacement = ''; + + $link_url = 'tel:' . htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + // Get field parameters + if ($this->element['class']) + { + $class = ' ' . (string) $this->element['class']; + } + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + // Get the (optionally formatted) value + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $value = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + // Create the HTML + $html = ''; + + if ($show_link) + { + $html .= ''; + } + + $html .= $value; + + if ($show_link) + { + $html .= ''; + } + + $html .= ''; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/text.php b/deployed/akeeba/libraries/fof/form/field/text.php new file mode 100644 index 00000000..89751f07 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/text.php @@ -0,0 +1,246 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $empty_replacement = ''; + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + return '' . + htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + // Initialise + $class = $this->id; + $format_string = ''; + $format_if_not_empty = false; + $parse_value = false; + $show_link = false; + $link_url = ''; + $empty_replacement = ''; + + // Get field parameters + if ($this->element['class']) + { + $class = (string) $this->element['class']; + } + + if ($this->element['format']) + { + $format_string = (string) $this->element['format']; + } + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['format_if_not_empty'] == 'true') + { + $format_if_not_empty = true; + } + + if ($this->element['parse_value'] == 'true') + { + $parse_value = true; + } + + if ($this->element['url']) + { + $link_url = $this->element['url']; + } + else + { + $show_link = false; + } + + if ($show_link && ($this->item instanceof FOFTable)) + { + $link_url = $this->parseFieldTags($link_url); + } + else + { + $show_link = false; + } + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + // Get the (optionally formatted) value + $value = $this->value; + + if (!empty($empty_replacement) && empty($this->value)) + { + $value = JText::_($empty_replacement); + } + + if ($parse_value) + { + $value = $this->parseFieldTags($value); + } + + if (!empty($format_string) && (!$format_if_not_empty || ($format_if_not_empty && !empty($this->value)))) + { + $format_string = $this->parseFieldTags($format_string); + $value = sprintf($format_string, $value); + } + else + { + $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + } + + // Create the HTML + $html = ''; + + if ($show_link) + { + $html .= ''; + } + + $html .= $value; + + if ($show_link) + { + $html .= ''; + } + + $html .= ''; + + return $html; + } + + /** + * Replace string with tags that reference fields + * + * @param string $text Text to process + * + * @return string Text with tags replace + */ + protected function parseFieldTags($text) + { + $ret = $text; + + // Replace [ITEM:ID] in the URL with the item's key value (usually: + // the auto-incrementing numeric ID) + $keyfield = $this->item->getKeyName(); + $replace = $this->item->$keyfield; + $ret = str_replace('[ITEM:ID]', $replace, $ret); + + // Replace the [ITEMID] in the URL with the current Itemid parameter + $ret = str_replace('[ITEMID]', JFactory::getApplication()->input->getInt('Itemid', 0), $ret); + + // Replace other field variables in the URL + $fields = $this->item->getTableFields(); + + foreach ($fields as $fielddata) + { + $fieldname = $fielddata->Field; + + if (empty($fieldname)) + { + $fieldname = $fielddata->column_name; + } + + $search = '[ITEM:' . strtoupper($fieldname) . ']'; + $replace = $this->item->$fieldname; + $ret = str_replace($search, $replace, $ret); + } + + return $ret; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/textarea.php b/deployed/akeeba/libraries/fof/form/field/textarea.php new file mode 100644 index 00000000..03c327dd --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/textarea.php @@ -0,0 +1,97 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + return '
      ' . + htmlspecialchars(nl2br($this->value), ENT_COMPAT, 'UTF-8') . + '
      '; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getStatic(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/timezone.php b/deployed/akeeba/libraries/fof/form/field/timezone.php new file mode 100644 index 00000000..272295d2 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/timezone.php @@ -0,0 +1,110 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $selected = FOFFormFieldGroupedlist::getOptionName($this->getOptions(), $this->value); + + if (is_null($selected)) + { + $selected = array( + 'group' => '', + 'item' => '' + ); + } + + return 'id . '-item" class="fof-groupedlist-item ' . $class . '>' . + htmlspecialchars($selected['item'], ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + return $this->getStatic(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/title.php b/deployed/akeeba/libraries/fof/form/field/title.php new file mode 100644 index 00000000..8ed993e3 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/title.php @@ -0,0 +1,67 @@ +element['slug_field']) + { + $slug_field = (string) $this->element['slug_field']; + } + else + { + $slug_field = $this->item->getColumnAlias('slug'); + } + + if ($this->element['slug_format']) + { + $slug_format = (string) $this->element['slug_format']; + } + + if ($this->element['slug_class']) + { + $slug_class = (string) $this->element['slug_class']; + } + + // Get the regular display + $html = parent::getRepeatable(); + + $slug = $this->item->$slug_field; + + $html .= '
      ' . ''; + $html .= JText::sprintf($slug_format, $slug); + $html .= ''; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/url.php b/deployed/akeeba/libraries/fof/form/field/url.php new file mode 100644 index 00000000..db3b2c1a --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/url.php @@ -0,0 +1,165 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $dolink = $this->element['show_link'] == 'true'; + $empty_replacement = ''; + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $innerHtml = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + if ($dolink) + { + $innerHtml = '' . + $innerHtml . ''; + } + + return '' . + $innerHtml . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + // Initialise + $class = $this->id; + $show_link = false; + $empty_replacement = ''; + + $link_url = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + // Get field parameters + if ($this->element['class']) + { + $class .= ' ' . (string) $this->element['class']; + } + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['empty_replacement']) + { + $empty_replacement = (string) $this->element['empty_replacement']; + } + + // Get the (optionally formatted) value + if (!empty($empty_replacement) && empty($this->value)) + { + $this->value = JText::_($empty_replacement); + } + + $value = htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'); + + // Create the HTML + $html = ''; + + if ($show_link) + { + $html .= ''; + } + + $html .= $value; + + if ($show_link) + { + $html .= ''; + } + + $html .= ''; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/user.php b/deployed/akeeba/libraries/fof/form/field/user.php new file mode 100644 index 00000000..f2d6f9bc --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/user.php @@ -0,0 +1,349 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + // Initialise + $show_username = true; + $show_email = false; + $show_name = false; + $show_id = false; + $class = ''; + + // Get the field parameters + if ($this->element['class']) + { + $class = ' class="' . (string) $this->element['class'] . '"'; + } + + if ($this->element['show_username'] == 'false') + { + $show_username = false; + } + + if ($this->element['show_email'] == 'true') + { + $show_email = true; + } + + if ($this->element['show_name'] == 'true') + { + $show_name = true; + } + + if ($this->element['show_id'] == 'true') + { + $show_id = true; + } + + // Get the user record + $user = JFactory::getUser($this->value); + + // Render the HTML + $html = '
      '; + + if ($show_username) + { + $html .= '' . $user->username . ''; + } + + if ($show_id) + { + $html .= '' . $user->id . ''; + } + + if ($show_name) + { + $html .= '' . $user->name . ''; + } + + if ($show_email) + { + $html .= '' . $user->email . ''; + } + + $html .= '
      '; + + return $html; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + // Initialise + $show_username = true; + $show_email = true; + $show_name = true; + $show_id = true; + $show_avatar = true; + $show_link = false; + $link_url = null; + $avatar_method = 'gravatar'; + $avatar_size = 64; + $class = ''; + + // Get the user record + $user = JFactory::getUser($this->value); + + // Get the field parameters + if ($this->element['class']) + { + $class = ' class="' . (string) $this->element['class'] . '"'; + } + + if ($this->element['show_username'] == 'false') + { + $show_username = false; + } + + if ($this->element['show_email'] == 'false') + { + $show_email = false; + } + + if ($this->element['show_name'] == 'false') + { + $show_name = false; + } + + if ($this->element['show_id'] == 'false') + { + $show_id = false; + } + + if ($this->element['show_avatar'] == 'false') + { + $show_avatar = false; + } + + if ($this->element['avatar_method']) + { + $avatar_method = strtolower($this->element['avatar_method']); + } + + if ($this->element['avatar_size']) + { + $avatar_size = $this->element['avatar_size']; + } + + if ($this->element['show_link'] == 'true') + { + $show_link = true; + } + + if ($this->element['link_url']) + { + $link_url = $this->element['link_url']; + } + else + { + if (FOFPlatform::getInstance()->isBackend()) + { + // If no link is defined in the back-end, assume the user edit + // link in the User Manager component + $link_url = 'index.php?option=com_users&task=user.edit&id=[USER:ID]'; + } + else + { + // If no link is defined in the front-end, we can't create a + // default link. Therefore, show no link. + $show_link = false; + } + } + + // Post-process the link URL + if ($show_link) + { + $replacements = array( + '[USER:ID]' => $user->id, + '[USER:USERNAME]' => $user->username, + '[USER:EMAIL]' => $user->email, + '[USER:NAME]' => $user->name, + ); + + foreach ($replacements as $key => $value) + { + $link_url = str_replace($key, $value, $link_url); + } + } + + // Get the avatar image, if necessary + if ($show_avatar) + { + $avatar_url = ''; + + if ($avatar_method == 'plugin') + { + // Use the user plugins to get an avatar + FOFPlatform::getInstance()->importPlugin('user'); + $jResponse = FOFPlatform::getInstance()->runPlugins('onUserAvatar', array($user, $avatar_size)); + + if (!empty($jResponse)) + { + foreach ($jResponse as $response) + { + if ($response) + { + $avatar_url = $response; + } + } + } + + if (empty($avatar_url)) + { + $show_avatar = false; + } + } + else + { + // Fall back to the Gravatar method + $md5 = md5($user->email); + + if (FOFPlatform::getInstance()->isCli()) + { + $scheme = 'http'; + } + else + { + $scheme = JURI::getInstance()->getScheme(); + } + + if ($scheme == 'http') + { + $avatar_url = 'http://www.gravatar.com/avatar/' . $md5 . '.jpg?s=' + . $avatar_size . '&d=mm'; + } + else + { + $avatar_url = 'https://secure.gravatar.com/avatar/' . $md5 . '.jpg?s=' + . $avatar_size . '&d=mm'; + } + } + } + + // Generate the HTML + $html = ''; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/field/usergroup.php b/deployed/akeeba/libraries/fof/form/field/usergroup.php new file mode 100644 index 00000000..fd9d4c07 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/field/usergroup.php @@ -0,0 +1,143 @@ +static)) + { + $this->static = $this->getStatic(); + } + + return $this->static; + break; + + case 'repeatable': + if (empty($this->repeatable)) + { + $this->repeatable = $this->getRepeatable(); + } + + return $this->repeatable; + break; + + default: + return parent::__get($name); + } + } + + /** + * Get the rendering of this field type for static display, e.g. in a single + * item view (typically a "read" task). + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getStatic() + { + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + $params = $this->getOptions(); + + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true); + + $query->select('a.id AS value, a.title AS text'); + $query->from('#__usergroups AS a'); + $query->group('a.id, a.title'); + $query->order('a.id ASC'); + $query->order($query->qn('title') . ' ASC'); + + // Get the options. + $db->setQuery($query); + $options = $db->loadObjectList(); + + // If params is an array, push these options to the array + if (is_array($params)) + { + $options = array_merge($params, $options); + } + + // If all levels is allowed, push it into the array. + elseif ($params) + { + array_unshift($options, JHtml::_('select.option', '', JText::_('JOPTION_ACCESS_SHOW_ALL_LEVELS'))); + } + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($options, $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } + + /** + * Get the rendering of this field type for a repeatable (grid) display, + * e.g. in a view listing many item (typically a "browse" task) + * + * @since 2.0 + * + * @return string The field HTML + */ + public function getRepeatable() + { + $class = $this->element['class'] ? (string) $this->element['class'] : ''; + + $db = FOFPlatform::getInstance()->getDbo(); + $query = $db->getQuery(true); + + $query->select('a.id AS value, a.title AS text'); + $query->from('#__usergroups AS a'); + $query->group('a.id, a.title'); + $query->order('a.id ASC'); + $query->order($query->qn('title') . ' ASC'); + + // Get the options. + $db->setQuery($query); + $options = $db->loadObjectList(); + + + return '' . + htmlspecialchars(FOFFormFieldList::getOptionName($options, $this->value), ENT_COMPAT, 'UTF-8') . + ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/form.php b/deployed/akeeba/libraries/fof/form/form.php new file mode 100644 index 00000000..9fe7983b --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/form.php @@ -0,0 +1,659 @@ +load($data, $replace, $xpath) == false) + { + throw new RuntimeException('FOFForm::getInstance could not load form'); + } + } + else + { + if ($forms[$name]->loadFile($data, $replace, $xpath) == false) + { + throw new RuntimeException('FOFForm::getInstance could not load file ' . $data . '.xml'); + } + } + } + + return $forms[$name]; + } + + /** + * Returns the value of an attribute of the form itself + * + * @param string $attribute The name of the attribute + * @param mixed $default Optional default value to return + * + * @return mixed + * + * @since 2.0 + */ + public function getAttribute($attribute, $default = null) + { + $value = $this->xml->attributes()->$attribute; + + if (is_null($value)) + { + return $default; + } + else + { + return (string) $value; + } + } + + /** + * Loads the CSS files defined in the form, based on its cssfiles attribute + * + * @return void + * + * @since 2.0 + */ + public function loadCSSFiles() + { + // Support for CSS files + $cssfiles = $this->getAttribute('cssfiles'); + + if (!empty($cssfiles)) + { + $cssfiles = explode(',', $cssfiles); + + foreach ($cssfiles as $cssfile) + { + FOFTemplateUtils::addCSS(trim($cssfile)); + } + } + + // Support for LESS files + $lessfiles = $this->getAttribute('lessfiles'); + + if (!empty($lessfiles)) + { + $lessfiles = explode(',', $lessfiles); + + foreach ($lessfiles as $def) + { + $parts = explode('||', $def, 2); + $lessfile = $parts[0]; + $alt = (count($parts) > 1) ? trim($parts[1]) : null; + FOFTemplateUtils::addLESS(trim($lessfile), $alt); + } + } + } + + /** + * Loads the Javascript files defined in the form, based on its jsfiles attribute + * + * @return void + * + * @since 2.0 + */ + public function loadJSFiles() + { + $jsfiles = $this->getAttribute('jsfiles'); + + if (empty($jsfiles)) + { + return; + } + + $jsfiles = explode(',', $jsfiles); + + foreach ($jsfiles as $jsfile) + { + FOFTemplateUtils::addJS(trim($jsfile)); + } + } + + /** + * Returns a reference to the protected $data object, allowing direct + * access to and manipulation of the form's data. + * + * @return JRegistry The form's data registry + * + * @since 2.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Attaches a FOFModel to this form + * + * @param FOFModel &$model The model to attach to the form + * + * @return void + */ + public function setModel(FOFModel &$model) + { + $this->model = $model; + } + + /** + * Returns the FOFModel attached to this form + * + * @return FOFModel + */ + public function &getModel() + { + return $this->model; + } + + /** + * Attaches a FOFView to this form + * + * @param FOFView &$view The view to attach to the form + * + * @return void + */ + public function setView(FOFView &$view) + { + $this->view = $view; + } + + /** + * Returns the FOFView attached to this form + * + * @return FOFView + */ + public function &getView() + { + return $this->view; + } + + /** + * Method to get an array of FOFFormHeader objects in the headerset. + * + * @return array The array of FOFFormHeader objects in the headerset. + * + * @since 2.0 + */ + public function getHeaderset() + { + $fields = array(); + + $elements = $this->findHeadersByGroup(); + + // If no field elements were found return empty. + + if (empty($elements)) + { + return $fields; + } + + // Build the result array from the found field elements. + + foreach ($elements as $element) + { + // Get the field groups for the element. + $attrs = $element->xpath('ancestor::headers[@name]/@name'); + $groups = array_map('strval', $attrs ? $attrs : array()); + $group = implode('.', $groups); + + // If the field is successfully loaded add it to the result array. + if ($field = $this->loadHeader($element, $group)) + { + $fields[$field->id] = $field; + } + } + + return $fields; + } + + /** + * Method to get an array of
      elements from the form XML document which are + * in a control group by name. + * + * @param mixed $group The optional dot-separated form group path on which to find the fields. + * Null will return all fields. False will return fields not in a group. + * @param boolean $nested True to also include fields in nested groups that are inside of the + * group for which to find fields. + * + * @return mixed Boolean false on error or array of SimpleXMLElement objects. + * + * @since 2.0 + */ + protected function &findHeadersByGroup($group = null, $nested = false) + { + $false = false; + $fields = array(); + + // Make sure there is a valid JForm XML document. + if (!($this->xml instanceof SimpleXMLElement)) + { + return $false; + } + + // Get only fields in a specific group? + if ($group) + { + // Get the fields elements for a given group. + $elements = &$this->findHeader($group); + + // Get all of the field elements for the fields elements. + foreach ($elements as $element) + { + // If there are field elements add them to the return result. + if ($tmp = $element->xpath('descendant::header')) + { + // If we also want fields in nested groups then just merge the arrays. + if ($nested) + { + $fields = array_merge($fields, $tmp); + } + + // If we want to exclude nested groups then we need to check each field. + else + { + $groupNames = explode('.', $group); + + foreach ($tmp as $field) + { + // Get the names of the groups that the field is in. + $attrs = $field->xpath('ancestor::headers[@name]/@name'); + $names = array_map('strval', $attrs ? $attrs : array()); + + // If the field is in the specific group then add it to the return list. + if ($names == (array) $groupNames) + { + $fields = array_merge($fields, array($field)); + } + } + } + } + } + } + elseif ($group === false) + { + // Get only field elements not in a group. + $fields = $this->xml->xpath('descendant::headers[not(@name)]/header | descendant::headers[not(@name)]/headerset/header '); + } + else + { + // Get an array of all the
      elements. + $fields = $this->xml->xpath('//header'); + } + + return $fields; + } + + /** + * Method to get a header field represented as a FOFFormHeader object. + * + * @param string $name The name of the header field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param mixed $value The optional value to use as the default for the field. + * + * @return mixed The FOFFormHeader object for the field or boolean false on error. + * + * @since 2.0 + */ + public function getHeader($name, $group = null, $value = null) + { + // Make sure there is a valid FOFForm XML document. + if (!($this->xml instanceof SimpleXMLElement)) + { + return false; + } + + // Attempt to find the field by name and group. + $element = $this->findHeader($name, $group); + + // If the field element was not found return false. + if (!$element) + { + return false; + } + + return $this->loadHeader($element, $group, $value); + } + + /** + * Method to get a header field represented as an XML element object. + * + * @param string $name The name of the form field. + * @param string $group The optional dot-separated form group path on which to find the field. + * + * @return mixed The XML element object for the field or boolean false on error. + * + * @since 2.0 + */ + protected function findHeader($name, $group = null) + { + $element = false; + $fields = array(); + + // Make sure there is a valid JForm XML document. + if (!($this->xml instanceof SimpleXMLElement)) + { + return false; + } + + // Let's get the appropriate field element based on the method arguments. + if ($group) + { + // Get the fields elements for a given group. + $elements = &$this->findGroup($group); + + // Get all of the field elements with the correct name for the fields elements. + foreach ($elements as $element) + { + // If there are matching field elements add them to the fields array. + if ($tmp = $element->xpath('descendant::header[@name="' . $name . '"]')) + { + $fields = array_merge($fields, $tmp); + } + } + + // Make sure something was found. + if (!$fields) + { + return false; + } + + // Use the first correct match in the given group. + $groupNames = explode('.', $group); + + foreach ($fields as &$field) + { + // Get the group names as strings for ancestor fields elements. + $attrs = $field->xpath('ancestor::headerfields[@name]/@name'); + $names = array_map('strval', $attrs ? $attrs : array()); + + // If the field is in the exact group use it and break out of the loop. + if ($names == (array) $groupNames) + { + $element = &$field; + break; + } + } + } + else + { + // Get an array of fields with the correct name. + $fields = $this->xml->xpath('//header[@name="' . $name . '"]'); + + // Make sure something was found. + if (!$fields) + { + return false; + } + + // Search through the fields for the right one. + foreach ($fields as &$field) + { + // If we find an ancestor fields element with a group name then it isn't what we want. + if ($field->xpath('ancestor::headerfields[@name]')) + { + continue; + } + + // Found it! + else + { + $element = &$field; + break; + } + } + } + + return $element; + } + + /** + * Method to load, setup and return a FOFFormHeader object based on field data. + * + * @param string $element The XML element object representation of the form field. + * @param string $group The optional dot-separated form group path on which to find the field. + * @param mixed $value The optional value to use as the default for the field. + * + * @return mixed The FOFFormHeader object for the field or boolean false on error. + * + * @since 2.0 + */ + protected function loadHeader($element, $group = null, $value = null) + { + // Make sure there is a valid SimpleXMLElement. + if (!($element instanceof SimpleXMLElement)) + { + return false; + } + + // Get the field type. + $type = $element['type'] ? (string) $element['type'] : 'field'; + + // Load the JFormField object for the field. + $field = $this->loadHeaderType($type); + + // If the object could not be loaded, get a text field object. + if ($field === false) + { + $field = $this->loadHeaderType('field'); + } + + // Setup the FOFFormHeader object. + $field->setForm($this); + + if ($field->setup($element, $value, $group)) + { + return $field; + } + else + { + return false; + } + } + + /** + * Method to remove a header from the form definition. + * + * @param string $name The name of the form field for which remove. + * @param string $group The optional dot-separated form group path on which to find the field. + * + * @return boolean True on success, false otherwise. + * + * @throws UnexpectedValueException + */ + public function removeHeader($name, $group = null) + { + // Make sure there is a valid JForm XML document. + if (!($this->xml instanceof SimpleXMLElement)) + { + throw new UnexpectedValueException(sprintf('%s::getFieldAttribute `xml` is not an instance of SimpleXMLElement', get_class($this))); + } + + // Find the form field element from the definition. + $element = $this->findHeader($name, $group); + + // If the element exists remove it from the form definition. + if ($element instanceof SimpleXMLElement) + { + $dom = dom_import_simplexml($element); + $dom->parentNode->removeChild($dom); + + return true; + } + + return false; + } + + /** + * Proxy for {@link FOFFormHelper::loadFieldType()}. + * + * @param string $type The field type. + * @param boolean $new Flag to toggle whether we should get a new instance of the object. + * + * @return mixed FOFFormField object on success, false otherwise. + * + * @since 2.0 + */ + protected function loadFieldType($type, $new = true) + { + return FOFFormHelper::loadFieldType($type, $new); + } + + /** + * Proxy for {@link FOFFormHelper::loadHeaderType()}. + * + * @param string $type The field type. + * @param boolean $new Flag to toggle whether we should get a new instance of the object. + * + * @return mixed FOFFormHeader object on success, false otherwise. + * + * @since 2.0 + */ + protected function loadHeaderType($type, $new = true) + { + return FOFFormHelper::loadHeaderType($type, $new); + } + + /** + * Proxy for {@link FOFFormHelper::loadRuleType()}. + * + * @param string $type The rule type. + * @param boolean $new Flag to toggle whether we should get a new instance of the object. + * + * @return mixed JFormRule object on success, false otherwise. + * + * @see FOFFormHelper::loadRuleType() + * @since 2.0 + */ + protected function loadRuleType($type, $new = true) + { + return FOFFormHelper::loadRuleType($type, $new); + } + + /** + * Proxy for {@link FOFFormHelper::addFieldPath()}. + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @since 2.0 + */ + public static function addFieldPath($new = null) + { + return FOFFormHelper::addFieldPath($new); + } + + /** + * Proxy for {@link FOFFormHelper::addHeaderPath()}. + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @since 2.0 + */ + public static function addHeaderPath($new = null) + { + return FOFFormHelper::addHeaderPath($new); + } + + /** + * Proxy for FOFFormHelper::addFormPath(). + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @see FOFFormHelper::addFormPath() + * @since 2.0 + */ + public static function addFormPath($new = null) + { + return FOFFormHelper::addFormPath($new); + } + + /** + * Proxy for FOFFormHelper::addRulePath(). + * + * @param mixed $new A path or array of paths to add. + * + * @return array The list of paths that have been added. + * + * @see FOFFormHelper::addRulePath() + * @since 2.0 + */ + public static function addRulePath($new = null) + { + return FOFFormHelper::addRulePath($new); + } +} diff --git a/deployed/akeeba/libraries/fof/form/header.php b/deployed/akeeba/libraries/fof/form/header.php new file mode 100644 index 00000000..881b3d81 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header.php @@ -0,0 +1,579 @@ + XML element that describes the header field. + * + * @var SimpleXMLElement + * @since 2.0 + */ + protected $element; + + /** + * The FOFForm object of the form attached to the header field. + * + * @var FOFForm + * @since 2.0 + */ + protected $form; + + /** + * The label for the header field. + * + * @var string + * @since 2.0 + */ + protected $label; + + /** + * The header HTML. + * + * @var string|null + * @since 2.0 + */ + protected $header; + + /** + * The filter HTML. + * + * @var string|null + * @since 2.0 + */ + protected $filter; + + /** + * The buttons HTML. + * + * @var string|null + * @since 2.0 + */ + protected $buttons; + + /** + * The options for a drop-down filter. + * + * @var array|null + * @since 2.0 + */ + protected $options; + + /** + * The name of the form field. + * + * @var string + * @since 2.0 + */ + protected $name; + + /** + * The name of the field. + * + * @var string + * @since 2.0 + */ + protected $fieldname; + + /** + * The group of the field. + * + * @var string + * @since 2.0 + */ + protected $group; + + /** + * The form field type. + * + * @var string + * @since 2.0 + */ + protected $type; + + /** + * The value of the filter. + * + * @var mixed + * @since 2.0 + */ + protected $value; + + /** + * The intended table data width (in pixels or percent). + * + * @var mixed + * @since 2.0 + */ + protected $tdwidth; + + /** + * The key of the filter value in the model state. + * + * @var mixed + * @since 2.0 + */ + protected $filterSource; + + /** + * Is this a sortable column? + * + * @var bool + * @since 2.0 + */ + protected $sortable = false; + + /** + * Method to instantiate the form field object. + * + * @param FOFForm $form The form to attach to the form field object. + * + * @since 2.0 + */ + public function __construct(FOFForm $form = null) + { + // If there is a form passed into the constructor set the form and form control properties. + if ($form instanceof FOFForm) + { + $this->form = $form; + } + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to the the value. + * + * @return mixed The property value or null. + * + * @since 2.0 + */ + public function __get($name) + { + switch ($name) + { + case 'description': + case 'name': + case 'type': + case 'fieldname': + case 'group': + case 'tdwidth': + return $this->$name; + break; + + case 'label': + if (empty($this->label)) + { + $this->label = $this->getLabel(); + } + + return $this->label; + + case 'value': + if (empty($this->value)) + { + $this->value = $this->getValue(); + } + + return $this->value; + break; + + case 'header': + if (empty($this->header)) + { + $this->header = $this->getHeader(); + } + + return $this->header; + break; + + case 'filter': + if (empty($this->filter)) + { + $this->filter = $this->getFilter(); + } + + return $this->filter; + break; + + case 'buttons': + if (empty($this->buttons)) + { + $this->buttons = $this->getButtons(); + } + + return $this->buttons; + break; + + case 'options': + if (empty($this->options)) + { + $this->options = $this->getOptions(); + } + + return $this->options; + break; + + case 'sortable': + if (empty($this->sortable)) + { + $this->sortable = $this->getSortable(); + } + + return $this->sortable; + break; + } + + return null; + } + + /** + * Method to attach a JForm object to the field. + * + * @param FOFForm $form The JForm object to attach to the form field. + * + * @return FOFFormHeader The form field object so that the method can be used in a chain. + * + * @since 2.0 + */ + public function setForm(FOFForm $form) + { + $this->form = $form; + + return $this; + } + + /** + * Method to attach a FOFForm object to the field. + * + * @param SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 2.0 + */ + public function setup(SimpleXMLElement $element, $value, $group = null) + { + // Make sure there is a valid JFormField XML element. + if ((string) $element->getName() != 'header') + { + return false; + } + + // Reset the internal fields + $this->label = null; + $this->header = null; + $this->filter = null; + $this->buttons = null; + $this->options = null; + $this->value = null; + $this->filterSource = null; + + // Set the XML element object. + $this->element = $element; + + // Get some important attributes from the form field element. + $class = (string) $element['class']; + $id = (string) $element['id']; + $name = (string) $element['name']; + $filterSource = (string) $element['filter_source']; + $tdwidth = (string) $element['tdwidth']; + + // Set the field description text. + $this->description = (string) $element['description']; + + // Set the group of the field. + $this->group = $group; + + // Set the td width of the field. + $this->tdwidth = $tdwidth; + + // Set the field name and id. + $this->fieldname = $this->getFieldName($name); + $this->name = $this->getName($this->fieldname); + $this->id = $this->getId($id, $this->fieldname); + $this->filterSource = $this->getFilterSource($filterSource); + + // Set the field default value. + $this->value = $this->getValue(); + + return true; + } + + /** + * Method to get the id used for the field input tag. + * + * @param string $fieldId The field element id. + * @param string $fieldName The field element name. + * + * @return string The id to be used for the field input tag. + * + * @since 2.0 + */ + protected function getId($fieldId, $fieldName) + { + $id = ''; + + // If the field is in a group add the group control to the field id. + + if ($this->group) + { + // If we already have an id segment add the group control as another level. + + if ($id) + { + $id .= '_' . str_replace('.', '_', $this->group); + } + else + { + $id .= str_replace('.', '_', $this->group); + } + } + + // If we already have an id segment add the field id/name as another level. + + if ($id) + { + $id .= '_' . ($fieldId ? $fieldId : $fieldName); + } + else + { + $id .= ($fieldId ? $fieldId : $fieldName); + } + + // Clean up any invalid characters. + $id = preg_replace('#\W#', '_', $id); + + return $id; + } + + /** + * Method to get the name used for the field input tag. + * + * @param string $fieldName The field element name. + * + * @return string The name to be used for the field input tag. + * + * @since 2.0 + */ + protected function getName($fieldName) + { + $name = ''; + + // If the field is in a group add the group control to the field name. + + if ($this->group) + { + // If we already have a name segment add the group control as another level. + $groups = explode('.', $this->group); + + if ($name) + { + foreach ($groups as $group) + { + $name .= '[' . $group . ']'; + } + } + else + { + $name .= array_shift($groups); + + foreach ($groups as $group) + { + $name .= '[' . $group . ']'; + } + } + } + + // If we already have a name segment add the field name as another level. + + if ($name) + { + $name .= '[' . $fieldName . ']'; + } + else + { + $name .= $fieldName; + } + + return $name; + } + + /** + * Method to get the field name used. + * + * @param string $fieldName The field element name. + * + * @return string The field name + * + * @since 2.0 + */ + protected function getFieldName($fieldName) + { + return $fieldName; + } + + /** + * Method to get the field label. + * + * @return string The field label. + * + * @since 2.0 + */ + protected function getLabel() + { + // Get the label text from the XML element, defaulting to the element name. + $title = $this->element['label'] ? (string) $this->element['label'] : ''; + + if (empty($title)) + { + $view = $this->form->getView(); + $params = $view->getViewOptionAndName(); + $title = $params['option'] . '_' . + FOFInflector::pluralize($params['view']) . '_FIELD_' . + (string) $this->element['name']; + $title = strtoupper($title); + $result = JText::_($title); + + if ($result === $title) + { + $title = ucfirst((string) $this->element['name']); + } + } + + return $title; + } + + /** + * Get the filter value for this header field + * + * @return mixed The filter value + */ + protected function getValue() + { + $model = $this->form->getModel(); + + return $model->getState($this->filterSource); + } + + /** + * Return the key of the filter value in the model state or, if it's not set, + * the name of the field. + * + * @param string $filterSource The filter source value to return + * + * @return string + */ + protected function getFilterSource($filterSource) + { + if ($filterSource) + { + return $filterSource; + } + else + { + return $this->name; + } + } + + /** + * Is this a sortable field? + * + * @return boolean True if it's sortable + */ + protected function getSortable() + { + $sortable = ($this->element['sortable'] != 'false'); + + if ($sortable) + { + if (empty($this->header)) + { + $this->header = $this->getHeader(); + } + + $sortable = !empty($this->header); + } + + return $sortable; + } + + /** + * Returns the HTML for the header row, or null if this element should + * render no header element + * + * @return string|null HTML code or null if nothing is to be rendered + * + * @since 2.0 + */ + protected function getHeader() + { + return null; + } + + /** + * Returns the HTML for a text filter to be rendered in the filter row, + * or null if this element should render no text input filter. + * + * @return string|null HTML code or null if nothing is to be rendered + * + * @since 2.0 + */ + protected function getFilter() + { + return null; + } + + /** + * Returns the HTML for the buttons to be rendered in the filter row, + * next to the text input filter, or null if this element should render no + * text input filter buttons. + * + * @return string|null HTML code or null if nothing is to be rendered + * + * @since 2.0 + */ + protected function getButtons() + { + return null; + } + + /** + * Returns the JHtml options for a drop-down filter. Do not include an + * empty option, it is added automatically. + * + * @return array The JHtml options for a drop-down filter + * + * @since 2.0 + */ + protected function getOptions() + { + return array(); + } +} diff --git a/deployed/akeeba/libraries/fof/form/header/accesslevel.php b/deployed/akeeba/libraries/fof/form/header/accesslevel.php new file mode 100644 index 00000000..7de32e13 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/accesslevel.php @@ -0,0 +1,43 @@ +getDbo(); + $query = $db->getQuery(true); + + $query->select('a.id AS value, a.title AS text'); + $query->from('#__viewlevels AS a'); + $query->group('a.id, a.title, a.ordering'); + $query->order('a.ordering ASC'); + $query->order($query->qn('title') . ' ASC'); + + // Get the options. + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } +} diff --git a/deployed/akeeba/libraries/fof/form/header/field.php b/deployed/akeeba/libraries/fof/form/header/field.php new file mode 100644 index 00000000..91f66356 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/field.php @@ -0,0 +1,44 @@ +element['sortable'] != 'false'); + + $label = $this->getLabel(); + + if ($sortable) + { + $view = $this->form->getView(); + + return JHTML::_('grid.sort', $label, $this->name, + $view->getLists()->order_Dir, $view->getLists()->order, + $this->form->getModel()->task + ); + } + else + { + return JText::_($label); + } + } +} diff --git a/deployed/akeeba/libraries/fof/form/header/fielddate.php b/deployed/akeeba/libraries/fof/form/header/fielddate.php new file mode 100644 index 00000000..475fcf15 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/fielddate.php @@ -0,0 +1,127 @@ +element['format'] ? (string) $this->element['format'] : '%Y-%m-%d'; + $attributes = array(); + + if ($this->element['size']) + { + $attributes['size'] = (int) $this->element['size']; + } + + if ($this->element['maxlength']) + { + $attributes['maxlength'] = (int) $this->element['maxlength']; + } + + if ($this->element['filterclass']) + { + $attributes['class'] = (string) $this->element['filterclass']; + } + + if ((string) $this->element['readonly'] == 'true') + { + $attributes['readonly'] = 'readonly'; + } + + if ((string) $this->element['disabled'] == 'true') + { + $attributes['disabled'] = 'disabled'; + } + + if ($this->element['onchange']) + { + $attributes['onchange'] = (string) $this->element['onchange']; + } + else + { + $onchange = 'document.adminForm.submit()'; + } + + if ((string) $this->element['placeholder']) + { + $attributes['placeholder'] = JText::_((string) $this->element['placeholder']); + } + + $name = $this->element['searchfieldname'] ? $this->element['searchfieldname'] : $this->name; + + if ($this->element['searchfieldname']) + { + $model = $this->form->getModel(); + $searchvalue = $model->getState((string) $this->element['searchfieldname']); + } + else + { + $searchvalue = $this->value; + } + + // Get some system objects. + $config = FOFPlatform::getInstance()->getConfig(); + $user = JFactory::getUser(); + + // If a known filter is given use it. + switch (strtoupper((string) $this->element['filter'])) + { + case 'SERVER_UTC': + // Convert a date to UTC based on the server timezone. + if ((int) $this->value) + { + // Get a date object based on the correct timezone. + $date = FOFPlatform::getInstance()->getDate($searchvalue, 'UTC'); + $date->setTimezone(new DateTimeZone($config->get('offset'))); + + // Transform the date string. + $searchvalue = $date->format('Y-m-d H:i:s', true, false); + } + break; + + case 'USER_UTC': + // Convert a date to UTC based on the user timezone. + if ((int) $searchvalue) + { + // Get a date object based on the correct timezone. + $date = FOFPlatform::getInstance()->getDate($this->value, 'UTC'); + $date->setTimezone($user->getTimezone()); + + // Transform the date string. + $searchvalue = $date->format('Y-m-d H:i:s', true, false); + } + break; + } + + return JHtml::_('calendar', $searchvalue, $name, $name, $format, $attributes); + } + + /** + * Get the buttons HTML code + * + * @return string The HTML + */ + protected function getButtons() + { + return ''; + } +} diff --git a/deployed/akeeba/libraries/fof/form/header/fieldfilterable.php b/deployed/akeeba/libraries/fof/form/header/fieldfilterable.php new file mode 100644 index 00000000..f51187af --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/fieldfilterable.php @@ -0,0 +1,75 @@ +element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; + $filterclass = $this->element['filterclass'] ? ' class="' . (string) $this->element['filterclass'] . '"' : ''; + $placeholder = $this->element['placeholder'] ? $this->element['placeholder'] : $this->getLabel(); + $name = $this->element['searchfieldname'] ? $this->element['searchfieldname'] : $this->name; + $placeholder = ' placeholder="' . JText::_($placeholder) . '"'; + + $single = in_array($this->element['single'], $valide) ? true : false; + $showMethod = in_array($this->element['showmethod'], $valide) ? true : false; + $method = $this->element['method'] ? $this->element['method'] : 'between'; + $fromName = $this->element['fromname'] ? $this->element['fromname'] : 'from'; + $toName = $this->element['toname'] ? $this->element['toname'] : 'to'; + + $values = $this->form->getModel()->getState($name); + $fromValue = $values[$fromName]; + $toValue = $values[$toName]; + + // Initialize JavaScript field attributes. + if ($this->element['onchange']) + { + $onchange = ' onchange="' . (string) $this->element['onchange'] . '"'; + } + else + { + $onchange = ' onchange="document.adminForm.submit();"'; + } + + if ($showMethod) + { + $html = ''; + } else + { + $html = ''; + } + + $html .= ''; + + if (!$single) + { + $html .= ''; + } + + return $html; + } +} \ No newline at end of file diff --git a/deployed/akeeba/libraries/fof/form/header/fieldsearchable.php b/deployed/akeeba/libraries/fof/form/header/fieldsearchable.php new file mode 100644 index 00000000..cc7bcaf2 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/fieldsearchable.php @@ -0,0 +1,85 @@ +element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; + $filterclass = $this->element['filterclass'] ? ' class="' . (string) $this->element['filterclass'] . '"' : ''; + $placeholder = $this->element['placeholder'] ? $this->element['placeholder'] : $this->getLabel(); + $name = $this->element['searchfieldname'] ? $this->element['searchfieldname'] : $this->name; + $placeholder = ' placeholder="' . JText::_($placeholder) . '"'; + + if ($this->element['searchfieldname']) + { + $model = $this->form->getModel(); + $searchvalue = $model->getState((string) $this->element['searchfieldname']); + } + else + { + $searchvalue = $this->value; + } + + // Initialize JavaScript field attributes. + if ($this->element['onchange']) + { + $onchange = ' onchange="' . (string) $this->element['onchange'] . '"'; + } + else + { + $onchange = ' onchange="document.adminForm.submit();"'; + } + + return ''; + } + + /** + * Get the buttons HTML code + * + * @return string The HTML + */ + protected function getButtons() + { + $buttonclass = $this->element['buttonclass'] ? (string) $this->element['buttonclass'] : 'btn hasTip hasTooltip'; + $buttonsState = strtolower($this->element['buttons']); + $show_buttons = !in_array($buttonsState, array('no', 'false', '0')); + + if (!$show_buttons) + { + return ''; + } + + $html = ''; + + $html .= '' . "\n"; + $html .= '' . "\n"; + + return $html; + } +} diff --git a/deployed/akeeba/libraries/fof/form/header/fieldselectable.php b/deployed/akeeba/libraries/fof/form/header/fieldselectable.php new file mode 100644 index 00000000..9e6a56b6 --- /dev/null +++ b/deployed/akeeba/libraries/fof/form/header/fieldselectable.php @@ -0,0 +1,109 @@ +element->children() as $option) + { + // Only add