mirror of
https://github.com/Combodo/iTop.git
synced 2026-10-28 18:18:57 +01:00
Compare commits
3 Commits
support/3.
...
feature/95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05ca95a2d8 | ||
|
|
e1eaf3b795 | ||
|
|
554d813050 |
@@ -29,6 +29,16 @@ require_once(APPROOT.'/application/application.inc.php');
|
||||
|
||||
require_once(APPROOT.'core/mutex.class.inc.php');
|
||||
|
||||
const BACKUP_DIR = APPROOT.'data/backups/';
|
||||
|
||||
class AjaxBackupException extends CoreException
|
||||
{
|
||||
public function __construct(array $aContextData = null, Exception $oPreviousException = null)
|
||||
{
|
||||
parent::__construct('Backup/Restore exception occurred', $aContextData, oPrevious: $oPreviousException);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WebPage $oPage
|
||||
* @param string $sHtmlErrorMessage the whole HTML error, cinluding div/p/...
|
||||
@@ -51,73 +61,79 @@ $sOperation = utils::ReadParam('operation', '');
|
||||
$oPage = new AjaxPage('');
|
||||
$oPage->SetContentType('text/html');
|
||||
|
||||
/**
|
||||
* Check security
|
||||
*/
|
||||
switch ($sOperation) {
|
||||
/**
|
||||
* Can't use normal check methods (DoLogin for ex) as the datamodel can't be loaded here
|
||||
* So we're only using a token generated in the restore_token operation
|
||||
*/
|
||||
case 'restore_exec':
|
||||
IssueLog::Enable(APPROOT.'log/error.log');
|
||||
if (utils::GetConfig()->Get('demo_mode')) {
|
||||
DisplayErrorAndDie(
|
||||
$oPage,
|
||||
'<div data-error-stimulus="Error">Sorry, '.ITOP_APPLICATION_SHORT.' is in <b>demonstration mode</b>: the feature is disabled.</div>'
|
||||
);
|
||||
}
|
||||
|
||||
$sToken = utils::ReadParam('token', '', false, 'raw_data');
|
||||
$sBasePath = APPROOT.'/data/';
|
||||
$sTokenFile = $sBasePath.'restore.'.$sToken.'.tok';
|
||||
$tokenRealPath = utils::RealPath($sTokenFile, $sBasePath);
|
||||
if (($tokenRealPath === false) || (!is_file($tokenRealPath))) {
|
||||
IssueLog::Error("ajax.backup.php operation=$sOperation ERROR = inexisting token $sToken");
|
||||
$sEscapedToken = utils::HtmlEntities($sToken);
|
||||
DisplayErrorAndDie($oPage, "<p>Error: missing token file: '$sEscapedToken'</p>");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
|
||||
LoginWebPage::DoLogin();
|
||||
|
||||
$sTransactionId = utils::ReadParam('transaction_id', '', true, 'transaction_id');
|
||||
// the consumer page is not reloaded after download, we need to keep the transaction_id
|
||||
$bRemoveTransactionId = ($sOperation !== 'download');
|
||||
if (!utils::IsTransactionValid($sTransactionId, $bRemoveTransactionId)) {
|
||||
$sEscapedOperation = utils::HtmlEntities($sOperation);
|
||||
DisplayErrorAndDie($oPage, "<div data-error-stimulus=\"Error\">Error: invalid Transaction ID. The operation '$sEscapedOperation' was NOT performed!</div>");
|
||||
}
|
||||
|
||||
ApplicationMenu::CheckMenuIdEnabled('BackupStatus');
|
||||
|
||||
if (utils::GetConfig()->Get('demo_mode')) {
|
||||
DisplayErrorAndDie($oPage, '<div data-error-stimulus="Error">Sorry, '.ITOP_APPLICATION_SHORT.' is in <b>demonstration mode</b>: the feature is disabled.</div>');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup from an interactive session
|
||||
*/
|
||||
try {
|
||||
|
||||
/**
|
||||
* Check security
|
||||
*/
|
||||
switch ($sOperation) {
|
||||
/**
|
||||
* Can't use normal check methods (DoLogin for ex) as the datamodel can't be loaded here
|
||||
* So we're only using a token generated in the restore_token operation
|
||||
*/
|
||||
case 'restore_exec':
|
||||
IssueLog::Enable(APPROOT.'log/error.log');
|
||||
if (utils::GetConfig()->Get('demo_mode')) {
|
||||
DisplayErrorAndDie(
|
||||
$oPage,
|
||||
'<div data-error-stimulus="Error">Sorry, '.ITOP_APPLICATION_SHORT.' is in <b>demonstration mode</b>: the feature is disabled.</div>'
|
||||
);
|
||||
}
|
||||
|
||||
$sToken = utils::ReadParam('token', '', false, 'raw_data');
|
||||
$sBasePath = APPROOT.'/data/';
|
||||
$sTokenFile = $sBasePath.'restore.'.$sToken.'.tok';
|
||||
$tokenRealPath = utils::RealPath($sTokenFile, $sBasePath);
|
||||
if (($tokenRealPath === false) || (!is_file($tokenRealPath))) {
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => 'Token file not found',
|
||||
'token' => $sToken,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
require_once(APPROOT.'/application/startup.inc.php');
|
||||
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
|
||||
|
||||
LoginWebPage::DoLogin();
|
||||
|
||||
$sTransactionId = utils::ReadParam('transaction_id', '', true, 'transaction_id');
|
||||
// the consumer page is not reloaded after download, we need to keep the transaction_id
|
||||
$bRemoveTransactionId = ($sOperation !== 'download');
|
||||
if (!utils::IsTransactionValid($sTransactionId, $bRemoveTransactionId)) {
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => 'Invalid transaction id',
|
||||
'transaction_id' => $sTransactionId,
|
||||
]);
|
||||
}
|
||||
|
||||
ApplicationMenu::CheckMenuIdEnabled('BackupStatus');
|
||||
|
||||
if (utils::GetConfig()->Get('demo_mode')) {
|
||||
DisplayErrorAndDie($oPage, '<div data-error-stimulus="Error">Sorry, '.ITOP_APPLICATION_SHORT.' is in <b>demonstration mode</b>: the feature is disabled.</div>');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup from an interactive session
|
||||
*/
|
||||
switch ($sOperation) {
|
||||
case 'backup':
|
||||
try {
|
||||
set_time_limit(0);
|
||||
$oBB = new BackupExec(APPROOT.'data/backups/manual/', 0 /*iRetentionCount*/);
|
||||
$sRes = $oBB->Process(time() + 36000); // 10 hours to complete should be sufficient!
|
||||
$oPage->output();
|
||||
} catch (Exception $e) {
|
||||
$oPage->p('Error: '.$e->getMessage());
|
||||
IssueLog::Error($sOperation.' - '.$e->getMessage());
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => $e->getMessage(),
|
||||
], $e);
|
||||
}
|
||||
|
||||
$oPage->output();
|
||||
break;
|
||||
|
||||
/*
|
||||
@@ -128,7 +144,7 @@ try {
|
||||
* As a result we're setting a token file to make sure the restore is called by an authenticated user with the correct rights !
|
||||
*/
|
||||
case 'restore_get_token':
|
||||
$oPage = new JsonPage();
|
||||
$oPage = new JsonPage() ;
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
|
||||
@@ -138,6 +154,15 @@ try {
|
||||
}
|
||||
|
||||
$sFile = utils::ReadParam('file', '', false, 'raw_data');
|
||||
$sBackupFilePath = utils::RealPath(BACKUP_DIR.$sFile, BACKUP_DIR);
|
||||
if ($sBackupFilePath === false) {
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => 'Invalid backup file path',
|
||||
'backup_file' => $sFile,
|
||||
]);
|
||||
}
|
||||
|
||||
$sToken = str_replace(' ', '', (string)microtime()).$sTransactionId;
|
||||
$sTokenFile = APPROOT.'/data/restore.'.$sToken.'.tok';
|
||||
file_put_contents($sTokenFile, $sFile);
|
||||
@@ -171,14 +196,15 @@ try {
|
||||
$oDBRS = new DBRestore($oItopConfig);
|
||||
$oDBRS->SetMySQLBinDir($sMySQLBinDir);
|
||||
|
||||
$sBackupDir = APPROOT.'data/backups/';
|
||||
$sBackupFile = $sBackupDir.$sFile;
|
||||
$sBackupFile = BACKUP_DIR.$sFile;
|
||||
$sRes = $oDBRS->RestoreFromCompressedBackup($sBackupFile, $sEnvironment);
|
||||
|
||||
IssueLog::Info('Backup Restore - Done, releasing the LOCK');
|
||||
} catch (Exception $e) {
|
||||
$oPage->p('Error: '.$e->getMessage());
|
||||
IssueLog::Error($sOperation.' - '.$e->getMessage());
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => $e->getMessage(),
|
||||
], $e);
|
||||
} finally {
|
||||
unlink($tokenRealPath);
|
||||
}
|
||||
@@ -192,14 +218,23 @@ try {
|
||||
}
|
||||
$sFile = utils::ReadParam('file', '', false, 'raw_data');
|
||||
$oBackup = new DBBackupScheduled();
|
||||
$sBackupDir = APPROOT.'data/backups/';
|
||||
$sBackupFilePath = utils::RealPath($sBackupDir.$sFile, $sBackupDir);
|
||||
$sBackupFilePath = utils::RealPath(BACKUP_DIR.$sFile, BACKUP_DIR);
|
||||
if ($sBackupFilePath === false) {
|
||||
throw new CoreUnexpectedValue('Invalid file path');
|
||||
throw new AjaxBackupException([
|
||||
'backup_operation' => $sOperation,
|
||||
'message' => 'Invalid backup file path',
|
||||
'backup_file' => $sFile,
|
||||
]);
|
||||
}
|
||||
$oBackup->DownloadBackup($sBackupFilePath);
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error($sOperation.' - '.$e->getMessage());
|
||||
// exception handler
|
||||
IssueLog::Error('Unable to perform backup `'.$e->getContextData()['backup_operation'].'` : Invalid backup file path', LogChannels::SECURITY, [
|
||||
'backup_operation' => $e->getContextData()['backup_operation'] ?? null,
|
||||
'backup_file_path' => $e->getContextData()['backup_file_path'] ?? null,
|
||||
]);
|
||||
http_response_code(500);
|
||||
die($e->getCode());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Module\ItopBackup;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
class BackupAjaxEndpointTest extends ItopDataTestCase
|
||||
{
|
||||
public const USE_TRANSACTION = false;
|
||||
private const AUTHENTICATION_PASSWORD = 'tagada-Secret,007';
|
||||
|
||||
public function testDownloadInvalidDownloadPathReturnsHttp500(): void
|
||||
{
|
||||
$sLogin = 'test-backup-endpoint-'.uniqid();
|
||||
$this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], self::AUTHENTICATION_PASSWORD);
|
||||
|
||||
UserRights::Login($sLogin);
|
||||
|
||||
$this->CallItopUri(
|
||||
'env-production/itop-backup/ajax.backup.php?'.http_build_query([
|
||||
'operation' => 'download',
|
||||
'file' => 'missing-'.uniqid().'.zip',
|
||||
'auth_user' => $sLogin,
|
||||
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
|
||||
'transaction_id' => utils::GetNewTransactionId(),
|
||||
]),
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
|
||||
CURLOPT_POST => 0,
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
$this->assertSame(500, $this->aLastCurlGetInfo['http_code']);
|
||||
}
|
||||
|
||||
public function testRestoreGetTokenInvalidTransactionIdReturnsHttp500(): void
|
||||
{
|
||||
$sLogin = 'test-backup-endpoint-'.uniqid();
|
||||
$this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], self::AUTHENTICATION_PASSWORD);
|
||||
|
||||
UserRights::Login($sLogin);
|
||||
|
||||
$this->CallItopUri(
|
||||
'env-production/itop-backup/ajax.backup.php?'.http_build_query([
|
||||
'operation' => 'restore_get_token',
|
||||
'auth_user' => $sLogin,
|
||||
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
|
||||
'transaction_id' => 'unknown-transaction-id-'.uniqid(),
|
||||
]),
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
|
||||
CURLOPT_POST => 0,
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
$this->assertSame(500, $this->aLastCurlGetInfo['http_code']);
|
||||
}
|
||||
|
||||
public function testRestoreGetToken(): void
|
||||
{
|
||||
$sLogin = 'test-backup-endpoint-'.uniqid();
|
||||
$this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'], self::AUTHENTICATION_PASSWORD);
|
||||
|
||||
UserRights::Login($sLogin);
|
||||
|
||||
$sData = $this->CallItopUri(
|
||||
'env-production/itop-backup/ajax.backup.php?'.http_build_query([
|
||||
'operation' => 'restore_get_token',
|
||||
'auth_user' => $sLogin,
|
||||
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
|
||||
'transaction_id' => utils::GetNewTransactionId(),
|
||||
]),
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
|
||||
CURLOPT_POST => 0,
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
$aData = json_decode($sData, true);
|
||||
|
||||
$this->assertSame(200, $this->aLastCurlGetInfo['http_code']);
|
||||
$this->assertArrayHasKey('token', $aData);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user