Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ All notable public-release changes are recorded here. Historical project notes r
- Updated database connection construction to honor `DB_PORT` while retaining port `3306` as the default.
- Updated the root landing page to trigger the installation guard before normal output.

### Fixed

- Enforced the installation lock for same-session requests to installer steps 1-8 after successful installation.
- Restricted the completion screen to one pending view and cleared completion-session state before the admin-login redirect.

### Compatibility

- No database table, column, index, foreign key, trigger, or migration was changed.
Expand Down
2 changes: 2 additions & 0 deletions RELEASE_NOTES_v5.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ After successful installation Licora creates:

The installation flag contains only product, version, and installation timestamp. Installer files remain on disk but execution is disabled.

The completion screen is a one-time same-session handoff. After installation, requests to installer steps 1-8 are denied even when the browser still holds the installer session. Step 10 clears the completion-session state before redirecting to the administrator login.

## Optional DEMO data

When selected, the installer creates clearly marked DEMO data using existing tables only:
Expand Down
31 changes: 31 additions & 0 deletions includes/installation.php
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,34 @@ function licora_installer_finalize(?string $root, array $data): array
}
}
}

if (!function_exists('licora_installer_locked_request_action')) {
/**
* Decide how an installer request is handled after installation.
*
* Only the one-time success handoff (step 9) and its subsequent login
* redirect (step 10) may proceed. Every other installed-state request,
* including same-session requests to steps 1-8, remains locked.
*/
function licora_installer_locked_request_action(
bool $locked,
int $step,
bool $hasSuccessData,
bool $successViewPending,
bool $loginRedirectPending
): string {
if (!$locked) {
return 'continue';
}

if ($step === 9 && $hasSuccessData && $successViewPending) {
return 'show_success';
}

if ($step === 10 && $hasSuccessData && $loginRedirectPending) {
return 'redirect_login';
}

return 'locked';
}
}
47 changes: 36 additions & 11 deletions install.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,36 @@ function installer_render_locked(): void
exit;
}

$requestedStep = filter_input(INPUT_GET, 'step', FILTER_VALIDATE_INT);
$step = $requestedStep !== false && $requestedStep !== null ? max(1, min(10, (int)$requestedStep)) : 1;
$successData = $_SESSION['licora_installer_success'] ?? null;
if (licora_installer_is_locked($root) && !is_array($successData)) {
$lockedAction = licora_installer_locked_request_action(
licora_installer_is_locked($root),
$step,
is_array($successData),
!empty($_SESSION['licora_installer_success_view_pending']),
!empty($_SESSION['licora_installer_login_redirect_pending'])
);

if ($lockedAction === 'show_success') {
unset($_SESSION['licora_installer_success_view_pending']);
$_SESSION['licora_installer_login_redirect_pending'] = true;
} elseif ($lockedAction === 'redirect_login') {
$adminUrl = (string)($successData['admin_url'] ?? '');
unset(
$_SESSION['licora_installer_success'],
$_SESSION['licora_installer_success_view_pending'],
$_SESSION['licora_installer_login_redirect_pending'],
$_SESSION['licora_installer'],
$_SESSION['licora_installer_csrf']
);
session_regenerate_id(true);
if ($adminUrl === '' || preg_match('/[\r\n]/', $adminUrl) === 1) {
installer_render_locked();
}
header('Location: ' . $adminUrl, true, 302);
exit;
} elseif ($lockedAction === 'locked') {
installer_render_locked();
}

Expand All @@ -56,19 +84,12 @@ function installer_render_locked(): void
}

$wizard =& $_SESSION['licora_installer'];
$requestedStep = filter_input(INPUT_GET, 'step', FILTER_VALIDATE_INT);
$step = $requestedStep !== false && $requestedStep !== null ? max(1, min(10, (int)$requestedStep)) : 1;
if (is_array($successData)) {
$wizard['max_step'] = 10;
if (is_array($successData) && $step === 9) {
$wizard['max_step'] = 9;
} elseif ($step > (int)($wizard['max_step'] ?? 1)) {
$step = (int)($wizard['max_step'] ?? 1);
}

if ($step === 10 && is_array($successData)) {
header('Location: ' . (string)$successData['admin_url'], true, 302);
exit;
}

$error = '';
$notice = '';
$webBasePath = licora_installation_base_path();
Expand Down Expand Up @@ -194,7 +215,11 @@ function installer_render_locked(): void
} else {
$result = licora_installer_finalize($root, $wizard['data']);
$_SESSION['licora_installer_success'] = $result;
unset($_SESSION['licora_installer']);
$_SESSION['licora_installer_success_view_pending'] = true;
unset(
$_SESSION['licora_installer_login_redirect_pending'],
$_SESSION['licora_installer']
);
$_SESSION['licora_installer_csrf'] = bin2hex(random_bytes(32));
session_regenerate_id(true);
header('Location: ?step=9');
Expand Down
36 changes: 36 additions & 0 deletions tests/installer_smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,36 @@
$assert(($flag['product'] ?? '') === 'Licora', 'installation flag identifies Licora');
$assert(($flag['version'] ?? '') === '5.1.0', 'installation flag records version');
$assert(!isset($flag['database_password']) && !isset($flag['encryption_key']), 'installation flag contains no secrets');

$assert(
licora_installer_locked_request_action(false, 1, false, false, false) === 'continue',
'fresh installer requests continue'
);
$assert(
licora_installer_locked_request_action(true, 1, true, true, false) === 'locked',
'same-session root revisit remains locked after installation'
);
$assert(
licora_installer_locked_request_action(true, 2, true, true, false) === 'locked',
'same-session wizard steps remain locked after installation'
);
$assert(
licora_installer_locked_request_action(true, 9, true, true, false) === 'show_success',
'one pending completion request may show the success screen'
);
$assert(
licora_installer_locked_request_action(true, 9, true, false, true) === 'locked',
'success screen cannot be replayed after its pending view is consumed'
);
$assert(
licora_installer_locked_request_action(true, 10, true, false, true) === 'redirect_login',
'pending completion flow may redirect to admin login'
);
$assert(
licora_installer_locked_request_action(true, 10, true, false, false) === 'locked',
'direct step-ten access without the completion handoff remains locked'
);

@unlink($tempRoot . '/includes/.licora-installed');
@rmdir($tempRoot . '/includes');
@rmdir($tempRoot);
Expand All @@ -102,9 +132,15 @@
'Installation already completed.',
'Complete Installation',
'Go to Login',
'licora_installer_success_view_pending',
'licora_installer_login_redirect_pending',
] as $marker) {
$assert(strpos($installer, $marker) !== false, 'installer marker present: ' . $marker);
}
$assert(
strpos($installer, '&& !is_array($successData)') === false,
'success-session data no longer bypasses the installation lock'
);

if ($failures !== []) {
fwrite(STDERR, "Installer smoke test failed:\n- " . implode("\n- ", $failures) . "\n");
Expand Down
Loading