Skip to content

Commit b2c4a4d

Browse files
committed
Stop the installer writing config.php where the next deploy erases it
install.php wrote config.php to __DIR__ . '/config.php', beside the panel. That is inside the document root, and the document root is rebuilt from scratch on every deploy -- so the installer's own output was erased by the next publish and the panel came back saying it had never been set up. That is not hypothetical: it is what happened, and it took the admin offline. config() already looks for nitesha-config/config.php in each directory above the panel, which is why the working config was moved there by hand. The installer now writes to that same place, one level above the document root, so an install survives a deploy without anyone having to know this. Three cases, all exercised against a real directory layout: nothing exists yet -> <account>/nitesha-config/config.php a config already exists -> that one is reused, wherever it is, so a working install never ends up with two files and config() picking a coin flip parent cannot be written -> falls back beside the panel, because a panel that works until the next deploy beats a panel that never starts The environment check was reporting on the wrong directory -- it tested the panel folder while the file was going somewhere else -- so it now checks the directory that will actually be written, and says which one that is and whether a deploy can reach it. The success screen names the path, and warns when it landed inside the website folder. storage_path moves above the document root for the same reason. Nothing reads it today, which is exactly why it would have been found the hard way later. The "not set up yet" response is now a page rather than one line of plain text on a 500. It is the first thing an owner sees after a deploy wipes the config, and "Not set up yet" reads, at that moment, like the data is gone. It now says which file is missing, shows the two paths that were actually searched -- computed, so they cannot drift from the code -- says plainly that the database is untouched, and links to the installer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
1 parent 2cf723f commit b2c4a4d

3 files changed

Lines changed: 147 additions & 13 deletions

File tree

‎admin/admin.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,10 @@ button { font-family: inherit; }
492492
}
493493
.install-checks li.ok::before { content: '✓'; color: #2F6B4F; }
494494
.install-checks li.no::before { content: '✕'; color: #B3261E; }
495+
/* Not a failure -- the install worked -- but the result needs acting on
496+
before the next deploy, so it cannot look like the passing rows. */
497+
.install-checks li.warn::before { content: '!'; color: #8A5A00; font-weight: 800; }
498+
.install-checks li.warn { color: var(--ink); }
495499
.install-checks li span { display: block; color: var(--ink-dim); font-size: 0.8rem; }
496500
.install-errors {
497501
background: #FDECEA; border: 1px solid #F3C4BF; border-radius: 10px;

‎admin/install.php‎

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,50 @@
1717
require_once __DIR__ . '/src/db.php';
1818
require_once __DIR__ . '/src/migrate.php';
1919

20-
const CONFIG_PATH = __DIR__ . '/config.php';
20+
/**
21+
* Where a new config.php should be written.
22+
*
23+
* It used to be __DIR__ . '/config.php', beside the panel. That is inside the
24+
* document root, and the document root is rebuilt from scratch on every
25+
* deploy -- so the installer's own output was erased by the next publish, and
26+
* the panel came back reporting it had never been set up. It happened, and it
27+
* took the admin down.
28+
*
29+
* config() looks for nitesha-config/config.php in each directory above the
30+
* panel, so a file one level above the document root is found and is out of
31+
* reach of anything that rewrites the site. That is where this writes.
32+
*
33+
* Beside the panel stays as the fallback for hosting where the parent
34+
* directory cannot be written, because a panel that works until the next
35+
* deploy beats a panel that never starts. install_config_path() says which
36+
* one was used so the last screen can be honest about it.
37+
*/
38+
function install_config_dir_preferred(): string
39+
{
40+
// __DIR__ is <docroot>/admin, so this is the directory holding the
41+
// document root -- above everything a deploy replaces.
42+
return dirname(__DIR__, 2) . '/nitesha-config';
43+
}
44+
45+
function install_config_path(): string
46+
{
47+
// An existing config wins wherever it is: rewriting a working install's
48+
// settings into a second file would leave two, and config() would pick
49+
// whichever it reached first.
50+
$existing = config_path();
51+
if ($existing !== null) {
52+
return $existing;
53+
}
54+
55+
$preferred = install_config_dir_preferred();
56+
if (is_dir($preferred) || @mkdir($preferred, 0750, true) || is_dir($preferred)) {
57+
if (is_writable($preferred)) {
58+
return $preferred . '/config.php';
59+
}
60+
}
61+
62+
return __DIR__ . '/config.php';
63+
}
2164
const MIN_PHP = '8.1';
2265
const MIN_PASSWORD = 10;
2366

@@ -39,9 +82,10 @@
3982
// The presence of config.php is not enough — a half-finished attempt leaves
4083
// one behind. What settles it is whether anyone can sign in.
4184
// ---------------------------------------------------------------------------
42-
if (is_file(CONFIG_PATH)) {
85+
$existingConfig = config_path();
86+
if ($existingConfig !== null) {
4387
try {
44-
$existing = require CONFIG_PATH;
88+
$existing = require $existingConfig;
4589
config_set(is_array($existing) ? $existing : []);
4690
$n = (int) (fetch_one('SELECT COUNT(*) AS n FROM users')['n'] ?? 0);
4791
if ($n > 0) {
@@ -158,7 +202,9 @@ function build_config(array $form): array
158202
'password' => (string) $form['db_pass'],
159203
'charset' => 'utf8mb4',
160204
],
161-
'storage_path' => dirname(__DIR__) . '/nitesha-storage',
205+
// Above the document root for the same reason config.php is: anything
206+
// inside it is temporary, because a deploy rebuilds it.
207+
'storage_path' => dirname(__DIR__, 2) . '/nitesha-storage',
162208
'public_site_origin' => $origins,
163209
// Hostinger serves these domains over HTTPS, and the session cookie
164210
// carries the sign-in, so it should never travel in the clear.
@@ -169,20 +215,36 @@ function build_config(array $form): array
169215

170216
function write_config(array $config): bool
171217
{
218+
$target = install_config_path();
219+
172220
$php = "<?php\n\n// Written by install.php. Holds the database password —\n"
173221
. "// keep it out of version control and off any public URL.\n\n"
174222
. 'return ' . var_export($config, true) . ";\n";
175223

176-
if (@file_put_contents(CONFIG_PATH, $php) === false) {
224+
$dir = dirname($target);
225+
if (!is_dir($dir) && !@mkdir($dir, 0750, true) && !is_dir($dir)) {
177226
return false;
178227
}
179-
@chmod(CONFIG_PATH, 0640);
228+
229+
if (@file_put_contents($target, $php) === false) {
230+
return false;
231+
}
232+
@chmod($target, 0640);
233+
234+
$GLOBALS['__config_written_to'] = $target;
180235
return true;
181236
}
182237

183238
function environment_checks(): array
184239
{
185-
$writable = is_writable(__DIR__);
240+
// The directory that matters is the one write_config() will use, which is
241+
// normally above the document root -- not this one. Checking __DIR__ told
242+
// people the install would work when the file was going somewhere else
243+
// entirely.
244+
$target = install_config_path();
245+
$dir = dirname($target);
246+
$writable = is_dir($dir) ? is_writable($dir) : is_writable(dirname($dir));
247+
$aboveRoot = $dir !== __DIR__;
186248
return [
187249
['label' => 'PHP ' . MIN_PHP . ' or newer',
188250
'ok' => version_compare(PHP_VERSION, MIN_PHP, '>='),
@@ -192,10 +254,13 @@ function environment_checks(): array
192254
'note' => extension_loaded('pdo_mysql') ? 'Available' : 'Enable it in hPanel → PHP Configuration'],
193255
['label' => 'JSON support',
194256
'ok' => extension_loaded('json'), 'note' => ''],
195-
['label' => 'This folder is writable',
257+
['label' => 'Somewhere to keep config.php',
196258
'ok' => $writable,
197-
'note' => $writable ? 'config.php can be written for you'
198-
: 'You will need to create config.php by hand'],
259+
'note' => !$writable
260+
? 'You will need to create ' . $dir . '/config.php by hand'
261+
: ($aboveRoot
262+
? 'Will be written to ' . $dir . ', above the website folder, where a deploy cannot erase it'
263+
: 'Will be written beside the panel. A deploy that rebuilds the website folder will erase it')],
199264
['label' => 'sql/ files present',
200265
'ok' => (glob(__DIR__ . '/sql/*.sql') ?: []) !== [],
201266
'note' => 'The table definitions'],
@@ -247,7 +312,15 @@ function page(string $title, string $body): void
247312
<ul class="install-checks">'
248313
. ($applied === [] ? '' : '<li class="ok">Tables created (' . h(implode(', ', $applied)) . ')</li>')
249314
. '<li class="ok">Your Super Admin account was created</li>
250-
<li class="ok">config.php was written</li>
315+
<li class="ok">config.php was written to <code>'
316+
. h((string) ($GLOBALS['__config_written_to'] ?? 'the panel folder')) . '</code></li>'
317+
. (str_contains((string) ($GLOBALS['__config_written_to'] ?? ''), 'nitesha-config')
318+
? ''
319+
: '<li class="warn">That is inside the website folder. A deploy that rebuilds the
320+
site will erase it and the panel will report it has never been set up. Move it
321+
to a folder named <code>nitesha-config</code> above the website folder when you
322+
can &mdash; the panel looks there first.</li>')
323+
. '
251324
</ul>
252325
<p style="margin-top:18px"><a class="btn btn-primary btn-block" href="index.php">Sign in</a></p>
253326
<p class="install-note" style="margin-top:18px"><strong>One last step:</strong>

‎admin/src/db.php‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,64 @@ function config_path(): ?string
6767
return null;
6868
}
6969

70+
/**
71+
* What the panel shows when it cannot find its configuration.
72+
*
73+
* This used to be one sentence of plain text on a 500. It is technically
74+
* accurate and it is a dead end: it does not say which file is missing, where
75+
* the panel looked, or that the database itself is untouched -- and it is the
76+
* first thing an owner sees after a deploy wipes the config, which is a moment
77+
* where "not set up yet" reads like the data is gone.
78+
*
79+
* The paths are the ones config_path() actually searched, computed rather than
80+
* described, so the instructions cannot drift away from the code. Nothing here
81+
* is a secret: no credentials are read at this point, because the file that
82+
* would hold them is the one that is missing.
83+
*/
84+
function config_missing_page(): never
85+
{
86+
http_response_code(500);
87+
header('Content-Type: text/html; charset=utf-8');
88+
header('X-Robots-Tag: noindex, nofollow');
89+
90+
$panel = dirname(__DIR__);
91+
$above = dirname($panel, 2) . '/nitesha-config';
92+
$e = static fn (string $v): string => htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
93+
94+
echo '<!doctype html><html lang="en"><head><meta charset="utf-8">'
95+
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
96+
. '<title>Panel not configured</title>'
97+
. '<style>'
98+
. 'body{margin:0;padding:32px 16px;background:#FAF7F2;color:#1A2233;'
99+
. 'font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}'
100+
. 'main{max-width:640px;margin:0 auto;background:#fff;border:1px solid #E7E0D6;'
101+
. 'border-radius:14px;padding:24px}'
102+
. 'h1{font-size:1.3rem;margin:0 0 8px}p{margin:0 0 12px}'
103+
. 'code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85rem;'
104+
. 'background:#FAF7F2;padding:2px 5px;border-radius:5px;word-break:break-all}'
105+
. 'ol{margin:0 0 12px;padding-left:20px}li{margin-bottom:6px}'
106+
. '.note{color:#5C5348;font-size:.92rem}'
107+
. 'a.btn{display:inline-block;margin-top:8px;background:#E8A317;color:#1A2233;'
108+
. 'text-decoration:none;font-weight:700;padding:10px 18px;border-radius:10px}'
109+
. '</style></head><body><main>'
110+
. '<h1>The panel cannot find its configuration</h1>'
111+
. '<p><code>config.php</code> holds the database details. It is deliberately not '
112+
. 'part of the deploy, so a rebuild of the website folder never overwrites it '
113+
. '&mdash; but that also means a rebuild cannot put it back.</p>'
114+
. '<p class="note"><strong>Your data is not affected.</strong> Bookings, vehicles '
115+
. 'and enquiries live in the database, which this file only points at.</p>'
116+
. '<p>Two places were checked:</p><ol>'
117+
. '<li><code>' . $e($panel . '/config.php') . '</code></li>'
118+
. '<li><code>' . $e($above . '/config.php') . '</code> &mdash; the better one, '
119+
. 'because it sits above the website folder where a deploy cannot reach it</li>'
120+
. '</ol>'
121+
. '<p>The installer will write it for you, in the second place, and will not '
122+
. 'touch an existing account if one is already in the database.</p>'
123+
. '<p><a class="btn" href="install.php">Open the installer</a></p>'
124+
. '</main></body></html>';
125+
exit;
126+
}
127+
70128
function config(?string $key = null): mixed
71129
{
72130
static $config = null;
@@ -78,8 +136,7 @@ function config(?string $key = null): mixed
78136
if ($config === null) {
79137
$path = config_path();
80138
if ($path === null) {
81-
http_response_code(500);
82-
exit('Not set up yet. Open install.php in your browser to get started.');
139+
config_missing_page();
83140
}
84141
$config = require $path;
85142
}

0 commit comments

Comments
 (0)