368 lines
14 KiB
PHP
368 lines
14 KiB
PHP
<?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";
|
|
}
|