Configuration file editor:

- support syntax highlighting and checking (ace editor)
- better "apply" and "reset" buttons management
- limit code injection when checking the configuration
- better syntax checking for PHP7

SVN:trunk[4926]
This commit is contained in:
Eric Espié
2017-09-20 15:07:05 +00:00
parent 048c1ecf72
commit 6011aa2ac9
13 changed files with 183 additions and 125 deletions

View File

@@ -39,13 +39,16 @@ function TestConfig($sContents, $oP)
{
ini_set('display_errors', 1);
ob_start();
eval('?'.'>'.trim($sContents));
$sNoise = trim(ob_get_contents());
// in PHP < 7.0.0 syntax errors are in output
// in PHP >= 7.0.0 syntax errors are thrown as Error
$sSafeContent = preg_replace(array('#^\s*<\?php#', '#\?>\s*$#'), '', $sContents);
eval('if(0){'.trim($sSafeContent).'}');
$sNoise = trim(ob_get_contents());
ob_end_clean();
}
catch (Exception $e)
{
// well, never reach in case of parsing error :-(
}
catch (Error $e)
{
// ParseError only thrown in PHP7
throw new Exception('Error in configuration: '.$e->getMessage());
}
if (strlen($sNoise) > 0)
@@ -62,14 +65,6 @@ function TestConfig($sContents, $oP)
for ($i = 0 ; $i < $iLine - 1; $i++) $iStart += strlen($aLines[$i]);
$iEnd = $iStart + strlen($aLines[$iLine - 1]);
$iTotalLines = count($aLines);
$oP->add_ready_script(
<<<EOF
setCursorPos($('#new_config')[0], $iStart, $iEnd);
$('#new_config')[0].focus();
var iScroll = Math.floor($('#new_config')[0].scrollHeight * ($iLine - 20) / $iTotalLines);
$('#new_config').scrollTop(iScroll);
EOF
);
$sMessage = Dict::Format('config-parse-error', $sMessage, $sLine);
throw new Exception($sMessage);
@@ -93,7 +88,11 @@ LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be
$oP = new iTopWebPage(Dict::S('config-edit-title'));
$oP->set_base(utils::GetAbsoluteUrlAppRoot().'pages/');
$oP->add_linked_script(utils::GetCurrentModuleUrl().'/js/ace.js');
$oP->add_linked_script(utils::GetCurrentModuleUrl().'/js/mode-php.js');
$oP->add_linked_script(utils::GetCurrentModuleUrl().'/js/theme-eclipse.js');
$oP->add_linked_script(utils::GetCurrentModuleUrl().'/js/ext-searchbox.js');
//$oP->add_linked_script(utils::GetCurrentModuleUrl().'/js/ext-textarea.js');
try
{
@@ -111,134 +110,129 @@ try
}
else
{
$oP->add_style(
<<<EOF
textarea {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 550px;
}
.current_line {
display: none;
margin-left: 20px;
}
EOF
);
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
if ($sOperation == 'save')
{
$sConfig = utils::ReadParam('new_config', '', false, 'raw_data');
$sTransactionId = utils::ReadParam('transaction_id', '');
$sOrginalConfig = utils::ReadParam('prev_config', '', false, 'raw_data');
if (!utils::IsTransactionValid($sTransactionId, true))
{
$oP->add("<div class=\"header_message message_info\">Error: invalid Transaction ID. The configuration was <b>NOT</b> modified.</div>");
}
else
{
if ($sConfig == $sOrginalConfig)
{
$oP->add('<div id="save_result" class="header_message">'.Dict::S('config-no-change').'</div>');
}
else
{
try
{
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, 0444); // Read-only
$oP->p('<div id="save_result" class="header_message message_ok">'.Dict::S('Successfully recorded.').'</div>');
$sOrginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
}
catch (Exception $e)
{
$oP->p('<div id="save_result" class="header_message message_error">'.$e->getMessage().'</div>');
}
}
}
}
else
{
$sConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
$sOrginalConfig = $sConfig;
}
$iEditorTopMargin = 9;
$sConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
$sOrginalConfig = $sConfig;
if (!empty($sOperation))
{
$iEditorTopMargin = 14;
$sConfig = utils::ReadParam('new_config', '', false, 'raw_data');
$sOrginalConfig = utils::ReadParam('prev_config', '', false, 'raw_data');
}
if ($sOperation == 'revert')
{
$oP->add('<div id="save_result" class="header_message message_info">'.Dict::S('config-reverted').'</div>');
}
if ($sOperation == 'save')
{
$sTransactionId = utils::ReadParam('transaction_id', '');
if (!utils::IsTransactionValid($sTransactionId, true))
{
$oP->add("<div class=\"header_message message_info\">Error: invalid Transaction ID. The configuration was <b>NOT</b> modified.</div>");
}
else
{
if ($sConfig == $sOrginalConfig)
{
$oP->add('<div id="save_result" class="header_message">'.Dict::S('config-no-change').'</div>');
}
else
{
try
{
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, 0444); // Read-only
$oP->p('<div id="save_result" class="header_message message_ok">'.Dict::S('config-saved').'</div>');
$sOrginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
}
catch (Exception $e)
{
$oP->p('<div id="save_result" class="header_message message_error">'.$e->getMessage().'</div>');
}
}
}
}
$sConfigEscaped = htmlentities($sConfig, ENT_QUOTES, 'UTF-8');
$sOriginalConfigEscaped = htmlentities($sOrginalConfig, ENT_QUOTES, 'UTF-8');
$oP->p(Dict::S('config-edit-intro'));
$oP->add("<form method=\"POST\">");
$oP->add("<input type=\"hidden\" name=\"operation\" value=\"save\">");
$oP->add("<input id=\"operation\" type=\"hidden\" name=\"operation\" value=\"save\">");
$oP->add("<input type=\"hidden\" name=\"transaction_id\" value=\"".utils::GetNewTransactionId()."\">");
$oP->add("<input type=\"submit\" value=\"".Dict::S('config-apply')."\"><button onclick=\"ResetConfig(); return false;\">".Dict::S('config-cancel')."</button>");
$oP->add("<span class=\"current_line\">".Dict::Format('config-current-line', "<span class=\"line_number\"></span>")."</span>");
$oP->add("<input id=\"submit_button\" type=\"submit\" value=\"".Dict::S('config-apply')."\"><button id=\"cancel_button\" disabled=\"disabled\" onclick=\"return ResetConfig();\">".Dict::S('config-cancel')."</button>");
$oP->add("<input type=\"hidden\" id=\"prev_config\" name=\"prev_config\" value=\"$sOriginalConfigEscaped\">");
$oP->add("<textarea id =\"new_config\" name=\"new_config\" onkeyup=\"UpdateLineNumber();\" onmouseup=\"UpdateLineNumber();\">$sConfigEscaped</textarea>");
$oP->add("<input type=\"submit\" value=\"".Dict::S('config-apply')."\"><button onclick=\"ResetConfig(); return false;\">".Dict::S('config-cancel')."</button>");
$oP->add("<span class=\"current_line\">".Dict::Format('config-current-line', "<span class=\"line_number\"></span>")."</span>");
$oP->add("<input type=\"hidden\" name=\"new_config\" value=\"$sConfigEscaped\">");
$oP->add("<div id =\"new_config\" style=\"position: absolute; top: ".$iEditorTopMargin."em; bottom: 0; left: 5px; right: 5px;\"></div>");
$oP->add("</form>");
$sConfirmCancel = addslashes(Dict::S('config-confirm-cancel'));
$oP->add_ready_script(
<<<EOF
var editor = ace.edit("new_config");
var textarea = $('input[name="new_config"]');
editor.getSession().setValue(textarea.val());
editor.getSession().on('change', function()
{
textarea.val(editor.getSession().getValue());
UpdateConfigEditorButtonState();
});
editor.getSession().on("changeAnnotation", function()
{
UpdateConfigEditorButtonState();
});
editor.setTheme("ace/theme/eclipse");
editor.getSession().setMode("ace/mode/php");
function UpdateConfigEditorButtonState()
{
var editor = ace.edit("new_config");
var isSameContent = editor.getValue() == $('#prev_config').val();
var hasNoError = $.isEmptyObject(editor.getSession().getAnnotations());
$('#cancel_button').attr('disabled', isSameContent);
$('#submit_button').attr('disabled', isSameContent || !hasNoError);
}
EOF
);
$oP->add_script(
<<<EOF
function UpdateLineNumber()
{
var oTextArea = $('#new_config')[0];
$('.line_number').html(oTextArea.value.substr(0, oTextArea.selectionStart).split("\\n").length);
$('.current_line').show();
}
function ResetConfig()
{
if ($('#new_config').val() != $('#prev_config').val())
var editor = ace.edit("new_config");
$("#operation").attr('value', 'revert');
if (editor.getValue() != $('#prev_config').val())
{
if (confirm('$sConfirmCancel'))
{
$('#new_config').val($('#prev_config').val());
$('input[name="new_config"]').val($('#prev_config').val());
return true;
}
}
$('.current_line').hide();
$('#save_result').hide();
return false;
}
function setCursorPos(input, start, end) {
if (arguments.length < 3) end = start;
if ("selectionStart" in input) {
setTimeout(function() {
input.selectionStart = start;
input.selectionEnd = end;
}, 1);
}
else if (input.createTextRange) {
var rng = input.createTextRange();
rng.moveStart("character", start);
rng.collapse();
rng.moveEnd("character", end - start);
rng.select();
}
}
EOF
);
}

View File

@@ -14,8 +14,10 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'config-edit-intro' => 'Při úpravách konfiguračního souboru buďte velice opatrní. Nesprávné nastavení může vést k nedostupnosti iTop', //In particular, only the upper items (i.e. the global configuration and modules settings) should be edited.
'config-apply' => 'Použít',
'config-cancel' => 'Zrušit',
'config-saved' => 'Successfully recorded.~~',
'config-confirm-cancel' => 'Vaše úpravy nebudou uloženy.',
'config-no-change' => 'Soubor nebyl změněn.',
'config-reverted' => 'The configuration has been reverted.~~',
'config-parse-error' => 'Řádek %2$d: %1$s.<br/>Soubor nebyl uložen.',
'config-current-line' => 'Řádek: %1$s',
));

View File

@@ -30,8 +30,10 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'config-edit-intro' => 'Seien sie bei der Bearbeitung der Konfigurationsdatei sehr vorsichtig.',
'config-apply' => 'Anwenden',
'config-cancel' => 'Zurücksetzen',
'config-saved' => 'Successfully recorded.~~',
'config-confirm-cancel' => 'Ihre Änderungen werden nicht gespeichert.',
'config-no-change' => 'Keine Änderungen: Die Datei wurde nicht verändert.',
'config-reverted' => 'The configuration has been reverted.~~',
'config-parse-error' => 'Zeile %2$d: %1$s.<br/>Die Datei wurde nicht aktualisiert.',
'config-current-line' => 'Editiere Zeile: %1$s',
));

View File

@@ -13,8 +13,10 @@ Dict::Add('EN US', 'English', 'English', array(
'config-edit-intro' => 'Be very cautious when editing the configuration file.',
'config-apply' => 'Apply',
'config-cancel' => 'Reset',
'config-saved' => 'Successfully recorded.',
'config-confirm-cancel' => 'Your changes will be lost.',
'config-no-change' => 'No change: the file has been left unchanged.',
'config-reverted' => 'The configuration has been reverted.',
'config-parse-error' => 'Line %2$d: %1$s.<br/>The file has NOT been updated.',
'config-current-line' => 'Editing line: %1$s',
));

View File

@@ -13,8 +13,10 @@ Dict::Add('FR FR', 'French', 'Français', array(
'config-edit-intro' => 'Attention: une configuration incorrecte peut rendre iTop inopérant pour tous les utilisateurs!',
'config-save' => 'Appliquer',
'config-restore' => 'Réinitialiser',
'config-saved' => 'Configuration enregistrée.',
'config-confirm-cancel' => 'Vos modifications seront perdues.',
'config-no-change' => 'Aucun changement : le fichier n\'a pas été altéré.',
'config-reverted' => 'Vos modifications ont été écrasées par la version enregistrée.',
'config-parse-error' => 'Ligne %2$d: %1$s.<br/>Le fichier n\'a PAS été modifié.',
'config-current-line' => 'Ligne en édition : %1$s',
'config-apply' => 'Enregistrer',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
ace.define("ace/theme/eclipse",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";t.isDark=!1,t.cssText='.ace-eclipse .ace_gutter {background: #ebebeb;border-right: 1px solid rgb(159, 159, 159);color: rgb(136, 136, 136);}.ace-eclipse .ace_print-margin {width: 1px;background: #ebebeb;}.ace-eclipse {background-color: #FFFFFF;color: black;}.ace-eclipse .ace_fold {background-color: rgb(60, 76, 114);}.ace-eclipse .ace_cursor {color: black;}.ace-eclipse .ace_storage,.ace-eclipse .ace_keyword,.ace-eclipse .ace_variable {color: rgb(127, 0, 85);}.ace-eclipse .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-eclipse .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-eclipse .ace_function {color: rgb(60, 76, 114);}.ace-eclipse .ace_string {color: rgb(42, 0, 255);}.ace-eclipse .ace_comment {color: rgb(113, 150, 130);}.ace-eclipse .ace_comment.ace_doc {color: rgb(63, 95, 191);}.ace-eclipse .ace_comment.ace_doc.ace_tag {color: rgb(127, 159, 191);}.ace-eclipse .ace_constant.ace_numeric {color: darkblue;}.ace-eclipse .ace_tag {color: rgb(25, 118, 116);}.ace-eclipse .ace_type {color: rgb(127, 0, 127);}.ace-eclipse .ace_xml-pe {color: rgb(104, 104, 91);}.ace-eclipse .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-eclipse .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-eclipse .ace_meta.ace_tag {color:rgb(25, 118, 116);}.ace-eclipse .ace_invisible {color: #ddd;}.ace-eclipse .ace_entity.ace_other.ace_attribute-name {color:rgb(127, 0, 127);}.ace-eclipse .ace_marker-layer .ace_step {background: rgb(255, 255, 0);}.ace-eclipse .ace_active-line {background: rgb(232, 242, 254);}.ace-eclipse .ace_gutter-active-line {background-color : #DADADA;}.ace-eclipse .ace_marker-layer .ace_selected-word {border: 1px solid rgb(181, 213, 255);}.ace-eclipse .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}',t.cssClass="ace-eclipse";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<licenses>
<license>
<product>ajaxorg/ace</product>
<author>Ajax.org B.V.</author>
<license_type>BSD 3-clause "New" or "Revised"</license_type>
<text><![CDATA[
Copyright (c) 2010, Ajax.org B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ajax.org B.V. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]></text>
</license>
</licenses>

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-config/1.0.2',
'itop-config/1.0.3',
array(
// Identification
//
@@ -49,5 +49,3 @@ SetupWebPage::AddModule(
),
)
);
?>

View File

@@ -17,8 +17,10 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'config-edit-intro' => 'Будьте очень осторожны при редактировании файла конфигурации. В частности, могут быть отредактированы только верхние элементы (т.е. глобальная конфигурация и настройки модулей).',
'config-apply' => 'Применить',
'config-cancel' => 'Сбросить',
'config-saved' => 'Successfully recorded.~~',
'config-confirm-cancel' => 'Ваши изменения будут утеряны.',
'config-no-change' => 'Изменений нет: файл не был изменён.',
'config-reverted' => 'The configuration has been reverted.~~',
'config-parse-error' => 'Строка %2$d: %1$s.<br/>Файл не был обновлен.',
'config-current-line' => 'Редактируемая строка: %1$s',
));