485 lines
13 KiB
PHP
485 lines
13 KiB
PHP
<?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);
|
|
}
|
|
}
|
|
}
|