first commit

This commit is contained in:
2024-07-15 11:28:08 +02:00
commit f52d538ea5
21891 changed files with 6161164 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check;
use Awf\Container\Container;
use Solo\Alice\Exception\CannotOpenLogfile;
use Solo\Alice\Exception\StopScanningEarly;
use Exception;
use InvalidArgumentException;
/**
* Abstract class for ALICE checks
*
* @since 7.0.0
*/
abstract class Base
{
/**
* Application container
*
* @var Container
*/
protected $container;
/**
* Check priority
*
* @var int
* @since 7.0.0
*/
protected $priority = 0;
/**
* The full path to the log file we are analyzing
*
* @var string
* @since 7.0.0
*/
protected $logFile = null;
/**
* Language key with the description of the check implemented by this class.
*
* @var string
* @since 7.0.0
*/
protected $checkLanguageKey = '';
/**
* Language key and sprintf() parameters for the detected error.
*
* Position 0 of the array is the language string. Positions 1 onwards (optional) are the sprintf() parameters.
*
* @var array
* @since 7.0.0
*/
protected $errorLanguageKey = [];
/**
* Status of the current check.
*
* 1 = success; 0 = warning; -1 = failure
*
* @var int
* @since 7.0.0
*/
protected $result = 1;
/**
* Check constructor
*
* @param Container $container Application container
* @param string $logFile The log file we will be analyzing
*
* @return void
* @since 7.0.0
*/
public function __construct(Container $container, $logFile)
{
$this->container = $container;
$this->logFile = $logFile;
}
/**
* Run a check
*
* @return void
* @throws CannotOpenLogfile If the log file cannot be opened
* @throws Exception If an unhandled error occurs
* @since 7.0.0
*/
abstract public function check();
/**
* Returns the solution that should be applied to fix the issue
*
* @return string Steps required to fixing the issue
* @since 7.0.0
*/
abstract public function getSolution();
/**
* Returns the status of this check.
*
* @return int 1 = success; 0 = warning; -1 = failure
* @since 7.0.0
*/
public function getResult()
{
return $this->result;
}
/**
* Set the result for current check.
*
* @param int $result 1 = success; 0 = warning; -1 = failure
*
* @return void
* @since 7.0.0
*/
public function setResult($result)
{
// Allow only a set of results
if (!in_array($result, [1, 0, -1], true))
{
$result = -1;
}
$this->result = $result;
}
/**
* Gets the priority of this check
*
* @return int
* @since 7.0.0
*/
public function getPriority()
{
return $this->priority;
}
/**
* Returns the language key and any sprintf() parameters for the error detected by this check
*
* @return array Position 0 is the lang key, everything else is the sprintf parameters
* @since 7.0.0
*/
public function getErrorLanguageKey()
{
return $this->errorLanguageKey;
}
/**
* @param array $errorLanguageKey
*
* @since 7.0.0
*/
public function setErrorLanguageKey($errorLanguageKey)
{
if (!is_array($errorLanguageKey))
{
throw new InvalidArgumentException(sprintf(
"Method %s now only accepts an array as its parameter", __METHOD__
));
}
$this->errorLanguageKey = $errorLanguageKey;
}
/**
* Returns the language key with this check's description
*
* @return string
* @since 7.0.0
*/
public function getCheckLanguageKey()
{
return $this->checkLanguageKey;
}
/**
* Runs a scanner callback against all lines of the log file
*
* @param callable $callback The scanner callback to execute on each line of the log file.
*
* @throws Exception If the scanner callback detects an error
*/
protected function scanLines(callable $callback)
{
// Open the log file for reading
$handle = @fopen($this->logFile, 'r');
// Did we fail to open the log file?
if ($handle === false)
{
throw new CannotOpenLogfile($this->logFile);
}
$prev_data = '';
$buffer = 65536;
while (!feof($handle))
{
$line = fgets($handle);
// Apply the callback on the current line.
try
{
call_user_func($callback, $line);
}
catch (StopScanningEarly $e)
{
/**
* This exception is used to stop scanning the log file, e.g. if the checker has found the information
* it was looking for. We just need to terminate the loop WITHOUT rethrowing the exception.
*/
break;
}
catch (Exception $e)
{
// The check detected an error condition. Close the log file and rethrow the exception.
fclose($handle);
throw $e;
}
}
// All right. We finished processing the log file. Close the handle.
fclose($handle);
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Filesystem;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup directories with a lot of files
*/
class LargeDirectories extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 30;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_DIRECTORIES';
parent::__construct($container, $logFile);
}
public function check()
{
$prev_dir = '';
$large_dir = [];
$this->scanLines(function ($data) use (&$prev_dir, &$large_dir) {
// Let's get all the involved directories
preg_match_all('#Scanning files of <root>/(.*)#', $data, $matches);
if (!isset($matches[1]) || empty($matches[1]))
{
return;
}
$dirs = $matches[1];
if ($prev_dir)
{
array_unshift($dirs, $prev_dir);
}
foreach ($dirs as $dir)
{
preg_match_all('#Adding ' . $dir . '/([^\/]*) to#', $data, $tmp_matches);
if (count($tmp_matches[0]) > 250)
{
$large_dir[] = ['position' => $dir, 'elements' => count($tmp_matches[0])];
}
}
$prev_dir = array_pop($dirs);
});
if (empty($large_dir))
{
return;
}
$errorMsg = [];
// Let's log all the results
foreach ($large_dir as $dir)
{
$errorMsg[] = $dir['position'] . ', ' . $dir['elements'] . ' files';
}
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_DIRECTORIES_ERROR', implode("\n", $errorMsg),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_DIRECTORIES_SOLUTION');
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Filesystem;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup too big files
*/
class LargeFiles extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 20;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_FILES';
parent::__construct($container, $logFile);
}
public function check()
{
$bigfiles = [];
$this->scanLines(function ($data) use (&$bigfiles) {
preg_match_all('#(_before_|\*after\*) large file: (<root>.*?) \- size: (\d+)#i', $data, $tmp_matches);
// Record valid matches only (i.e. with a filesize)
if (!isset($tmp_matches[3]) || empty($tmp_matches[3]))
{
return;
}
for ($i = 0; $i < count($tmp_matches[2]); $i++)
{
// Get flagged files only once; I could have a breaking step after, before or BOTH a large file
$key = md5($tmp_matches[2][$i]);
if (!isset($bigfiles[$key]))
{
$bigfiles[$key] = [
'filename' => $tmp_matches[2][$i],
'size' => round($tmp_matches[3][$i] / 1024 / 1024, 2),
];
}
}
});
if (empty($bigfiles))
{
return;
}
/**
* Depending on the size of the detected files this could be a success, warning or error condition.
*
* Files over 10MB : error
* Files 2 to 10MB : warning
* Files < 2MB : success (user not warned)
*/
foreach ($bigfiles as $file)
{
// More than 10 Mb? Always set the result to error, no matter what
if ($file['size'] >= 10)
{
$this->setResult(-1);
break;
}
// Warning for "smaller" files, set the warn only if we don't already have a failure state
if ($file['size'] > 2)
{
$this->setResult(0);
}
}
// If all files were too small to report just go away.
if ($this->getResult() == 1)
{
return;
}
$errorMsg = [];
foreach ($bigfiles as $bad)
{
$errorMsg[] = 'File: ' . $bad['filename'] . ' ' . $bad['size'] . ' Mb';
}
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_FILES_ERROR', implode("\n", $errorMsg),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_LARGE_FILES_SOLUTION');
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Filesystem;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup multiple Joomla! installations with a single backup
*/
class MultipleSites extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 10;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_MULTIPLE_SITES';
parent::__construct($container, $logFile);
}
public function check()
{
$subfolders = [];
$this->scanLines(function ($data) use (&$subfolders) {
preg_match_all('#Adding\s(.*?)/administrator/index\.php to archive#i', $data, $matches);
if (!$matches[1])
{
return;
}
$subfolders = array_merge($subfolders, $matches[1]);
});
if (empty($subfolders))
{
return;
}
$this->setResult(0);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_MULTIPLE_SITES_ERROR', implode("\n", $subfolders),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_MULTIPLE_SITES_SOLUTION');
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Filesystem;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup old backups
*/
class OldBackups extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 40;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_OLD_BACKUPS';
parent::__construct($container, $logFile);
}
public function check()
{
$bigfiles = [];
$this->scanLines(function ($data) use (&$bigfiles) {
// Only looking files with extensions like .jpa, .jps, .j01, .j02, ..., .j99, .j100, ..., .j99999, .z01, ...
preg_match_all('#-- Adding.*? <root>/(.*?)(\.(?:jpa|jps|j\d{2,5}|z\d{2,5}))#i', $data, $tmp_matches);
if (!isset($tmp_matches[1]) || !$tmp_matches[1])
{
return;
}
// Record valid matches only
for ($i = 0; $i < count($tmp_matches[1]); $i++)
{
// Get flagged files only once
$key = md5($tmp_matches[1][$i] . $tmp_matches[2][$i]);
if (isset($bigfiles[$key]))
{
continue;
}
$filename = $tmp_matches[1][$i] . $tmp_matches[2][$i];
$filePath = $this->container->basePath . '/' . $filename;
$fileSize = 0;
if (@file_exists($filePath) && @is_file($filePath))
{
$fileSize = @filesize($filePath);
}
if ($fileSize > 1048576)
{
$bigfiles[$key] = [
'filename' => $filename,
];
}
}
});
if (empty($bigfiles))
{
return;
}
$errorMsg = [];
$this->setResult(-1);
foreach ($bigfiles as $bad)
{
$errorMsg[] = 'File: ' . $bad['filename'];
}
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_OLD_BACKUPS_ERROR', implode("\n", $errorMsg),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_FILESYSTEM_OLD_BACKUPS_SOLUTION');
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Requirements;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Akeeba\Engine\Factory;
use Exception;
use Awf\Text\Text;
/**
* Checks for database permissions (SHOW permissions)
*/
class DatabasePermissions extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 40;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DBPERMISSIONS';
parent::__construct($container, $logFile);
}
public function check()
{
$db = Factory::getDatabase();
// Can I execute SHOW statements?
try
{
$result = $db->setQuery('SHOW TABLES')->query();
}
catch (Exception $e)
{
$result = false;
}
if (!$result)
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DBPERMISSIONS_ERROR',
]);
return;
}
try
{
$result = $db->setQuery('SHOW CREATE TABLE ' . $db->nameQuote('#__ak_profiles'))->query();
}
catch (Exception $e)
{
$result = false;
}
if (!$result)
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DBPERMISSIONS_ERROR',
]);
return;
}
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DBPERMISSIONS_SOLUTION');
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Requirements;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks for supported DB type and version
*/
class DatabaseVersion extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 20;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DATABASE';
parent::__construct($container, $logFile);
}
public function check()
{
// Instead of reading the log, I can simply take the JDatabase object and test it
$db = $this->container->db;
$connector = strtolower($db->name);
$version = $db->getVersion();
switch ($connector)
{
case 'mysql':
case 'mysqli':
case 'pdomysql':
if (version_compare($version, '5.0.47', 'lt'))
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DATABASE_VERSION_TOO_OLD', $version,
]);
}
break;
case 'pdo':
case 'sqlite':
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DATABASE_UNSUPPORTED', $connector,
]);
break;
default:
$this->setResult(-1);
$this->setErrorLanguageKey(['COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DATABASE_UNKNOWN', $connector]);
break;
}
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_DATABASE_SOLUTION');
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Requirements;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if we have enough memory to perform backup; at least 16Mb
*/
class Memory extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 30;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_MEMORY';
parent::__construct($container, $logFile);
}
public function check()
{
$limit = null;
$usage = false;
$this->scanLines(function ($line) use (&$limit, &$usage) {
if (is_null($limit))
{
$pos = strpos($line, '|Memory limit');
if ($pos !== false)
{
$limit = trim(substr($line, strpos($line, ':', $pos) + 1));
$limit = str_ireplace('M', '', $limit);
// Convert to integer for better handling and checks
$limit = (int) $limit;
}
}
if (!$usage)
{
$pos = strpos($line, '|Current mem. usage');
if ($pos !== false)
{
$usage = trim(substr($line, strpos($line, ':', $pos) + 1));
// Converting to Mb for better handling
$usage = round($usage / 1024 / 1024, 2);
}
}
throw new StopScanningEarly();
});
if (empty($limit) || empty($usage))
{
// Inconclusive check. Cannot get the memory information.
return;
}
$available = $limit - $usage;
if ($limit < 0)
{
// Stupid host uses a negative memory limit. This is the same as setting no memory limit. Bleh.
return;
}
if ($available >= 16)
{
// We have enough memory.
return;
}
$this->setResult(-1);
$this->setErrorLanguageKey(['COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_MEMORY_TOO_FEW', $available]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_MEMORY_SOLUTION');
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Requirements;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if the user is using a too old or too new PHP version
*/
class PHPVersion extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 10;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_PHP_VERSION';
parent::__construct($container, $logFile);
}
public function check()
{
$this->scanLines(function ($line) {
$pos = strpos($line, '|PHP Version');
if ($pos === false)
{
return;
}
$version = trim(substr($line, strpos($line, ':', $pos) + 1));
// PHP too old (well, this should never happen)
if (version_compare($version, '5.6', 'lt'))
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_PHP_VERSION_ERR_TOO_OLD',
]);
}
throw new StopScanningEarly();
});
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_REQUIREMENTS_PHP_VERSION_SOLUTION');
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Akeeba\Engine\Factory;
use Awf\Text\Text;
/**
* Check if the user added the site database as additional database. Some servers won't allow more than one connection
* to the same database, causing the backup process to fail
*/
class AddedCoreDatabaseAsExtra extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 100;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_JSAME';
parent::__construct($container, $logFile);
}
public function check()
{
$profile = 0;
$this->scanLines(function ($line) use (&$profile) {
$pos = strpos($line, '|Loaded profile');
if ($pos === false)
{
return;
}
preg_match('/profile\s+#(\d+)/', $line, $matches);
if (isset($matches[1]))
{
$profile = (int) $matches[1];
}
throw new StopScanningEarly();
});
// Mhm... no profile ID? Something weird happened better stop here and mark the test as skipped
if ($profile <= 0)
{
return;
}
// Do I have to switch profile?
$container = $this->container;
$cur_profile = $container->segment->get('profile', null);
if ($cur_profile != $profile)
{
$container->segment->set('profile', $profile);
}
$error = false;
$config = $container->appConfig;
$filters = Factory::getFilters();
$multidb = $filters->getFilterData('multidb');
$jdb = [
'driver' => $config->get('dbtype'),
'host' => $config->get('host'),
'username' => $config->get('user'),
'password' => $config->get('password'),
'database' => $config->get('db'),
];
foreach ($multidb as $addDb)
{
$options = [
'driver' => $addDb['driver'],
'host' => $addDb['host'],
'username' => $addDb['username'],
'password' => $addDb['password'],
'database' => $addDb['database'],
];
// It's the same database used by Joomla, this could led to errors
if ($jdb == $options)
{
$error = true;
}
}
// If needed set the old profile again
if ($cur_profile != $profile)
{
$container->segment->set('profile', $cur_profile);
}
if (!$error)
{
return;
}
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_JSAME_ERROR',
]);
}
public function getSolution()
{
// Test skipped? No need to provide a solution
if ($this->getResult() === 0)
{
return '';
}
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_JSAME_SOLUTION');
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if the user is using a too old or too new PHP version
*/
class CorruptInstallation extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 60;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_CORRUPTED_INSTALL';
parent::__construct($container, $logFile);
}
public function check()
{
$error = false;
$foundLoadedProfile = false;
$this->scanLines(function ($line) use (&$foundLoadedProfile) {
// First we need to find the "Loaded profile" line
if (!$foundLoadedProfile)
{
$pos = strpos($line, '|Loaded profile');
if ($pos !== false)
{
// Mark the line as found. We are interested in the line AFTER this one.
$foundLoadedProfile = true;
}
// Since at this point we are not past the "Loaded profile" we need to keep parsing the log file.
return;
}
// Ok, we are just past the "Loaded profile" line. Let's see if it's a broken install.
$logline = trim(substr($line, 24));
// If it's not an empty line then it is definitely not a broken install
if ($logline != '|')
{
throw new StopScanningEarly();
}
// Empty line?? Most likely it's a broken install
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_CORRUPTED_INSTALL_ERROR',
]);
throw new StopScanningEarly();
});
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_CORRUPTED_INSTALL_SOLUTION');
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if error logs are included inside the backup. Since their size grows while we're trying to backup them,
* this could led to corrupted archives.
*/
class ErrorLogsInArchive extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 80;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_ERRORFILES';
parent::__construct($container, $logFile);
}
public function check()
{
$error_files = [];
$this->scanLines(function ($data) use (&$error_files) {
preg_match_all('#Adding(.*?(/php_error_cpanel\.|php_error_cpanel\.|/error_)log)#', $data, $tmp_matches);
if (isset($tmp_matches[1]))
{
$error_files = array_merge($error_files, $tmp_matches[1]);
}
});
if (empty($error_files))
{
return;
}
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_ERRORFILES_FOUND', implode("\n", $error_files),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_ERRORFILES_SOLUTION');
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Awf\Database\Driver;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Akeeba\Engine\Factory;
use Exception;
use Awf\Text\Text;
/**
* Check if the user add one or more additional database, but the connection details are wrong
* In such cases Akeeba Backup will receive an error, halting the whole backup process
*/
class ExtraDatabaseCannotConnect extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 90;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_WRONG';
parent::__construct($container, $logFile);
}
public function check()
{
$profile = 0;
$this->scanLines(function ($line) use (&$profile) {
$pos = strpos($line, '|Loaded profile');
if ($pos === false)
{
return;
}
preg_match('/profile\s+#(\d+)/', $line, $matches);
if (isset($matches[1]))
{
$profile = (int) $matches[1];
}
throw new StopScanningEarly();
});
// Mhm... no profile ID? Something weird happened better stop here and mark the test as skipped
if ($profile <= 0)
{
return;
}
// Do I have to switch profile?
$container = $this->container;
$cur_profile = $container->segment->get('profile', null);
if ($cur_profile != $profile)
{
$container->segment->set('profile', $profile);
}
$error = false;
$filters = Factory::getFilters();
$multidb = $filters->getFilterData('multidb');
foreach ($multidb as $addDb)
{
$options = [
'driver' => $addDb['driver'],
'host' => $addDb['host'],
'port' => $addDb['port'],
'user' => $addDb['username'],
'password' => $addDb['password'],
'database' => $addDb['database'],
'prefix' => $addDb['prefix'],
];
try
{
$db = Driver::getInstance($options);
$db->connect();
$db->disconnect();
}
catch (Exception $e)
{
$error = true;
}
}
// If needed set the old profile again
if ($cur_profile != $profile)
{
$container->segment->set('profile', $cur_profile);
}
if ($error)
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_WRONG_ERROR',
]);
}
}
public function getSolution()
{
// Test skipped? No need to provide a solution
if ($this->getResult() === 0)
{
return '';
}
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_DBADD_WRONG_SOLUTION');
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if a fatal error occurred during the backup process
*/
class FatalError extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 110;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_FATALERROR';
parent::__construct($container, $logFile);
}
public function check()
{
$this->scanLines(function ($data) {
preg_match('#ERROR \|.*?\|(.*)#', $data, $tmp_matches);
if (!isset($tmp_matches[1]))
{
return;
}
$error = $tmp_matches[1];
if (empty($error))
{
return;
}
$this->setResult(-1);
$this->setErrorLanguageKey(['COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_FATALERROR_ERROR', $error]);
throw new StopScanningEarly();
});
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_FATALERROR_SOLUTION');
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks that the Kettenrad instance is not dead; the number of "Starting step" and "Saving Kettenrad" instance
* must be the same, plus none of the steps could be repeated (except the first one).
*/
class Kettenrad extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 10;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_KETTENRAD';
parent::__construct($container, $logFile);
}
public function check()
{
$starting = [];
$saving = [];
$this->scanLines(function ($data) use (&$starting, &$saving) {
preg_match_all('#Starting Step number (\d+)#i', $data, $tmp_matches);
if (isset($tmp_matches[1]))
{
$starting = array_merge($starting, $tmp_matches[1]);
}
preg_match_all('#Finished Step number (\d+)#i', $data, $tmp_matches);
if (isset($tmp_matches[1]))
{
$saving = array_merge($saving, $tmp_matches[1]);
}
});
/**
* Check that none of "Starting step" number is repeated, EXCEPT for the first one (it's ok).
* That could happen when some poorly configured server processes the same request twice
*/
foreach ($starting as $stepNumber)
{
if ($stepNumber == 1)
{
continue;
}
/**
* Did a step run more than once?
*
* It is OK if it started multiple times but was only logged as finished once. This means it failed and the
* user took advantage of our retry-on-error feature for backend backups.
*
* However, if we see that it was logged as *finished* multiple times then it means that the same step ran
* multiple times in parallel. This is where the real problem is.
*/
if (count(array_keys($starting, $stepNumber)) > 1)
{
if (count(array_keys($saving, $stepNumber)) > 1)
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_KETTENRAD_STARTING_MORE_ONCE', $stepNumber,
]);
return;
}
}
}
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_KETTENRAD_SOLUTION');
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if the user is post processing the archive but didn't set any part size.
* Most likely this could lead to timeouts while uploading
*/
class PartSize extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 70;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_PART_SIZE';
parent::__construct($container, $logFile);
}
public function check()
{
$partsize = 0;
$postproc = '';
$this->scanLines(function ($data) use (&$partsize, &$postproc) {
if (empty($partsize))
{
preg_match('#\|Part size.*:(\d+)#i', $data, $match);
if (isset($match[1]))
{
$partsize = $match[1];
}
}
if (empty($postproc))
{
preg_match('#Loading.*post-processing.*?\((.*?)\)#i', $data, $match);
if (isset($match[1]))
{
$postproc = trim($match[1]);
}
}
// Wait until I have both pieces of data
if (empty($partsize) || empty($postproc))
{
return;
}
if (($partsize > 2000000000) && ($postproc != 'none'))
{
$this->setResult(0);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_PART_SIZE_ERROR',
]);
}
throw new StopScanningEarly();
});
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_PART_SIZE_SOLUTION');
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Exception;
use Awf\Text\Text;
/**
* Checks that every page load is not hitting the timeout limit.
* Time diff is performed against the "Start step" and "Saving Kettenrad" timestamps.
*
* TODO This needs to be rewritten. It makes no sense. A backup CAN NOT POSSIBLY take longer than PHP's time limit!
*/
class Timeout extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 20;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TIMEOUT';
parent::__construct($container, $logFile);
}
public function check()
{
$starting = [];
$saving = [];
$isCli = false;
$this->scanLines(function ($data) use (&$starting, &$saving, &$isCli) {
if (preg_match('/PHP SAPI\s{1,}:\s*cli/', $data) == 1)
{
// This is CLI backup.
$isCli = true;
}
preg_match_all('#(\d{6}\s\d{2}:\d{2}:\d{2})\|.*?Starting Step number#i', $data, $tmp_matches);
if (isset($tmp_matches[1]))
{
$starting = array_merge($starting, $tmp_matches[1]);
}
preg_match_all('#(\d{6}\s\d{2}:\d{2}:\d{2})\|.*?Finished Step number#i', $data, $tmp_matches);
if (isset($tmp_matches[1]))
{
$saving = array_merge($saving, $tmp_matches[1]);
}
});
// If there is an issue with starting and saving instances, I can't go on, first of all fix that
if (count($saving) != count($starting))
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TIMEOUT_KETTENRAD_BROKEN',
]);
return;
}
$temp = [];
// Let's expand the date part so I can safely work with that strings
foreach ($starting as $item)
{
$temp[] = '20' . substr($item, 0, 2) . '-' . substr($item, 2, 2) . '-' . substr($item, 4, 2) . substr($item, 6);
}
$starting = $temp;
$temp = [];
// Let's expand the date part so I can safely work with that strings
foreach ($saving as $item)
{
$temp[] = '20' . substr($item, 0, 2) . '-' . substr($item, 2, 2) . '-' . substr($item, 4, 2) . substr($item, 6);
}
$saving = $temp;
$maxExecution = $this->detectMaxExec($isCli);
/**
* If I detected a CLI backup without a max execution time limit (THIS IS THE ONLY WAY, PER PHP'S DOCUMENTATION)
* I immediately quit since we can't possibly time out.
*/
if ($maxExecution == -1)
{
return;
}
// Ok, did I have any timeout between the start and saving step (ie page loads)?
for ($i = 0; $i < count($starting); $i++)
{
$duration = strtotime($saving[$i]) - strtotime($starting[$i]);
if ($duration > $maxExecution)
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TIMEOUT_MAX_EXECUTION', $duration,
]);
return;
}
}
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TIMEOUT_SOLUTION');
}
/**
* Detects max execution time, reading backup log. If the maximum execution time is set to 0 or it's bigger
* than 100, it gets the default value of 100.
*
* @return int
* @throws Exception
*/
private function detectMaxExec($isCli = false)
{
$time = 0;
$this->scanLines(function ($line) use (&$time) {
$pos = stripos($line, '|Max. exec. time');
if ($pos === false)
{
return;
}
$time = (int) trim(substr($line, strpos($line, ':', $pos) + 1));
});
/**
* CLI backups.
* Negative, zero or no detected time: we return -1 (no limit).
*/
$time = ($time <= 0) ? -1 : $time;
/**
* Over a web server backups.
* Negative, zero or no detected time: we consider it to be 100 seconds.
* Values over 100 seconds: we cap the to 100 seconds.
*
* The time limit cap has to do with Apache's internal timeout.
*/
$time = ($time <= 0) ? 100 : min($time, 100);
return $time;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup tables with too many rows, causing the system to fail
*/
class TooManyRows extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 50;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYROWS';
parent::__construct($container, $logFile);
}
public function check()
{
$tables = [];
$row_limit = 1000000;
$this->scanLines(function ($data) use (&$tables, &$row_limit) {
// Let's save every scanned table
preg_match_all('#Continuing dump of (.*?) from record \#(\d+)#i', $data, $matches);
if (!isset($matches[1]) || empty($matches[1]))
{
return;
}
for ($i = 0; $i < count($matches[1]); $i++)
{
if ($matches[2][$i] >= $row_limit)
{
$table = trim($matches[1][$i]);
$tables[$table] = $matches[2][$i];
}
}
});
if (!count($tables))
{
return;
}
$errorMsg = [];
foreach ($tables as $table => $rows)
{
$errorMsg[] = sprintf(
"%s %d %s %s",
Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYROWS_TABLE'),
$table,
number_format((float) $rows),
Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYROWS_ROWS'
));
}
// Let's raise only a warning, maybe the server is powerful enough to dump huge tables and the problem is somewhere else
$this->setResult(0);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYROWS_ERROR', implode("\n", $errorMsg),
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYROWS_SOLUTION');
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Awf\Text\Text;
/**
* Checks if the user is trying to backup too many databases, causing the system to fail
*/
class TooManyTables extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 40;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYDBS';
parent::__construct($container, $logFile);
}
public function check()
{
$tables = [];
$ex_tables = [];
$this->scanLines(function ($data) use (&$tables, &$ex_tables) {
// Let's save every scanned table
preg_match_all('#Native\\[a-zA-Z]* :: Adding.*?\(internal name (.*?)\)#i', $data, $matches);
if (!isset($matches[1]) || empty($matches[1]))
{
return;
}
$tables = array_merge($tables, $matches[1]);
});
if (empty($tables))
{
return;
}
// Let's loop on saved tables and look at their prefixes
foreach ($tables as $table)
{
preg_match('/^(.*?_)/', $table, $matches);
if ($matches[1] !== '#_' && !in_array($matches[1], $ex_tables))
{
$ex_tables[] = $matches[1];
}
}
if (!count($ex_tables))
{
return;
}
$this->setResult(-1);
if (count($ex_tables) > 0 && count($ex_tables) <= 3)
{
$this->setResult(0);
}
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYDBS_ERROR',
]);
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_TOOMANYDBS_SOLUTION');
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Check\Runtimeerrors;
use Awf\Container\Container;
use Solo\Alice\Check\Base;
use Solo\Alice\Exception\StopScanningEarly;
use Awf\Text\Text;
/**
* Checks if Akeeba Backup failed to write data inside the archive (WIN hosts only)
*/
class WindowsCannotAppend extends Base
{
public function __construct(Container $container, $logFile = null)
{
$this->priority = 30;
$this->checkLanguageKey = 'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_WINCANTAPPEND';
parent::__construct($container, $logFile);
}
public function check()
{
// Customer is not on windows, this problem happened on Windows only
if (!$this->isWin())
{
return;
}
$this->scanLines(function ($data) {
if (preg_match('#Could not open archive file.*? for append#i', $data))
{
$this->setResult(-1);
$this->setErrorLanguageKey([
'COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_WINCANTAPPEND_ERROR',
]);
throw new StopScanningEarly();
}
});
}
public function getSolution()
{
return Text::_('COM_AKEEBA_ALICE_ANALYZE_RUNTIME_ERRORS_WINCANTAPPEND_SOLUTION');
}
private function isWin()
{
$OS = '';
$this->scanLines(function ($line) use (&$OS) {
$pos = stripos($line, '|OS Version');
if ($pos !== false)
{
$OS = trim(substr($line, strpos($line, ':', $pos) + 1));
throw new StopScanningEarly();
}
});
if (stripos($OS, 'windows') !== false)
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Exception;
use Awf\Text\Text;
use RuntimeException;
use Throwable;
/**
* ALICE Exception: cannot open log file
*/
class CannotOpenLogfile extends RuntimeException
{
public function __construct($logFile, Throwable $previous = null)
{
$message = Text::sprintf('COM_AKEEBA_ALICE_ERR_CANNOT_OPEN_LOGFILE', $logFile);
parent::__construct($message, 500, $previous);
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Alice\Exception;
use RuntimeException;
/**
* This exception tells ALICE to stop reading lines from the log file. It is not rethrown. It's only meant to stop the
* scanning early.
*/
class StopScanningEarly extends RuntimeException
{
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!--
This only works on IIS 7 or later. See https://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions
-->
<configuration>
<system.webServer>
<security>
<requestFiltering>
<fileExtensions allowUnlisted="false" >
<clear />
<add fileExtension=".html" allowed="true"/>
</fileExtensions>
</requestFiltering>
</security>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,484 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Application\TransparentAuthentication;
use Awf\Text\Text;
use Awf\Uri\Uri;
use Solo\Helper\SecretWord;
class Application extends \Awf\Application\Application
{
const secretKeyRelativePath = '/engine/secretkey.php';
public function initialise()
{
// Let AWF know that the prefix for our system JavaScript is 'akeeba.System.'
\Awf\Html\Grid::$javascriptPrefix = 'akeeba.System.';
// This line must appear before the user manager initializes, or it won't find the users table!
$this->container->appConfig->set('user_table', '#__ak_users');
// If the PHP session save path is not writeable we will use the 'session' subdirectory inside our tmp directory
$this->discoverSessionSavePath();
// Set up the template (theme) to use
$this->setTemplate('default');
// Load the language files
$this->loadLanguages();
// Redirect to the setup page if the configuration does not exist yet
$configPath = $this->container->appConfig->getDefaultPath();
$this->redirectToSetup($configPath);
// Load the configuration if it's present
if (@file_exists($configPath))
{
// Load the application's configuration
$this->container->appConfig->loadConfiguration();
// Apply the timezone
$this->applyTimezonePreference();
// Load Akeeba Engine's settings encryption preferences
$this->loadEngineEncryptionKey();
// Enforce encryption of the front-end Secret Word
SecretWord::enforceEncryption('frontend_secret_word');
// Load Akeeba Engine's configuration
$this->loadBackupProfile();
// Session timeout check
$this->applySessionTimeout();
}
// Load the application routes
$this->loadRoutes();
// Attach the user privileges to the user manager
$manager = $this->container->userManager;
$this->attachPrivileges($manager);
// Only apply TFA when debug mode has not been enabled
$this->applyTwoFactorAuthentication($manager);
// Show the login page when necessary
$this->redirectToLogin();
// Set up the media query key
$this->setupMediaVersioning();
}
/**
* Language file processing callback. It converts _QQ_ to " and replaces the product name in the legacy INI files
* imported from Akeeba Backup for Joomla!.
*
* @param string $filename The full path to the file being loaded
* @param array $strings The key/value array of the translations
*
* @return boolean|array False to prevent loading the file, or array of processed language string, or true to
* ignore this processing callback.
*/
public function processLanguageIniFile($filename, $strings)
{
foreach ($strings as $k => $v)
{
$v = str_replace('_QQ_', '"', $v);
$v = str_replace('Akeeba Backup', 'Akeeba Solo', $v);
$strings[$k] = $v;
}
return $strings;
}
/**
* Apply the session timeout setting.
*/
public function applySessionTimeout()
{
// Get the session timeout
$sessionTimeout = (int)$this->container->appConfig->get('session_timeout', 1440);
// Get the base URL and set the cookie path
$uri = new Uri(Uri::base(false, $this->container), $this);
// Force the cookie timeout to coincide with the session timeout
if ($sessionTimeout > 0)
{
$this->container->session->setCookieParams(array(
'lifetime' => $sessionTimeout * 60,
'path' => $uri->getPath(),
'domain' => $uri->getHost(),
'secure' => $uri->getScheme() == 'https',
'httponly' => true,
));
}
// Calculate a hash for the current user agent and IP address
$ip = \Awf\Utils\Ip::getUserIP();
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
$uniqueData = $ip . $user_agent . $this->container->basePath . Application::secretKeyRelativePath;
$hash_algos = function_exists('hash_algos') ? hash_algos() : array();
// Prefer SHA-512...
if (in_array('sha512', $hash_algos))
{
$sessionKey = hash('sha512', $uniqueData, false);
}
// ...then SHA-256...
elseif (in_array('sha256', $hash_algos))
{
$sessionKey = hash('sha256', $uniqueData, false);
}
// ...then SHA-1...
elseif (function_exists('sha1'))
{
$sessionKey = sha1($uniqueData);
}
// ...then MD5...
elseif (function_exists('md5'))
{
$sessionKey = md5($uniqueData);
}
// ... CRC32?! ...
elseif (function_exists('crc32'))
{
$sessionKey = crc32($uniqueData);
}
// ... base64_encode????! ...
elseif (function_exists('base64_encode'))
{
$sessionKey = base64_encode($uniqueData);
}
// ... paint your server a deep blue and toss it in the middle of the ocean where it will never be found!
else
{
throw new \Exception('Your server does not provide any kind of hashing method. Please use a decent host.', 500);
}
// Get the current session's key
$currentSessionKey = $this->container->segment->get('session_key', '');
// If there is no key, set it
if (empty($currentSessionKey))
{
$this->container->segment->set('session_key', $sessionKey);
}
// If there is a key and it doesn't match, trash the session and restart.
elseif ($currentSessionKey != $sessionKey)
{
$this->container->session->destroy();
$this->redirect($this->container->router->route('index.php'));
}
// If the session timeout is 0 or less than 0 there is no limit. Nothing to check.
if ($sessionTimeout <= 0)
{
return;
}
// What is the last session timestamp?
$lastCheck = $this->container->segment->get('session_timestamp', 0);
$now = time();
// If there is a session timestamp make sure it's valid, otherwise trash the session and restart
if (($lastCheck != 0) && (($now - $lastCheck) > ($sessionTimeout * 60)))
{
$this->container->session->destroy();
$this->redirect($this->container->router->route('index.php'));
}
// In any other case, refresh the session timestamp
else
{
$this->container->segment->set('session_timestamp', $now);
}
}
/**
* Creates or updates the custom session save path
*
* @param string $path The custom session save path
* @param boolean $silent Should I suppress all errors?
*
* @return void
*
* @throws \Exception If $silent is set to false
*/
public function createOrUpdateSessionPath($path, $silent = true)
{
try
{
$fs = $this->container->fileSystem;
$protectFolder = false;
if (!@is_dir($path))
{
$fs->mkdir($path, 0777);
}
elseif (!is_writeable($path))
{
$fs->chmod($path, 0777);
$protectFolder = true;
}
else
{
if (!@file_exists($path . '/.htaccess'))
{
$protectFolder = true;
}
if (!@file_exists($path . '/web.config'))
{
$protectFolder = true;
}
}
if ($protectFolder)
{
$fs->copy($this->container->basePath . '/.htaccess', $path . '/.htaccess');
$fs->copy($this->container->basePath . '/web.config', $path . '/web.config');
$fs->chmod($path . '/.htaccess', 0644);
$fs->chmod($path . '/web.config', 0644);
}
}
catch (\Exception $e)
{
if (!$silent)
{
throw $e;
}
}
}
/**
* @return void
* @throws \Exception
*/
private function discoverSessionSavePath()
{
$sessionPath = $this->container->session->getSavePath();
if (!@is_dir($sessionPath) || !@is_writable($sessionPath))
{
$sessionPath = APATH_BASE . '/tmp/session';
$this->createOrUpdateSessionPath($sessionPath);
$this->container->session->setSavePath($sessionPath);
}
}
/**
* @return void
*/
private function loadLanguages()
{
// Load the language files
Text::loadLanguage(null, 'akeebabackup', '.com_akeebabackup.ini', false, $this->container->languagePath);
Text::loadLanguage('en-GB', 'akeebabackup', '.com_akeebabackup.ini', false, $this->container->languagePath);
// Load the extra language files
Text::loadLanguage(null, 'akeeba', '.com_akeeba.ini', false, $this->container->languagePath);
Text::loadLanguage('en-GB', 'akeeba', '.com_akeeba.ini', false, $this->container->languagePath);
}
/**
* @param $configPath
*
* @return void
*/
private function redirectToSetup($configPath)
{
if (!@file_exists($configPath) && !in_array($this->getContainer()->input->getCmd('view', ''), array(
'setup', 'ftpbrowser', 'sftpbrowser'
)))
{
$this->getContainer()->input->setData(array(
'view' => 'setup'
));
}
}
/**
* @return void
*/
private function applyTimezonePreference()
{
if (function_exists('date_default_timezone_get') && function_exists('date_default_timezone_set'))
{
if (function_exists('error_reporting'))
{
$oldLevel = error_reporting(0);
}
$serverTimezone = @date_default_timezone_get();
if (empty($serverTimezone) || !is_string($serverTimezone))
{
$serverTimezone = $this->container->appConfig->get('timezone', 'UTC');
}
if (function_exists('error_reporting'))
{
error_reporting($oldLevel);
}
@date_default_timezone_set($serverTimezone);
}
}
/**
* @return void
*/
private function loadEngineEncryptionKey()
{
$secretKeyFile = $this->container->basePath . static::secretKeyRelativePath;
if (@file_exists($secretKeyFile))
{
require_once $secretKeyFile;
}
Factory::getSecureSettings()->setKeyFilename('secretkey.php');
}
/**
* @return void
*/
private function loadBackupProfile()
{
try
{
Platform::getInstance()->load_configuration();
}
catch (\Exception $e)
{
// Ignore database exceptions, they simply mean we need to install or update the database
}
}
/**
* @return void
*/
private function loadRoutes()
{
// Load the routes from JSON, if they are present
$routesJSONPath = $this->container->basePath . '/assets/private/routes.json';
$router = $this->container->router;
$importedRoutes = false;
if (@file_exists($routesJSONPath))
{
$json = @file_get_contents($routesJSONPath);
if (!empty($json))
{
$router->importRoutes($json);
$importedRoutes = true;
}
}
// If we could not import routes from routes.json, try loading routes.php
$routesPHPPath = $this->container->basePath . '/assets/private/routes.php';
if (!$importedRoutes && @file_exists($routesPHPPath))
{
require_once $routesPHPPath;
}
}
/**
* @param $manager
*
* @return void
*/
private function attachPrivileges($manager)
{
$manager->registerPrivilegePlugin('akeeba', '\\Solo\\Application\\UserPrivileges');
$manager->registerAuthenticationPlugin('password', '\\Solo\\Application\\UserAuthenticationPassword');
}
/**
* @param $manager
*
* @return void
*/
private function applyTwoFactorAuthentication($manager)
{
if (!defined('AKEEBADEBUG'))
{
$manager->registerAuthenticationPlugin('yubikey', '\\Solo\\Application\\UserAuthenticationYubikey');
$manager->registerAuthenticationPlugin('google', '\\Solo\\Application\\UserAuthenticationGoogle');
}
}
/**
* @return void
*/
private function redirectToLogin()
{
// Get the view. Necessary to go through $this->getContainer()->input as it may have already changed
$view = $this->getContainer()->input->getCmd('view', '');
// Get the user manager
$manager = $this->container->userManager;
// Show the login page if there is no logged in user and we're not in the setup or login page already
// and we're not using the remote (front-end backup), json (remote JSON API) views of the (S)FTP
// browser views (required by the session task of the setup view).
if (!in_array($view, array(
'check', 'login', 'setup', 'json', 'api', 'Api', 'remote', 'ftpbrowser', 'sftpbrowser',
)) && !$manager->getUser()->getId())
{
// Try to perform transparent authentication
$transparentAuth = new TransparentAuthentication($this->container);
$credentials = $transparentAuth->getTransparentAuthenticationCredentials();
if (!is_null($credentials))
{
$this->container->segment->setFlash('auth_username', $credentials['username']);
$this->container->segment->setFlash('auth_password', $credentials['password']);
$this->container->segment->setFlash('auto_login', 1);
}
$return_url = $this->container->segment->getFlash('return_url');
if (empty($return_url))
{
$return_url = Uri::getInstance()->toString();
}
$this->container->segment->setFlash('return_url', $return_url);
$this->getContainer()->input->setData(array(
'view' => 'login',
));
}
}
/**
* @return void
*/
private function setupMediaVersioning()
{
$this->getContainer()->mediaQueryKey = md5(microtime(false));
$isDebug = !defined('AKEEBADEBUG');
$hasVersion = defined('AKEEBABACKUP_VERSION') && defined('AKEEBABACKUP_DATE');
$isDevelopment = $hasVersion ? ((strpos(AKEEBABACKUP_VERSION, 'svn') !== false) || (strpos(AKEEBABACKUP_VERSION, 'dev') !== false) || (strpos(AKEEBABACKUP_VERSION, 'rev') !== false)) : true;
if (!$isDebug && !$isDevelopment && $hasVersion)
{
$this->getContainer()->mediaQueryKey = md5(AKEEBABACKUP_VERSION . AKEEBABACKUP_DATE);
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Application;
use Awf\Encrypt\Totp;
class UserAuthenticationGoogle extends UserAuthenticationOtep
{
/**
* Is this user authenticated by this object? The $params array contains at least one key, 'password'.
*
* @param array $params The parameters used in the authentication process
*
* @return boolean True if the user is authenticated (or this plugin doesn't apply), false otherwise
*/
public function onAuthentication($params = array())
{
$result = true;
$userParams = $this->user->getParameters();
if ($userParams->get('tfa.method', 'none') == 'google')
{
$result = false;
$secret = isset($params['secret']) ? $params['secret'] : '';
if (!empty($secret))
{
$result = $this->validateGoogleOTP($secret);
if (!$result)
{
$result = $this->validateOtep($secret);
}
}
}
return $result;
}
public function onTfaSave($tfaParams)
{
$tfaMethod = isset($tfaParams['method']) ? $tfaParams['method'] : '';
if ($tfaMethod == 'google')
{
// The Google Authenticator key set by the user in the form
$newKey = isset($tfaParams['google']) ? $tfaParams['google'] : '';
// The Google Authenticator key in the user object
$oldKey = $this->user->getParameters()->get('tfa.google', '');
// The Google Authenticator generated secret code given in the form
$secret = isset($tfaParams['secret']) ? $tfaParams['secret'] : '';
// What was the old TFA method?
$oldTfaMethod = $this->user->getParameters()->get('tfa.method');
if (($oldTfaMethod == 'google') && ($newKey == $oldKey))
{
// We had already set up Google Authenticator and the code is unchanged. No change performed here.
return true;
}
else
{
// Safe fallback until we can verify the new yubikey
$this->user->getParameters()->set('tfa', null);
$this->user->getParameters()->set('tfa.method', 'none');
if (!empty($secret) && $this->validateGoogleOTP($secret, $newKey))
{
$this->user->getParameters()->set('tfa.method', 'google');
$this->user->getParameters()->set('tfa.google', $newKey);
}
}
}
return true;
}
/**
* Validates a Google Authenticator key
*
* @param string $otp The OTP generated by Google Authenticator
* @param string $key The TOTP key (base32 encoded)
*
* @return boolean True if it's a valid OTP
*/
public function validateGoogleOTP($otp, $key = null)
{
// Create a new TOTP class with Google Authenticator compatible settings
$totp = new Totp(30, 6, 10);
// Get the key if none is defined
if (empty($key))
{
$key = $this->user->getParameters()->get('tfa.google', '');
}
// Check the code
$code = $totp->getCode($key);
$check = $code == $otp;
/*
* If the check fails, test the previous 30 second slot. This allow the
* user to enter the security code when it's becoming red in Google
* Authenticator app (reaching the end of its 30 second lifetime)
*/
if (!$check)
{
$time = time() - 30;
$code = $totp->getCode($key, $time);
$check = $code == $otp;
}
/*
* If the check fails, test the next 30 second slot. This allows some
* time drift between the authentication device and the server
*/
if (!$check)
{
$time = time() + 30;
$code = $totp->getCode($key, $time);
$check = $code == $otp;
}
return $check;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Application;
use Awf\User\Authentication;
abstract class UserAuthenticationOtep extends Authentication
{
/**
* Validates an OTEP. If the OTEP is valid it will be removed from the list of OTEPs and the user account will be
* saved with the updated list of OTEPs.
*
* @param string $otp The OTP generated by Google Authenticator
*
* @return boolean True if it's a valid OTEP.
*/
public function validateOtep($otp)
{
// Get the OTEPs
$oteps = $this->user->getParameters()->get('tfa.otep', array());
// If there is no OTEP we can't authenticate
if (empty($oteps))
{
return false;
}
$oteps = (array)$oteps;
// Does this OTEP exist in the list?
$tempOtp = preg_filter('/\D/', '', $otp);
$otp = is_null($tempOtp) ? $otp : $tempOtp;
// No. Can't authenticate.
if (!in_array($otp, $oteps))
{
return false;
}
// Remove the OTEP from the list
$array_pos = array_search($otp, $oteps);
$temp = array();
// Ugly as heck, but PHP freaks out with the number-as-string array indexes it produces.
foreach ($oteps as $foo)
{
if ($foo == $otp)
{
continue;
}
$temp[] = $foo;
}
// Save the modified user
$this->user->getParameters()->set('tfa.otep', $temp);
$userManager = \Awf\Application\Application::getInstance()->getContainer()->userManager;
$userManager->saveUser($this->user);
// OK, we can authenticate
return true;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Application;
use Awf\User\Authentication;
class UserAuthenticationPassword extends Authentication
{
/**
* Is this user authenticated by this object? The $params array contains at least one key, 'password'.
*
* @param array $params The parameters used in the authentication process
*
* @return boolean True if the user is authenticated (or this plugin doesn't apply), false otherwise
*/
public function onAuthentication($params = array())
{
$password = isset($params['password']) ? $params['password'] : '';
$hashedPassword = $this->user->getPassword();
if (substr($hashedPassword, 0, 4) == '$2y$')
{
return password_verify($password, $hashedPassword);
}
else
{
$parts = explode(':', $hashedPassword, 3);
switch ($parts[0])
{
case 'SHA512':
return $this->timingSafeEquals($parts[1], hash('sha512', $password . $parts[2], false));
break;
case 'SHA256':
return $this->timingSafeEquals($parts[1], hash('sha256', $password . $parts[2], false));
break;
case 'SHA1':
return $this->timingSafeEquals($parts[1], sha1($password . $parts[2]));
break;
case 'MD5':
return $this->timingSafeEquals($parts[1], md5($password . $parts[2]));
break;
}
}
// If all else fails, we assume we can't verify this password
return false;
}
public function onTfaSave($tfaParams)
{
$tfaMethod = isset($tfaParams['method']) ? $tfaParams['method'] : '';
if ($tfaMethod == 'none')
{
// Reset other TFA options
$this->user->getParameters()->set('tfa', null);
// Set the TFA method to "none"
$this->user->getParameters()->set('tfa.method', 'none');
}
return true;
}
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @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.
*/
protected function timingSafeEquals($safe, $user)
{
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
$safeLen = strlen($safe);
$userLen = strlen($user);
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Application;
use Akeeba\Engine\Platform;
use Awf\Application\Application;
use Awf\Download\Download;
use Awf\Uri\Uri;
class UserAuthenticationYubikey extends UserAuthenticationOtep
{
/**
* Is this user authenticated by this object? The $params array contains at least one key, 'password'.
*
* @param array $params The parameters used in the authentication process
*
* @return boolean True if the user is authenticated (or this plugin doesn't apply), false otherwise
*/
public function onAuthentication($params = array())
{
$result = true;
$userParams = $this->user->getParameters();
if ($userParams->get('tfa.method', 'none') == 'yubikey')
{
$result = false;
$secret = isset($params['secret']) ? $params['secret'] : '';
if (!empty($secret))
{
$result = $this->validateYubikeyOTP($secret);
if (!$result)
{
$result = $this->validateOtep($secret);
}
}
}
return $result;
}
public function onTfaSave($tfaParams)
{
$tfaMethod = isset($tfaParams['method']) ? $tfaParams['method'] : '';
if ($tfaMethod == 'yubikey')
{
// The YubiKey code set by the user in the form
$newCode = isset($tfaParams['yubikey']) ? $tfaParams['yubikey'] : '';
// The YubiKey code in the user object
$oldCode = $this->user->getParameters()->get('tfa.yubikey', '');
// What was the old TFA method?
$oldTfaMethod = $this->user->getParameters()->get('tfa.method');
if (($oldTfaMethod == 'yubikey') && ($newCode == $oldCode))
{
// We had already set up YubiKey and the code is unchanged. No change performed here.
return true;
}
else
{
// Safe fallback until we can verify the new yubikey
$this->user->getParameters()->set('tfa', null);
$this->user->getParameters()->set('tfa.method', 'none');
if (!empty($newCode) && $this->validateYubikeyOTP($newCode))
{
$this->user->getParameters()->set('tfa.method', 'yubikey');
$this->user->getParameters()->set('tfa.yubikey', $newCode);
}
}
}
return true;
}
/**
* Validates a Yubikey OTP against the Yubikey servers
*
* @param string $otp The OTP generated by your Yubikey
*
* @return boolean True if it's a valid OTP
*/
public function validateYubikeyOTP($otp)
{
$server_queue = array(
'api.yubico.com', 'api2.yubico.com', 'api3.yubico.com',
'api4.yubico.com', 'api5.yubico.com'
);
shuffle($server_queue);
$gotResponse = false;
$check = false;
$options = [];
$proxyParams = Platform::getInstance()->getProxySettings();
if ($proxyParams['enabled'])
{
$options['proxy'] = [
'host' => $proxyParams['host'],
'port' => $proxyParams['port'],
'user' => $proxyParams['user'],
'pass' => $proxyParams['pass'],
];
}
$http = new Download();
$http->setAdapterOptions($options);
$token = Application::getInstance()->getContainer()->session->getCsrfToken()->getValue();
$nonce = md5($token . uniqid(rand()));
$response = '';
while (!$gotResponse && !empty($server_queue))
{
$server = array_shift($server_queue);
$uri = new Uri('https://' . $server . '/wsapi/2.0/verify');
// I don't see where this ID is used?
$uri->setVar('id', 1);
// The OTP we read from the user
$uri->setVar('otp', $otp);
// This prevents a REPLAYED_OTP status of the token doesn't change
// after a user submits an invalid OTP
$uri->setVar('nonce', $nonce);
// Minimum service level required: 50% (at least 50% of the YubiCloud
// servers must reply positively for the OTP to validate)
$uri->setVar('sl', 50);
// Timeou waiting for YubiCloud servers to reply: 5 seconds.
$uri->setVar('timeout', 5);
try
{
$response = $http->getFromURL($uri->toString());
if (!empty($response))
{
$gotResponse = true;
}
else
{
continue;
}
}
catch (\Exception $exc)
{
// No response, continue with the next server
continue;
}
}
// No server replied; we can't validate this OTP
if (!$gotResponse)
{
return false;
}
// Parse response
$lines = explode("\n", $response);
$data = array();
foreach ($lines as $line)
{
$line = trim($line);
$parts = explode('=', $line, 2);
if (count($parts) < 2)
{
continue;
}
$data[$parts[0]] = $parts[1];
}
// Validate the response - We need an OK message reply
if ($data['status'] != 'OK')
{
return false;
}
// Validate the response - We need a confidence level over 50%
if ($data['sl'] < 50)
{
return false;
}
// Validate the response - The OTP must match
if ($data['otp'] != $otp)
{
return false;
}
// Validate the response - The token must match
if ($data['nonce'] != $nonce)
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Application;
use Awf\User\Privilege;
class UserPrivileges extends Privilege
{
public function __construct()
{
$this->name = 'akeeba';
// Set up the privilege names and their default values
$this->privileges = array(
'backup' => false,
'configure' => false,
'download' => false,
);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo;
use Awf\Database\Driver;
/**
* Dependency injection container for Solo
*
* @property-read string $iconBaseName The base name for logo icon files
*/
class Container extends \Awf\Container\Container
{
public function __construct(array $values = array())
{
$this->iconBaseName = 'solo';
if (!isset($values['application_name']))
{
$values['application_name'] = 'Solo';
}
if (!isset($values['session_segment_name']))
{
$installationId = 'default';
if (function_exists('base64_encode'))
{
$installationId = base64_encode(__DIR__);
}
if (function_exists('md5'))
{
$installationId = md5(__DIR__);
}
if (function_exists('sha1'))
{
$installationId = sha1(__DIR__);
}
$values['session_segment_name'] = $values['application_name'] . '_' . $installationId;
}
parent::__construct($values);
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
class Alice extends ControllerDefault
{
public function ajax()
{
/** @var \Solo\Model\Alice $model */
$model = $this->getModel();
$model->setState('ajax', $this->input->get('ajax', '', 'cmd'));
$model->setState('log', $this->input->get('log', '', 'cmd'));
$ret_array = $model->runAnalysis();
@ob_end_clean();
header('Content-type: text/plain');
echo '###' . json_encode($ret_array) . '###';
flush();
$this->container->application->close();
}
public function domains()
{
$return = array();
$domains = \AliceUtilScripting::getDomainChain();
foreach($domains as $domain)
{
$return[] = array($domain['domain'], $domain['name']);
}
@ob_end_clean();
header('Content-type: text/plain');
echo '###'.json_encode($return).'###';
flush();
$this->container->application->close();
}
/**
* Translates language key in English strings
*/
public function translate()
{
$return = array();
$strings = $this->input->getString('keys', '');
$strings = json_decode($strings);
// Text always loads all the languages, so we have to convince him very hard to do what we want
// First of all let's empty the $strings variable
$property = new \ReflectionProperty('\Awf\Text\Text', 'strings');
$property->setAccessible(true);
$property->setValue(array());
// Then load only the English language
Text::loadLanguage('en-GB', 'akeebabackup', '.com_akeebabackup.ini', false, $this->container->languagePath);
Text::loadLanguage('en-GB', 'akeeba', '.com_akeeba.ini', false, $this->container->languagePath);
foreach ($strings as $string)
{
$temp['check'] = Text::_($string->check);
// If I have an array, it means that I have to use sprintf to translate the error
if (is_array($string->error))
{
$trans[] = Text::_($string->error[0]);
$args = array_merge($trans, array_slice($string->error, 1));
$error = call_user_func_array('sprintf', $args);
}
else
{
$error = Text::_($string->error);
}
$temp['error'] = $error;
$return[] = $temp;
}
@ob_end_clean();
header('Content-type: text/plain');
echo '###' . json_encode($return) . '###';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Akeeba\Engine\Platform;
use Solo\Helper\Utils;
/**
* The controller for the Backup view
*/
class Backup extends ControllerDefault
{
/**
* Default task; shows the initial page where the user selects a profile
* and enters description and comment
*
* @return void
*/
public function main()
{
// Push models to view
$model = $this->getModel();
$model->setState('returnform', $this->input->get('returnform', '', 'raw'));
$newProfile = (int)$this->input->get('profile', -10, 'int');
// Apply the CSRF protection if we're switching profile or passing variables for POST redirection
if ($newProfile > 0 || $model->getState('returnform', ''))
{
$this->csrfProtection(true);
$this->applyProfile();
}
$srpinfo = array(
'tag' => $this->input->get('tag', 'backend', 'cmd'),
'type' => $this->input->get('type', '', 'cmd'),
'name' => $this->input->get('name', '', 'cmd'),
'group' => $this->input->get('group', '', 'cmd'),
'customdirs' => $this->input->get('customdirs', array(), 'array'),
'extraprefixes' => $this->input->get('extraprefixes', array(), 'array'),
'customtables' => $this->input->get('customtables', array(), 'array'),
'skiptables' => $this->input->get('skiptables', array(), 'array'),
'xmlname' => $this->input->get('xmlname', '', 'string')
);
// Sanitize the return URL
$returnUrl = $this->input->get('returnurl', '', 'raw');
$returnUrl = Utils::safeDecodeReturnUrl($returnUrl);
$model->setState('srpinfo', $srpinfo);
$model->setState('description', $this->input->get('description', null, 'raw'));
$model->setState('comment', $this->input->get('comment', null, 'raw'));
$model->setState('jpskey', $this->input->get('jpskey', '', 'raw'));
$model->setState('angiekey', $this->input->get('angiekey', '', 'raw'));
$model->setState('returnurl', $returnUrl);
$model->setState('backupid', $this->input->get('backupid', null, 'cmd'));
$this->display();
}
/**
* Handle an AJAX request
*
* @return void
*/
public function ajax()
{
$model = $this->getModel();
$model->setState('profile', $this->input->get('profile', Platform::getInstance()->get_active_profile(), 'int'));
$model->setState('ajax', $this->input->get('ajax', '', 'cmd'));
$model->setState('description', $this->input->get('description', '', 'raw'));
$model->setState('comment', $this->input->get('comment', '','raw'));
$model->setState('jpskey', $this->input->get('jpskey', '', 'raw'));
$model->setState('angiekey', $this->input->get('angiekey', '', 'raw'));
$model->setState('backupid', $this->input->get('backupid', null, 'cmd'));
$model->setState('errorMessage', $this->input->get('errorMessage', '', 'string'));
$model->setState('tag', $this->input->get('tag', 'backend', 'cmd'));
$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'));
$model->setState('customfiles', $this->input->get('customfiles', array(),'array'));
$model->setState('extraprefixes', $this->input->get('extraprefixes', array(),'array'));
$model->setState('customtables', $this->input->get('customtables', array(),'array'));
$model->setState('skiptables', $this->input->get('skiptables', array(),'array'));
$model->setState('langfiles', $this->input->get('langfiles', array(),'array'));
$model->setState('xmlname', $this->input->getString('xmlname', ''));
define('AKEEBA_BACKUP_ORIGIN', $this->input->get('tag', 'backend', 'cmd'));
$ret_array = $model->runBackup();
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo '#"\#\"#' . json_encode($ret_array) . '#"\#\"#';
flush();
$this->container->application->close();
}
/**
* Applies a profile change based on the request's "profile" parameter
*/
private function applyProfile()
{
// Get the currently active profile
$current_profile = Platform::getInstance()->get_active_profile();
// Get the profile from the request
$profile = (int)$this->input->get('profile', $current_profile, 'int');
// Sanity check
if (!is_numeric($profile) || ($profile <= 0))
{
$profile = $current_profile;
}
// Change and reload the profile if necessary
if ($profile != $current_profile)
{
$session = \Awf\Application\Application::getInstance()->getContainer()->segment;
$session->profile = $profile;
/**
* 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($profile);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
class Browser extends ControllerDefault
{
/**
* Handle the directory listing display
*/
public function main()
{
$folder = $this->input->getString('folder', '');
$processfolder = $this->input->getInt('processfolder', 0);
/** @var \Solo\Model\Browser $model */
$model = $this->getModel();
$model->setState('folder', $folder);
$model->setState('processfolder', $processfolder);
$model->makeListing();
parent::display();
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
use Akeeba\Engine\Platform;
use Awf\Application\Application;
use Awf\Mvc\Model;
use Awf\Text\Text;
class Check extends ControllerDefault
{
public function execute($task)
{
$this->checkPermissions();
define('AKEEBA_BACKUP_ORIGIN', 'frontend');
return parent::execute('main');
}
public function main()
{
$cpanelModel = Model::getInstance('Solo', 'Main', $this->container);
$result = $cpanelModel->notifyFailed();
$message = $result['result'] ? '200 ' : '500 ';
$message .= implode(', ', $result['message']);
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo $message;
flush();
$this->container->application->close();
}
/**
* Check that the user has sufficient permissions, or die in error
*
*/
private function checkPermissions()
{
// Is frontend backup enabled?
$febEnabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', 0);
$febEnabled = in_array($febEnabled, array('on', 'checked', 'true', 1, 'yes'));
$validKey = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', '');
if (!\Akeeba\Engine\Util\Complexify::isStrongEnough($validKey, false))
{
$febEnabled = false;
}
$validKeyTrim = trim($validKey);
if (!$febEnabled || empty($validKey))
{
throw new \RuntimeException(Text::_('SOLO_REMOTE_ERROR_NOT_ENABLED'), 403);
}
// Is the key good?
$key = $this->input->get('key', '', 'none', 2);
if (($key != $validKey) || (empty($validKeyTrim)))
{
throw new \RuntimeException(Text::_('SOLO_REMOTE_ERROR_INVALID_KEY'), 403);
}
}
private function setProfile()
{
// Set profile
$profile = $this->input->get('profile', 1, 'int');
if (empty($profile))
{
$profile = 1;
}
$session = Application::getInstance()->getContainer()->segment;
$session->profile = $profile;
Platform::getInstance()->load_configuration($profile);
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
use Solo\Model\Profiles;
/**
* The Controller for the Configuration view
*/
class Configuration extends ControllerDefault
{
/**
* Handle the apply task which saves settings and shows the editor again
*
* @return void
*/
public function apply()
{
// CSRF prevention
$this->csrfProtection();
// Get the var array from the request
$data = $this->input->get('var', array(), 'array');
// Mark this profile as configured
$data['akeeba.flag.confwiz'] = 1;
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('engineconfig', $data);
$model->saveEngineConfig();
// Finally, save the profile description if it has changed
$profileID = \Akeeba\Engine\Platform::getInstance()->get_active_profile();
// Get profile name
/** @var Profiles $profileRecord */
$profileRecord = $this->getModel('Profiles')->getClone()->setIgnoreRequest(1);
$profileRecord->reset(true, true)->find($profileID);
$oldProfileName = $profileRecord->description;
$oldQuickIcon = $profileRecord->quickicon;
$newProfileName = $this->input->getString('profilename', null);
$newProfileName = trim($newProfileName);
$newQuickIcon = $this->input->getCmd('quickicon', '');
$newQuickIcon = !empty($newQuickIcon);
$mustSaveProvile = !empty($newProfileName) && ($newProfileName != $oldProfileName);
$mustSaveProvile = $mustSaveProvile || ($newQuickIcon != $oldQuickIcon);
if ($mustSaveProvile)
{
$profileRecord->save(array(
'description' => $newProfileName,
'quickicon' => $newQuickIcon,
));
}
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=configuration'), Text::_('COM_AKEEBA_CONFIG_SAVE_OK'));
}
/**
* Handle the save task which saves settings and returns to the main page
*
* @return void
*/
public function save()
{
$this->apply();
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=main'), Text::_('COM_AKEEBA_CONFIG_SAVE_OK'));
}
/**
* Handle the save task which saves settings, creates a new backup profile, activates it and proceed to the
* configuration page once more.
*
* @return void
*/
public function savenew()
{
// Save the current profile
$this->apply();
// Create a new profile
/** @var Profiles $profileModel */
$profileModel = $this->getModel('Profiles')->getClone();
$profileID = \Akeeba\Engine\Platform::getInstance()->get_active_profile();
$profileModel->find($profileID);
$profileModel->id = null;
$profileModel->save(array(
'id' => 0,
'description' => Text::_('COM_AKEEBA_CONFIG_SAVENEW_DEFAULT_PROFILE_NAME')
));
$newProfileId = (int)($profileModel->getId());
// Activate and edit the new profile
$returnUrl = base64_encode($this->redirect);
$router = $this->container->router;
$token = $this->container->session->getCsrfToken()->getValue();
$url = $router->route('index.php?view=main&task=switchProfile&profile=' . $newProfileId .
'&returnurl=' . $returnUrl . '&' . $token . '=1');
$this->setRedirect($url);
}
/**
* Handle the cancel task which doesn't save anything and returns to the cpanel
*
* @return void
*/
public function cancel()
{
$this->csrfProtection();
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=main'));
}
/**
* Tests the validity of the FTP connection details
*
* @return void
*/
public function testftp()
{
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('isCurl', $this->input->get('isCurl', 0, 'int'));
$model->setState('host', $this->input->get('host', '', 'raw'));
$model->setState('port', $this->input->get('port', 21, 'int'));
$model->setState('user', $this->input->get('user', '', 'raw'));
$model->setState('pass', $this->input->get('pass', '', 'raw'));
$model->setState('initdir', $this->input->get('initdir', '', 'raw'));
$model->setState('usessl', $this->input->get('usessl', '', 'raw') == 'true');
$model->setState('passive', $this->input->get('passive', '', 'raw') == 'true');
$model->setState('passive_mode_workaround', $this->input->get('passive_mode_workaround', '', 'raw') == 'true');
$result = true;
try
{
$model->testFTP();
}
catch (\Exception $e)
{
$result = $e->getMessage();
}
@ob_end_clean();
echo '#"\#\"#' . json_encode($result) . '#"\#\"#';
flush();
$this->container->application->close();
}
/**
* Tests the validity of the SFTP connection details
*
* @return void
*/
public function testsftp()
{
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('isCurl', $this->input->get('isCurl', 0, 'int'));
$model->setState('host', $this->input->get('host', '', 'raw'));
$model->setState('port', $this->input->get('port', 21, 'int'));
$model->setState('user', $this->input->get('user', '', 'raw'));
$model->setState('pass', $this->input->get('pass', '', 'raw'));
$model->setState('privkey', $this->input->get('privkey', '', 'raw'));
$model->setState('pubkey', $this->input->get('pubkey', '', 'raw'));
$model->setState('initdir', $this->input->get('initdir', '', 'raw'));
$result = true;
try
{
$model->testSFTP();
}
catch (\Exception $e)
{
$result = $e->getMessage();
}
@ob_end_clean();
echo '#"\#\"#' . json_encode($result) . '#"\#\"#';
flush();
$this->container->application->close();
}
/**
* Opens an OAuth window for the selected data processing engine
*
* @return void
*/
public function dpeoauthopen()
{
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('engine', $this->input->get('engine', '', 'raw'));
$model->setState('params', $this->input->get('params', array(), 'array'));
@ob_end_clean();
$model->dpeOAuthOpen();
flush();
$this->container->application->close();
}
/**
* Runs a custom API call against the selected data processing engine
*
* @return void
*/
public function dpecustomapi()
{
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('engine', $this->input->get('engine', '', 'raw'));
$model->setState('method', $this->input->getVar('method', '', 'raw'));
$model->setState('params', $this->input->get('params', array(), 'array'));
@ob_end_clean();
echo '#"\#\"#' . json_encode($model->dpeCustomAPICall()) . '#"\#\"#';
flush();
$this->container->application->close();
}
/**
* Runs a custom API call against the selected data processing engine
*
* @return void
*/
public function dpecustomapiraw()
{
/** @var \Solo\Model\Configuration $model */
$model = $this->getModel();
$model->setState('engine', $this->input->get('engine', '', 'raw'));
$model->setState('method', $this->input->getVar('method', '', 'raw'));
$model->setState('params', $this->input->get('params', array(), 'array'));
@ob_end_clean();
echo $model->dpeCustomAPICall();
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
/**
* Common controller superclass. Reserved for future use.
*/
abstract class ControllerDefault extends \Awf\Mvc\Controller
{
protected $aclChecks = array(
'alice' => array('*' => array('configure')),
'backup' => array('*' => array('backup')),
'browser' => array('*' => array('configure')),
'configuration' => array('*' => array('configure')),
'dbfilters' => array('*' => array('configure')),
'discover' => array('*' => array('configure')),
'errortest' => array('*' => array('configure')),
'extradirs' => array('*' => array('configure')),
'fsfilters' => array('*' => array('configure')),
'log' => array('*' => array('configure')),
'manage' => array(
'manage' => array(),
'showComment' => array('backup'),
'cancel' => array('backup'),
'download' => array('download'),
'restore' => array('configure'),
'*' => array('download'),
),
'multidb' => array('*' => array('configure')),
'phpinfo' => array('*' => array('configure', 'backup', 'download')),
'profiles' => array('*' => array('configure')),
'profile' => array('*' => array('configure')),
'regexdbfilters' => array('*' => array('configure')),
'regexfsfilters' => array('*' => array('configure')),
'remotefiles' => array('*' => array('download')),
'restore' => array('*' => array('configure')),
's3import' => array('*' => array('configure')),
'schedule' => array('*' => array('configure')),
'sysconfig' => array('*' => array('configure', 'backup', 'download')),
'transfer' => array('*' => array('download')),
'update' => array('*' => array('configure', 'backup', 'download')),
'upload' => array('*' => array('backup')),
'users' => array('*' => array('configure', 'backup', 'download')),
'wizard' => array('*' => array('configure')),
);
/**
* Executes a given controller task. The onBefore<task> and onAfter<task>
* methods are called automatically if they exist.
*
* @param string $task The task to execute, e.g. "browse"
*
* @return null|bool False on execution failure
*
* @throws \Exception When the task is not found
*/
public function execute($task)
{
$view = $this->input->getCmd('view', 'main');
$this->aclCheck($view, $task);
return parent::execute($task);
}
/**
* Performs automatic access control checks
*
* @param string $view The view being accessed
* @param string $task The task being accessed
*
* @throws \RuntimeException
*/
protected function aclCheck($view, $task)
{
$view = strtolower($view);
$task = strtolower($task);
if (!isset($this->aclChecks[$view]))
{
return;
}
if (!isset($this->aclChecks[$view][$task]))
{
if (!isset($this->aclChecks[$view]['*']))
{
return;
}
$requiredPrivileges = $this->aclChecks[$view]['*'];
}
else
{
$requiredPrivileges = $this->aclChecks[$view][$task];
}
$user = $this->container->userManager->getUser();
foreach ($requiredPrivileges as $privilege)
{
if (!$user->getPrivilege('akeeba.' . $privilege))
{
throw new \RuntimeException(Text::_('SOLO_ERR_ACLDENIED'), 403);
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Mvc\DataController;
use Awf\Text\Text;
/**
* Common controller superclass. Reserved for future use.
*/
abstract class DataControllerDefault extends DataController
{
protected $aclChecks = array(
'alice' => array('*' => array('configure')),
'backup' => array('*' => array('backup')),
'browser' => array('*' => array('configure')),
'configuration' => array('*' => array('configure')),
'dbfilters' => array('*' => array('configure')),
'discover' => array('*' => array('configure')),
'extradirs' => array('*' => array('configure')),
'fsfilters' => array('*' => array('configure')),
'log' => array('*' => array('configure')),
'manage' => array(
'manage' => array(),
'showComment' => array('backup'),
'cancel' => array('backup'),
'download' => array('download'),
'restore' => array('configure'),
'*' => array('download'),
),
'multidb' => array('*' => array('configure')),
'profiles' => array('*' => array('configure')),
'profile' => array('*' => array('configure')),
'regexdbfilters' => array('*' => array('configure')),
'regexfsfilters' => array('*' => array('configure')),
'remotefiles' => array('*' => array('download')),
'restore' => array('*' => array('configure')),
's3import' => array('*' => array('configure')),
'schedule' => array('*' => array('configure')),
'sysconfig' => array('*' => array('configure', 'backup', 'download')),
'transfer' => array('*' => array('download')),
'update' => array('*' => array('configure', 'backup', 'download')),
'upload' => array('*' => array('backup')),
'users' => array('*' => array('configure', 'backup', 'download')),
'wizard' => array('*' => array('configure')),
);
public function execute($task)
{
$view = $this->input->getCmd('view', 'main');
$this->aclCheck($view, $task);
return parent::execute($task);
}
protected function aclCheck($view, $task)
{
$view = strtolower($view);
$task = strtolower($task);
if (!isset($this->aclChecks[$view]))
{
return;
}
if (!isset($this->aclChecks[$view][$task]))
{
if (!isset($this->aclChecks[$view]['*']))
{
return;
}
$requiredPrivileges = $this->aclChecks[$view]['*'];
}
else
{
$requiredPrivileges = $this->aclChecks[$view][$task];
}
$user = $this->container->userManager->getUser();
foreach ($requiredPrivileges as $privilege)
{
if (!$user->getPrivilege('akeeba.' . $privilege))
{
throw new \RuntimeException(Text::_('SOLO_ERR_ACLDENIED'), 403);
}
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
class Dbfilters extends ControllerDefault
{
public function __construct($config = array())
{
parent::__construct($config);
// Register the two additional tasks
$this->registerTask('normal', 'main');
$this->registerTask('tabular', 'main');
}
/**
* Default task
*
* @return void
*/
public function main()
{
$task = $this->input->getCmd('task', 'normal');
if ($task == 'main')
{
$task = 'normal';
}
$this->getModel()->setState('browse_task', $task);
$this->display();
}
/**
* AJAX proxy method
*
* @return void
*/
public function ajax()
{
// Parse the JSON data and reset the action query param to the resulting array
$action_json = $this->input->get('akaction', '', 'raw');
$action = json_decode($action_json);
/** @var \Solo\Model\Dbfilters $model */
$model = $this->getModel();
$model->setState('action', $action);
$ret = $model->doAjax();
@ob_end_clean();
echo '#"\#\"#' . json_encode($ret) . '#"\#\"#';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
class Errortest extends ControllerDefault
{
public function main()
{
throw new \RuntimeException('I am a runtime exception with error code 500', 500);
}
public function notfound()
{
throw new \RuntimeException('I am a runtime exception with error code 404 Not Found', 404);
}
public function fatal()
{
kalimera();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
class Fsfilters extends ControllerDefault
{
public function __construct($config = array())
{
parent::__construct($config);
// Register the two additional tasks
$this->registerTask('normal', 'main');
$this->registerTask('tabular', 'main');
}
/**
* Default task
*
* @return void
*/
public function main()
{
$task = $this->input->getCmd('task', 'normal');
if ($task == 'main')
{
$task = 'normal';
}
$this->getModel()->setState('browse_task', $task);
$this->display();
}
/**
* AJAX proxy method
*
* @return void
*/
public function ajax()
{
// Parse the JSON data and reset the action query param to the resulting array
$action_json = $this->input->get('akaction', '', 'raw');
$action = json_decode($action_json);
/** @var \Solo\Model\Fsfilters $model */
$model = $this->getModel();
$model->setState('action', $action);
$ret = $model->doAjax();
@ob_end_clean();
echo '#"\#\"#' . json_encode($ret) . '#"\#\"#';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
/**
* The controller for FTP browser
*/
class Ftpbrowser extends ControllerDefault
{
public function execute($task)
{
// If we are running inside a CMS but there is no active user we have to throw a 403
$inCMS = $this->container->segment->get('insideCMS', false);
if ($inCMS && !$this->container->userManager->getUser()->getId())
{
return false;
}
return parent::execute($task);
}
public function main()
{
/** @var \Solo\Model\Ftpbrowser $model */
$model = $this->getModel();
// Grab the data and push them to the model
$model->setState('host', $this->input->getString('host', ''));
$model->setState('port', $this->input->getInt('port', 21));
$model->setState('passive', $this->input->getInt('passive', 1));
$model->setState('ssl', $this->input->getInt('ssl', 0));
$model->setState('username', $this->input->getRaw('username', ''));
$model->setState('password', $this->input->getRaw('password', ''));
$model->setState('directory', $this->input->getRaw('directory', ''));
$ret = $model->doBrowse();
@ob_end_clean();
echo '#"\#\"#'.json_encode($ret).'#"\#\"#';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
class Json extends ControllerDefault
{
/**
* Always execute the 'json' task
*
* @param string $task
*
* @return boolean|null
*/
public function execute($task)
{
$this->input->set('task', 'json');
$task = 'json';
return parent::execute($task);
}
/**
* Handles API calls
*/
public function json()
{
// Use the model to parse the JSON message
if (function_exists('ob_start'))
{
@ob_start();
}
$sourceJSON = $this->input->get('json', null, 'raw');
// On some !@#$%^& servers where magic_quotes_gpc is On we might get extra slashes added
if (function_exists('get_magic_quotes_gpc'))
{
if (get_magic_quotes_gpc())
{
$sourceJSON = stripslashes($sourceJSON);
}
}
/** @var \Solo\Model\Json $model */
$model = $this->getModel();
$json = $model->execute($sourceJSON);
if (function_exists('ob_end_clean'))
{
@ob_end_clean();
}
// Just dump the JSON and tear down the application, without plugins executing
header('Content-type: text/plain');
header('Connection: close');
echo $json;
$this->container->application->close();
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
class Log extends ControllerDefault
{
/**
* Executes a given controller task. The onBefore<task> and onAfter<task>
* methods are called automatically if they exist.
*
* This method is overridden to add support for the profileid query parameter which switches the active
* backup profile.
*
* @param string $task The task to execute, e.g. "browse"
*
* @return null|bool False on execution failure
*
* @throws \Exception When the task is not found
*/
public function execute($task)
{
// If the profile_id parameter is defined and it's a positive integer change the active profile
$profile_id = $this->input->getInt('profileid', null);
if (!empty($profile_id) && is_numeric($profile_id) && ($profile_id > 0))
{
\Awf\Application\Application::getInstance()->getContainer()->segment->profile = $profile_id;
}
// Execute the controller
return parent::execute($task);
}
/**
* Allows the user to select the log origin to display or display the log file itself
*
* @return void
*/
public function main()
{
$tag = $this->input->get('tag', null, 'cmd');
$latest = $this->input->get('latest', false, 'int');
if (empty($tag))
{
$tag = null;
}
/** @var \Solo\Model\Log $model */
$model = $this->getModel();
if ($latest)
{
$logFiles = $model->getLogFiles();
$tag = array_shift($logFiles);
}
$model->setState('tag', $tag);
$this->display();
}
/**
* Renders the log contents for use in an iFrame
*
* @return void
*/
public function iframe()
{
$tag = $this->input->get('tag', null, 'cmd');
if (empty($tag))
{
$tag = null;
}
$model = $this->getModel();
$model->setState('tag', $tag);
$this->display();
}
/**
* Downloads the log file as a plain text file
*
* @return void
*/
public function download()
{
$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');
$inCMS = $this->container->segment->get('insideCMS', false);
$filename = 'Akeeba ' . ($inCMS ? 'Backup' : 'Solo') . ' Debug Log.txt';
if ($asAttachment)
{
header('Content-Disposition: attachment; filename="' . $filename . '"');
}
/** @var \Solo\Model\Log $model */
$model = $this->getModel();
$model->setState('tag', $tag);
$model->echoRawLog();
@flush();
$this->container->application->close();
}
public function inlineRaw()
{
$tag = $this->input->get('tag', null, 'cmd');
if (empty($tag))
{
$tag = null;
}
/** @var \Solo\Model\Log $model */
$model = $this->getModel();
$model->setState('tag', $tag);
$model->echoRawLog();
}
}

View File

@@ -0,0 +1,378 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\RandomValue;
use AkeebaBackupWPUpdater;
use Awf\Application\Application;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Exception;
use RuntimeException;
use Solo\Model\Update;
use Solo\View\Main\Html;
class Main extends ControllerDefault
{
public function switchProfile()
{
$this->csrfProtection();
// Switch the active profile
$session = Application::getInstance()->getContainer()->segment;
$session->profile = $this->input->getInt('profile', 1);
// Redirect
$url = $this->container->router->route('index.php?view=main');
$returnURL = $this->input->get('returnurl', '', 'raw');
if (!empty($returnURL))
{
$url = base64_decode($returnURL);
}
$this->setRedirect($url);
return true;
}
public function getUpdateInformation()
{
// Protect against direct access
$this->csrfProtection();
// Initialise
$ret = [
'hasUpdate' => false,
'version' => '',
'noticeHTML' => '',
];
// Am I running inside a CMS?
$inCMS = $this->container->segment->get('insideCMS', false);
/** @var Update $updateModel */
$updateModel = Model::getTmpInstance($this->container->application_name, 'Update', $this->container);
$ret['hasUpdate'] = $updateModel->getUpdateInformation()->get('hasUpdate', false);
$ret['version'] = $updateModel->getUpdateInformation()->get('version', 'dev');
if ($ret['hasUpdate'])
{
$router = $this->container->router;
$updateHeader = Text::sprintf('SOLO_UPDATE_LBL_MAINNOTICE_TEXT', '<span class="label label-success">' . $ret['version'] . '</span>');
$updateButton = Text::_('SOLO_UPDATE_BTN_UPDATE_NOW');
$updateLink = $router->route('index.php?view=update');
$ret['noticeHTML'] = <<< HTML
<div class="akeeba-block--warning">
<h3>
$updateHeader
</h3>
<p style="text-align: center">
<a href="$updateLink" class="akeeba-btn--large--teal">
<span class="akion-refresh"></span>
$updateButton
</a>
</p>
</div>
HTML;
}
echo '#"\#\"#' . json_encode($ret) . '#"\#\"#';
$this->container->application->close();
}
public function applyDownloadId()
{
// Protect against direct access
$this->csrfProtection();
$msg = Text::_('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;
$config = $this->container->appConfig;
$config->set('options.update_dlid', $dlid);
$config->saveConfiguration();
}
// Akeeba Backup for WordPress: reset update information
if (defined('WPINC'))
{
$transient = (object) [
'response' => [],
];
AkeebaBackupWPUpdater::getupdates($transient);
}
// Redirect
$url = $this->container->router->route('index.php?view=main');
$returnURL = $this->input->get('returnurl', '', 'raw');
if (!empty($returnURL))
{
$url = base64_decode($returnURL);
}
$this->setRedirect($url, $msg, $msgType);
return true;
}
/**
* Reset the Secret Word for front-end and remote backup
*
* @return bool
*/
public function resetSecretWord()
{
// CSRF prevention
$this->csrfProtection();
$session = $this->container->segment;
$newSecret = $session->get('newSecretWord', null);
if (empty($newSecret))
{
$random = new RandomValue();
$newSecret = $random->generateString(32);
$session->set('newSecretWord', $newSecret);
}
$config = $this->container->appConfig;
$config->set('options.frontend_secret_word', $newSecret);
$config->saveConfiguration();
$msg = Text::sprintf('COM_AKEEBA_CPANEL_MSG_FESECRETWORD_RESET', $newSecret);
$url = $this->container->router->route('index.php?view=Main');
$this->setRedirect($url, $msg);
return true;
}
/**
* Resets the "updatedb" flag and forces the database updates
*/
public function forceUpdateDb()
{
// Reset the flag so the updates could take place
$this->container->appConfig->set('updatedb', null);
$this->container->appConfig->saveConfiguration();
/** @var \Solo\Model\Main $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
}
$url = $this->container->router->route('index.php?view=Main');
$this->setRedirect($url);
}
/**
* Dismisses the Core to Pro upsell for 15 days
*
* @return void
*/
public function dismissUpsell()
{
// Reset the flag so the updates could take place
$this->container->appConfig->set('lastUpsellDismiss', time());
$this->container->appConfig->saveConfiguration();
$url = $this->container->router->route('index.php?view=Main');
$this->setRedirect($url);
}
/**
* Check the security of the backup output directory and return the results for consumption through AJAX
*
* @return void
*
* @throws Exception
*
* @since 7.0.3
*/
public function checkOutputDirectory()
{
/** @var \Solo\Model\Main $model */
$model = $this->getModel();
$outDir = $model->getOutputDirectory();
$inCMS = $this->container->segment->get('insideCMS', false);
try
{
$result = $model->getOutputDirectoryWebAccessibleState($outDir);
if (!$inCMS)
{
$altResult = $model->getOutputDirectoryWebAccessibleState($outDir, true);
foreach ($altResult as $k => $v)
{
$result[$k] = $result[$k] || $altResult[$k];
}
}
}
catch (RuntimeException $e)
{
$result = [
'readFile' => false,
'listFolder' => false,
'isSystem' => $model->isOutputDirectoryInSystemFolder(),
'hasRandom' => $model->backupFilenameHasRandom(),
];
}
@ob_end_clean();
echo '#"\#\"#' . json_encode($result) . '#"\#\"#';
$this->container->application->close();
}
/**
* Add security files to the output directory of the currently configured backup profile
*
* @return void
*
* @throws Exception
*
* @since 7.0.3
*/
public function fixOutputDirectory()
{
// CSRF prevention
$this->csrfProtection();
/** @var \Solo\Model\Main $model */
$model = $this->getModel();
$outDir = $model->getOutputDirectory();
$fsUtils = Factory::getFilesystemTools();
$fsUtils->ensureNoAccess($outDir, true);
$this->setRedirect($this->container->router->route('index.php'));
}
/**
* Adds the [RANDOM] variable to the backup output filename, save the configuration and reload the Control Panel.
*
* @return void
*
* @throws Exception
*
* @since 7.0.3
*/
public function addRandomToFilename()
{
// CSRF prevention
$this->csrfProtection();
$registry = Factory::getConfiguration();
$templateName = $registry->get('akeeba.basic.archive_name');
if (strpos($templateName, '[RANDOM]') === false)
{
$templateName .= '-[RANDOM]';
$registry->set('akeeba.basic.archive_name', $templateName);
Platform::getInstance()->save_configuration();
}
$this->setRedirect($this->container->router->route('index.php'));
}
protected function onBeforeDefault()
{
// If we are running inside a CMS but there is no active user we have to throw a 403
$inCMS = $this->container->segment->get('insideCMS', false);
if ($inCMS && !$this->container->userManager->getUser()->getId())
{
return false;
}
/** @var \Solo\Model\Main $model */
$model = $this->getModel();
try
{
$model->checkAndFixDatabase();
}
catch (RuntimeException $e)
{
// The update is stuck. We will display a warning in the Control Panel
}
try
{
if ($inCMS)
{
$model->updateAutomationConfiguration();
}
}
catch (RuntimeException $e)
{
// Oh, well.
}
// Run the update scripts, if necessary
if ($model->postUpgradeActions())
{
$url = $this->container->router->route('index.php?view=main');
$this->container->application->redirect($url);
}
// Let's make sure the temporary and output directories are set correctly and writable...
$wizmodel = new \Solo\Model\Wizard($this->container);
$wizmodel->autofixDirectories();
// Rebase Off-site Folder Inclusion filters to use site path variables
if (class_exists('\Solo\Model\Extradirs'))
{
$incFoldersModel = new \Solo\Model\Extradirs($this->container);
$incFoldersModel->rebaseFiltersToSiteDirs();
}
// Apply settings encryption preferences
$model->checkEngineSettingsEncryption();
// Convert existing log files to the new .log.php format
$model->convertLogFiles();
// Update magic configuration parameters
$model->updateMagicParameters();
// Flag stuck backups
$model->flagStuckBackups();
// Reload the quirks definitions, since flagging stuck backups will reset the factory state,
// deleting temp objects and their settings
Platform::getInstance()->apply_quirk_definitions();
// Copy the ACL checks to the view. We'll use that information to show or hide icons
/** @var Html $view */
$view = $this->getView();
$view->aclChecks = $this->aclChecks;
return true;
}
}

View File

@@ -0,0 +1,575 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Text\Text;
use Exception;
class Manage extends ControllerDefault
{
public function main()
{
$this->container->segment->set('solo_manage_task', 'main');
$this->display();
}
/**
* Allows the editing of the backup comment
*/
public function showComment()
{
$this->csrfProtection();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
$model = $this->getModel();
// Get the ID
$id = $model->getState('id', 0);
$part = $this->input->get('part', -1, 'int');
$cid = $this->input->get('cid', array(), 'array');
if (empty($id))
{
if (is_array($cid) && !empty($cid))
{
$id = $cid[0];
}
else
{
$id = -1;
}
}
if ($id <= 0)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
}
else
{
$this->getModel()->setState('id', $id);
}
$this->getView()->setLayout('comment');
$this->display();
}
/**
* Downloads the backup file of a specific backup attempt, if it's available on the server
*
* @return void
*/
public function download()
{
$router = $this->container->router;
$model = $this->getModel();
$id = $model->getState('id', 0);
$part = $this->input->get('part', -1, 'int');
$cid = $this->input->get('cid', array(), 'array');
if (empty($id))
{
if (is_array($cid) && !empty($cid))
{
$id = $cid[0];
}
else
{
$id = -1;
}
}
if ($id <= 0)
{
$url = $router->route('index.php?view=manage');
$this->setRedirect($url, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return;
}
$stat = Platform::getInstance()->get_statistics($id);
$allFileNames = Factory::getStatistics()->get_all_filenames($stat);
// 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];
}
else
{
$fileName = null;
}
if (is_null($fileName) || empty($fileName) || !@file_exists($fileName))
{
$url = $router->route('index.php?view=manage');
$this->setRedirect($url, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDDOWNLOAD'), 'error');
return;
}
else
{
// Remove PHP's time limit (is this actually still applicable in 2014?)
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 the file size, 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 > 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);
}
}
else
{
// If the file size is not reported, hope that readfile works
@readfile($fileName);
}
exit(0);
}
}
/**
* Deletes one or several backup statistics records and their associated backup files
*
* @return void
*/
public function remove()
{
// CSRF prevention
$this->csrfProtection();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
// Get the ID
$cid = $this->input->get('cid', array(), 'array');
$id = $this->input->get('id', 0, 'int');
if (empty($id))
{
if (!empty($cid) && is_array($cid))
{
foreach ($cid as $id)
{
$result = $this->_remove($id);
if (!$result)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return;
}
}
}
else
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return;
}
}
else
{
$result = $this->_remove($id);
if (!$result)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
}
}
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_MSG_DELETED'));
}
/**
* Deletes backup files associated to one or several backup statistics records
*
* @return void
*/
public function deleteFiles()
{
// CSRF prevention
$this->csrfProtection();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
// Get the ID
$cid = $this->input->get('cid', array(), 'array');
$id = $this->input->get('id', 0, 'int');
if (empty($id))
{
if (!empty($cid) && is_array($cid))
{
foreach ($cid as $id)
{
$result = $this->_removeFiles($id);
if (!$result)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return;
}
}
}
else
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return;
}
}
else
{
$result = $this->_remove($id);
if (!$result)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
}
}
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_MSG_DELETEDFILE'));
}
/**
* Freeze select records
*
* @throws Exception
*/
public function freeze()
{
$this->csrfProtection();
$id = $this->input->get('id', 0, 'int');
$ids = (array) $id;
/** @var \Solo\Model\Manage $model */
$model = $this->getModel();
$message = Text::_('COM_AKEEBA_BUADMIN_FREEZE_OK');
$type = 'message';
try
{
$model->freezeUnfreezeRecords($ids, 1);
}
catch (Exception $e)
{
$message = Text::sprintf('COM_AKEEBA_BUADMIN_FREEZE_ERROR', $e->getMessage());
$type = 'error';
}
$this->setRedirect($this->container->router->route('index.php?view=Manage'), $message, $type);
}
/**
* Unfreeze select records
*
* @throws Exception
*/
public function unfreeze()
{
$this->csrfProtection();
$id = $this->input->get('id', 0, 'int');
$ids = (array) $id;
/** @var \Solo\Model\Manage $model */
$model = $this->getModel();
$message = Text::_('COM_AKEEBA_BUADMIN_UNFREEZE_OK');
$type = 'message';
try
{
$model->freezeUnfreezeRecords($ids, 0);
}
catch (Exception $e)
{
$message = Text::sprintf('COM_AKEEBA_BUADMIN_UNFREEZE_ERROR', $e->getMessage());
$type = 'error';
}
$this->setRedirect($this->container->router->route('index.php?view=Manage'), $message, $type);
}
/**
* Removes the backup file linked to a statistics entry and the entry itself
*
* @param integer $id The ID of the backup record
*
* @return boolean True on success
*/
private function _remove($id)
{
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
if ($id <= 0)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return true;
}
/** @var \Solo\Model\Manage $model */
$model = $this->getModel();
$model->setState('id', $id);
try
{
$model->delete();
return true;
}
catch (\RuntimeException $e)
{
return false;
}
}
/**
* Removes only the backup file linked to a statistics entry
*
* @param integer $id The ID of the backup record
*
* @return boolean True on success
*/
private function _removeFiles($id)
{
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
if ($id <= 0)
{
$this->setRedirect($returnUrl, Text::_('COM_AKEEBA_BUADMIN_ERROR_INVALIDID'), 'error');
return true;
}
/** @var \Solo\Model\Manage $model */
$model = $this->getModel();
$model->setState('id', $id);
try
{
$model->deleteFile();
return true;
}
catch (\RuntimeException $e)
{
return false;
}
}
/**
* Save an edited backup record
*
* @return void
*/
public function save()
{
// CSRF prevention
$this->csrfProtection();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
$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;
$result = Platform::getInstance()->set_or_update_statistics($id, $statistic);
if ($result !== false)
{
$message = Text::_('COM_AKEEBA_BUADMIN_LOG_SAVEDOK');
$type = 'message';
}
else
{
$message = Text::_('COM_AKEEBA_BUADMIN_LOG_SAVEERROR');
$type = 'error';
}
$this->setRedirect($returnUrl, $message, $type);
}
/**
* Redirect to the restoration page for this backup record
*
* @return void
*/
public function restore()
{
// CSRF prevention
$this->csrfProtection();
$router = $this->container->router;
$id = null;
$cid = $this->input->get('cid', array(), 'array');
if (!empty($cid))
{
$id = intval($cid[0]);
if ($id <= 0)
{
$id = null;
}
}
if (empty($id))
{
$id = $this->input->get('id', -1, 'int');
}
if ($id <= 0)
{
$id = null;
}
$url = $router->route('index.php?view=restore&id=' . $id);
$this->setRedirect($url);
return;
}
/**
* Cancel the editing operation
*
* @return void
*/
public function cancel()
{
// CSRF prevention
$this->csrfProtection();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
$this->setRedirect($returnUrl);
}
public function hideModal()
{
/** @var \Solo\Model\Manage $model */
$model = $this->getModel();
$model->hideRestorationInstructionsModal();
// Get the return URL
$router = $this->container->router;
$task = $this->container->segment->get('solo_manage_task', 'main');
$returnUrl = $router->route('index.php?view=manage&task=' . $task);
$this->setRedirect($returnUrl);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
/**
* The Controller for the Phpinfo view
*/
class Phpinfo extends ControllerDefault
{
public function phpinfo()
{
@ob_end_clean();
phpinfo();
$this->container->application->close(200);
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Inflector\Inflector;
use Awf\Text\Text;
use RuntimeException;
class Profiles extends DataControllerDefault
{
/**
* Imports an exported profile .json file
*
* @return void
*/
public function import()
{
// CSRF prevention
$this->csrfProtection();
// Get the reference to the uploaded file
$file = $_FILES['importfile'];
// Get a URL router
$router = $this->container->router;
if (!isset($file['name']))
{
$this->setRedirect($router->route('index.php?view=profiles'), Text::_('MSG_UPLOAD_INVALID_REQUEST'), 'error');
}
/** @var \Solo\Model\Profiles $model */
$model = $this->getModel();
// Load the file data
$data = file_get_contents($file['tmp_name']);
@unlink($file['tmp_name']);
// JSON decode
$data = json_decode($data, true);
// Import
$message = Text::_('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($router->route('index.php?view=profiles'), $message, $messageType);
}
/**
* Enable the Quick Icon for a record
*
* @since 3.1.2
* @throws \Exception
*/
public function publish()
{
$this->setQuickIcon(1);
}
/**
* Disable the Quick Icon for a record
*
* @since 3.1.2
* @throws \Exception
*/
public function unpublish()
{
$this->setQuickIcon(0);
}
/**
* Sets the Quick Icon status for the record.
*
* @param int|bool $published Should this profile have a Quick Icon?
*
* @return void
* @throws \Exception
*
* @since 3.1.2
*/
public function setQuickIcon($published)
{
// CSRF prevention
$this->csrfProtection();
/** @var \Solo\Model\Profiles $model */
$model = $this->getModel();
$ids = $this->getIDsFromRequest($model, false);
try
{
$status = true;
foreach ($ids as $id)
{
$model->find($id);
$model->save(array(
'quickicon' => $published ? 1 : 0
));
}
}
catch (\Exception $e)
{
$status = false;
$error = $e->getMessage();
}
// Redirect
if ($customURL = $this->input->getBase64('returnurl', ''))
{
$customURL = base64_decode($customURL);
}
$router = $this->container->router;
$url = !empty($customURL) ? $customURL : $router->route('index.php?view=' . Inflector::pluralize($this->view));
if (!$status)
{
$this->setRedirect($url, $error, 'error');
}
else
{
$this->setRedirect($url);
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Application\Application;
use Awf\Date\Date;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Solo\Model\Backup;
class Remote extends ControllerDefault
{
public function execute($task)
{
$this->checkPermissions();
define('AKEEBA_BACKUP_ORIGIN', 'frontend');
return parent::execute($task);
}
public function main()
{
// Set the profile
$this->setProfile();
// Get the backup ID
$backupId = $this->input->get('backupid', null, 'cmd');
if (empty($backupId))
{
$backupId = null;
}
/** @var Backup $model */
$model = Model::getTmpInstance($this->container->application_name, 'Backup', $this->container);
$dateNow = new Date();
$model->setState('tag', AKEEBA_BACKUP_ORIGIN);
$model->setState('backupid', $backupId);
$model->setState('description', Text::_('COM_AKEEBA_BACKUP_DEFAULT_DESCRIPTION') . ' ' . $dateNow->format(Text::_('DATE_FORMAT_LC2'), true));
$model->setState('comment', '');
$array = $model->startBackup();
$backupId = $model->getState('backupid', null, 'cmd');
$this->processEngineReturnArray($array, $backupId);
}
public function step()
{
// Set the profile
$this->setProfile();
// Get the backup ID
$backupId = $this->input->get('backupid', null, 'cmd');
if (empty($backupId))
{
$backupId = null;
}
/** @var Backup $model */
$model = Model::getTmpInstance($this->container->application_name, 'Backup', $this->container);
$model->setState('tag', AKEEBA_BACKUP_ORIGIN);
$model->setState('backupid', $backupId);
$array = $model->stepBackup();
$backupId = $model->getState('backupid', null, 'cmd');
$this->processEngineReturnArray($array, $backupId);
}
/**
* Used by the tasks to process Akeeba Engine's return array. Depending on the result and the component options we
* may throw text output or send an HTTP redirection header.
*
* @param array $array The return array to process
* @param string $backupId The backup ID (used to step the backup process)
*/
private function processEngineReturnArray($array, $backupId)
{
$noredirect = $this->input->get('noredirect', 0, 'int');
if ($array['Error'] != '')
{
// An error occured
if ($noredirect)
{
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo '500 ERROR -- ' . $array['Error'];
flush();
$this->container->application->close();
}
throw new \RuntimeException($array['Error'], 500);
}
if ($array['HasRun'] == 1)
{
// All done
Factory::nuke();
Factory::getFactoryStorage()->reset();
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo '200 OK';
flush();
$this->container->application->close();
}
if ($noredirect != 0)
{
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo "301 More work required -- BACKUPID ###$backupId###";
flush();
$this->container->application->close();
}
$router = $this->container->router;
$url = 'index.php?view=remote&task=step&key=' . $this->input->get('key', '', 'none', 2) . '&profile=' . $this->input->get('profile', 1, 'int');
if (!empty($backupId))
{
$url .= '&backupid=' . $backupId;
}
$this->setRedirect($router->route($url));
}
/**
* Check that the user has sufficient permissions, or die in error
*
* @return void
*/
private function checkPermissions()
{
// Is frontend backup enabled?
$febEnabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', 0);
$febEnabled = in_array($febEnabled, array('on', 'checked', 'true', 1, 'yes'));
$validKey = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', '');
if (!\Akeeba\Engine\Util\Complexify::isStrongEnough($validKey, false))
{
$febEnabled = false;
}
$validKeyTrim = trim($validKey);
if (!$febEnabled || empty($validKey))
{
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo "403 Operation not permitted";
flush();
$this->container->application->close();
throw new \RuntimeException('Operation not permitted', 403);
}
// Is the key good?
$key = $this->input->get('key', '', 'none', 2);
if (($key != $validKey) || (empty($validKeyTrim)))
{
@ob_end_clean();
header('Content-type: text/plain');
header('Connection: close');
echo "403 Operation not permitted";
flush();
$this->container->application->close();
throw new \RuntimeException('Operation not permitted', 403);
}
}
/**
* Set the active profile from the input parameters
*/
private function setProfile()
{
// Set profile
$profile = $this->input->get('profile', 1, 'int');
if (empty($profile))
{
$profile = 1;
}
$session = Application::getInstance()->getContainer()->segment;
$session->profile = $profile;
/**
* 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($profile);
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
class Restore extends ControllerDefault
{
/**
* Show the main page, where the user selects the restoration options
*
* @return void
*/
public function main()
{
/** @var \Solo\Model\Restore $model */
$model = $this->getModel();
// Get the ID
$id = $model->getState('id', 0);
$cid = $this->input->get('cid', array(), 'array');
if (empty($id))
{
if (is_array($cid) && !empty($cid))
{
$id = $cid[0];
}
else
{
$id = -1;
}
}
$model->setState('id', $id);
$profileID = $this->input->getInt('profileid', 0);
$model->setState('profileid', $profileID);
try
{
$model->validateRequest();
}
catch (\Exception $e)
{
$message = $e->getMessage();
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=manage'), $message, 'error');
$this->redirect();
return;
}
$model->setState('restorationstep', 0);
$this->display();
}
/**
* Show the restoration user interface and start the restoration
*
* @return void
*/
public function start()
{
$this->csrfProtection();
$this->getView()->setLayout('restore');
/** @var \Solo\Model\Restore $model */
$model = $this->getModel();
$model->setState('restorationstep', 1);
// This is required. validateRequest loads the correct backup profile. We need it to get the site directory.
try
{
$model->validateRequest();
}
catch (\Exception $e)
{
$message = $e->getMessage();
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=manage'), $message, 'error');
$this->redirect();
return;
}
// Set the model's state
$model->setState('jps_key', $this->input->get('jps_key', '', 'cmd'));
$model->setState('procengine', $this->input->get('procengine', 'direct', 'cmd'));
$model->setState('zapbefore', $this->input->get('zapbefore', 0, 'int'));
$model->setState('min_exec', $this->input->get('min_exec', 0, 'int'));
$model->setState('max_exec', $this->input->get('max_exec', 5, 'int'));
$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');
try
{
$model->createRestorationFile();
}
catch (\Exception $e)
{
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=manage'),
Text::_('COM_AKEEBA_RESTORE_ERROR_CANT_WRITE') . '<br/>' . $e->getMessage(), 'error');
$this->redirect();
return;
}
$this->display();
}
/**
* Perform an AJAX request, returning the result encoded in JSON and surrounded by triple hashes
*
* @return void
*/
public function ajax()
{
/** @var \Solo\Model\Restore $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->application->close();
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
class Schedule extends ControllerDefault
{
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
/**
* The controller for FTP browser
*/
class Sftpbrowser extends ControllerDefault
{
public function execute($task)
{
// If we are running inside a CMS but there is no active user we have to throw a 403
$inCMS = $this->container->segment->get('insideCMS', false);
if ($inCMS && !$this->container->userManager->getUser()->getId())
{
return false;
}
return parent::execute($task);
}
public function main()
{
/** @var \Solo\Model\Sftpbrowser $model */
$model = $this->getModel();
// Grab the data and push them to the model
$directory = $this->input->getRaw('directory', '');
$directory = '/' . ltrim($directory, '/');
$model->setState('host', $this->input->getString('host', ''));
$model->setState('port', $this->input->getInt('port', 22));
$model->setState('username', $this->input->getRaw('username', ''));
$model->setState('password', $this->input->getRaw('password', ''));
$model->setState('directory', $directory);
$model->setState('privKey', $this->input->getRaw('privkey', ''));
$model->setState('pubKey', $this->input->getRaw('pubkey', ''));
$ret = $model->doBrowse();
@ob_end_clean();
echo '#"\#\"#'.json_encode($ret).'#"\#\"#';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
class Sysconfig extends ControllerDefault
{
public function save()
{
$this->csrfProtection();
$urlredirect = $this->input->get('urlredirect', null, 'raw');
$data = $this->input->getData();
unset($data['view']);
unset($data['task']);
unset($data['layout']);
unset($data['token']);
if (isset($data['urlredirect']))
{
unset($data['urlredirect']);
}
$keys = array_keys($data);
$checkboxKeys = array(
'mail.online', 'mail.smtpauth', 'options.legacyapi_enabled', 'options.jsonapi_enabled', 'options.frontend_email_on_finish',
'options.usesvnsource', 'options.displayphpwarning'
);
foreach ($keys as $key)
{
if (strpos($key, 'fs_') === 0)
{
$data['fs.' . substr($key, 3)] = $data[$key];
unset($data[$key]);
$key = 'fs.' . substr($key, 3);
}
elseif (strpos($key, 'mail_') === 0)
{
$data['mail.' . substr($key, 5)] = $data[$key];
unset($data[$key]);
$key = 'mail.' . substr($key, 5);
}
if (in_array($key, $checkboxKeys))
{
$data[$key] = in_array($data[$key], array('on', 'yes', 'true', 1, true));
}
elseif ($key == 'options')
{
foreach ($data[$key] as $k => $v)
{
$check = 'options.' . $k;
if (in_array($check, $checkboxKeys))
{
$data[$key][$k] = in_array($data[$key][$k], array('on', 'yes', 'true', 1, true));
}
}
}
}
$config = $this->container->appConfig;
foreach ($data as $k => $v)
{
if (is_array($v))
{
foreach ($v as $sk => $sv)
{
$config->set($k . '.' . $sk, $sv);
}
}
else
{
$config->set($k, $v);
}
}
$this->container->appConfig->saveConfiguration();
if ($urlredirect)
{
$url = base64_decode($urlredirect);
}
else
{
$url = $this->container->router->route('index.php');
}
$this->setRedirect($url, Text::_('SOLO_SYSCONFIG_SAVE'));
// Akeeba Backup for WordPress: reset update information
if (defined('WPINC'))
{
$transient = (object) [
'response' => []
];
\AkeebaBackupWPUpdater::getupdates($transient);
}
}
public function apply()
{
$this->save();
$url = $this->container->router->route('index.php?view=sysconfig');
$this->setRedirect($url, Text::_('SOLO_SYSCONFIG_SAVE'));
}
public function testemail()
{
$config = $this->container->appConfig;
$mailer = $this->container->mailer;
$user = $this->container->userManager->getUser();
$from = $config->get('mail.mailfrom');
$fromName = $config->get('mail.fromname');
$subject = Text::sprintf('SOLO_SYSCONFIG_TESTEMAIL_SUBJECT', $this->container->appConfig->get('base_url', ''));
$body = Text::_('SOLO_SYSCONFIG_TESTEMAIL_BODY');
try
{
$mailer->sendMail($from, $fromName, $user->getEmail(), $subject, $body);
$type = 'info';
$msg = Text::_('SOLO_SYSCONFIG_TESTMEMAIL_SENT');
}
catch(\Exception $e)
{
$type = 'error';
$msg = $e->getMessage();
}
$this->setRedirect($this->container->router->route('index.php?view=sysconfig'), $msg, $type);
}
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Controller;
use Solo\Model\Exception\TransferIgnorableError;
use Solo\Model\Transfers;
class Transfer extends ControllerDefault
{
/** @var array The tasks this controller is allowed to use */
private $allowedTasks = array('wizard', 'checkUrl', 'applyConnection', 'initialiseUpload', 'upload', 'reset');
/**
* Override execute() to only allow specific tasks to run.
*
* @param string $task The task we are asked to run.
*
* @return bool|null
* @throws \Exception
*/
public function execute($task)
{
if (!in_array($task, $this->allowedTasks))
{
$task = $this->allowedTasks[0];
}
return parent::execute($task);
}
/**
* Default task, shows the wizard interface
*/
public function wizard()
{
parent::display();
}
/**
* Reset the wizard
*
* @return void
*/
public function reset()
{
$session = $this->container->segment;
$session->set('transfer', null);
$session->set('transfer.url', null);
$session->set('transfer.url_status', null);
$session->set('transfer.ftpsupport', null);
/** @var Transfers $model */
$model = $this->getModel();
$model->resetUpload();
$this->setRedirect($this->container->router->route('index.php?view=transfer'));
}
/**
* Cleans and checks the validity of the new site's URL
*/
public function checkUrl()
{
$url = $this->input->get('url', '', 'raw');
/** @var Transfers $model */
$model = $this->getModel(null, array('savestate' => 1));
$result = $model->checkAndCleanUrl($url);
$session = $this->container->segment;
$session->set('transfer.url', $result['url']);
$session->set('transfer.url_status', $result['status']);
@ob_end_clean();
echo '###' . json_encode($result) . '###';
$this->container->application->close();
}
/**
* Applies the FTP/SFTP connection information and makes some preliminary validation
*/
public function applyConnection()
{
$result = (object)array(
'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');
$ftpPort = $this->input->getInt('port', null);
$ftpUsername = $this->input->get('username', '', 'raw');
$ftpPassword = $this->input->get('password', '', 'raw');
$ftpPubKey = $this->input->get('pubKey', '', 'raw');
$ftpPrivateKey = $this->input->get('privKey', '', 'raw');
$ftpPassive = $this->input->getInt('passive', 1);
$ftpPassiveFix = $this->input->getInt('passive_fix', 1);
$ftpDirectory = $this->input->get('directory', '', 'raw');
$chunkMode = $this->input->get('chunkMode', 'chunked', 'cmd');
$chunkSize = $this->input->get('chunkSize', '5242880', 'int');
// 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
$session = $this->container->segment;
$session->set('transfer.transferOption', $transferOption);
$session->set('transfer.force', $force);
$session->set('transfer.ftpHost', $ftpHost);
$session->set('transfer.ftpPort', $ftpPort);
$session->set('transfer.ftpUsername', $ftpUsername);
$session->set('transfer.ftpPassword', $ftpPassword);
$session->set('transfer.ftpPubKey', $ftpPubKey);
$session->set('transfer.ftpPrivateKey', $ftpPrivateKey);
$session->set('transfer.ftpDirectory', $ftpDirectory);
$session->set('transfer.ftpPassive', $ftpPassive ? 1 : 0);
$session->set('transfer.ftpPassiveFix', $ftpPassiveFix ? 1 : 0);
$session->set('transfer.chunkMode', $chunkMode);
$session->set('transfer.chunkSize', $chunkSize);
/** @var Transfers $model */
$model = $this->getModel();
try
{
$config = $model->getFtpConfig();
$model->testConnection($config);
}
catch (TransferIgnorableError $e)
{
$result = (object)array(
'status' => false,
'message' => $e->getMessage(),
'ignorable' => true,
);
}
catch (\Exception $e)
{
$result = (object)array(
'status' => false,
'message' => $e->getMessage(),
'ignorable' => false,
);
}
@ob_end_clean();
echo '###' . json_encode($result) . '###';
$this->container->application->close();
}
/**
* Initialise the upload: sends Kickstart and our add-on script to the remote server
*/
public function initialiseUpload()
{
$result = (object)array(
'status' => true,
'message' => '',
'ignorable' => false,
);
/** @var Transfers $model */
$model = $this->getModel();
try
{
$config = $model->getFtpConfig();
$model->initialiseUpload($config);
}
catch (TransferIgnorableError $e)
{
$result = (object) [
'status' => false,
'message' => $e->getMessage(),
'ignorable' => true,
];
}
catch (\Exception $e)
{
$result = (object)array(
'status' => false,
'message' => $e->getMessage(),
'ignorable' => false,
);
}
@ob_end_clean();
echo '###' . json_encode($result) . '###';
$this->container->application->close();
}
/**
* Perform an upload step. Pass start=1 to reset the upload and start over.
*/
public function upload()
{
/** @var Transfers $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)array(
'status' => false,
'message' => $e->getMessage(),
'totalSize' => 0,
'doneSize' => 0,
'done' => false
);
}
$result = (object)$uploadResult;
@ob_end_clean();
echo '###' . json_encode($result) . '###';
$this->container->application->close();
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
use Awf\Text\Text;
class Update extends ControllerDefault
{
public function main()
{
$force = $this->input->getInt('force', 0) == 1;
/** @var \Solo\Model\Update $model */
$model = $this->getModel();
$model->load($force);
parent::main();
}
public function download()
{
/** @var \Solo\Model\Update $model */
$model = $this->getModel();
$model->prepareDownload();
$this->layout = 'download';
$this->display();
}
public function downloader()
{
$json = $this->input->get('json', '', 'raw');
$params = json_decode($json, true);
/** @var \Solo\Model\Update $model */
$model = $this->getModel();
if (is_array($params) && !empty($params))
{
foreach ($params as $k => $v)
{
$model->setState($k, $v);
}
}
$ret = $model->stepDownload();
echo '#"\#\"#' . json_encode($ret) . '#"\#\"#';
}
public function extract()
{
$this->csrfProtection();
$this->layout = 'extract';
/** @var \Solo\Model\Update $model */
$model = $this->getModel();
$model->createRestorationINI();
$this->display();
}
public function finalise()
{
// Do not add CSRF protection in this view; it called after the
// installation of the update. At this point the session MAY have
// already expired.
/** @var \Solo\Model\Update $model */
$model = $this->getModel();
$model->finalise();
$router = $this->container->router;
$this->setRedirect($router->route('index.php?view=update&force=1'), Text::_('SOLO_UPDATE_COMPLETE_OK'), 'success');
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Controller;
/**
* The Configuration Wizard controller
*/
class Wizard extends ControllerDefault
{
/**
* Executes a given controller task. The onBefore<task> and onAfter<task>
* methods are called automatically if they exist.
*
* @param string $task The task to execute, e.g. "browse"
*
* @return null|bool False on execution failure
*
* @throws \Exception When the task is not found
*/
public function execute($task)
{
// If we are running inside another CMS skip the first page
$inCMS = $this->container->segment->get('insideCMS', false);
if ($inCMS && !in_array($task, array('wizard', 'ajax')))
{
$task = 'wizard';
}
return parent::execute($task);
}
/**
* Tests and saves the site configuration settings, then redirects to the wizard task
*/
public function applySiteSettings()
{
$this->csrfProtection();
$siteParams = $this->input->get('var', array(), 'array');
try
{
/** @var \Solo\Model\Wizard $model */
$model = $this->getModel();
$model->testSiteParams($siteParams);
$model->saveSiteParams($siteParams);
}
catch (\Exception $e)
{
$url = $this->container->router->route('index.php?view=wizard');
$this->setRedirect($url, $e->getMessage(), 'error');
return;
}
$url = $this->container->router->route('index.php?view=wizard&task=wizard');
$this->setRedirect($url);
}
/**
* Show the main page of the wizard
*
* @return void
*/
public function wizard()
{
$this->getView()->setLayout('wizard');
$this->display();
}
public function ajax()
{
$act = $this->input->getCmd('akact', '');
/** @var \Solo\Model\Wizard $model */
$model = $this->getModel();
$model->setState('act', $act);
$ret = $model->runAjax();
@ob_end_clean();
echo '#"\#\"#' . json_encode( $ret ) . '#"\#\"#';
flush();
$this->container->application->close();
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo;
use Awf\Dispatcher\Dispatcher as BaseDispatcher;
use Awf\Utils\Template;
class Dispatcher extends BaseDispatcher
{
public function onBeforeDispatch()
{
$this->loadCommonCSS();
$this->loadCommonJavascript();
return true;
}
/**
* Loads JavaScript required throughout the application
*
* @return void
*/
private function loadCommonJavascript()
{
// FEF JavaScript
Template::addJS('media://js/fef/menu.min.js', $this->container->application);
Template::addJS('media://js/fef/tabs.min.js', $this->container->application);
$this->container->application->getDocument()->addScriptDeclaration(<<< JS
window.addEventListener("DOMContentLoaded", function (event)
{
akeeba.fef.menuButton();
akeeba.fef.tabs();
});
JS
);
// Application JavaScript
Template::addJS('media://js/solo/gui-helpers.js', $this->container->application);
Template::addJS('media://js/solo/modal.js', $this->container->application);
Template::addJS('media://js/solo/ajax.js', $this->container->application);
Template::addJS('media://js/solo/system.js', $this->container->application);
Template::addJS('media://js/solo/tooltip.js', $this->container->application);
Template::addJS('media://js/piecon.js', $this->container->application);
}
/**
* Loads CSS files required throughout the application
*/
private function loadCommonCSS()
{
$darkMode = $this->container->appConfig->get('darkmode', -1);
Template::addCss('media://css/fef.css', $this->container->application);
Template::addCss('media://css/theme.css', $this->container->application);
if ($darkMode != 0)
{
Template::addCss('media://css/dark.css', $this->container->application);
Template::addCss('media://css/theme_dark.css', $this->container->application);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Exception\Update;
use Exception;
use RuntimeException;
class ConnectionError extends UpdateError
{
public function __construct($message = "", $code = 0, Exception $previous = null)
{
if (empty($message))
{
$message = 'Cannot connect to Akeeba Backup\'s update server';
}
if (empty($code))
{
$code = 500;
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Exception\Update;
use Exception;
use RuntimeException;
class PlatformError extends UpdateError
{
public function __construct($message = "", $code = 0, Exception $previous = null)
{
if (empty($message))
{
$message = 'There is an update to Akeeba Backup but the minimum PHP or WordPress version is higher than what is available on this site.';
}
if (empty($code))
{
$code = 500;
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Exception\Update;
use Exception;
use RuntimeException;
class StabilityError extends UpdateError
{
public function __construct($message = "", $code = 0, Exception $previous = null)
{
if (empty($message))
{
$message = 'There is an update to Akeeba Backup but its stability is lower than the minimum update stability specified in System Configuration.';
}
if (empty($code))
{
$code = 500;
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Exception\Update;
use Exception;
use RuntimeException;
class UpdateError extends RuntimeException
{
public function __construct($message = "", $code = 0, Exception $previous = null)
{
if (empty($message))
{
$message = 'There was an error fetching the Akeeba Backup update information';
}
if (empty($code))
{
$code = 500;
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
/**
* A helper class to escape JSON data
*/
class Escape
{
/**
* Escapes a string returned from Text::_() for use with Javascript
*
* @param $string string The string to escape
* @param $extras string The characters to escape
*
* @return string The escaped string
*/
static function escapeJS($string, $extras = '')
{
// Make sure we escape single quotes, slashes and brackets
if (empty($extras))
{
$extras = "'\\[]\"";
}
return addcslashes($string, $extras);
}
}

View File

@@ -0,0 +1,754 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
use Awf\Html\Html;
use Awf\Text\Text;
use Awf\Utils\ArrayHelper;
use RuntimeException;
use stdClass;
abstract class FEFSelect
{
/**
* Default values for options. Organized by option group.
*
* @var array
*/
protected static $optionDefaults = array(
'option' => array(
'option.attr' => null,
'option.disable' => 'disable',
'option.id' => null,
'option.key' => 'value',
'option.key.toHtml' => true,
'option.label' => null,
'option.label.toHtml' => true,
'option.text' => 'text',
'option.text.toHtml' => true,
'option.class' => 'class',
'option.onclick' => 'onclick',
),
);
/**
* Generates a yes/no radio list.
*
* @param string $name The value of the HTML name attribute
* @param array $attribs Additional HTML attributes for the `<select>` tag
* @param string $selected The key that is selected
* @param string $yes Language key for Yes
* @param string $no Language key for no
* @param mixed $id The id for the field or false for no id
*
* @return string HTML for the radio list
*
* @since 1.5
* @see JFormFieldRadio
*/
public static function booleanlist($name, $attribs = array(), $selected = null, $yes = 'AWF_YES', $no = 'AWF_NO', $id = false)
{
$arr = [
static::option('0', Text::_($no)),
static::option('1', Text::_($yes))
];
return static::radiolist($arr, $name, $attribs, 'value', 'text', (int) $selected, $id);
}
/**
* Generates an HTML selection list.
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see JHtml::$formatOptions}.
* Selection options, see {@see JHtmlSelect::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
* @since 1.5
*/
public static function genericlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
// Set default options
$options = array_merge(Html::$formatOptions, array('format.depth' => 0, 'id' => false));
if (is_array($attribs) && func_num_args() === 3)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
}
else
{
// Get options from the parameters
$options['id'] = $idtag;
$options['list.attr'] = $attribs;
$options['list.translate'] = $translate;
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(array('[', ']', ' '), '', $id);
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '" ' . $attribs . '>' . $options['format.eol']
. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];
return $html;
}
/**
* Generates a grouped HTML selection list from nested arrays.
*
* @param array $data An array of groups, each of which is an array of options.
* @param string $name The value of the HTML name attribute
* @param array $options Options, an array of key/value pairs. Valid options are:
* Format options, {@see JHtml::$formatOptions}.
* Selection options. See {@see JHtmlSelect::options()}.
* group.id: The property in each group to use as the group id
* attribute. Defaults to none.
* group.label: The property in each group to use as the group
* label. Defaults to "text". If set to null, the data array index key is
* used.
* group.items: The property in each group to use as the array of
* items in the group. Defaults to "items". If set to null, group.id and
* group. label are forced to null and the data element is assumed to be a
* list of selections.
* id: Value to use as the select element id attribute. Defaults to
* the same as the name.
* list.attr: Attributes for the select element. Can be a string or
* an array of key/value pairs. Defaults to none.
* list.select: either the value of one selected option or an array
* of selected options. Default: none.
* list.translate: Boolean. If set, text and labels are translated via
* JText::_().
*
* @return string HTML for the select list
*
* @throws RuntimeException If a group has contents that cannot be processed.
*/
public static function groupedlist($data, $name, $options = array())
{
// Set default options and overwrite with anything passed in
$options = array_merge(
Html::$formatOptions,
array('format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true, 'id' => false),
$options
);
// Apply option rules
if ($options['group.items'] === null)
{
$options['group.label'] = null;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(array('[', ']', ' '), '', $id);
// Disable groups in the options.
$options['groups'] = false;
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'];
$groupIndent = str_repeat($options['format.indent'], $options['format.depth']++);
foreach ($data as $dataKey => $group)
{
$label = $dataKey;
$id = '';
$noGroup = is_int($dataKey);
if ($options['group.items'] == null)
{
// Sub-list is an associative array
$subList = $group;
}
elseif (is_array($group))
{
// Sub-list is in an element of an array.
$subList = $group[$options['group.items']];
if (isset($group[$options['group.label']]))
{
$label = $group[$options['group.label']];
$noGroup = false;
}
if (isset($options['group.id']) && isset($group[$options['group.id']]))
{
$id = $group[$options['group.id']];
$noGroup = false;
}
}
elseif (is_object($group))
{
// Sub-list is in a property of an object
$subList = $group->{$options['group.items']};
if (isset($group->{$options['group.label']}))
{
$label = $group->{$options['group.label']};
$noGroup = false;
}
if (isset($options['group.id']) && isset($group->{$options['group.id']}))
{
$id = $group->{$options['group.id']};
$noGroup = false;
}
}
else
{
throw new RuntimeException('Invalid group contents.', 1);
}
if ($noGroup)
{
$html .= static::options($subList, $options);
}
else
{
$html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="'
. ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol']
. static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol'];
}
}
$html .= $baseIndent . '</select>' . $options['format.eol'];
return $html;
}
/**
* Generates a selection list of integers.
*
* @param integer $start The start integer
* @param integer $end The end integer
* @param integer $inc The increment
* @param string $name The value of the HTML name attribute
* @param mixed $attribs Additional HTML attributes for the `<select>` tag, an array of
* attributes, or an array of options. Treated as options if it is the last
* argument passed.
* @param mixed $selected The key that is selected
* @param string $format The printf format to be applied to the number
*
* @return string HTML for the select list
*/
public static function integerlist($start, $end, $inc, $name, $attribs = null, $selected = null, $format = '')
{
// Set default options
$options = array_merge(Html::$formatOptions, array('format.depth' => 0, 'option.format' => '', 'id' => null));
if (is_array($attribs) && func_num_args() === 5)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
// Extract the format and remove it from downstream options
$format = $options['option.format'];
unset($options['option.format']);
}
else
{
// Get options from the parameters
$options['list.attr'] = $attribs;
$options['list.select'] = $selected;
}
$start = (int) $start;
$end = (int) $end;
$inc = (int) $inc;
$data = array();
for ($i = $start; $i <= $end; $i += $inc)
{
$data[$i] = $format ? sprintf($format, $i) : $i;
}
// Tell genericlist() to use array keys
$options['option.key'] = null;
return static::genericlist($data, $name, $options);
}
/**
* Create an object that represents an option in an option list.
*
* @param string $value The value of the option
* @param string $text The text for the option
* @param mixed $optKey If a string, the returned object property name for
* the value. If an array, options. Valid options are:
* attr: String|array. Additional attributes for this option.
* Defaults to none.
* disable: Boolean. If set, this option is disabled.
* label: String. The value for the option label.
* option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* option.key: The property that will hold the selection value.
* Defaults to "value".
* option.label: The property in each option array to use as the
* selection label attribute. If a "label" option is provided, defaults to
* "label", if no label is given, defaults to null (none).
* option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The property that will hold the the displayed text. This
* parameter is ignored if an options array is passed.
* @param boolean $disable Not used.
*
* @return stdClass
*/
public static function option($value, $text = '', $optKey = 'value', $optText = 'text', $disable = false)
{
$options = array(
'attr' => null,
'disable' => false,
'option.attr' => null,
'option.disable' => 'disable',
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
);
if (is_array($optKey))
{
// Merge in caller's options
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['disable'] = $disable;
}
$obj = new stdClass;
$obj->{$options['option.key']} = $value;
$obj->{$options['option.text']} = trim($text) ? $text : $value;
/*
* If a label is provided, save it. If no label is provided and there is
* a label name, initialise to an empty string.
*/
$hasProperty = $options['option.label'] !== null;
if (isset($options['label']))
{
$labelProperty = $hasProperty ? $options['option.label'] : 'label';
$obj->$labelProperty = $options['label'];
}
elseif ($hasProperty)
{
$obj->{$options['option.label']} = '';
}
// Set attributes only if there is a property and a value
if ($options['attr'] !== null)
{
$attrKey = !empty($options['option.attr']) ? $options['option.attr'] : 'attr';
$obj->{$attrKey} = $options['attr'];
}
// Set disable only if it has a property and a value
if ($options['disable'] !== null)
{
$obj->{$options['option.disable']} = $options['disable'];
}
return $obj;
}
/**
* Generates the option tags for an HTML select list (with no select tag
* surrounding the options).
*
* @param array $arr An array of objects, arrays, or values.
* @param mixed $optKey If a string, this is the name of the object variable for
* the option value. If null, the index of the array of objects is used. If
* an array, this is a set of options, as key/value pairs. Valid options are:
* -Format options, {@see JHtml::$formatOptions}.
* -groups: Boolean. If set, looks for keys with the value
* "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
* true for backwards compatibility.
* -list.select: either the value of one selected option or an array
* of selected options. Default: none.
* -list.translate: Boolean. If set, text and labels are translated via
* JText::_(). Default is false.
* -option.id: The property in each option array to use as the
* selection id attribute. Defaults to none.
* -option.key: The property in each option array to use as the
* selection value. Defaults to "value". If set to null, the index of the
* option array is used.
* -option.label: The property in each option array to use as the
* selection label attribute. Defaults to null (none).
* -option.text: The property in each option array to use as the
* displayed text. Defaults to "text". If set to null, the option array is
* assumed to be a list of displayable scalars.
* -option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* -option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* -option.key: The property that will hold the selection value.
* Defaults to "value".
* -option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string)
* @param boolean $translate Translate the option values.
*
* @return string HTML for the select list
*/
public static function options($arr, $optKey = 'value', $optText = 'text', $selected = null, $translate = false)
{
$options = array_merge(
Html::$formatOptions,
static::$optionDefaults['option'],
array('format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false)
);
if (is_array($optKey))
{
// Set default options and overwrite with anything passed in
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
$options['list.translate'] = $translate;
}
$html = '';
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']);
foreach ($arr as $elementKey => &$element)
{
$attr = '';
$extra = '';
$label = '';
$id = '';
if (is_array($element))
{
$key = $options['option.key'] === null ? $elementKey : $element[$options['option.key']];
$text = $element[$options['option.text']];
if (isset($element[$options['option.attr']]))
{
$attr = $element[$options['option.attr']];
}
if (isset($element[$options['option.id']]))
{
$id = $element[$options['option.id']];
}
if (isset($element[$options['option.label']]))
{
$label = $element[$options['option.label']];
}
if (isset($element[$options['option.disable']]) && $element[$options['option.disable']])
{
$extra .= ' disabled="disabled"';
}
}
elseif (is_object($element))
{
$key = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']};
$text = $element->{$options['option.text']};
if (isset($element->{$options['option.attr']}))
{
$attr = $element->{$options['option.attr']};
}
if (isset($element->{$options['option.id']}))
{
$id = $element->{$options['option.id']};
}
if (isset($element->{$options['option.label']}))
{
$label = $element->{$options['option.label']};
}
if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']})
{
$extra .= ' disabled="disabled"';
}
if (isset($element->{$options['option.class']}) && $element->{$options['option.class']})
{
$extra .= ' class="' . $element->{$options['option.class']} . '"';
}
if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']})
{
$extra .= ' onclick="' . $element->{$options['option.onclick']} . '"';
}
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
/*
* The use of options that contain optgroup HTML elements was
* somewhat hacked for J1.5. J1.6 introduces the grouplist() method
* to handle this better. The old solution is retained through the
* "groups" option, which defaults true in J1.6, but should be
* deprecated at some point in the future.
*/
$key = (string) $key;
if ($key === '<OPTGROUP>' && $options['groups'])
{
$html .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol'];
$baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']);
}
elseif ($key === '</OPTGROUP>' && $options['groups'])
{
$baseIndent = str_repeat($options['format.indent'], --$options['format.depth']);
$html .= $baseIndent . '</optgroup>' . $options['format.eol'];
}
else
{
// If no string after hyphen - take hyphen out
$splitText = explode(' - ', $text, 2);
$text = $splitText[0];
if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1]))
{
$text .= ' - ' . $splitText[1];
}
if (!empty($label) && $options['list.translate'])
{
$label = Text::_($label);
}
if ($options['option.label.toHtml'])
{
$label = htmlentities($label);
}
if (is_array($attr))
{
$attr = ArrayHelper::toString($attr);
}
else
{
$attr = trim($attr);
}
$extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra;
if (is_array($options['list.select']))
{
foreach ($options['list.select'] as $val)
{
$key2 = is_object($val) ? $val->{$options['option.key']} : $val;
if ($key == $key2)
{
$extra .= ' selected="selected"';
break;
}
}
}
elseif ((string) $key === (string) $options['list.select'])
{
$extra .= ' selected="selected"';
}
if ($options['list.translate'])
{
$text = Text::_($text);
}
// Generate the option, encoding as required
$html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"'
. $extra . '>';
$html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text;
$html .= '</option>' . $options['format.eol'];
}
}
return $html;
}
/**
* Generates an HTML radio list.
*
* @param array $data An array of objects
* @param string $name The value of the HTML name attribute
* @param string $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $optKey The key that is selected
* @param string $optText The name of the object variable for the option value
* @param string $selected The name of the object variable for the option text
* @param boolean $idtag Value of the field id or null by default
* @param boolean $translate True if options will be translated
*
* @return string HTML for the select list
*/
public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
$forToggle = false;
$colorBoolean = false;
if (isset($attribs['forToggle']))
{
$forToggle = (bool) $attribs['forToggle'];
unset($attribs['forToggle']);
}
if (isset($attribs['colorBoolean']))
{
$colorBoolean = (bool) $attribs['colorBoolean'];
unset($attribs['colorBoolean']);
}
if (is_array($attribs))
{
$attribs = ArrayHelper::toString($attribs);
}
$id_text = empty($idtag) ? $name : $idtag;
$html = '';
foreach ($data as $optionObject)
{
$optionValue = $optionObject->$optKey;
$labelText = $translate ? Text::_($optionObject->$optText) : $optionObject->$optText;
$id = (isset($optionObject->id) ? $optionObject->id : '');
$extra = '';
$id = ($id ? $id : $id_text) . '_' . $optionValue;
if (is_array($selected))
{
foreach ($selected as $val)
{
$k2 = is_object($val) ? $val->$optKey : $val;
if ($optionValue == $k2)
{
$extra .= ' selected="selected" ';
break;
}
}
}
else
{
$extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : '');
}
$labelAttributes = (isset($optionObject->attr) && is_array($optionObject->attr)) ? $optionObject->attr : [];
if ($forToggle && $colorBoolean)
{
$labelAttributes['class'] = isset($labelAttributes['class']) ? $labelAttributes['class'] : '';
$labelAttributes['class'] .= ' ' . ($optionValue ? 'green' : 'red');
}
$labelAttributes = ArrayHelper::toString($labelAttributes);
if ($forToggle)
{
$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />';
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl" ' . $labelAttributes . '>' . $labelText . '</label>';
}
else
{
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl" ' . $labelAttributes . '>';
$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />' . $labelText;
$html .= "\n\t" . '</label>';
}
}
$html .= "\n";
return $html;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
abstract class Format
{
/**
* Format a size in bytes in a human readable format (e.g. 1.2Gb)
*
* @param integer $sizeInBytes The size to convert, in bytes
* @param integer $decimals Accuracy, in decimal points (default: 2)
* @param boolean|string $force_unit Force a particular unit? Choose one of b, Kb, Mb, Gb, Tb or false for
* automatic determination of the best unit.
* @param string $dec_char Decimal separator character, default dot
* @param string $thousands_char Thousands separator character, default none
*
* @return string The formatted number
*/
public static function fileSize($sizeInBytes, $decimals = 2, $force_unit = false, $dec_char = '.', $thousands_char = '')
{
if ($sizeInBytes <= 0)
{
return '-';
}
$units = array('b', 'KB', 'MB', 'GB', 'TB', 'PB');
if ($force_unit === false)
{
$unit = floor(log($sizeInBytes, 2) / 10);
}
else
{
$unit = $force_unit;
}
if ($unit == 0)
{
$decimals = 0;
}
return number_format($sizeInBytes / (1024 ** $unit), $decimals, $dec_char, $thousands_char) . ' ' . $units[$unit];
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
use Akeeba\Engine\Factory;
use Awf\Application\Application;
abstract class SecretWord
{
/**
* Enforce (reversible) encryption for the component setting $settingsKey
*
* @param string $settingsKey The key for the setting containing the secret word
*
* @return void
*
* @throws \Awf\Exception\App
*
* @since 5.5.2
*/
public static function enforceEncryption($settingsKey)
{
$params = Application::getInstance()->getContainer()->appConfig;
// If encryption is not enabled in the Engine we can't encrypt the Secret Word
if ($params->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('options.' . $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('options.' . $settingsKey, $encrypted);
$params->saveConfiguration();
}
/**
* 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 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
*
* @throws \Awf\Exception\App
*
* @since 5.5.2
*/
public static function enforceDecrypted($settingsKey, $encryptionKey = null)
{
// Get the raw version of frontend_secret_word and check if it has a valid encryption signature
$params = Application::getInstance()->getContainer()->appConfig;
$raw = $params->get('options.' . $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();
$decrypted = $secureSettings->decryptSettings($raw, $encryptionKey);
// Finally, I need to save it back to the database
$params->set('options.' . $settingsKey, $decrypted);
$params->saveConfiguration();
}
}

View File

@@ -0,0 +1,349 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
use Akeeba\Engine\Factory;
use Awf\Database\Driver;
use Awf\Html\Select;
use Awf\Text\Text;
/**
* Setup helper class
*/
abstract class Setup
{
/**
* Get a dropdown list for database drivers
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: driver
*
* @return string HTML
*/
public static function databaseTypesSelect($selected = '', $name = 'driver')
{
$connectors = Driver::getConnectors();
$html = '<select name="' . $name . '" id="' . $name . '">' . "\n";
foreach ($connectors as $connector)
{
// Unsupported driver types
if (in_array(strtoupper($connector), ['PDO', 'NONE', 'SQLITE', 'PGSQL']))
{
continue;
}
$checked = (strtoupper($selected) == strtoupper($connector)) ? 'selected="selected"' : '';
$html .= "\t<option value=\"$connector\" $checked>" . Text::_('SOLO_SETUP_LBL_DATABASE_DRIVER_' . $connector) . "</option>\n";
}
$html .= "</select>";
return $html;
}
/**
* Get a dropdown list for script types
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: installer
*
* @return string HTML
*/
public static function restorationScriptSelect($selected = '', $name = 'installer')
{
$installers = Factory::getEngineParamsProvider()->getInstallerList(true);
$options = array();
foreach ($installers as $key => $installer)
{
$options[] = Select::option($key, $installer['name']);
}
return Select::genericList($options, $name, array(), 'value', 'text', $selected, $name, false);
}
/**
* Get a dropdown list for restoration scripts
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: scripttype
*
* @return string HTML
*/
public static function scriptTypesSelect($selected = '', $name = 'scripttype')
{
$scriptTypes = array(
'generic',
'joomla',
'wordpress',
);
$options = array();
foreach ($scriptTypes as $scriptType)
{
$options[] = Select::option($scriptType, Text::_('SOLO_CONFIG_PLATFORM_SCRIPTTYPE_' . $scriptType));
}
return Select::genericList($options, $name, array(), 'value', 'text', $selected, $name, false);
}
/**
* Get a dropdown list for mailer engines
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: mailer
*
* @return string HTML
*/
public static function mailerSelect($selected = '', $name = 'mailer')
{
$scriptTypes = array('mail', 'smtp', 'sendmail');
$options = array();
foreach ($scriptTypes as $scriptType)
{
$options[] = Select::option($scriptType, Text::_('SOLO_SYSCONFIG_EMAIL_MAILER_' . $scriptType));
}
return Select::genericList($options, $name, array(), 'value', 'text', $selected, $name, false);
}
/**
* Get a dropdown list for SMTP security settings
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: smtpsecure
*
* @return string HTML
*/
public static function smtpSecureSelect($selected = '', $name = 'smtpsecure')
{
$options = array();
$options[] = Select::option(0, Text::_('SOLO_SYSCONFIG_EMAIL_SMTPSECURE_NONE'));
$options[] = Select::option(1, Text::_('SOLO_SYSCONFIG_EMAIL_SMTPSECURE_SSL'));
$options[] = Select::option(2, Text::_('SOLO_SYSCONFIG_EMAIL_SMTPSECURE_TLS'));
return Select::genericList($options, $name, array(), 'value', 'text', $selected, $name, false);
}
/**
* Get a dropdown of available timezones
*
* @param string $selected Pre-selected value
* @param string $name The name and id of the input element
*
* @return string HTML
*/
public static function timezoneSelect($selected = '', $name = 'timezone', $includeDefaults = false, $disabled = false)
{
$groups = array();
$zoneHeaders = array(
'Africa',
'America',
'Antarctica',
'Arctic',
'Asia',
'Atlantic',
'Australia',
'Europe',
'Indian',
'Pacific',
);
$zones = \DateTimeZone::listIdentifiers();
// Build the group lists.
foreach ($zones as $zone)
{
// Time zones not in a group we will ignore.
if (strpos($zone, '/') === false)
{
continue;
}
// Get the group/locale from the timezone.
list ($group, $locale) = explode('/', $zone, 2);
// Only use known groups.
if (true || in_array($group, $zoneHeaders))
{
// Initialize the group if necessary.
if (!isset($groups[$group]))
{
$groups[$group] = array();
}
// Only add options where a locale exists.
if (!empty($locale))
{
$groups[$group][$zone] = Select::option($zone, str_replace('_', ' ', $locale));
}
}
}
// Sort the group lists.
ksort($groups);
foreach ($groups as &$location)
{
sort($location);
}
if ($includeDefaults)
{
$defaultGroup = array(
Select::option('GMT', 'GMT'),
Select::option('AKEEBA/DEFAULT', Text::_('COM_AKEEBA_CONFIG_FORCEDBACKUPTZ_DEFAULT')),
);
$groups[Text::_('COM_AKEEBA_CONFIG_FORCEDBACKUPTZ_DEFAULTGROUP')] = $defaultGroup;
ksort($groups);
}
$options = array(
'id' => $name,
'list.select' => $selected,
'group.items' => null,
);
if ($disabled)
{
$options['list.attr'] = ['disabled' => 'disabled'];
}
return Select::groupedList($groups, $name, $options);
}
/**
* Get a dropdown of available timezone formats
*
* @param string $selected Pre-selected value
*
* @return string HTML
*/
public static function timezoneFormatSelect($selected = '')
{
$rawOptions = array(
'COM_AKEEBA_CONFIG_BACKEND_TIMEZONETEXT_NONE' => '',
'COM_AKEEBA_CONFIG_BACKEND_TIMEZONETEXT_ABBREVIATION' => 'T',
'COM_AKEEBA_CONFIG_BACKEND_TIMEZONETEXT_GMTOFFSET' => '\\G\\M\\TP',
);
$html = '<select name="timezonetext" id="timezonetext">' . "\n";
foreach ($rawOptions as $label => $value)
{
$checked = (strtoupper($selected) == strtoupper($value)) ? 'selected="selected"' : '';
$label = Text::_($label);
$html .= "\t<option value=\"$value\" $checked>$label</option>\n";
}
$html .= "</select>";
return $html;
}
/**
* Get a dropdown for the filesystem driver selection
*
* @param string $selected The pre-selected value
*
* @return string HTML
*/
public static function fsDriverSelect($selected = '', $showDirect = true)
{
$drivers = array();
if ($showDirect)
{
$drivers[] = 'file';
}
if (function_exists('ftp_connect'))
{
$drivers[] = 'ftp';
}
if (extension_loaded('ssh2'))
{
$drivers[] = 'sftp';
}
$html = '<select name="fs_driver" id="fs_driver">' . "\n";
foreach ($drivers as $driver)
{
$checked = (strtoupper($selected) == strtoupper($driver)) ? 'selected="selected"' : '';
$html .= "\t<option value=\"$driver\" $checked>" . Text::_('SOLO_SETUP_LBL_FS_DRIVER_' . $driver) . "</option>\n";
}
$html .= "</select>";
return $html;
}
/**
* Get a dropdown for the minimum update stability
*
* @param string $selected The pre-selected value
*
* @return string HTML
*/
public static function minstabilitySelect($selected = '')
{
$levels = array('alpha', 'beta', 'rc', 'stable');
$html = '<select name="options[minstability]" id="minstability">' . "\n";
foreach ($levels as $level)
{
$checked = (strtoupper($selected) == strtoupper($level)) ? 'selected="selected"' : '';
$html .= "\t<option value=\"$level\" $checked>" . Text::_('SOLO_CONFIG_MINSTABILITY_' . $level) . "</option>\n";
}
$html .= "</select>";
return $html;
}
/**
* Get a dropdown for the two factor authentication methods
*
* @param string $name The name of the field
* @param string $selected The pre-selected value
*
* @return string HTML
*/
public static function tfaMethods($name = 'tfamethod', $selected = 'none')
{
$methods = array('none', 'yubikey', 'google');
$html = '<select name="' . $name . '" id="' . $name . '">' . "\n";
foreach ($methods as $method)
{
$checked = (strtoupper($selected) == strtoupper($method)) ? 'selected="selected"' : '';
$html .= "\t<option value=\"$method\" $checked>" . Text::_('SOLO_USERS_TFA_' . $method) . "</option>\n";
}
$html .= "</select>";
return $html;
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Application\Application;
use Awf\Date\Date;
use Awf\Text\Text;
/**
* Status helper. Used by the Control Panel and the backup page to report detected warnings which may impact your backup
* experience.
*/
class Status
{
/**
* Are we ready to take a new backup?
*
* @var bool
*/
public $status = false;
/**
* Is the output directory writable?
*
* @var bool
*/
public $outputWritable = false;
/**
* The detected warnings
*
* @var array
*/
protected $warnings = array();
/**
* Get a Singleton instance
*
* @return self
*/
public static function &getInstance()
{
static $instance = null;
if (empty($instance))
{
$instance = new self();
}
return $instance;
}
/**
* Public constructor. Automatically initializes the object with the status and warnings.
*
* @return self
*/
public function __construct()
{
$this->status = Factory::getConfigurationChecks()->getShortStatus();
$this->warnings = Factory::getConfigurationChecks()->getDetailedStatus();
$status = Factory::getConfigurationChecks()->getFolderStatus();
$this->outputWritable = $status['output'];
}
/**
* 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 = '<div class="akeeba-block--success"><p>' . Text::_('COM_AKEEBA_CPANEL_LBL_STATUS_OK') . '</p></div>';
}
elseif ($status && !empty($quirks))
{
$html = '<div class="akeeba-block--warning"><p>' . Text::_('COM_AKEEBA_CPANEL_LBL_STATUS_WARNING') . '</p></div>';
}
else
{
$html = '<div class="akeeba-block--failure"><p>' . Text::_('COM_AKEEBA_CPANEL_LBL_STATUS_ERROR') . '</p></div>';
}
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 = '<p>' . Text::_('COM_AKEEBA_CPANEL_WARNING_QNONE') . '</p>';
$quirks = Factory::getConfigurationChecks()->getDetailedStatus();
if (!empty($quirks))
{
$html = "<ul>\n";
foreach ($quirks as $quirk)
{
$html .= $this->renderWarnings($quirk, $onlyErrors);
}
$html .= "</ul>\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 '<li><a class="severity-' . $quirk['severity'] .
'" href="' . $quirk['help_url'] . '" target="_blank">' . $quirk['description'] . '</a>' . "</li>\n";
}
/**
* Returns the details of the latest backup as HTML
*
* @return string HTML
* @throws \Awf\Exception\App
*/
public function getLatestBackupDetails()
{
$db = Application::getInstance()->getContainer()->db;
$query = $db->getQuery(true)
->select('MAX(' . $db->qn('id') . ')')
->from($db->qn('#__ak_stats'));
$db->setQuery($query);
$id = $db->loadResult();
$backup_types = Factory::getEngineParamsProvider()->loadScripting();
if (empty($id))
{
return '<p class="label">' . Text::_('COM_AKEEBA_BACKUP_STATUS_NONE') . '</p>';
}
$record = Platform::getInstance()->get_statistics($id);
switch ($record['status'])
{
case 'run':
$status = Text::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_PENDING');
$statusClass = "akeeba-label--warning";
break;
case 'fail':
$status = Text::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_FAIL');
$statusClass = "akeeba-label--failure";
break;
case 'complete':
$status = Text::_('COM_AKEEBA_BUADMIN_LABEL_STATUS_OK');
$statusClass = "akeeba-label--success";
break;
default:
$status = '';
$statusClass = '';
}
switch ($record['origin'])
{
case 'frontend':
$origin = Text::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_FRONTEND');
break;
case 'backend':
$origin = Text::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_BACKEND');
break;
case 'cli':
$origin = Text::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN_CLI');
break;
default:
$origin = '&ndash;';
break;
}
$type = '';
if (array_key_exists($record['type'], $backup_types['scripts']))
{
$type = Platform::getInstance()->translate($backup_types['scripts'][$record['type']]['text']);
}
$startTime = new Date($record['backupstart'], 'UTC');
$tz = new \DateTimeZone(Application::getInstance()->getContainer()->appConfig->get('timezone', 'UTC'));
$startTime->setTimezone($tz);
$html = '<table class="akeeba-table--striped">';
$html .= '<tr><td>' . Text::_('COM_AKEEBA_BUADMIN_LABEL_START') . '</td><td>' . $startTime->format(Text::_('DATE_FORMAT_LC2'), true) . '</td></tr>';
$html .= '<tr><td>' . Text::_('COM_AKEEBA_BUADMIN_LABEL_DESCRIPTION') . '</td><td>' . $record['description'] . '</td></tr>';
$html .= '<tr><td>' . Text::_('COM_AKEEBA_BUADMIN_LABEL_STATUS') . '</td><td><span class="label ' . $statusClass . '">' . $status . '</span></td></tr>';
$html .= '<tr><td>' . Text::_('COM_AKEEBA_BUADMIN_LABEL_ORIGIN') . '</td><td>' . $origin . '</td></tr>';
$html .= '<tr><td>' . Text::_('COM_AKEEBA_BUADMIN_LABEL_TYPE') . '</td><td>' . $type . '</td></tr>';
$html .= '</table>';
return $html;
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Helper;
use Awf\Input\Filter;
use Awf\Text\Text;
use Awf\Uri\Uri;
/**
* Various utility methods
*/
class Utils
{
/**
* Get the relative path of a directory ($to) against a base directory ($from). Both directories are given as
* absolute paths.
*
* @param string $from The base directory
* @param string $to The directory to convert to a relative path
*
* @return string The path of $to relative to $from
*/
public static function getRelativePath($from, $to)
{
// Some compatibility fixes for Windows paths
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);
$from = explode('/', $from);
$to = explode('/', $to);
$relPath = $to;
foreach ($from as $depth => $dir)
{
// find first non-matching dir
if ($dir === $to[ $depth ])
{
// ignore this directory
array_shift($relPath);
}
else
{
// 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;
}
else
{
$relPath[0] = './' . $relPath[0];
}
}
}
return implode('/', $relPath);
}
/**
* Get a dropdown list for database drivers
*
* @param string $selected Selected value
* @param string $name The name (also used for id) of the field, default: driver
*
* @return string HTML
*/
public static function engineDatabaseTypesSelect($selected = '', $name = 'driver')
{
$connectors = array('mysql', 'mysqli', 'none', 'pdomysql', 'sqlite');
$html = '<select class="form-control" name="' . $name . '" id="' . $name . '">' . "\n";
foreach($connectors as $connector)
{
$checked = (strtoupper($selected) == strtoupper($connector)) ? 'selected="selected"' : '';
$html .= "\t<option value=\"$connector\" $checked>" . Text::_('SOLO_SETUP_LBL_DATABASE_DRIVER_' . $connector) . "</option>\n";
}
$html .= "</select>";
return $html;
}
/**
* Safely decode a return URL, used in the Backup view.
*
* Return URLs can have two sources:
* - The Backup on Update plugin. In this case the URL is base sixty four encoded and we need to decode it first.
* - A custom backend menu item. In this case the URL is a simple string which does not need decoding.
*
* Further to that, we have to make a few security checks:
* - The URL must be internal, i.e. starts with our site's base URL or index.php (this check is executed by Joomla)
* - It must not contain single quotes, double quotes, lower than or greater than signs (could be used to execute
* arbitrary JavaScript).
*
* If any of these violations is detected we return an empty string.
*
* @param ?string $returnUrl
*
* @return string
*/
static function safeDecodeReturnUrl($returnUrl)
{
// Nulls and non-strings are not allowed
if (is_null($returnUrl) || !is_string($returnUrl))
{
return '';
}
// Make sure it's not an empty string
$returnUrl = trim($returnUrl);
if (empty($returnUrl))
{
return '';
}
// Decode a base sixty four encoded string.
$filter = new Filter();
$encoded = $filter->clean($returnUrl, 'base64');
if (($returnUrl == $encoded) && (strpos($returnUrl, 'index.php') === false) && (strpos($returnUrl, 'akeebabackupwp') === false))
{
$possibleReturnUrl = base64_decode($returnUrl);
if ($possibleReturnUrl !== false)
{
$returnUrl = $possibleReturnUrl;
}
}
// Check if it's an internal URL
if (!Uri::isInternal($returnUrl))
{
return '';
}
$disallowedCharacters = ['"' ,"'", '>', '<'];
foreach ($disallowedCharacters as $check)
{
if (strpos($returnUrl, $check) !== false)
{
return '';
}
}
return $returnUrl;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model;
use Akeeba\Engine\Factory;
use Awf\Mvc\Model;
class Alice extends Model
{
public function runAnalysis()
{
$ret_array = array();
$ajaxTask = $this->getState('ajax');
$log = $this->getState('log');
switch($ajaxTask)
{
case 'start':
$tag = 'alice';
\AliceUtilLogger::WriteLog(true);
\AliceUtilLogger::WriteLog(_AE_LOG_INFO, 'Starting analysis');
\AliceCoreKettenrad::reset(array(
'maxrun'=> 0
));
\AliceUtilTempvars::reset($tag);
$kettenrad = \AliceCoreKettenrad::load($tag);
$options = array('logToAnalyze' => Factory::getLog()->getLogFilename($log));
$kettenrad->setup($options);
$kettenrad->tick();
if(($kettenrad->getState() != 'running'))
{
$kettenrad->tick();
}
$ret_array = $kettenrad->getStatusArray();
$kettenrad->resetWarnings(); // So as not to have duplicate warnings reports
\AliceCoreKettenrad::save($tag);
break;
case 'step':
$tag = 'alice';
$kettenrad = \AliceCoreKettenrad::load($tag);
$kettenrad->tick();
$ret_array = $kettenrad->getStatusArray();
$kettenrad->resetWarnings(); // So as not to have duplicate warnings reports
\AliceCoreKettenrad::save($tag);
if($ret_array['HasRun'] == 1)
{
// Let's get tests result
$config = \AliceFactory::getConfiguration();
$feedback = $config->get('volatile.alice.feedback');
$ret_array['Results'] = json_encode($feedback);
// Clean up
\AliceFactory::nuke();
\AliceUtilTempvars::reset($tag);
}
break;
default:
break;
}
return $ret_array;
}
}

View File

@@ -0,0 +1,479 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\PushMessages;
use Awf\Application\Application;
use Awf\Database\Driver;
use Awf\Date\Date;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Closure;
use Exception;
use Psr\Log\LogLevel;
class Backup extends Model
{
/**
* Starts or step a backup process. Set the state variable "ajax" to the task you want to execute OR call the
* relevant public method directly.
*
* @return array An Akeeba Engine return array
*/
public function runBackup()
{
$ret_array = [];
$ajaxTask = $this->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 = [])
{
/**
* Make sure the database schema is OK. Absolutely necessary in case the update is installed but the application
* has never been visited. Practical examples: WordPress automatic updates, user updating Solo/ABWP through FTP
* and in all cases only ever running scheduled and / or remove backups.
*/
try
{
/** @var Main $mainModel */
$mainModel = Model::getInstance($this->container->application_name, 'Main', $this->container);
$mainModel->checkAndFixDatabase();
}
catch (Exception $e)
{
// Hopefully the backup dies in an informative way, thank you very much.
}
// Get information from the session
$tag = $this->getState('tag', null, 'string');
$description = $this->getState('description', '', 'string');
$comment = $this->getState('comment', '', 'html');
$jpskey = $this->getState('jpskey', null, 'raw');
$angiekey = $this->getState('angiekey', null, 'raw');
$backupId = $this->getBackupId();
// Use the default description if none specified
$description = $description ?: $this->getDefaultDescription();
// Try resetting the engine
try
{
Factory::resetState([
'maxrun' => 0,
]);
}
catch (Exception $e)
{
// This will fail if the output directory is unwriteable / unreadable / missing.
}
// 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();
// Autofix the output directory
$confWizModel = new Wizard($this->container);
$confWizModel->autofixDirectories();
// Rebase Off-site Folder Inclusion filters to use site path variables
if (class_exists('\Solo\Model\Extradirs'))
{
$incFoldersModel = new Extradirs($this->container);
$incFoldersModel->rebaseFiltersToSiteDirs();
}
// 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.akeeba.com/documentation/warnings/q' . $checkItem['code'] . '.html for more information and troubleshooting instructions.',
'Warnings' => [],
'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);
/**
* Convert log files in the backup output directory
*
* This removes the obsolete, default log files (akeeba.(backend|frontend|cli|json).log and converts the old .log
* files into their .php counterparts.
*
* We are doing this when taking a new backup on top of the Control Panel page because some people might be
* installing updates and taking backups automatically, without visiting the Control Panel except in rare cases.
*/
/** @var Main $cpModel */
$cpModel = Model::getTmpInstance($this->container->application_name, 'Main', $this->container);
$cpModel->convertLogFiles(3);
// 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.
*
* Furthermore, if the first tick reaches the end of backup or an error condition we MUST NOT run the second
* tick() since the engine state will be invalid. Hence the check for the state that performs a hard break. This
* could happen if you have a sufficiently high max execution time, no break between steps and we fail to
* execute any step, e.g. the installer image is missing, a database error occurred or we can not list the files
* and directories to back up.
*
* THEREFORE, DO NOT REMOVE THE LOOP OR THE if-BLOCK IN IT, THEY ARE THERE FOR A GOOD REASON!
*/
$kettenrad->setIgnoreMinimumExecutionTime(true);
for ($i = 0; $i < 2; $i++)
{
$kettenrad->tick();
if (in_array($kettenrad->getState(), [Part::STATE_FINISHED, Part::STATE_ERROR]))
{
break;
}
$kettenrad->setIgnoreMinimumExecutionTime(false);
}
$ret_array = $kettenrad->getStatusArray();
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 information from the model state
$tag = $this->getState('tag', defined('AKEEBA_BACKUP_ORIGIN') ? AKEEBA_BACKUP_ORIGIN : null, 'string');
$backupId = $this->getState('backupid', null, 'string');
// Get the profile from the session, the AKEEBA_PROFILE constant or the model state in this order
$profile = max(0, (int) $this->getState('profile', 0)) ?: $this->getLastBackupProfile($tag, $backupId);
// Set the active profile
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
if (!defined('AKEEBA_PROFILE'))
{
define('AKEEBA_PROFILE', $profile);
}
// Run a backup step
$ret_array = [
'HasRun' => 0,
'Domain' => 'init',
'Step' => '',
'Substep' => '',
'Error' => '',
'Warnings' => [],
'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->tick();
$ret_array = $kettenrad->getStatusArray();
}
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();
$origin = Platform::getInstance()->get_backup_origin();
if (($origin == 'backend') && $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);
}
public function getDefaultDescription()
{
return Text::_('COM_AKEEBA_BACKUP_DEFAULT_DESCRIPTION') . ' ' .
Platform::getInstance()->get_local_timestamp(Text::_('DATE_FORMAT_LC2') . ' T');
}
/**
* 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
*/
public function getLastBackupProfile($tag, $backupId = null)
{
$filters = [
['field' => 'tag', 'value' => $tag],
];
if (!empty($backupId))
{
$filters[] = ['field' => 'backupid', 'value' => $backupId];
}
$statList = Platform::getInstance()->get_statistics_list([
'filters' => $filters,
'order' => [
'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;
}
/**
* Get a new backup ID string.
*
* In the past we were trying to get the next backup record ID using two methods:
* - Querying the information_schema.tables metadata table. In many cases we saw this returning the wrong value,
* even though the MySQL documentation said this should return the next autonumber (WTF?)
* - Doing a MAX(id) on the table and adding 1. This didn't work correctly if the latest records were deleted by the
* user.
*
* However, the backup ID does not need to be the same as the backup record ID. It only needs to be *unique*. So
* this time around we are using a simple, unique ID based on the current GMT date and time.
*
* @return string
*/
private function getBackupId(): string
{
$microtime = explode(' ', microtime(false));
$microseconds = (int) ($microtime[0] * 1000000);
return 'id-' . gmdate('Ymd-His') . '-' . $microseconds;
}
}

View File

@@ -0,0 +1,170 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Mvc\Model;
class Browser extends Model
{
/**
* Make a directory listing and push all relevant information back into the model state
*
* @return void
*/
function makeListing()
{
// Get the folder to browse
$folder = $this->getState('folder', '');
$processFolder = $this->getState('processfolder', 0);
$siteRoot = Factory::getFilesystemTools()->TranslateWinPath(APATH_BASE);
if (empty($folder))
{
$folder = $siteRoot;
}
$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...
$folder = Factory::getFilesystemTools()->TranslateWinPath($folder);
$old_folder = $folder;
$folder = @realpath($folder);
if ($folder === false)
{
$folder = $old_folder;
}
if (@is_dir($folder))
{
$isFolderThere = true;
}
else
{
$isFolderThere = false;
}
// Check if it's a subdirectory of the site's root
$isInRoot = (strpos($folder, $siteRoot) === 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);
}
// Writable check and contents listing if it's in site root and not restricted
if ($isFolderThere && !$isOpenbasedirRestricted)
{
// Get writability status
$isWritable = is_writable($folder);
// Get contained folders
$subFolders = array();
try
{
$di = new \DirectoryIterator($folder);
/** @var \DirectoryIterator $item */
foreach ($di as $item)
{
if ($item->isDot())
{
continue;
}
if (!$item->isDir())
{
continue;
}
$subFolders[] = $item->getFilename();
}
}
catch (\UnexpectedValueException $e)
{
$isWritable = false;
}
}
else
{
if ($isFolderThere && !$isOpenbasedirRestricted)
{
$isWritable = is_writable($folder);
}
else
{
$isWritable = false;
}
$subFolders = array();
}
// Get parent directory
$breadcrumbs = array();
$pathParts = explode('/', $folder);
if (is_array($pathParts))
{
$path = '';
foreach ($pathParts as $part)
{
$path .= empty($path) ? $part : '/' . $part;
if (empty($part))
{
if (DIRECTORY_SEPARATOR != '\\')
{
$path = '/';
}
$part = '/';
}
$crumb['label'] = $part;
$crumb['folder'] = $path;
$breadcrumbs[] = $crumb;
}
$junk = array_pop($pathParts);
$parent = implode('/', $pathParts);
}
else
{
// Can't identify parent dir, use ourselves.
$parent = $folder;
$breadcrumbs = array();
}
$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);
}
}

View File

@@ -0,0 +1,238 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Akeeba\Engine\Archiver\Directftp;
use Akeeba\Engine\Archiver\Directsftp;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\Transfer\FtpCurl;
use Akeeba\Engine\Util\Transfer\SftpCurl;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Exception;
use RuntimeException;
/**
* The Model for the Configuration view
*/
class Configuration extends Model
{
/**
* Save the engine configuration
*
* @return void
*/
public function saveEngineConfig()
{
$data = $this->getState('engineconfig', []);
// 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)
{
$this->container->application->enqueueMessage(Text::_('COM_AKEEBA_CONFIG_OUTDIR_ROOT'), 'warning');
$data['akeeba.basic.output_directory'] = '[DEFAULT_OUTPUT]';
}
else
{
$data['akeeba.basic.output_directory'] = Factory::getFilesystemTools()->rebaseFolderToStockDirs($data['akeeba.basic.output_directory']);
}
}
// 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
* @throws RuntimeException
*/
public function testFTP()
{
$config = [
'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(Text::_('COM_AKEEBA_CONFIG_FTPTEST_BADPREFIX'), 500);
}
// Special case for cURL transport
if ($this->getState('isCurl'))
{
$this->testFtpCurl();
return;
}
// Perform the FTP connection test
$test = new Directftp();
$test->initialize('', $config);
}
/**
* Test the SFTP connection.
*
* @return void
* @throws RuntimeException
*/
public function testSFTP()
{
$config = [
'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(Text::_('COM_AKEEBA_CONFIG_SFTPTEST_BADPREFIX'), 500);
}
// Special case for cURL transport
if ($this->getState('isCurl'))
{
$this->testSftpCurl();
return;
}
// Perform the FTP connection test
$test = new Directsftp();
$test->initialize('', $config);
}
/**
* Opens an OAuth window for the selected post-processing engine
*
* @return void
* @throws Exception
*/
public function dpeOAuthOpen()
{
$engine = $this->getState('engine');
$params = $this->getState('params', []);
// Get a callback URI for OAuth 2
$params['callbackURI'] = $this->container->router->route('index.php?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', []);
// Get the Input object
$params['input'] = $this->input->getData();
$engineObject = Factory::getPostprocEngine($engine);
if ($engineObject === false)
{
return false;
}
return $engineObject->customAPICall($method, $params);
}
/**
* Test the connection to a remote FTP server using cURL transport
*
* @return void
* @throws RuntimeException
*/
private function testFtpCurl()
{
$options = [
'host' => $this->getState('host'),
'port' => $this->getState('port'),
'username' => $this->getState('user'),
'password' => $this->getState('pass'),
'directory' => $this->getState('initdir'),
'usessl' => $this->getState('usessl'),
'passive' => $this->getState('passive'),
'passive_fix' => $this->getState('passive_mode_workaround'),
];
$sftpTransfer = new FtpCurl($options);
$sftpTransfer->connect();
}
/**
* Test the connection to a remote SFTP server using cURL transport
*
* @return void
* @throws RuntimeException
*/
private function testSftpCurl()
{
$options = [
'host' => $this->getState('host'),
'port' => $this->getState('port'),
'username' => $this->getState('user'),
'password' => $this->getState('pass'),
'directory' => $this->getState('initdir'),
'privateKey' => $this->getState('privkey'),
'publicKey' => $this->getState('pubkey'),
];
$sftpTransfer = new SftpCurl($options);
$sftpTransfer->connect();
}
}

View File

@@ -0,0 +1,439 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Akeeba\Engine\Factory;
use Awf\Mvc\Model;
use Awf\Text\Text;
use Exception;
class Dbfilters extends Model
{
/**
* 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 Which database definition to use
*
* @return array
*/
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();
}
$tableMeta = [];
try
{
$db->setQuery('SHOW TABLE STATUS');
$temp = $db->loadAssocList();
foreach ($temp as $record)
{
$tableMeta[$db->getAbstract($record['Name'])] = [
'engine' => $record['Engine'],
'rows' => $record['Rows'],
'dataLength' => $record['Data_length'],
'indexLength' => $record['Index_length'],
];
}
}
catch (Exception $e)
{
}
// Process filters
$tables = array();
if (!empty($table_data))
{
foreach ($table_data as $table_name => $table_type)
{
$status = [
'engine' => null,
'rows' => null,
'dataLength' => null,
'indexLength' => null,
];
if (array_key_exists($table_name, $tableMeta))
{
$status = $tableMeta[$table_name];
}
// 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);
if ($table_type != 'table')
{
// We can't filter contents of views, merge tables, black holes,
// procedures, functions and triggers :)
$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 = Text::_('COM_AKEEBA_DBFILTER_LABEL_SITEDB');
$entry = (object)array(
'value' => $name,
'text' => $root,
);
$ret[] = $entry;
}
return $ret;
}
/**
* Toggle a filter
*
* @param string $root Root directory
* @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, $item, $filter)
{
if (empty($item)) return array(
'success' => false,
'newstate' => false
);
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->toggle($root, $item, $new_status);
// Save the data on success
if ($success) $filters->save();
// Make a return array
return array(
'success' => $success,
'newstate' => $new_status
);
}
/**
* Set a filter
*
* @param string $root Root directory
* @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
*
* @return array
*/
public function remove($root, $item, $filter)
{
if (empty($item)) return array(
'success' => false,
'newstate' => false
);
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->remove($root, $item);
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
// The new state of the filter. It is removed if and only if the transaction succeeded
'newstate' => !$success
);
}
/**
* Set a filter
*
* @param string $root Root directory
* @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
*
* @return array
*/
public function setFilter($root, $item, $filter)
{
if (empty($item)) return array(
'success' => false,
'newstate' => false
);
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->set($root, $item);
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
// The new state of the filter. It is set if and only if the transaction succeeded
'newstate' => $success
);
}
/**
* Swap a filter
*
* @param string $root Root directory
* @param string $old_item The old child item of the current directory we want to set the filter for
* @param string $new_item The new 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, $old_item, $new_item, $filter)
{
if (empty($new_item)) return array(
'success' => false,
'newstate' => false
);
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
if (!empty($old_item))
{
$success = $filter->remove($root, $old_item);
}
else
{
$success = true;
}
if ($success)
{
$success = $filter->set($root, $new_item);
}
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
'newstate' => $success // The new state of the filter. It is set if and only if the transaction succeeded
);
}
/**
* 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)
{
// A reference to the global Akeeba Engine filter object
$filters = Factory::getFilters();
// Initialize the return array
$ret = array();
// Define the known filter types and loop through them
$filter_types = array('tables', 'tabledata');
foreach ($filter_types as $type)
{
$rawFilterData = $filters->getFilterData($type);
if (array_key_exists($root, $rawFilterData))
{
if (!empty($rawFilterData[$root]))
{
foreach ($rawFilterData[$root] as $node)
{
$ret[] = array(
// Make sure we get a COPY, not a reference to the original data
'node' => substr($node, 0),
'type' => $type
);
}
}
}
}
/*
* Return array format:
* [array] :
* [array] :
* 'node' => 'somedir'
* 'type' => 'directories'
* [array] :
* 'node' => 'somefile'
* 'type' => 'files'
* ...
*/
return $ret;
}
/**
* Resets the filters
*
* @param string $root Root directory
*
* @return array
*/
public function resetFilters($root)
{
// Get a reference to the global Filters object
$filters = Factory::getFilters();
$filter = Factory::getFilterObject('tables');
$filter->reset($root);
$filter = Factory::getFilterObject('tabledata');
$filter->reset($root);
$filters->save();
return $this->make_listing($root);
}
/**
* Performs an AJAX request based on the action set in the state
*
* @return array A return array, whose format depends on the verb of the action state variable
*/
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, $action->node);
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;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model\Exception;
use RuntimeException;
class TransferFatalError extends RuntimeException
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model\Exception;
use RuntimeException;
class TransferIgnorableError extends RuntimeException
{
}

View File

@@ -0,0 +1,664 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Mvc\Model;
use Awf\Text\Text;
class Fsfilters extends Model
{
/**
* 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);
if (($node == '/'))
{
// Just a dir. sep. is treated as no dir at all
$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);
asort($folders);
$folders_out = array();
if (!empty($folders))
{
foreach ($folders as $folder)
{
$folder = Factory::getFilesystemTools()->TranslateWinPath($folder);
$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);
$status['link'] = @is_link($directory . '/' . $folder);
// 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);
asort($files);
$files_out = array();
if (!empty($files))
{
foreach ($files as $file)
{
$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);
$status['link'] = @is_link($directory . '/' . $file);
// 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 string $crumbs
* @param string $child
*
* @return string
*/
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_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 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
$filter = Factory::getFilterObject('extradirs');
$directories_list = $filter->getInclusions('dir');
$ret = array(
array(
'value' => '[SITEROOT]',
'text' => Text::_('COM_AKEEBA_FILEFILTERS_LABEL_SITEROOT'),
)
);
if (!empty($directories_list))
{
foreach($directories_list as $root => $info)
{
$ret[] = array(
'value' => $root,
'text' => $info[0],
);
}
}
return $directories_list;
}
/**
* Returns an array with the listing and filter status of a directory
*
* @param string $root Root directory
* @param array $crumbs Components of the current directory relative to the root
* @param string $child The child directory of the current directory we want to scan
*
* @return array
*/
public function make_listing($root, $crumbs = array(), $child = null)
{
// 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)
{
if (empty($item))
{
return array(
'success' => false,
'newstate' => false
);
}
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the object to toggle
$node = $this->glue_crumbs($crumbs, $item);
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->toggle($root, $node, $new_status);
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
'newstate' => $new_status
);
}
/**
* 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)
{
if (empty($item))
{
return array(
'success' => false,
'newstate' => false
);
}
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the object to toggle
$node = $this->glue_crumbs($crumbs, $item);
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->set($root, $node);
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
'newstate' => $success // The new state of the filter. It is set if and only if the transaction succeeded
);
}
/**
* 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 set 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)
{
if (empty($item))
{
return array(
'success' => false,
'newstate' => false
);
}
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the object to toggle
$node = $this->glue_crumbs($crumbs, $item);
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
$success = $filter->remove($root, $node);
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
'newstate' => !$success // The new state of the filter. It is set if and only if the transaction succeeded
);
}
/**
* Swap a filter
*
* @param string $root Root directory
* @param array $crumbs Components of the current directory relative to the root
* @param string $old_item The old child item of the current directory we want to set the filter for
* @param string $new_item The new 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)
{
if (empty($new_item))
{
return array(
'success' => false,
'newstate' => false
);
}
// Get a reference to the global Filters object
$filters = Factory::getFilters();
// Get the object to toggle
$old_node = $this->glue_crumbs($crumbs, $old_item);
$new_node = $this->glue_crumbs($crumbs, $new_item);
// Get the specific filter object
$filter = Factory::getFilterObject($filter);
// Toggle the filter
if (!empty($old_item))
{
$success = $filter->remove($root, $old_node);
}
else
{
$success = true;
}
if ($success)
{
$success = $filter->set($root, $new_node);
}
// Save the data on success
if ($success)
{
$filters->save();
}
// Make a return array
return array(
'success' => $success,
'newstate' => $success // The new state of the filter. It is set if and only if the transaction succeeded
);
}
/**
* 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)
{
// A reference to the global Akeeba Engine filter object
$filters = Factory::getFilters();
// Initialize the return array
$ret = array();
// Define the known filter types and loop through them
$filter_types = array('directories', 'skipdirs', 'skipfiles', 'files');
foreach ($filter_types 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 array format:
* [array] :
* [array] :
* 'node' => 'somedir'
* 'type' => 'directories'
* [array] :
* 'node' => 'somefile'
* 'type' => 'files'
* ...
*/
return $ret;
}
/**
* Resets the filters
*
* @param string $root Root directory
*
* @return array
*/
public function resetFilters($root)
{
// Get a reference to the global Filters object
$filters = Factory::getFilters();
$filter = Factory::getFilterObject('directories');
$filter->reset($root);
$filter = Factory::getFilterObject('files');
$filter->reset($root);
$filter = Factory::getFilterObject('skipdirs');
$filter->reset($root);
$filter = Factory::getFilterObject('skipfiles');
$filter->reset($root);
$filters->save();
return $this->make_listing($root);
}
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);
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Solo\Model;
use Awf\Filesystem\Ftp;
use Awf\Mvc\Model;
class Ftpbrowser extends Model
{
public function doBrowse()
{
$dir = $this->getState('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 = '';
}
$options = array(
'host' => $this->getState('host'),
'port' => $this->getState('port'),
'username' => $this->getState('username'),
'password' => $this->getState('password'),
'ssl' => $this->getState('ssl'),
'passive' => $this->getState('passive'),
'directory' => $this->getState('directory'),
);
$list = false;
$error = '';
try
{
$ftp = new Ftp($options);
if(!$dir)
{
$dir = $ftp->cwd();
$parsed_dir = trim($dir, '/');
$parts = empty($parsed_dir) ? array() : explode('/', $parsed_dir);
$parent_directory = $dir;
}
$list = $ftp->listFolders();
}
catch (\RuntimeException $e)
{
$error = $e->getMessage();
}
$response_array = array(
'error' => $error,
'list' => $list,
'breadcrumbs' => $parts,
'directory' => $this->getState('directory'),
'parent' => $parent_directory
);
return $response_array;
}
}

View File

@@ -0,0 +1,214 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model;
use Akeeba\Engine\Platform;
use Awf\Mvc\Model;
use Solo\Model\Json\Encapsulation;
use Solo\Model\Json\Task;
// JSON API version number
define('AKEEBA_JSON_API_VERSION', '340');
/*
* Short API version history:
* 300 First draft. Basic backup working. Encryption semi-broken.
* 316 Fixed download feature.
* 320 Minor bug fixes
* 330 Introduction of Akeeba Solo
* 335 Configuration overrides in startBackup
* 340 Advanced API allows full configuration
*/
if (!defined('AKEEBA_BACKUP_ORIGIN'))
{
define('AKEEBA_BACKUP_ORIGIN', 'json');
}
class Json extends Model
{
const COM_AKEEBA_CPANEL_LBL_STATUS_OK = 200; // Normal reply
const STATUS_NOT_AUTH = 401; // Invalid credentials
const STATUS_NOT_ALLOWED = 403; // Not enough privileges
const STATUS_NOT_FOUND = 404; // Requested resource not found
const STATUS_INVALID_METHOD = 405; // Unknown JSON method
const COM_AKEEBA_CPANEL_LBL_STATUS_ERROR = 500; // An error occurred
const STATUS_NOT_IMPLEMENTED = 501; // Not implemented feature
const STATUS_NOT_AVAILABLE = 503; // Remote service not activated
/** @var int The status code */
private $status = 200;
/** @var int Data encapsulation format */
private $encapsulationType = 1;
/** @var Encapsulation */
private $encapsulation;
/** @var mixed Any data to be returned to the caller */
private $data = '';
/** @var string A password passed to us by the caller */
private $password = null;
/** @var string The method called by the client */
private $method_name = null;
/**
* Override the constructor to construct the objects we need to handle JSON API requests
*
* @param \Awf\Container\Container $container
*/
public function __construct(\Awf\Container\Container $container = null)
{
parent::__construct($container);
$this->encapsulation = new Encapsulation($this->serverKey());
}
public function execute($json)
{
// Check if we're activated
$enabled = Platform::getInstance()->get_platform_configuration_option('frontend_enable', 0);
// Is the Secret Key strong enough?
$validKey = $this->serverKey();
if (!\Akeeba\Engine\Util\Complexify::isStrongEnough($validKey, false))
{
$enabled = false;
}
$rawEncapsulation = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
if (!$enabled)
{
return $this->getResponse('Access denied', 503);
}
// Try to JSON-decode the request's input first
$request = @json_decode($json, true);
if (is_null($request))
{
return $this->getResponse('JSON decoding error', 500);
}
// Transform legacy requests
if (!is_array($request))
{
$request = array(
'encapsulation' => $rawEncapsulation,
'body' => $request
);
}
// Transform partial requests
if (!isset($request['encapsulation']))
{
$request['encapsulation'] = $rawEncapsulation;
}
// Make sure we have a request body
if (!isset($request['body']))
{
$request['body'] = '';
}
try
{
$request['body'] = $this->encapsulation->decode($request['encapsulation'], $request['body']);
}
catch (\Exception $e)
{
return $this->getResponse($e->getMessage(), $e->getCode());
}
// Replicate the encapsulation preferences of the client for our own output
$this->encapsulationType = $request['encapsulation'];
// Store the client-specified key, or use the server key if none specified and the request
// came encrypted.
$this->password = isset($request['body']['key']) ? $request['body']['key'] : $this->serverKey();
// Run the method
$params = array();
if (isset($request['body']['data']))
{
$params = (array)$request['body']['data'];
}
try
{
$taskHandler = new Task();
$data = $taskHandler->execute($request['body']['method'], $params);
}
catch (\RuntimeException $e)
{
return $this->getResponse($e->getMessage(), $e->getCode());
}
return $this->getResponse($data);
}
/**
* Packages the response to a JSON-encoded object, optionally encrypting the data part with a caller-supplied
* password.
*
* @return string The JSON-encoded response
*/
private function getResponse($data, $status = 200)
{
// Initialize the response
$response = array(
'encapsulation' => $this->encapsulationType,
'body' => array(
'status' => $status,
'data' => null
)
);
if ($status != 200)
{
$response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
}
try
{
$response['body']['data'] = $this->encapsulation->encode($response['encapsulation'], $data, $this->password);
}
catch (\Exception $e)
{
$response['encapsulation'] = $this->encapsulation->getEncapsulationByCode('ENCAPSULATION_RAW');
$response['body'] = array(
'status' => $e->getCode(),
'data' => $e->getMessage(),
);
}
return '###' . json_encode($response) . '###';
}
/**
* Get the server key, i.e. the Secret Word for the front-end backups and JSON API
*
* @return mixed
*/
private function serverKey()
{
static $key = null;
if (is_null($key))
{
$key = Platform::getInstance()->get_platform_configuration_option('frontend_secret_word', '');
}
return $key;
}
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json;
/**
* Handles data encapsulation
*/
class Encapsulation
{
/**
* Known encapsulation handlers
*
* @var EncapsulationInterface[]
*/
protected $handlers = array();
/**
* List of encapsulation types
*
* @var array
*/
protected $encapsulations = array();
/**
* The server key used to decrypt / encrypt data and check the authorisation
*
* @var string
*/
protected $serverKey;
/**
* Public constructor
*
* @param string $serverKey The server key used for data encyrption/decryption and authorisation checks
*/
public function __construct($serverKey)
{
$this->serverKey = $serverKey;
// Populate the list of encapsulation handlers
$this->initialiseHandlers();
}
/**
* Returns the encapsulation ID given its code. For example given $code == 'ENCAPSULATION_AESCTR256' it will return
* the ID integer 3.
*
* @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256
*
* @return int The numeric ID, e.g. 3
*/
public function getEncapsulationByCode($code)
{
$info = $this->getEncapsulationInfoByCode($code);
return $info['id'];
}
/**
* Returns the encapsulation information array given its code. For example given $code == 'ENCAPSULATION_AESCTR256'
* it will return the information for the data in AES-256 stream (CTR) mode encrypted JSON type.
*
* @param string $code The encapsulation code, e.g. ENCAPSULATION_AESCTR256
*
* @return array The information of the encapsulation handler
*/
public function getEncapsulationInfoByCode($code)
{
// Normalise the code
$code = strtoupper($code);
// If we have no idea what the encapsulation should be revert to raw (plain text)
if (!isset($this->encapsulations[$code]))
{
return $this->encapsulations['ENCAPSULATION_RAW'];
}
return $this->encapsulations[$code];
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param int $encapsulation The encapsulation type
* @param string $data Encoded data
*
* @return array The decoded data.
*
* @throw \RuntimeException When the server capabilities don't match the requested encapsulation
* @throw \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($encapsulation, $data)
{
$body = null;
// Find the suitable handler and encode the data
foreach ($this->handlers as $handler)
{
if ($handler->isSupported($encapsulation))
{
$body = $handler->decode($this->serverKey, $data);
break;
}
}
// If the data cannot be encoded throw an exception
if (!isset($handler) || is_null($body))
{
throw new \RuntimeException('The requested encapsulation type is not supported', 503);
}
$authorised = true;
$body = rtrim($body, chr(0));
// Make sure it looks like a valid JSON string and is at least 12 characters (minimum valid message length)
if ((strlen($body) < 12) || (substr($body, 0, 1) != '{') || (substr($body, -1) != '}'))
{
$authorised = false;
}
// Try to JSON decode the body
if ($authorised)
{
$body = json_decode($body, true);
if (is_null($body))
{
$authorised = false;
}
elseif (!is_array($body))
{
$authorised = false;
}
}
// Make sure there is a requested method
if ($authorised)
{
if (!isset($body['method']) || empty($body['method']))
{
$authorised = false;
}
}
if ($authorised)
{
$authorised = $handler->isAuthorised($this->serverKey, $body);
}
if (!$authorised)
{
throw new \InvalidArgumentException('Authentication failed', 401);
}
return (array)$body;
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param int $encapsulation The encapsulation type
* @param mixed $data The data to encode, typically a string, array or object
* @param string $key Key to use for encoding. If not provided we revert to $this->serverKey
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throw \RuntimeException When the server capabilities don't match the requested encapsulation
* @throw \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($encapsulation, $data, $key = null)
{
// Try to JSON-encode the data
$data = json_encode($data);
// If the data cannot be JSON-encoded throw an exception
if ($data === false)
{
throw new \InvalidArgumentException('Data cannot be encapsulated in the requested format', 500);
}
// Make sure we have a valid key
if (empty($key))
{
$key = $this->serverKey;
}
// Find the suitable handler and encode the data
foreach ($this->handlers as $handler)
{
if ($handler->isSupported($encapsulation))
{
return $handler->encode($key, $data);
}
}
// If the data cannot be encoded throw an exception
throw new \RuntimeException('Data cannot be encapsulated in the requested format', 500);
}
/**
* Initialises the encapsulation handlers
*
* @return void
*/
protected function initialiseHandlers()
{
// Reset the arrays
$this->handlers = array();
$this->encapsulations = array();
// Look all files in the Encapsulation handlers' directory
$dh = new \DirectoryIterator(__DIR__ . '/Encapsulation');
/** @var \DirectoryIterator $entry */
foreach ($dh as $entry)
{
$fileName = $entry->getFilename();
// Ignore non-PHP files
if (substr($fileName, -4) != '.php')
{
continue;
}
// Ignore the Base class
if ($fileName == 'Base.php')
{
continue;
}
// Get the class name
$className = '\\Solo\\Model\\Json\\Encapsulation\\' . substr($fileName, 0, -4);
// Check if the class really exists
if (!class_exists($className, true))
{
continue;
}
/** @var EncapsulationInterface $o */
$o = new $className;
$info = $o->getInformation();
$this->encapsulations[$info['code']] = $info;
$this->handlers[] = $o;
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
use Akeeba\Engine\Factory;
/**
* AES CBC 128 encapsulation
*/
class AesCbc128 extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(4, 'ENCAPSULATION_AESCBC128', ' Data in AES-128 standard (CBC) mode encrypted JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
$data = base64_decode($data);
return $this->getEncryption()->AESDecryptCBC($data, $serverKey, 128);
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return base64_encode($this->getEncryption()->AESEncryptCBC($data, $serverKey, 128));
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
use Akeeba\Engine\Factory;
/**
* AES CBC 256 encapsulation
*/
class AesCbc256 extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(5, 'ENCAPSULATION_AESCBC256', ' Data in AES-256 standard (CBC) mode encrypted JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
$data = base64_decode($data);
return $this->getEncryption()->AESDecryptCBC($data, $serverKey, 256);
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return base64_encode($this->getEncryption()->AESEncryptCBC($data, $serverKey, 256));
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
use Akeeba\Engine\Factory;
/**
* AES CTR 128 encapsulation
*/
class AesCtr128 extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(2, 'ENCAPSULATION_AESCTR128', 'Data in AES-128 stream (CTR) mode encrypted JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
return $this->getEncryption()->AESDecryptCtr($data, $serverKey, 128);
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return $this->getEncryption()->AESEncryptCtr($data, $serverKey, 128);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
use Akeeba\Engine\Factory;
/**
* AES CTR 256 encapsulation
*/
class AesCtr256 extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(3, 'ENCAPSULATION_AESCTR256', 'Data in AES-256 stream (CTR) mode encrypted JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
return $this->getEncryption()->AESDecryptCtr($data, $serverKey, 256);
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return $this->getEncryption()->AESEncryptCtr($data, $serverKey, 256);
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\Encrypt;
use Solo\Model\Json\EncapsulationInterface;
abstract class Base implements EncapsulationInterface
{
/**
* The numeric ID of this encapsulation
*
* @var int
*/
protected $id = 0;
/**
* The code of this encapsulation
*
* @var string
*/
protected $code = 'ENCAPSULATION_VOID';
/**
* The description of this encapsulation
*
* @var string
*/
protected $description = 'Invalid encapsulation';
/**
* The encryption object which is set up for use with the JSON API
*
* @var Encrypt
*/
private $encryption;
/**
* Public constructor. Called by children to customise the encapsulation handler object
*
* @param int $id Numeric ID
* @param string $code Code
* @param string $description Human readable description
*/
function __construct($id, $code, $description)
{
$this->id = $id;
$this->code = strtoupper($code);
$this->description = $description;
}
/**
* Returns information about the encapsulation supported by this class. The return array has the following keys:
* id: The numeric ID of the encapsulation, e.g. 3
* code: The short code of the encapsulation, e.g. ENCAPSULATION_AESCTR256
* description: A human readable descriptions, e.g. "Data in AES-256 stream (CTR) mode encrypted JSON"
*
* @return array See above
*/
public function getInformation()
{
return array(
'id' => $this->id,
'code' => $this->code,
'description' => $this->description,
);
}
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body)
{
return true;
}
/**
* Is the provided encapsulation type supported by this class?
*
* @param int $encapsulation Encapsulation type
*
* @return bool True if supported
*/
public function isSupported($encapsulation)
{
return $encapsulation == $this->id;
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
}
/**
* Returns an encryption object normalized for use in the JSON API: PBKDF2 uses a dynamic salt with SHA1 algorithm.
* This is necessary when we are running a backup against a profile which uses a static salt. In this case the
* static salt is not included in the ciphertext, making it impossible for the remote side to decipher our message,
* leading to backup failure.
*
* @return Encrypt
*/
protected function getEncryption()
{
if (is_null($this->encryption))
{
$encryption = Factory::getEncryption();
$this->encryption = clone $encryption;
$this->encryption->setPbkdf2UseStaticSalt(false);
$this->encryption->setPbkdf2Algorithm('sha1');
}
return $this->encryption;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Encapsulation;
/**
* Raw (plain text) encapsulation
*/
class Raw extends Base
{
/**
* Constructs the encapsulation handler object
*/
function __construct()
{
parent::__construct(1, 'ENCAPSULATION_RAW', 'Data in plain-text JSON');
}
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it and then JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data)
{
return $data;
}
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data)
{
return $data;
}
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body)
{
$authenticated = false;
if (isset($body['challenge']) && (strpos($body['challenge'], ':') >= 2) && (strlen($body['challenge']) >= 3))
{
list ($challengeData, $providedHash) = explode(':', $body['challenge']);
$computedHash = strtolower(md5($challengeData . $serverKey));
$authenticated = ($computedHash == $providedHash);
}
return $authenticated;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json;
/**
* Interface for Encapsulation data handlers
*/
interface EncapsulationInterface
{
/**
* Is the provided encapsulation type supported by this class?
*
* @param int $encapsulation Encapsulation type
*
* @return bool True if supported
*/
public function isSupported($encapsulation);
/**
* Returns information about the encapsulation supported by this class. The return array has the following keys:
* id: The numeric ID of the encapsulation, e.g. 3
* code: The short code of the encapsulation, e.g. ENCAPSULATION_AESCTR256
* description: A human readable descriptions, e.g. "Data in AES-256 stream (CTR) mode encrypted JSON"
*
* @return array See above
*/
public function getInformation();
/**
* Decodes the data. For encrypted encapsulations this means base64-decoding the data, decrypting it but *NOT* JSON-
* decoding the result. If any error occurs along the way the appropriate exception is thrown.
*
* The data being decoded corresponds to the Request Body described in the API documentation
*
* @param string $serverKey The server key we need to decode data
* @param string $data Encoded data
*
* @return string The decoded data.
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be decoded successfully
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02.html
*/
public function decode($serverKey, $data);
/**
* Encodes the data. The data is JSON encoded by this method before encapsulation takes place. Encrypted
* encapsulations will then encrypt the data and base64-encode it before returning it.
*
* The data being encoded correspond to the body > data structure described in the API documentation
*
* @param string $serverKey The server key we need to encode data
* @param mixed $data The data to encode, typically a string, array or object
*
* @return string The encapsulated data
*
* @see https://www.akeebabackup.com/documentation/json-api/ar01s02s02.html
*
* @throws \RuntimeException When the server capabilities don't match the requested encapsulation
* @throws \InvalidArgumentException When $data cannot be converted to JSON
*/
public function encode($serverKey, $data);
/**
* Checks if the request body authorises the user to use the API. Each encapsulation can implement its own
* authorisation method. This method is only called after the request body has been successfully decoded, therefore
* encrypted encapsulations can simply return true.
*
* @param string $serverKey The server key we need to check the authorisation
* @param array $body The decoded body (as returned by the decode() method)
*
* @return bool True if authorised
*/
public function isAuthorised($serverKey, $body);
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json;
/**
* Handles task execution
*/
class Task
{
/** @var TaskInterface[] The task handlers known to us */
protected $handlers = array();
/**
* Public constructor. Populates the list of task handlers.
*/
public function __construct()
{
// Populate the list of task handlers
$this->initialiseHandlers();
}
/**
* Do I have a specific task handling method?
*
* @param string $method The method to check for
*
* @return bool
*/
public function hasMethod($method)
{
$method = strtolower($method);
return isset($this->handlers[$method]);
}
/**
* Execute a JSON API method
*
* @param string $method The method's name
* @param array $parameters The parameters to the method (optional)
*
* @return mixed
*
* @throws \RuntimeException When the method requested is not known to us
*/
public function execute($method, $parameters = array())
{
if (!$this->hasMethod($method))
{
throw new \RuntimeException("Invalid method $method", 405);
}
$method = strtolower($method);
return $this->handlers[$method]->execute($parameters);
}
/**
* Initialises the encapsulation handlers
*
* @return void
*/
protected function initialiseHandlers()
{
// Reset the array
$this->handlers = array();
// Look all files in the Task handlers' directory
$dh = new \DirectoryIterator(__DIR__ . '/Task');
/** @var \DirectoryIterator $entry */
foreach ($dh as $entry)
{
$fileName = $entry->getFilename();
// Ignore non-PHP files
if (substr($fileName, -4) != '.php')
{
continue;
}
// Ignore the Base class
if ($fileName == 'Base.php')
{
continue;
}
// Get the class name
$className = '\\Solo\\Model\\Json\\Task\\' . substr($fileName, 0, -4);
// Check if the class really exists
if (!class_exists($className, true))
{
continue;
}
/** @var TaskInterface $o */
$o = new $className;
$name = $o->getMethodName();
$name = strtolower($name);
$this->handlers[$name] = $o;
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Solo\Model\Browser;
use Solo\Model\Json\TaskInterface;
/**
* Return folder browser results
*/
class Browse implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'browse';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
$filter = \Awf\Input\Filter::getInstance();
// Get the passed configuration values
$defConfig = array(
'folder' => '',
'processfolder' => 0
);
$defConfig = array_merge($defConfig, $parameters);
$folder = $filter->clean($defConfig['folder'], 'string');
$processFolder = $filter->clean($defConfig['processfolder'], 'bool');
/** @var \Solo\Model\Browser $model */
$model = new Browser();
$model->setState('folder', $folder);
$model->setState('processfolder', $processFolder);
$model->makeListing();
$ret = array(
'folder' => $model->getState('folder'),
'folder_raw' => $model->getState('folder_raw'),
'parent' => $model->getState('parent'),
'exists' => $model->getState('exists'),
'inRoot' => $model->getState('inRoot'),
'openbasedirRestricted' => $model->getState('openbasedirRestricted'),
'writable' => $model->getState('writable'),
'subfolders' => $model->getState('subfolders'),
'breadcrumbs' => $model->getState('breadcrumbs'),
);
return $ret;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Solo\Model\Json\TaskInterface;
use Solo\Model\Manage;
/**
* Delete a backup record
*/
class Delete implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'delete';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$model = new Manage();
$model->setState('id', $backup_id);
try
{
$model->delete();
}
catch (\Exception $e)
{
throw new \RuntimeException($e->getMessage(), 500);
}
return true;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Solo\Model\Json\TaskInterface;
use Solo\Model\Manage;
/**
* Delete the backup archives of a backup record
*/
class DeleteFiles implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'deleteFiles';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$model = new Manage();
$model->setState('id', $backup_id);
try
{
$model->deleteFile();
}
catch (\Exception $e)
{
throw new \RuntimeException($e->getMessage(), 500);
}
return true;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Solo\Model\Json\TaskInterface;
use Solo\Model\Profiles;
/**
* Delete a backup profile
*/
class DeleteProfile implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'deleteProfile';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$profile = (int)$defConfig['profile'];
// You need to specify the profile
if (empty($profile))
{
throw new \RuntimeException('Invalid profile ID', 404);
}
if ($profile == 1)
{
throw new \RuntimeException('You cannot delete the default backup profile', 404);
}
// Get a profile model
$profileModel = new Profiles();
$profileModel->findOrFail($profile);
$profileModel->delete();
return true;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Solo\Model\Json\TaskInterface;
/**
* Download a chunk of a backup archive over HTTP
*/
class Download implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'download';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
'part_id' => 1,
'segment' => 1,
'chunk_size' => 1
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$part_id = (int)$defConfig['part_id'];
$segment = (int)$defConfig['segment'];
$chunk_size = (int)$defConfig['chunk_size'];
$backup_stats = Platform::getInstance()->get_statistics($backup_id);
if (empty($backup_stats))
{
// Backup record doesn't exist
throw new \RuntimeException('Invalid backup record identifier', 404);
}
$files = Factory::getStatistics()->get_all_filenames($backup_stats);
if ((count($files) < $part_id) || ($part_id <= 0))
{
// Invalid part
throw new \RuntimeException('Invalid backup part', 404);
}
$file = $files[ $part_id - 1 ];
$filesize = @filesize($file);
$seekPos = $chunk_size * 1048576 * ($segment - 1);
if ($seekPos > $filesize)
{
// Trying to seek past end of file
throw new \RuntimeException('Invalid segment', 404);
}
$fp = fopen($file, 'rb');
if ($fp === false)
{
// Could not read file
throw new \RuntimeException('Error reading backup archive', 500);
}
rewind($fp);
if (fseek($fp, $seekPos, SEEK_SET) === -1)
{
// Could not seek to position
throw new \RuntimeException('Error reading specified segment', 500);
}
$buffer = fread($fp, 1048576);
if ($buffer === false)
{
throw new \RuntimeException('Error reading specified segment', 500);
}
fclose($fp);
return base64_encode($buffer);
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Application\Application;
use Solo\Model\Json\TaskInterface;
/**
* Download an entire backup archive directly over HTTP
*/
class DownloadDirect implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'downloadDirect';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
'part_id' => 1,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
$part_id = (int)$defConfig['part_id'];
$container = Application::getInstance()->getContainer();
$backup_stats = Platform::getInstance()->get_statistics($backup_id);
if (empty($backup_stats))
{
// Backup record doesn't exist
@ob_end_clean();
header('HTTP/1.1 500 Invalid backup record identifier');
flush();
$container->application->close();
}
$files = Factory::getStatistics()->get_all_filenames($backup_stats);
if ((count($files) < $part_id) || ($part_id <= 0))
{
// Invalid part
@ob_end_clean();
header('HTTP/1.1 500 Invalid backup part');
flush();
$container->application->close();
}
$filename = $files[ $part_id - 1 ];
@clearstatcache();
// For a certain unmentionable browser
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 file size, 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 > 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);
}
}
else
{
// If the filesize is not reported, hope that readfile works
@readfile($filename);
}
flush();
$container->application->close();
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Factory;
use Solo\Application;
use Solo\Model\Json\TaskInterface;
use Solo\Model\Profiles;
/**
* Export the profile's configuration
*/
class ExportConfiguration implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'exportConfiguration';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$profile_id = (int)$defConfig['profile'];
if ($profile_id <= 0)
{
$profile_id = 1;
}
/** @var Profiles $profile */
$profile = new Profiles();
$data = $profile->findOrFail($profile_id)->getData();
if (substr($data['configuration'], 0, 12) == '###AES128###')
{
// Load the server key file if necessary
if (!defined('AKEEBA_SERVERKEY'))
{
$filename = \Awf\Application\Application::getInstance()->getContainer()->basePath . '/engine/secretkey.php';
include_once $filename;
}
$key = Factory::getSecureSettings()->getKey();
$data['configuration'] = Factory::getSecureSettings()->decryptSettings($data['configuration'], $key);
}
return array(
'description' => $data['description'],
'configuration' => $data['configuration'],
'filters' => $data['filters'],
);
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Solo\Model\Json\TaskInterface;
/**
* Get information for a given backup record
*/
class GetBackupInfo implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getBackupInfo';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'backup_id' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$backup_id = (int)$defConfig['backup_id'];
// Get the basic statistics
$record = Platform::getInstance()->get_statistics($backup_id);
// Get a list of filenames
$backup_stats = Platform::getInstance()->get_statistics($backup_id);
// Backup record doesn't exist
if (empty($backup_stats))
{
throw new \RuntimeException('Invalid backup record identifier', 404);
}
$filenames = Factory::getStatistics()->get_all_filenames($record);
if (empty($filenames))
{
// Archives are not stored on the server or no files produced
$record['filenames'] = array();
}
else
{
$filedata = array();
$i = 0;
// Get file sizes per part
foreach ($filenames as $file)
{
$i++;
$size = @filesize($file);
$size = is_numeric($size) ? $size : 0;
$filedata[] = array(
'part' => $i,
'name' => basename($file),
'size' => $size
);
}
// Add the file info to $record['filenames']
$record['filenames'] = $filedata;
}
return $record;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Dbfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the database entities along with their filtering status (typically for rendering a GUI)
*/
class GetDBEntities implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getDBEntities';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
$filter = \Awf\Input\Filter::getInstance();
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
'root' => '[SITEDB]',
);
$defConfig = array_merge($defConfig, $parameters);
$profile = $filter->clean($defConfig['profile'], 'int');
$root = $filter->clean($defConfig['root'], 'string');
// We need a valid profile ID
if ($profile <= 0)
{
$profile = 1;
}
// We need a root
if (empty($root))
{
throw new \RuntimeException('Unknown database root', 500);
}
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Dbfilters $model */
$model = new Dbfilters();
return $model->make_listing($root);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Dbfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the database filters
*/
class GetDBFilters implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getDBFilters';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
$filter = \Awf\Input\Filter::getInstance();
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
'root' => '[SITEDB]',
);
$defConfig = array_merge($defConfig, $parameters);
$profile = $filter->clean($defConfig['profile'], 'int');
$root = $filter->clean($defConfig['root'], 'string');
// We need a valid profile ID
if ($profile <= 0)
{
$profile = 1;
}
// We need a root
if (empty($root))
{
throw new \RuntimeException('Unknown database root', 500);
}
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Dbfilters $model */
$model = new Dbfilters();
return $model->get_filters($root);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Dbfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the database roots (database definitions)
*/
class GetDBRoots implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getDBRoots';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$profile = (int)$defConfig['profile'];
if ($profile <= 0)
{
$profile = 1;
}
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Dbfilters $model */
$model = new Dbfilters();
return $model->get_roots();
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Fsfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the filesystem entities along with their filtering status (typically for rendering a GUI)
*/
class GetFSEntities implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getFSEntities';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
$filter = \Awf\Input\Filter::getInstance();
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
'root' => '[SITEROOT]',
'subdirectory' => '',
);
$defConfig = array_merge($defConfig, $parameters);
$profile = $filter->clean($defConfig['profile'], 'int');
$root = $filter->clean($defConfig['root'], 'string');
$subdirectory = $filter->clean($defConfig['subdirectory'], 'path');
$crumbs = array();
// We need a valid profile ID
if ($profile <= 0)
{
$profile = 1;
}
// We need a root
if (empty($root))
{
throw new \RuntimeException('Unknown filesystem root', 500);
}
// Get the subdirectory and explode it to its parts
if (!empty($subdirectory))
{
$subdirectory = trim($subdirectory, '/');
}
if (!empty($subdirectory))
{
$crumbs = explode('/', $subdirectory);
}
// Set the active profile
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Fsfilters $model */
$model = new Fsfilters();
return $model->make_listing($root, $crumbs);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Fsfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the filesystem filters
*/
class GetFSFilters implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getFSFilters';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
$filter = \Awf\Input\Filter::getInstance();
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
'root' => '[SITEROOT]',
);
$defConfig = array_merge($defConfig, $parameters);
$profile = $filter->clean($defConfig['profile'], 'int');
$root = $filter->clean($defConfig['root'], 'string');
// We need a valid profile ID
if ($profile <= 0)
{
$profile = 1;
}
// We need a root
if (empty($root))
{
throw new \RuntimeException('Unknown filesystem root', 500);
}
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Fsfilters $model */
$model = new Fsfilters();
return $model->get_filters($root);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model\Json\Task;
use Akeeba\Engine\Platform;
use Solo\Application;
use Solo\Model\Fsfilters;
use Solo\Model\Json\TaskInterface;
/**
* Get the filesystem roots (site root and extra included directories)
*/
class GetFSRoots implements TaskInterface
{
/**
* Return the JSON API task's name ("method" name). Remote clients will use it to call us.
*
* @return string
*/
public function getMethodName()
{
return 'getFSRoots';
}
/**
* Execute the JSON API task
*
* @param array $parameters The parameters to this task
*
* @return mixed
*
* @throws \RuntimeException In case of an error
*/
public function execute(array $parameters = array())
{
// Get the passed configuration values
$defConfig = array(
'profile' => 0,
);
$defConfig = array_merge($defConfig, $parameters);
$profile = (int)$defConfig['profile'];
if ($profile <= 0)
{
$profile = 1;
}
$session = Application::getInstance()->getContainer()->segment;
$session->set('profile', $profile);
// Load the configuration
Platform::getInstance()->load_configuration($profile);
/** @var \Solo\Model\Fsfilters $model */
$model = new Fsfilters();
return $model->get_roots();
}
}

Some files were not shown because too many files have changed in this diff Show More