v-wordpress-plugin-updater/v-update-api/app/Controllers/HomeController.php
Nikolai X. Shadeauxs 8fc3bc20ad
Some checks failed
CI & Security / CI Scan (push) Failing after 9s
CI & Security / CodeQL (JavaScript) (push) Failing after 6s
CI & Security / Semgrep (PHP) (push) Failing after 8s
modified: .github/copilot-instructions.md
modified:   CHANGELOG.md
	modified:   README.md
2026-04-06 14:39:21 -04:00

307 lines
11 KiB
PHP

<?php
// phpcs:ignoreFile PSR1.Files.SideEffects.FoundWithSymbols
/**
* Project: UpdateAPI
* Author: Vontainment <services@vontainment.com>
* License: https://opensource.org/licenses/MIT MIT License
* Link: https://vontainment.com
* Version: 4.5.0
*
* File: HomeController.php
* Description: WordPress Update API
*/
namespace App\Controllers;
use App\Helpers\ValidationHelper;
use App\Helpers\EncryptionHelper;
use App\Helpers\SessionHelper;
use App\Core\ErrorManager;
use App\Models\HostsModel;
use App\Helpers\MessageHelper;
use App\Core\Response;
class HomeController
{
private const REVEAL_WINDOW_SECONDS = 30;
/**
* Handles GET requests for managing hosts.
*
* @return Response
*/
public function handleRequest(): Response
{
$this->pruneExpiredReveals();
return Response::view('home', [
'hostsTableHtml' => $this->getHostsTableHtml(),
]);
}
/**
* Handles POST submissions for host actions.
*
* @return Response
*/
public function handleSubmission(): Response
{
$token = $_POST['csrf_token'] ?? '';
if (!ValidationHelper::validateCsrfToken($token)) {
$error = 'Invalid Form Action.';
ErrorManager::log($error);
MessageHelper::addMessage($error);
return Response::redirect('/home');
}
$domain = isset($_POST['domain']) ? ValidationHelper::validateDomain($_POST['domain']) : null;
if (isset($_POST['add_entry'])) {
$newKey = ValidationHelper::generateKey();
if ($domain !== null && HostsModel::addEntry($domain, $newKey)) {
MessageHelper::addMessage('Entry added successfully.');
} else {
$error = 'Failed to add entry.';
ErrorManager::log($error);
MessageHelper::addMessage($error);
}
} elseif (isset($_POST['regen_entry'])) {
$newKey = ValidationHelper::generateKey();
if ($domain !== null && HostsModel::updateEntry($domain, $newKey)) {
MessageHelper::addMessage('Key regenerated successfully.');
} else {
$error = 'Failed to regenerate key.';
ErrorManager::log($error);
MessageHelper::addMessage($error);
}
} elseif (isset($_POST['reveal_entry'])) {
if ($domain !== null && $this->revealKey($domain)) {
MessageHelper::addMessage('Key revealed for 30 seconds.');
} else {
$error = 'Failed to reveal key.';
ErrorManager::log($error);
MessageHelper::addMessage($error);
}
} elseif (isset($_POST['delete_entry'])) {
if ($domain !== null && HostsModel::deleteEntry($domain)) {
MessageHelper::addMessage('Entry deleted successfully.');
} else {
$error = 'Failed to delete entry.';
error_log($error);
MessageHelper::addMessage($error);
}
}
return Response::redirect('/home');
}
/**
* Generates an HTML table row for a host entry.
*/
private function generateHostsTableRow(int $lineNumber, string $domain, string $key): string
{
$revealed = $this->getRevealedKey($domain);
$showingPlaintext = $revealed !== null && $revealed['key'] === $key;
$displayKey = $showingPlaintext ? $key : $this->maskKey($key);
$expiresAt = $showingPlaintext ? (int) $revealed['expires_at'] : 0;
$copyDisabled = $showingPlaintext ? '' : ' disabled';
$rowId = 'host-key-' . $lineNumber;
return '<tr>
<form method="post" action="/home">
<input type="hidden" name="csrf_token" value="' .
htmlspecialchars(SessionHelper::get('csrf_token') ?? '', ENT_QUOTES, 'UTF-8') . '">
<td><input class="hosts-domain" type="text" name="domain" value="' .
htmlspecialchars($domain, ENT_QUOTES, 'UTF-8') .
'" readonly></td>
<td>
<input id="' . htmlspecialchars($rowId, ENT_QUOTES, 'UTF-8') . '" class="hosts-key" type="text" value="' .
htmlspecialchars($displayKey, ENT_QUOTES, 'UTF-8') .
'" data-masked-value="' . htmlspecialchars($this->maskKey($key), ENT_QUOTES, 'UTF-8') . '" data-expires-at="' .
htmlspecialchars((string) $expiresAt, ENT_QUOTES, 'UTF-8') . '" readonly>
</td>
<td>
<input class="hosts-submit" type="submit" name="reveal_entry" value="Reveal">
<button class="hosts-submit copy-key-btn" type="button" data-copy-target="' .
htmlspecialchars($rowId, ENT_QUOTES, 'UTF-8') . '"' . $copyDisabled . '>Copy</button>
<input class="hosts-submit" type="submit" name="regen_entry" value="Regen">
<input class="hosts-submit" type="submit" name="delete_entry" value="Delete">
</td>
</form>
</tr>';
}
/**
* Generates the hosts table HTML for display.
*
* @return string HTML table or message.
*/
private function getHostsTableHtml(): string
{
$entries = HostsModel::getEntries();
$hostsTableHtml = '';
if (count($entries) > 0) {
$halfCount = (int) ceil(count($entries) / 2);
$entriesColumn1 = array_slice($entries, 0, $halfCount);
$entriesColumn2 = array_slice($entries, $halfCount);
$hostsTableHtml .= '<div class="row">';
// Column 1
$hostsTableHtml .= '<div class="column">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Key</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($entriesColumn1 as $index => $entry) {
$lineNumber = $index; // Correct line number for column 1
$domain = $entry['domain'] ?? '';
$encryptedKey = $entry['key'] ?? '';
$key = EncryptionHelper::decrypt($encryptedKey) ?? '';
if ($key !== '') {
HostsModel::migrateLegacyKey($domain, $encryptedKey, $key);
}
$hostsTableHtml .= $this->generateHostsTableRow($lineNumber, $domain, $key);
}
$hostsTableHtml .= '</tbody></table></div>';
// Column 2
$hostsTableHtml .= '<div class="column">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Key</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($entriesColumn2 as $index => $entry) {
$lineNumber = $index + $halfCount; // Correct line number for column 2
$domain = $entry['domain'] ?? '';
$encryptedKey = $entry['key'] ?? '';
$key = EncryptionHelper::decrypt($encryptedKey) ?? '';
if ($key !== '') {
HostsModel::migrateLegacyKey($domain, $encryptedKey, $key);
}
$hostsTableHtml .= $this->generateHostsTableRow($lineNumber, $domain, $key);
}
$hostsTableHtml .= '</tbody></table></div></div>';
} else {
$hostsTableHtml = "No entries found.";
}
return $hostsTableHtml;
}
/**
* Reveal a host key for a short duration.
*/
private function revealKey(string $domain): bool
{
$decrypted = $this->lookupDecryptedKey($domain);
if ($decrypted === null || $decrypted === '') {
return false;
}
$revealed = SessionHelper::get('revealed_keys', []);
if (!is_array($revealed)) {
$revealed = [];
}
$revealed[$domain] = [
'key' => $decrypted,
'expires_at' => time() + self::REVEAL_WINDOW_SECONDS,
];
SessionHelper::set('revealed_keys', $revealed);
return true;
}
/**
* Look up and decrypt a host key by domain.
*
* @param string $domain Domain name.
* @return string|null Decrypted key or null if not found.
*/
private function lookupDecryptedKey(string $domain): ?string
{
foreach (HostsModel::getEntries() as $entry) {
$entryDomain = $entry['domain'] ?? '';
if ($entryDomain !== $domain) {
continue;
}
$encryptedKey = $entry['key'] ?? '';
$key = EncryptionHelper::decrypt($encryptedKey);
if ($key !== null && $key !== '') {
HostsModel::migrateLegacyKey($domain, $encryptedKey, $key);
}
return $key;
}
return null;
}
/**
* Return revealed key metadata when still valid.
*
* @return array{key: string, expires_at: int}|null
*/
private function getRevealedKey(string $domain): ?array
{
$revealed = SessionHelper::get('revealed_keys', []);
if (!is_array($revealed) || !isset($revealed[$domain]) || !is_array($revealed[$domain])) {
return null;
}
$entry = $revealed[$domain];
$key = $entry['key'] ?? null;
$expiresAt = $entry['expires_at'] ?? 0;
if (!is_string($key) || !is_int($expiresAt) || $expiresAt < time()) {
return null;
}
return [
'key' => $key,
'expires_at' => $expiresAt,
];
}
/**
* Purge expired key-reveal entries from session.
*/
private function pruneExpiredReveals(): void
{
$revealed = SessionHelper::get('revealed_keys', []);
if (!is_array($revealed)) {
SessionHelper::set('revealed_keys', []);
return;
}
$filtered = [];
$now = time();
foreach ($revealed as $domain => $entry) {
if (!is_string($domain) || !is_array($entry)) {
continue;
}
$key = $entry['key'] ?? null;
$expiresAt = $entry['expires_at'] ?? null;
if (!is_string($key) || !is_int($expiresAt) || $expiresAt < $now) {
continue;
}
$filtered[$domain] = $entry;
}
SessionHelper::set('revealed_keys', $filtered);
}
/**
* Mask a key for default UI rendering.
*/
private function maskKey(string $key): string
{
if ($key === '') {
return '••••••••';
}
return str_repeat('•', max(8, strlen($key)));
}
}