This commit is contained in:
2026-07-04 00:34:59 +02:00
+367
View File
@@ -0,0 +1,367 @@
<?php
/**
* rebuild_roundcube_config.php
*
* Regenerates a Roundcube config/config.inc.php file:
* - parameter order and comments follow config/defaults.inc.php
* - only parameters whose effective value differs from the default are kept
* - parameters that now equal the default, or that no longer exist in this
* Roundcube version, are written to a "*.removed-params.txt" file together
* with the reason and their last known value
* - parameters listed in FORCE_KEEP_PARAMS are kept even if their value
* matches the default; this does NOT resurrect a parameter that no
* longer exists in Roundcube at all (defaults.inc.php / core source)
*
* Usage:
* php rebuild_roundcube_config.php <path-to-roundcube-config-dir> [--apply]
*
* Without --apply (dry run, default):
* writes config.inc.php.new and config.inc.php.removed-params.<timestamp>.txt
* next to the existing files, does NOT touch config.inc.php itself.
*
* With --apply:
* additionally backs up the existing config.inc.php to
* config.inc.php.bak.<timestamp> and replaces it with the regenerated file.
*/
// ---------------------------------------------------------------------------
// Parameters that must always end up in the regenerated config.inc.php, even
// if their value equals the default (they are still kept explicit/visible
// per site policy). Add further keys here as needed. Note: this only
// suppresses the "equals default" removal - a parameter that no longer
// exists in Roundcube at all is still dropped, see UNDOCUMENTED_CORE_PARAMS
// below for parameters that need to be recognized as still valid.
// ---------------------------------------------------------------------------
const FORCE_KEEP_PARAMS = [
'imap_host',
'imap_auth_type',
'smtp_host',
'smtp_port',
'smtp_user',
'smtp_pass',
];
// ---------------------------------------------------------------------------
// Parameters that exist in Roundcube 1.7.1 core (verified against program/
// source via `->config->get('key', ...)` calls) but are NOT documented in
// the shipped config/defaults.inc.php. Keys found here are kept (not treated
// as "removed"), using this one-line note as their comment since
// defaults.inc.php has none. Verified 2026-07-03 against roundcubemail-1.7.1.
// ---------------------------------------------------------------------------
const UNDOCUMENTED_CORE_PARAMS = [
'additional_logo_types' => 'Additional file extensions accepted as skin logo/favicon images.',
'dark_mode_support' => 'Tell the skin/UI that dark mode is supported (exposed to the client as env var).',
'devel_mode' => 'Enable developer/debug mode (verbose errors, disables some internal caching).',
'editor_css_location' => 'Path to a custom CSS file loaded into the HTML message editor (TinyMCE).',
'embed_css_location' => 'Path to a custom CSS file loaded for embedded/quoted HTML messages.',
'enable_caching' => 'Enable IMAP/message caching in the database.',
'lock_special_folders' => "Prevent users from unsubscribing/renaming special folders (Inbox, Sent, ...).",
'media_browser_css_location' => 'Path to a custom CSS file for the TinyMCE media browser.',
'namespace_fixed' => 'Treat the configured IMAP namespace as fixed instead of auto-detecting it.',
'pagesize' => 'Legacy fallback for addressbook_pagesize (see mail_pagesize/addressbook_pagesize).',
'performance_stats' => 'Log performance/timing statistics (like devel_mode).',
'shared_cache_ttl' => 'TTL for the shared memcache/redis object cache.',
'spellcheck_atd_key' => 'API key for the After-the-Deadline spell checking service.',
'storage_driver' => "Storage backend driver; currently only 'imap' is implemented by core.",
'tnef_decode' => 'Enable/disable decoding of winmail.dat (TNEF) attachments.',
];
function fail(string $msg): void
{
fwrite(STDERR, "ERROR: $msg\n");
exit(1);
}
function read_lines(string $file): array
{
$lines = file($file);
if ($lines === false) {
fail("Cannot read $file");
}
return $lines;
}
/**
* Parse a Roundcube config source file into an ordered list of parameter
* blocks: key => ['comment' => <preceding comment/blank lines verbatim>,
* 'value_src' => <raw PHP source of the right-hand side>]
*
* Returns [orderedKeys (first-seen order), paramsByKey (last occurrence wins,
* matching PHP's own "last assignment wins" semantics for duplicate keys)]
*/
function parse_config_source(string $file): array
{
$lines = read_lines($file);
$n = count($lines);
$order = [];
$params = [];
$commentBuf = [];
$i = 0;
while ($i < $n) {
$line = $lines[$i];
// $config = []; / $config = array(); is the file's own initializer,
// not a parameter - discard any accumulated header/license comments.
if (preg_match('/^\$config\s*=\s*(\[\]|array\(\))\s*;/', trim($line))) {
$commentBuf = [];
$i++;
continue;
}
if (preg_match("/^\\\$config\\['([a-zA-Z0-9_]+)'\\]\\s*=/", $line, $m)) {
$key = $m[1];
$stmtLines = [$line];
$depth = substr_count($line, '(') - substr_count($line, ')')
+ substr_count($line, '[') - substr_count($line, ']');
$j = $i;
while (!(preg_match('/;\s*$/', rtrim($line)) && $depth <= 0)) {
$j++;
if ($j >= $n) {
fail("Unterminated statement for \$config['$key'] in $file at line " . ($i + 1));
}
$line = $lines[$j];
$stmtLines[] = $line;
$depth += substr_count($line, '(') - substr_count($line, ')')
+ substr_count($line, '[') - substr_count($line, ']');
}
$raw = implode('', $stmtLines);
if (!preg_match("/^\\\$config\\['" . preg_quote($key, '/') . "'\\]\\s*=\\s*(.*);\\s*$/s", $raw, $vm)) {
fail("Could not extract value for \$config['$key'] in $file");
}
$comment = rtrim(implode('', $commentBuf));
$params[$key] = [
'comment' => $comment,
'value_src' => rtrim($vm[1]),
];
if (!in_array($key, $order, true)) {
$order[] = $key;
}
$commentBuf = [];
$i = $j + 1;
continue;
}
$commentBuf[] = $line;
$i++;
}
return [$order, $params];
}
/**
* Evaluate a raw PHP expression (right-hand side of a $config[...] = ...;
* assignment) in isolation. These expressions in defaults.inc.php /
* config.inc.php are self-contained literals (strings, bools, numbers,
* arrays) with no external references, so isolated eval() is safe here.
*/
function eval_value_src(string $key, string $src, string $originFile)
{
try {
return eval("return $src;");
} catch (\Throwable $e) {
fail("Failed to evaluate \$config['$key'] from $originFile: " . $e->getMessage());
}
}
function values_equal($a, $b): bool
{
return var_export($a, true) === var_export($b, true);
}
function format_value(string $key, $val): string
{
// Prefer var_export for the (rare) fallback path where we don't have
// the original source text available.
$s = var_export($val, true);
return $s;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
$configDir = isset($argv[1]) ? rtrim($argv[1], '/') : null;
$apply = in_array('--apply', $argv, true);
if (!$configDir || !is_dir($configDir)) {
fail("Usage: php rebuild_roundcube_config.php <path-to-roundcube-config-dir> [--apply]");
}
$defaultsFile = "$configDir/defaults.inc.php";
$localFile = "$configDir/config.inc.php";
if (!is_file($defaultsFile)) {
fail("Not found: $defaultsFile");
}
if (!is_file($localFile)) {
fail("Not found: $localFile");
}
// defaults.inc.php references RCUBE_INSTALL_PATH (e.g. for log_dir/temp_dir
// defaults). Define it exactly as Roundcube's own bootstrap does: the
// installation root (parent of config/) with a trailing slash.
if (!defined('RCUBE_INSTALL_PATH')) {
define('RCUBE_INSTALL_PATH', dirname($configDir) . '/');
}
[$defaultsOrder, $defaultsParams] = parse_config_source($defaultsFile);
[$localOrder, $localParams] = parse_config_source($localFile);
// Detect duplicate keys in the local file, and report which value wins
// (informational - matches actual PHP "last wins" behaviour).
$rawLocalLines = read_lines($localFile);
$assignCounts = [];
foreach ($rawLocalLines as $line) {
if (preg_match("/^\\\$config\\['([a-zA-Z0-9_]+)'\\]\\s*=/", $line, $m)) {
$assignCounts[$m[1]] = ($assignCounts[$m[1]] ?? 0) + 1;
}
}
$duplicates = array_filter($assignCounts, fn($c) => $c > 1);
$keep = []; // key => value_src (raw source text to emit)
$removed = []; // key => reason string
foreach ($localOrder as $key) {
$localSrc = $localParams[$key]['value_src'];
$localVal = eval_value_src($key, $localSrc, $localFile);
$forceKeep = in_array($key, FORCE_KEEP_PARAMS, true);
if (array_key_exists($key, $defaultsParams)) {
$defaultVal = eval_value_src($key, $defaultsParams[$key]['value_src'], $defaultsFile);
if (!$forceKeep && values_equal($localVal, $defaultVal)) {
$removed[$key] = [
'reason' => 'entspricht dem Default-Wert',
'value' => format_value($key, $localVal),
];
continue;
}
$keep[$key] = $localSrc;
} elseif (array_key_exists($key, UNDOCUMENTED_CORE_PARAMS)) {
// Known to still be a valid core parameter, just undocumented.
$keep[$key] = $localSrc;
} else {
// Parameter no longer exists at all (neither in defaults.inc.php nor
// in UNDOCUMENTED_CORE_PARAMS) - FORCE_KEEP_PARAMS cannot resurrect
// it, there is nothing left to keep it in sync with.
$removed[$key] = [
'reason' => 'Parameter existiert in dieser Roundcube-Version nicht (mehr) / ist unbekannt',
'value' => format_value($key, $localVal),
];
}
}
// Force-keep parameters that were not set in config.inc.php at all: fall
// back to the default value so the parameter still shows up explicitly.
// Only applies to parameters that still exist in defaults.inc.php.
foreach (FORCE_KEEP_PARAMS as $key) {
if (array_key_exists($key, $keep) || array_key_exists($key, $localParams)) {
continue;
}
if (array_key_exists($key, $defaultsParams)) {
$keep[$key] = $defaultsParams[$key]['value_src'];
}
}
// ---------------------------------------------------------------------------
// Build new config.inc.php content
// ---------------------------------------------------------------------------
$timestamp = date('Y-m-d-Hi');
$totalKept = count($keep);
$out = [];
$out[] = '<?php';
$out[] = '';
$out[] = '/* Local configuration for Roundcube Webmail';
$out[] = ' *';
$out[] = " * Regenerated by rebuild_roundcube_config.php on " . date('Y-m-d H:i');
$out[] = ' * Parameter order and comments follow config/defaults.inc.php.';
$out[] = ' * Parameters dropped during regeneration are listed in the';
$out[] = ' * accompanying *.removed-params.*.txt file.';
$out[] = ' */';
$out[] = '';
$out[] = '$config = [];';
$out[] = '';
foreach ($defaultsOrder as $key) {
if (!array_key_exists($key, $keep)) {
continue;
}
$comment = $defaultsParams[$key]['comment'];
if ($comment !== '') {
$out[] = $comment;
}
$out[] = "\$config['$key'] = " . $keep[$key] . ';';
$out[] = '';
unset($keep[$key]);
}
// Remaining kept keys are the "undocumented but valid core parameter" ones -
// defaults.inc.php has no position/comment for them.
if (!empty($keep)) {
$out[] = '// ----------------------------------------------------------------';
$out[] = '// Parameters valid in this Roundcube core version but not documented';
$out[] = '// in config/defaults.inc.php (see rebuild_roundcube_config.php).';
$out[] = '// ----------------------------------------------------------------';
$out[] = '';
foreach ($keep as $key => $src) {
$out[] = '// ' . (UNDOCUMENTED_CORE_PARAMS[$key] ?? '');
$out[] = "\$config['$key'] = " . $src . ';';
$out[] = '';
}
}
$newContent = implode("\n", $out) . "\n";
// ---------------------------------------------------------------------------
// Build removed-params report
// ---------------------------------------------------------------------------
$report = [];
$report[] = "# Parameter removed from config.inc.php on regeneration ($timestamp)";
$report[] = "# Format: key = last effective value // reason";
$report[] = "";
foreach ($removed as $key => $info) {
$report[] = "$key = {$info['value']} // {$info['reason']}";
}
$reportContent = implode("\n", $report) . "\n";
// ---------------------------------------------------------------------------
// Write output / apply
// ---------------------------------------------------------------------------
$newFile = "$localFile.new";
$reportFile = "$configDir/config.inc.php.removed-params.$timestamp.txt";
file_put_contents($newFile, $newContent);
file_put_contents($reportFile, $reportContent);
echo "Wrote: $newFile\n";
echo "Wrote: $reportFile\n";
echo "\n";
echo "Kept parameters : $totalKept\n";
echo "Removed parameters: " . count($removed) . "\n";
if (!empty($duplicates)) {
echo "\nWARNING: the following keys were assigned more than once in config.inc.php\n";
echo "(PHP uses the LAST assignment - already reflected above, listed here for review):\n";
foreach ($duplicates as $key => $count) {
echo " - $key ($count assignments)\n";
}
}
if ($apply) {
$backupFile = "$localFile.bak.$timestamp";
if (!copy($localFile, $backupFile)) {
fail("Could not create backup $backupFile");
}
if (!rename($newFile, $localFile)) {
fail("Could not move $newFile to $localFile");
}
echo "\nApplied: backed up original to $backupFile and replaced $localFile\n";
} else {
echo "\nDry run only. Review $newFile and $reportFile, then re-run with --apply.\n";
}