Merge pull request #109 from GameServerPanel/copilot/fix-broken-xml-in-dayz-configs

Fix broken XML in DayZ game configs; add CDATA auto-sanitizer to XML editor
This commit is contained in:
Frank Harris 2026-05-03 16:46:23 -07:00 committed by GitHub
commit 13de7c8383
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 16 deletions

View file

@ -169,6 +169,60 @@ function config_games_validate_xml_file(string $config_file): array
return [];
}
/**
* Script-like element names whose text content is shell/batch code.
* These nodes should be stored as CDATA sections so that characters such as
* '<', '>', '&', etc. survive round-trips through the XML parser unchanged.
*/
function config_games_script_node_names(): array
{
return ['pre_install', 'post_install', 'pre_start', 'post_start', 'precmd', 'postcmd'];
}
/**
* Auto-sanitize raw XML text: for every script-like element whose text content
* contains a bare '<' outside an existing CDATA block, wrap that content in a
* CDATA section so the file becomes well-formed. Non-script elements are left
* untouched.
*
* Assumptions / limitations:
* - Script elements are treated as leaf nodes (no child elements expected).
* Nested XML tags inside a script block are not supported and the regex
* will not handle them correctly.
* - The detection of an already-present CDATA section relies on the opening
* tag being immediately followed by '<![CDATA[' (with optional whitespace).
* If a script block mixes text and CDATA it is left unchanged.
*
* This is applied to raw XML submitted through the editor's "Raw XML" path
* before the validation step, giving a best-effort fix rather than a hard
* rejection for the most common authoring mistake.
*
* @param string $xml Raw XML string (may be malformed).
* @return string XML string with script content wrapped in CDATA where needed.
*/
function config_games_sanitize_xml_scripts(string $xml): string
{
$tags = config_games_script_node_names();
foreach ($tags as $tag) {
$xml = preg_replace_callback(
'/<' . preg_quote($tag, '/') . '(\s[^>]*)?>(?!\s*<!\[CDATA\[)(.*?)<\/' . preg_quote($tag, '/') . '>/si',
function ($m) use ($tag) {
$attrs = $m[1];
$content = $m[2];
// Only wrap if the content contains a raw '<' character. XML
// entities such as &lt; do not contain a literal '<' so they
// are not matched here and do not trigger CDATA wrapping.
if (strpos($content, '<') === false) {
return $m[0];
}
return '<' . $tag . $attrs . '><![CDATA[' . $content . ']]></' . $tag . '>';
},
$xml
);
}
return $xml;
}
function config_games_print_editor_css()
{
static $printed = false;
@ -388,13 +442,19 @@ function config_games_save_xml($db, $home_cfg_id, array $nodesPayload)
continue;
}
$hasChildren = !empty($nodeData['has_children']);
$nodeName = strtolower(($slashPos = strrpos($path, '/')) !== false ? substr($path, $slashPos + 1) : $path);
$isScriptNode = in_array($nodeName, config_games_script_node_names(), true);
if (array_key_exists('value', (array)$nodeData)) {
$normalizedValue = config_games_normalize_newlines($nodeData['value']);
while ($domNode->firstChild) {
$domNode->removeChild($domNode->firstChild);
}
if ($normalizedValue !== '') {
$domNode->appendChild($dom->createTextNode($normalizedValue));
if ($isScriptNode) {
$domNode->appendChild($dom->createCDATASection($normalizedValue));
} else {
$domNode->appendChild($dom->createTextNode($normalizedValue));
}
}
} elseif (!$hasChildren) {
while ($domNode->firstChild) {
@ -530,9 +590,11 @@ function exec_ogp_module() {
if ($cfg_info !== FALSE) {
$config_file = SERVER_CONFIG_LOCATION . $cfg_info['home_cfg_file'];
$raw_content = $_POST['raw_xml_content'];
// Apply best-effort auto-fix: wrap bare '<' chars in script blocks with CDATA.
$sanitized_content = config_games_sanitize_xml_scripts($raw_content);
// Write to a temp file for validation
$tmp = tempnam(sys_get_temp_dir(), 'gsp_xml_');
file_put_contents($tmp, $raw_content);
file_put_contents($tmp, $sanitized_content);
$xmlErrors = config_games_validate_xml_file($tmp);
@unlink($tmp);
if (!empty($xmlErrors)) {
@ -542,7 +604,7 @@ function exec_ogp_module() {
}
echo "</ul></div>";
} else {
if (file_put_contents($config_file, $raw_content) !== false) {
if (file_put_contents($config_file, $sanitized_content) !== false) {
print_success(get_lang('configs_updated_ok'));
$config = read_server_config($config_file);
if ($config !== FALSE) {

View file

@ -65,7 +65,7 @@ Make sure if you install a MOD, you list the name here or else it wont get loade
</param>
</server_params>
<post_install>
<post_install><![CDATA[
mkdir -p ./cfg
touch ./cfg/dayz_arma2co_win32.xml.txt
@ -81,7 +81,7 @@ rm -f dayzmod1.9.0.tar
#Create Database ---------------------------------------
dbPass=$(</dev/urandom tr -dc 'A-Za-z0-9_' | head -c 12)
dbPass=$(tr -dc 'A-Za-z0-9_' </dev/urandom | head -c 12)
srvID=${PWD##*/}
dbID="server_${srvID}"
@ -105,8 +105,6 @@ VALUES (1, ${srvID}, '${dbID}', '${dbPass}', '${dbID}', 1);
mysql --force "${dbID}" < 1.9.0_fresh.sql
# Create alsoRun.bat -----------------------------------
printf '%s\r\n' \
@ -119,8 +117,7 @@ printf '%s\r\n' \
'for /f "tokens=2 delims==" %%P in ('"'"'wmic process where "ExecutablePath='"'"'%cd:\=\\%\\bec.exe'"'"'" get ProcessId /value ^| find "="'"'"') do >"..\_alsoRun.pid" echo %%P' \
> _alsoRun.bat
</post_install>
]]></post_install>
<pre_start>
</pre_start>

View file

@ -65,7 +65,7 @@ Make sure if you install a MOD, you list the name here or else it wont get loade
</param>
</server_params>
<post_install>
<post_install><![CDATA[
mkdir -p ./cfg
touch ./cfg/epochmod_win32.xml
@ -81,7 +81,7 @@ rm -f epochmod.tar
#Create Database ---------------------------------------
dbPass=$(&lt;/dev/urandom tr -dc 'A-Za-z0-9_' | head -c 12)
dbPass=$(tr -dc 'A-Za-z0-9_' </dev/urandom | head -c 12)
srvID=${PWD##*/}
dbID="server_${srvID}"
@ -102,9 +102,7 @@ INSERT INTO panel.gsp_mysql_databases
VALUES (1, ${srvID}, '${dbID}', '${dbPass}', '${dbID}', 1);
"
mysql --force "${dbID}" &lt; 1.9.0_fresh.sql
mysql --force "${dbID}" < 1.9.0_fresh.sql
# Create _alsoRun.bat -----------------------------------
@ -119,8 +117,7 @@ printf '%s\r\n' \
'for /f "tokens=2 delims==" %%P in ('"'"'wmic process where "ExecutablePath='"'"'%cd:\=\\%\\bec.exe'"'"'" get ProcessId /value ^| find "="'"'"') do >"..\_alsoRun.pid" echo %%P' \
> _alsoRun.bat
</post_install>
]]></post_install>
<pre_start>
</pre_start>