From a6a459967ead3f0581048f0943121e6b1e4781d5 Mon Sep 17 00:00:00 2001 From: Timmy38 <101416770+Timmy38@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:04:49 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B06759=20factorize=20config=20(#738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * N°6759 - Factorize code in config --- core/config.class.inc.php | 6 + datamodels/2.x/itop-config/config.php | 352 +----------------- .../2.x/itop-config/module.itop-config.php | 2 + .../src/Controller/ConfigEditorController.php | 175 +++++++++ .../src/Validator/iTopConfigAstValidator.php | 4 +- .../Validator/iTopConfigSyntaxValidator.php | 24 +- .../src/Validator/iTopConfigValidator.php | 59 +++ .../2.x/itop-config/templates/Edit.html.twig | 29 ++ .../itop-config/templates/Edit.ready.js.twig | 129 +++++++ 9 files changed, 416 insertions(+), 364 deletions(-) create mode 100644 datamodels/2.x/itop-config/src/Controller/ConfigEditorController.php create mode 100644 datamodels/2.x/itop-config/src/Validator/iTopConfigValidator.php create mode 100644 datamodels/2.x/itop-config/templates/Edit.html.twig create mode 100644 datamodels/2.x/itop-config/templates/Edit.ready.js.twig diff --git a/core/config.class.inc.php b/core/config.class.inc.php index bc2dc839d..8852f6e63 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -20,6 +20,9 @@ */ +use Combodo\iTop\Config\Validator\iTopConfigAstValidator; +use Combodo\iTop\Config\Validator\iTopConfigSyntaxValidator; + define('ITOP_APPLICATION', 'iTop'); define('ITOP_APPLICATION_SHORT', 'iTop'); @@ -1816,6 +1819,7 @@ class Config return (array_key_exists($sPropCode, $this->m_aSettings)); } + /** * @return string identifier that can be used for example to name WebStorage/SessionStorage keys (they * are related to a whole domain, and a domain can host multiple itop) @@ -3023,4 +3027,6 @@ class ConfigPlaceholdersResolver IssueLog::Error($sErrorMessage, self::class, array($sSourceName, $sKey, $sDefault, $sWholeMask)); throw new ConfigException($sErrorMessage); } + + } diff --git a/datamodels/2.x/itop-config/config.php b/datamodels/2.x/itop-config/config.php index 593ee0ebf..5b8f2c4b3 100644 --- a/datamodels/2.x/itop-config/config.php +++ b/datamodels/2.x/itop-config/config.php @@ -4,362 +4,20 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -use Combodo\iTop\Application\UI\Base\Component\Alert\Alert; use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\Form\Form; -use Combodo\iTop\Application\UI\Base\Component\Html\Html; -use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory; -use Combodo\iTop\Application\WebPage\iTopWebPage; -use Combodo\iTop\Config\Validator\iTopConfigAstValidator; -use Combodo\iTop\Config\Validator\iTopConfigSyntaxValidator; +use Combodo\iTop\Config\Controller\ConfigEditorController; +use Combodo\iTop\Config\Validator\iTopConfigValidator; require_once(APPROOT.'application/startup.inc.php'); -const CONFIG_ERROR = 0; -const CONFIG_WARNING = 1; -const CONFIG_INFO = 2; - - -/** - * @param $sContents - * @param $oP - * - * @throws \Exception - */ -function TestConfig($sContents, $oP) -{ - /// 1- first check if there is no malicious code - $oiTopConfigValidator = new iTopConfigAstValidator(); - $oiTopConfigValidator->Validate($sContents); - - /// 2 - only after we are sure that there is no malicious cade, we can perform a syntax check! - $oiTopConfigValidator = new iTopConfigSyntaxValidator(); - $oiTopConfigValidator->Validate($sContents); -} - -/** - * @param $sSafeContent - * - * @return bool - */ -function DBPasswordInNewConfigIsOk($sSafeContent) -{ - $bIsWindows = (array_key_exists('WINDIR', $_SERVER) || array_key_exists('windir', $_SERVER)); - - if ($bIsWindows && (preg_match("@'db_pwd' => '[^%!\"]+',@U", $sSafeContent) === 0)) { - return false; - } - - return true; -} - -function CheckAsyncTasksRetryConfig(Config $oTempConfig, iTopWebPage $oP) -{ - $iWarnings = 0; - foreach (get_declared_classes() as $sPHPClass) { - $oRefClass = new ReflectionClass($sPHPClass); - if ($oRefClass->isSubclassOf('AsyncTask') && !$oRefClass->isAbstract()) { - $aMessages = AsyncTask::CheckRetryConfig($oTempConfig, $oRefClass->getName()); - - if (count($aMessages) !== 0) { - foreach ($aMessages as $sMessage) { - $oAlert = AlertUIBlockFactory::MakeForWarning('', $sMessage); - $oP->AddUiBlock($oAlert); - $iWarnings++; - } - } - } - } - - return $iWarnings; -} - -/** - * @param \Exception $e - * - * @return \Combodo\iTop\Application\UI\Base\Component\Alert\Alert - */ -function GetAlertFromException(Exception $e): Alert -{ - switch ($e->getCode()) { - case CONFIG_WARNING: - $oAlert = AlertUIBlockFactory::MakeForWarning('', $e->getMessage()); - break; - case CONFIG_INFO: - $oAlert = AlertUIBlockFactory::MakeForInformation('', $e->getMessage()); - break; - case CONFIG_ERROR: - default: - $oAlert = AlertUIBlockFactory::MakeForDanger('', $e->getMessage()); - } - - return $oAlert; -} - ///////////////////////////////////////////////////////////////////// // Main program // LoginWebPage::DoLogin(); // Check user rights and prompt if needed ApplicationMenu::CheckMenuIdEnabled('ConfigEditor'); -//$sOperation = utils::ReadParam('operation', 'menu'); -//$oAppContext = new ApplicationContext(); -$oP = new iTopWebPage(Dict::S('config-edit-title')); -$oP->set_base(utils::GetAbsoluteUrlAppRoot().'pages/'); -$sAceDir = 'node_modules/ace-builds/src-min/'; -$oP->LinkScriptFromAppRoot($sAceDir.'ace.js'); -$oP->LinkScriptFromAppRoot($sAceDir.'mode-php.js'); -$oP->LinkScriptFromAppRoot($sAceDir.'theme-eclipse.js'); -$oP->LinkScriptFromAppRoot($sAceDir.'ext-searchbox.js'); +$oConfigEditorController = new ConfigEditorController(); +$oConfigEditorController->SetDefaultOperation('Edit'); +$oConfigEditorController->HandleOperation(); -try { - $sOperation = utils::ReadParam('operation', ''); - $iEditorTopMargin = 2; - if (UserRights::IsAdministrator() && ExecutionKPI::IsEnabled()) { - $iEditorTopMargin += 6; - } - $oP->AddUiBlock(TitleUIBlockFactory::MakeForPage(Dict::S('config-edit-title'))); - - if (MetaModel::GetConfig()->Get('demo_mode')) { - throw new Exception(Dict::S('config-not-allowed-in-demo'), CONFIG_INFO); - } - - if (MetaModel::GetModuleSetting('itop-config', 'config_editor', '') == 'disabled') { - throw new Exception(Dict::S('config-interactive-not-allowed'), CONFIG_WARNING); - } - - $sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php'; - - $iEditorTopMargin += 9; - $sConfigContent = file_get_contents($sConfigFile); - $sConfigChecksum = md5($sConfigContent); - $sConfig = str_replace("\r\n", "\n", $sConfigContent); - $sOriginalConfig = $sConfig; - - if (!empty($sOperation)) { - $iEditorTopMargin += 5; - $sConfig = utils::ReadParam('new_config', '', false, 'raw_data'); - } - - try { - if ($sOperation == 'revert') { - throw new Exception(Dict::S('config-reverted'), CONFIG_WARNING); - } - - if ($sOperation == 'save') { - $sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id'); - if (!utils::IsTransactionValid($sTransactionId, true)) { - throw new Exception(Dict::S('config-error-transaction'), CONFIG_ERROR); - } - - $sChecksum = utils::ReadParam('checksum'); - if ($sChecksum !== $sConfigChecksum) { - throw new Exception(Dict::S('config-error-file-changed'), CONFIG_ERROR); - } - - if ($sConfig === $sOriginalConfig) { - throw new Exception(Dict::S('config-no-change'), CONFIG_INFO); - } - TestConfig($sConfig, $oP); // throws exceptions - - @chmod($sConfigFile, 0770); // Allow overwriting the file - $sTmpFile = tempnam(SetupUtils::GetTmpDir(), 'itop-cfg-'); - // Don't write the file as-is since it would allow to inject any kind of PHP code. - // Instead, write the interpreted version of the file - // Note: - // The actual raw PHP code will anyhow be interpreted exactly twice: once in TestConfig() above - // and a second time during the load of the Config object below. - // If you are really concerned about an iTop administrator crafting some malicious - // PHP code inside the config file, then turn off the interactive configuration - // editor by adding the configuration parameter: - // 'itop-config' => array( - // 'config_editor' => 'disabled', - // ) - file_put_contents($sTmpFile, $sConfig); - $oTempConfig = new Config($sTmpFile, true); - $oTempConfig->WriteToFile($sConfigFile); - @unlink($sTmpFile); - @chmod($sConfigFile, 0440); // Read-only - - if (DBPasswordInNewConfigIsOk($sConfig)) { - $oAlert = AlertUIBlockFactory::MakeForSuccess('', Dict::S('config-saved')); - } else { - $oAlert = AlertUIBlockFactory::MakeForInformation('', Dict::S('config-saved-warning-db-password')); - } - $oP->AddUiBlock($oAlert); - - $iWarnings = CheckAsyncTasksRetryConfig($oTempConfig, $oP); - - // Read the config from disk after save - $sConfigContent = file_get_contents($sConfigFile); - $sConfigChecksum = md5($sConfigContent); - $sConfig = str_replace("\r\n", "\n", $sConfigContent); - $sOriginalConfig = $sConfig; - } - } - catch (Exception $e) { - $oAlert = GetAlertFromException($e); - $oP->AddUiBlock($oAlert); - } - - // (remove EscapeHtml) N°5914 - Wrong encoding in modules configuration editor - $oP->AddUiBlock(new Html('

'.Dict::S('config-edit-intro').'

')); - - $oForm = new Form(); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'save', 'operation')); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', utils::GetNewTransactionId())); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('checksum', $sConfigChecksum)); - - //--- Cancel button - $oCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('config-cancel'), 'cancel_button', null, true, 'cancel_button'); - $oCancelButton->SetOnClickJsCode("return ResetConfig();"); - $oForm->AddSubBlock($oCancelButton); - - //--- Submit button - $oSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('config-apply'), null, Dict::S('config-apply'), true, 'submit_button'); - $oForm->AddSubBlock($oSubmitButton); - - //--- Config editor - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('prev_config', $sOriginalConfig, 'prev_config')); - $oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('new_config', $sOriginalConfig)); - $oForm->AddHtml("
"); - $oP->AddUiBlock($oForm); - - $oP->add_script( - <<<'JS' -var EditorUtils = (function() { - var STORAGE_RANGE_KEY = 'cfgEditorRange'; - var STORAGE_LINE_KEY = 'cfgEditorFirstline'; - var _editorSavedRange = null; - var _editorSavedFirstLine = null; - - var saveEditorDisplay = function(editor) { - _initObjectValues(editor); - _persistObjectValues(); - }; - - var _initObjectValues = function(editor) { - _editorSavedRange = editor.getSelectionRange(); - _editorSavedFirstLine = editor.renderer.getFirstVisibleRow(); - }; - - var _persistObjectValues = function() { - sessionStorage.setItem(EditorUtils.STORAGE_RANGE_KEY, JSON.stringify(_editorSavedRange)); - sessionStorage.setItem(EditorUtils.STORAGE_LINE_KEY, _editorSavedFirstLine); - }; - - var restoreEditorDisplay = function(editor) { - _restoreObjectValues(); - _setEditorDisplay(editor); - }; - - var _restoreObjectValues = function() { - if ((sessionStorage.getItem(STORAGE_RANGE_KEY) == null) - || (sessionStorage.getItem(STORAGE_LINE_KEY) == null)) { - return; - } - - _editorSavedRange = JSON.parse(sessionStorage.getItem(EditorUtils.STORAGE_RANGE_KEY)); - _editorSavedFirstLine = sessionStorage.getItem(EditorUtils.STORAGE_LINE_KEY); - sessionStorage.removeItem(STORAGE_RANGE_KEY); - sessionStorage.removeItem(STORAGE_LINE_KEY); - }; - - var _setEditorDisplay = function(editor) { - if ((_editorSavedRange == null) || (_editorSavedFirstLine == null)) { - return; - } - - editor.selection.setRange(_editorSavedRange); - editor.renderer.scrollToRow(_editorSavedFirstLine); - }; - - var getEditorForm = function(editor) { - var editorContainer = $(editor.container); - return editorContainer.closest("form"); - }; - - var updateConfigEditorButtonState = function(editor) { - var isSameContent = (editor.getValue() == $('#prev_config').val()); - var hasNoError = $.isEmptyObject(editor.getSession().getAnnotations()); - $('#cancel_button').prop('disabled', isSameContent); - $('#submit_button').prop('disabled', isSameContent || !hasNoError); - }; - - return { - STORAGE_RANGE_KEY: STORAGE_RANGE_KEY, - STORAGE_LINE_KEY : STORAGE_LINE_KEY, - saveEditorDisplay : saveEditorDisplay, - restoreEditorDisplay : restoreEditorDisplay, - getEditorForm : getEditorForm, - updateConfigEditorButtonState : updateConfigEditorButtonState - }; -})(); -JS - ); - $oP->add_ready_script(<<<'JS' -var editor = ace.edit("new_config"); - -var configurationSource = $('input[name="new_config"]'); -editor.getSession().setValue(configurationSource.val()); - -editor.getSession().on('change', function() -{ - configurationSource.val(editor.getSession().getValue()); - EditorUtils.updateConfigEditorButtonState(editor); -}); -editor.getSession().on("changeAnnotation", function() -{ - EditorUtils.updateConfigEditorButtonState(editor); -}); - -editor.setTheme("ace/theme/eclipse"); -editor.getSession().setMode("ace/mode/php"); -editor.commands.addCommand({ - name: 'save', - bindKey: {win: "Ctrl-S", "mac": "Cmd-S"}, - exec: function(editor) { - var editorForm = EditorUtils.getEditorForm(editor); - var submitButton = $('#submit_button'); - - if (submitButton.is(":enabled")) { - editorForm.trigger('submit'); - } - } -}); - - -var editorForm = EditorUtils.getEditorForm(editor); -editorForm.on('submit', function() { - EditorUtils.saveEditorDisplay(editor); -}); - - -EditorUtils.restoreEditorDisplay(editor); -editor.focus(); -JS - ); - - $sConfirmCancel = addslashes(Dict::S('config-confirm-cancel')); - $oP->add_script(<<AddUiBlock($oAlert); -} - -$oP->output(); diff --git a/datamodels/2.x/itop-config/module.itop-config.php b/datamodels/2.x/itop-config/module.itop-config.php index 2c8f2d2a1..70c2882a6 100644 --- a/datamodels/2.x/itop-config/module.itop-config.php +++ b/datamodels/2.x/itop-config/module.itop-config.php @@ -22,6 +22,8 @@ SetupWebPage::AddModule( 'src/Validator/ConfigNodesVisitor.php', 'src/Validator/iTopConfigAstValidator.php', 'src/Validator/iTopConfigSyntaxValidator.php', + 'src/Validator/iTopConfigValidator.php', + 'src/Controller/ConfigEditorController.php', ), 'webservice' => array(), 'dictionary' => array( diff --git a/datamodels/2.x/itop-config/src/Controller/ConfigEditorController.php b/datamodels/2.x/itop-config/src/Controller/ConfigEditorController.php new file mode 100644 index 000000000..48405d694 --- /dev/null +++ b/datamodels/2.x/itop-config/src/Controller/ConfigEditorController.php @@ -0,0 +1,175 @@ +Get('demo_mode')) { + throw new Exception(Dict::S('config-not-allowed-in-demo'), iTopConfigValidator::CONFIG_INFO); + } + + if (MetaModel::GetModuleSetting('itop-config', 'config_editor', '') == 'disabled') { + throw new Exception(Dict::S('config-interactive-not-allowed'), iTopConfigValidator::CONFIG_WARNING); + } + + $sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php'; + + $sCurrentConfig = file_get_contents($sConfigFile); + $sConfigChecksum = md5($sCurrentConfig); + + try { + if ($sOperation == 'revert') { + $this->AddAlert(Dict::S('config-reverted'), iTopConfigValidator::CONFIG_WARNING); + } + else if ($sOperation == 'save') { + $sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id'); + if (!utils::IsTransactionValid($sTransactionId)) { + throw new Exception(Dict::S('config-error-transaction'), iTopConfigValidator::CONFIG_ERROR); + } + $sChecksum = utils::ReadParam('checksum'); + if ($sChecksum !== $sConfigChecksum) { + throw new Exception(Dict::S('config-error-file-changed'), iTopConfigValidator::CONFIG_ERROR); + } + + $sNewConfig = utils::ReadParam('new_config', '', false, 'raw_data'); + $sNewConfig = str_replace("\r\n", "\n", $sNewConfig); + if ($sNewConfig === $sCurrentConfig) { + throw new Exception(Dict::S('config-no-change'), iTopConfigValidator::CONFIG_INFO); + } + $oValidator = new iTopConfigValidator(); + + $oValidator->Validate($sNewConfig);// throws exceptions + + @chmod($sConfigFile, 0770); // Allow overwriting the file + $sTmpFile = tempnam(SetupUtils::GetTmpDir(), 'itop-cfg-'); + // Don't write the file as-is since it would allow to inject any kind of PHP code. + // Instead, write the interpreted version of the file + // Note: + // The actual raw PHP code will anyhow be interpreted exactly twice: once in TestConfig() above + // and a second time during the load of the Config object below. + // If you are really concerned about an iTop administrator crafting some malicious + // PHP code inside the config file, then turn off the interactive configuration + // editor by adding the configuration parameter: + // 'itop-config' => array( + // 'config_editor' => 'disabled', + // ) + file_put_contents($sTmpFile, $sNewConfig); + $oTempConfig = new Config($sTmpFile, true); + $oTempConfig->WriteToFile($sConfigFile); + @unlink($sTmpFile); + @chmod($sConfigFile, 0440); // Read-only + + if ($oValidator->DBPasswordIsOk($oTempConfig->Get('db_pwd'))) { + $this->AddAlert(Dict::S('config-saved'), iTopConfigValidator::CONFIG_SUCCESS); + } else { + $this->AddAlert(Dict::S('config-saved-warning-db-password'), iTopConfigValidator::CONFIG_INFO); + } + + $this->AddAlert($oValidator->CheckAsyncTasksRetryConfig($oTempConfig), iTopConfigValidator::CONFIG_WARNING); + + + // Read the config from disk after save + $sCurrentConfig = file_get_contents($sConfigFile); + $sConfigChecksum = md5($sCurrentConfig); + } + } + catch (Exception $e) { + $this->AddAlertFromException($e); + } + + $this->AddAceScripts(); + } + catch (Exception $e) { + $bShowEditor = false; + $this->AddAlertFromException($e); + } + + // display page + $this->DisplayPage([ + 'aErrors' => $this->aErrors, + 'aWarnings' => $this->aWarnings, + 'aNotices' => $this->aInfo, + 'aSuccesses' => $this->aSuccesses, + 'bShowEditor' => $bShowEditor, + 'sTransactionId' => utils::GetNewTransactionId(), + 'sChecksum' => $sConfigChecksum, + 'sPrevConfig' => $sCurrentConfig, + 'sNewConfig' => $sCurrentConfig, + ]); + } + + /** + * @return void + * @throws \Exception + */ + protected function AddAceScripts(): void + { + $sAceDir = 'node_modules/ace-builds/src-min/'; + $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().$sAceDir.'ace.js'); + $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().$sAceDir.'mode-php.js'); + $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().$sAceDir.'theme-eclipse.js'); + $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().$sAceDir.'ext-searchbox.js'); + } + + + public function AddAlertFromException(Exception $e): void + { + $this->AddAlert($e->getMessage(), $e->getCode()); + } + + public function AddAlert(array|string $sMessage, $iLevel): void + { + if (is_array($sMessage)) { + foreach ($sMessage as $sSingleMessage) { + $this->AddAlert($sSingleMessage, $iLevel); + } + return; + } + switch ($iLevel) { + case iTopConfigValidator::CONFIG_SUCCESS : + $this->aSuccesses[] = $sMessage; + break; + case iTopConfigValidator::CONFIG_WARNING : + $this->aWarnings[] = $sMessage; + break; + case iTopConfigValidator::CONFIG_INFO : + $this->aInfo[] = $sMessage; + break; + default : + $this->aErrors[] = $sMessage; + } + } + +} \ No newline at end of file diff --git a/datamodels/2.x/itop-config/src/Validator/iTopConfigAstValidator.php b/datamodels/2.x/itop-config/src/Validator/iTopConfigAstValidator.php index 0d8cbfba9..6d0fcf2fb 100644 --- a/datamodels/2.x/itop-config/src/Validator/iTopConfigAstValidator.php +++ b/datamodels/2.x/itop-config/src/Validator/iTopConfigAstValidator.php @@ -31,10 +31,10 @@ class iTopConfigAstValidator $aInitialNodes = $oParser->parse($sConfig); } catch (\Error $e) { $sMessage = 'Invalid configuration: '. \Dict::Format('config-parse-error', $e->getMessage(), $e->getLine()); - throw new \Exception($sMessage, 0, $e); + throw new \Exception($sMessage, iTopConfigValidator::CONFIG_ERROR, $e); }catch (\Exception $e) { $sMessage = 'Invalid configuration: '. \Dict::Format('config-parse-error', $e->getMessage(), $e->getLine()); - throw new \Exception($sMessage, 0, $e); + throw new \Exception($sMessage, iTopConfigValidator::CONFIG_ERROR, $e); } $oTraverser = new NodeTraverser(); diff --git a/datamodels/2.x/itop-config/src/Validator/iTopConfigSyntaxValidator.php b/datamodels/2.x/itop-config/src/Validator/iTopConfigSyntaxValidator.php index 7c5c6a168..4ecbed82e 100644 --- a/datamodels/2.x/itop-config/src/Validator/iTopConfigSyntaxValidator.php +++ b/datamodels/2.x/itop-config/src/Validator/iTopConfigSyntaxValidator.php @@ -17,8 +17,7 @@ class iTopConfigSyntaxValidator */ public function Validate($sRawConfig) { - try - { + try { ini_set('display_errors', 1); ob_start(); // in PHP < 7.0.0 syntax errors are in output @@ -27,29 +26,24 @@ class iTopConfigSyntaxValidator eval('if(0){'.trim($sConfig).'}'); $sNoise = trim(ob_get_contents()); } - catch (\Error $e) - { + catch (\Error $e) { // ParseError only thrown in PHP7 - throw new \Exception('Error in configuration: '.$e->getMessage().' at line '.$e->getLine()); + throw new \Exception('Error in configuration: '.$e->getMessage().' at line '.$e->getLine(), iTopConfigValidator::CONFIG_ERROR); } - finally - { + finally { ob_end_clean(); } - if (strlen($sNoise) > 0) - { - if (preg_match("/(Error|Parse error|Notice|Warning): (.+) in \S+ : eval\(\)'d code on line (\d+)/i", strip_tags($sNoise), $aMatches)) - { + if (strlen($sNoise) > 0) { + if (preg_match("/(Error|Parse error|Notice|Warning): (.+) in \S+ : eval\(\)'d code on line (\d+)/i", strip_tags($sNoise), $aMatches)) { $sMessage = $aMatches[2]; $sLine = $aMatches[3]; $sMessage = \Dict::Format('config-parse-error', $sMessage, $sLine); - throw new \Exception($sMessage); + throw new \Exception($sMessage, iTopConfigValidator::CONFIG_ERROR); } - else - { + else { // Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack) - throw new \Exception('Syntax error in configuration file: '.$sNoise.''); + throw new \Exception('Syntax error in configuration file: '.$sNoise.'', iTopConfigValidator::CONFIG_ERROR); } } } diff --git a/datamodels/2.x/itop-config/src/Validator/iTopConfigValidator.php b/datamodels/2.x/itop-config/src/Validator/iTopConfigValidator.php new file mode 100644 index 000000000..3c5faa84a --- /dev/null +++ b/datamodels/2.x/itop-config/src/Validator/iTopConfigValidator.php @@ -0,0 +1,59 @@ +Validate($sRawConfig); + + /// 2 - only after we are sure that there is no malicious code, we can perform a syntax check! + $oiTopConfigValidator = new iTopConfigSyntaxValidator(); + $oiTopConfigValidator->Validate($sRawConfig); + } + + function DBPasswordIsOk($sPassword):bool + { + $bIsWindows = (array_key_exists('WINDIR', $_SERVER) || array_key_exists('windir', $_SERVER)); + + if ($bIsWindows && (preg_match("/[%!\"]/U", $sPassword) !== 0)) { + return false; + } + + return true; + } + + + public function CheckAsyncTasksRetryConfig(\Config $oTempConfig): array + { + $aWarnings = []; + foreach (get_declared_classes() as $sPHPClass) { + $oRefClass = new ReflectionClass($sPHPClass); + if ($oRefClass->isSubclassOf('AsyncTask') && !$oRefClass->isAbstract()) { + $aMessages = AsyncTask::CheckRetryConfig($oTempConfig, $oRefClass->getName()); + + if (count($aMessages) !== 0) { + foreach ($aMessages as $sMessage) { + $aWarnings[] = $sMessage; + } + } + } + } + return $aWarnings; + + } +} \ No newline at end of file diff --git a/datamodels/2.x/itop-config/templates/Edit.html.twig b/datamodels/2.x/itop-config/templates/Edit.html.twig new file mode 100644 index 000000000..c959ab8c6 --- /dev/null +++ b/datamodels/2.x/itop-config/templates/Edit.html.twig @@ -0,0 +1,29 @@ +{% for sError in aErrors %} + {% UIAlert ForDanger{sContent: sError} %}{% EndUIAlert %} +{% endfor %} +{% for sWarning in aWarnings %} + {% UIAlert ForWarning{sContent: sWarning} %}{% EndUIAlert %} +{% endfor %} +{% for sNotice in aNotices %} + {% UIAlert ForInformation{sContent: sNotice} %}{% EndUIAlert %} +{% endfor %} +{% for sSuccess in aSuccesses %} + {% UIAlert ForSuccess{sContent: sSuccess} %}{% EndUIAlert %} +{% endfor %} + +{% UITitle ForPage {'sTitle':'config-edit-title'|dict_s} %}{% EndUITitle %} + +{% if bShowEditor %} +

{{ 'config-edit-intro'|dict_s }}

+ {% UIForm Standard {} %} + {% UIButton ForCancel { 'sLabel':'config-cancel'|dict_s, 'bIsSubmit':true, 'sId':'cancel_button', 'sName':'edit_operation', 'sValue': 'revert'} %} + {% UIButton ForPrimaryAction {'sLabel':'config-apply'|dict_s, 'bIsSubmit':true, 'sId':'submit_button', 'sName':'edit_operation', 'sValue': 'save' } %} + + {% UIInput ForHidden {'sName':'transaction_id', 'sValue':sTransactionId} %} + {% UIInput ForHidden {'sName':'checksum', 'sValue':sChecksum} %} + {% UIInput ForHidden {'sName':'prev_config', 'sValue':sPrevConfig} %} + {% UIInput ForHidden {'sName':'new_config', 'sValue':sNewConfig} %} + +
+ {% EndUIForm %} +{% endif %} \ No newline at end of file diff --git a/datamodels/2.x/itop-config/templates/Edit.ready.js.twig b/datamodels/2.x/itop-config/templates/Edit.ready.js.twig new file mode 100644 index 000000000..a4e80b254 --- /dev/null +++ b/datamodels/2.x/itop-config/templates/Edit.ready.js.twig @@ -0,0 +1,129 @@ +var EditorUtils = (function() { + var STORAGE_RANGE_KEY = 'cfgEditorRange'; + var STORAGE_LINE_KEY = 'cfgEditorFirstline'; + var _editorSavedRange = null; + var _editorSavedFirstLine = null; + + var saveEditorDisplay = function(editor) { + _initObjectValues(editor); + _persistObjectValues(); + }; + + var _initObjectValues = function(editor) { + _editorSavedRange = editor.getSelectionRange(); + _editorSavedFirstLine = editor.renderer.getFirstVisibleRow(); + }; + + var _persistObjectValues = function() { + sessionStorage.setItem(EditorUtils.STORAGE_RANGE_KEY, JSON.stringify(_editorSavedRange)); + sessionStorage.setItem(EditorUtils.STORAGE_LINE_KEY, _editorSavedFirstLine); + }; + + var restoreEditorDisplay = function(editor) { + _restoreObjectValues(); + _setEditorDisplay(editor); + }; + + var _restoreObjectValues = function() { + if ((sessionStorage.getItem(STORAGE_RANGE_KEY) == null) + || (sessionStorage.getItem(STORAGE_LINE_KEY) == null)) { + return; + } + + _editorSavedRange = JSON.parse(sessionStorage.getItem(EditorUtils.STORAGE_RANGE_KEY)); + _editorSavedFirstLine = sessionStorage.getItem(EditorUtils.STORAGE_LINE_KEY); + sessionStorage.removeItem(STORAGE_RANGE_KEY); + sessionStorage.removeItem(STORAGE_LINE_KEY); + }; + + var _setEditorDisplay = function(editor) { + if ((_editorSavedRange == null) || (_editorSavedFirstLine == null)) { + return; + } + + editor.selection.setRange(_editorSavedRange); + editor.renderer.scrollToRow(_editorSavedFirstLine); + }; + + var getEditorForm = function(editor) { + var editorContainer = editor.container; + return editorContainer.parentElement; + }; + + var updateConfigEditorButtonState = function(editor) { + var isSameContent = (editor.getValue() === document.querySelector('input[name="prev_config"]').value); + var hasNoError = editor.getSession().getAnnotations().length === 0; + document.getElementById('cancel_button').disabled = isSameContent; + document.getElementById('submit_button').disabled = isSameContent || !hasNoError; + }; + + return { + STORAGE_RANGE_KEY: STORAGE_RANGE_KEY, + STORAGE_LINE_KEY : STORAGE_LINE_KEY, + saveEditorDisplay : saveEditorDisplay, + restoreEditorDisplay : restoreEditorDisplay, + getEditorForm : getEditorForm, + updateConfigEditorButtonState : updateConfigEditorButtonState + }; +})(); + + + + +editor = ace.edit("new_config"); + +var configurationSource = document.querySelector('input[name="new_config"]'); +editor.getSession().setValue(configurationSource.value); + +editor.getSession().on('change', function() +{ + configurationSource.value = editor.getSession().getValue(); + EditorUtils.updateConfigEditorButtonState(editor); +}); +editor.getSession().on("changeAnnotation", function() +{ + EditorUtils.updateConfigEditorButtonState(editor); +}); + +editor.setTheme("ace/theme/eclipse"); +editor.getSession().setMode("ace/mode/php"); +editor.commands.addCommand({ + name: 'save', + bindKey: {win: "Ctrl-S", "mac": "Cmd-S"}, + exec: function(editor) { + var editorForm = EditorUtils.getEditorForm(editor); + var submitButton = document.getElementById('submit_button'); + + if (submitButton.is(":enabled")) { + editorForm.trigger('submit'); + } + } +}); + + +var editorForm = EditorUtils.getEditorForm(editor); +editorForm.addEventListener('submit', function() { + EditorUtils.saveEditorDisplay(editor); +}); + + +EditorUtils.restoreEditorDisplay(editor); +editor.focus(); + + +const repositionEditor = () => { + let oSubmitButton = document.getElementById('submit_button'); + let iBottomPosition = oSubmitButton.offsetTop + oSubmitButton.offsetHeight + 10; + document.getElementById('new_config').style.top = iBottomPosition+"px"; +}; +repositionEditor(); + +document.getElementById('ibo-main-content').addEventListener('click',repositionEditor); + +document.getElementById('cancel_button').onclick = ()=>{ + if (confirm({{ 'config-confirm-cancel'|dict_s|json_encode|raw }})) { + document.querySelector('input[name="new_config"]').value = document.querySelector('input[name="prev_config"]').value; + return true; + } + return false; +}; \ No newline at end of file