add Response class

This commit is contained in:
Vontainment 2025-09-11 17:37:50 -04:00
parent e5113af400
commit 92c4f40a4b
16 changed files with 268 additions and 185 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# Ignore files and directories that should not be tracked by Git
.vscode/
update-api/storage/BLACKLIST.json
update-api/storage/logs/
vendor/
node_modules/

View file

@ -10,6 +10,14 @@ See [standard-version](https://github.com/conventional-changelog/standard-versio
- Consolidated `VONTMENT_PLUGINS` and `VONTMENT_THEMES` into a single `VONTMNT_API_URL` constant.
- **Split update loops into single-item tasks**: Refactored plugin and theme updaters to use asynchronous per-item processing. Daily update checks now schedule individual `wp_schedule_single_event()` tasks for each plugin/theme instead of processing all items synchronously. Added `vontmnt_plugin_update_single()` and `vontmnt_theme_update_single()` callback functions.
- Added comprehensive test coverage for database manager, router dispatching, plugin model uploads, session manager, updater error handling, and URL encoding.
- Stored admin password as a hash and verified with `password_verify` during login.
- Controllers now return structured `Response` objects; router and session handling updated accordingly.
- Expanded filename validation to allow digits and underscores in slugs and updated tests.
- Introduced configurable `LOG_FILE` and centralized logging through `ErrorManager`.
- Made `SessionManager::requireAuth` non-terminating, returning a boolean instead.
- Enhanced `vontmnt_get_api_key` with wp-config backups and validation.
- Streamlined plugin updates using a single streaming `wp_remote_get` call.
- Removed HTML escaping in `HostsModel` in favor of parameterized queries.

## 4.0.0
- Added PHP_CodeSniffer with WordPress Coding Standards for linting.

View file

@ -487,7 +487,7 @@ The v-wordpress-plugin-updater project is designed to streamline the management
mkdir -p /storage/themes
mkdir -p /storage/logs
```
3. Edit `/config.php` and set the login credentials and directory constants. Adjust `VALID_USERNAME`, `VALID_PASSWORD`, and paths under `BASE_DIR` if the defaults do not match your setup.
3. Edit `/config.php` and set the login credentials and directory constants. Adjust `VALID_USERNAME`, `VALID_PASSWORD_HASH` (generate with `password_hash()`), `LOG_FILE`, and paths under `BASE_DIR` if the defaults do not match your setup.
4. Set an `ENCRYPTION_KEY` environment variable used to secure host keys:

```sh
@ -500,7 +500,7 @@ The v-wordpress-plugin-updater project is designed to streamline the management
define('VONTMNT_UPDATE_KEYREGEN', true); // set to true to fetch/regenerate the key
```
The updater will fetch the API key from `/api/key` when this constant is true or when no key is stored. The key is saved as the `vontmnt_api_key` option and `wp-config.php` is rewritten to disable regeneration after the first retrieval.
6. Ensure the web server user owns the `/storage` directory so uploads and logs can be written.
6. Ensure the web server user owns the `/storage` directory so uploads and logs can be written. Application logs are written to `LOG_FILE` (default `/storage/logs/app.log`).

7. From the `update-api/` directory run `php install.php` to create the SQLite database and required tables, including the blacklist. Ensure `storage/updater.sqlite` is writable by the web server.


View file

@ -46,8 +46,18 @@ function vontmnt_get_api_key(): string {
if ( file_exists( $wp_config ) && is_writable( $wp_config ) ) {
$config = file_get_contents( $wp_config );
if ( false !== $config ) {
$config = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config );
file_put_contents( $wp_config, $config );
$backup = $wp_config . '.bak';
if ( ! copy( $wp_config, $backup ) ) {
error_log( 'Failed to back up wp-config.php' );
return is_string( $key ) ? $key : '';
}
$updated = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config, 1, $count );
if ( null === $updated || 0 === $count || false === file_put_contents( $wp_config, $updated ) ) {
error_log( 'Failed to update VONTMNT_UPDATE_KEYREGEN in wp-config.php' );
copy( $backup, $wp_config );
return '';
}
unlink( $backup );
}
}
}
@ -268,101 +278,96 @@ function vontmnt_plugin_update_single( string $plugin_path, string $installed_ve
VONTMNT_API_URL
);

// Use wp_remote_get instead of cURL.
$response = wp_remote_get( $api_url );
if ( is_wp_error( $response ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, 0, 0, 'failed', 'HTTP error: ' . $response->get_error_message() );
delete_option( $lock_key );
return;
}
$http_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
require_once ABSPATH . 'wp-admin/includes/file.php';

if ( $http_code === 200 && ! empty( $response_body ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
// Initialize WP_Filesystem before any file operations
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
WP_Filesystem();
}
$upload_dir = wp_upload_dir();
// Ensure the upload directory exists
if ( ! wp_mkdir_p( $upload_dir['path'] ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'failed', 'Failed to create upload directory' );
delete_option( $lock_key );
return;
}
$plugin_zip_file = $upload_dir['path'] . '/' . $plugin_slug . '.zip';
// Stream large files to disk instead of loading into memory
$temp_file = wp_tempnam( $plugin_zip_file );
$stream_response = wp_remote_get( $api_url, array( 'stream' => true, 'filename' => $temp_file ) );
if ( is_wp_error( $stream_response ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'failed', 'Failed to stream update package' );
delete_option( $lock_key );
return;
}
// Move temp file to final location (allow overwrite)
if ( file_exists( $plugin_zip_file ) ) {
wp_delete_file( $plugin_zip_file );
}
if ( ! $wp_filesystem->move( $temp_file, $plugin_zip_file ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'failed', 'Failed to move update package' );
wp_delete_file( $temp_file );
delete_option( $lock_key );
return;
}
$response_size = filesize( $plugin_zip_file );
// Initialize WP_Filesystem before any file operations
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
WP_Filesystem();
}

// Validate ZIP package before installation
if ( ! vontmnt_validate_zip_package( $plugin_zip_file ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'failed', 'Invalid ZIP package' );
wp_delete_file( $plugin_zip_file );
delete_option( $lock_key );
return;
}
$upload_dir = wp_upload_dir();

require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$upgrader = new Plugin_Upgrader();
$callback = function ( $options ) use ( $plugin_zip_file ) {
$options['package'] = $plugin_zip_file;
$options['clear_destination'] = true;
return $options;
};
add_filter( 'upgrader_package_options', $callback );
$result = $upgrader->install( $plugin_zip_file );
remove_filter( 'upgrader_package_options', $callback );
// Ensure the upload directory exists
if ( ! wp_mkdir_p( $upload_dir['path'] ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, 0, 0, 'failed', 'Failed to create upload directory' );
delete_option( $lock_key );
return;
}

// Delete the plugin zip file using wp_delete_file.
wp_delete_file( $plugin_zip_file );
// Post-install housekeeping: refresh plugin update data
if ( function_exists( 'wp_clean_plugins_cache' ) ) {
wp_clean_plugins_cache( true );
}
// Log success or failure
if ( ! is_wp_error( $result ) && $result ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'success', 'Plugin updated successfully' );
} else {
$error_msg = is_wp_error( $result ) ? $result->get_error_message() : 'Installation failed';
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'failed', $error_msg );
}
} elseif ( 204 === $http_code ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'skipped', 'No update available' );
} else {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'failed', 'Unexpected HTTP response' );
}
// Release the lock
delete_option( $lock_key );
$plugin_zip_file = $upload_dir['path'] . '/' . $plugin_slug . '.zip';

// Stream download directly to temp file
$temp_file = wp_tempnam( $plugin_zip_file );
$response = wp_remote_get( $api_url, array( 'stream' => true, 'filename' => $temp_file ) );

if ( is_wp_error( $response ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, 0, 0, 'failed', 'HTTP error: ' . $response->get_error_message() );
delete_option( $lock_key );
return;
}

$http_code = wp_remote_retrieve_response_code( $response );

if ( 200 === $http_code && file_exists( $temp_file ) ) {
// Move temp file to final location (allow overwrite)
if ( file_exists( $plugin_zip_file ) ) {
wp_delete_file( $plugin_zip_file );
}
if ( ! $wp_filesystem->move( $temp_file, $plugin_zip_file ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, 0, 'failed', 'Failed to move update package' );
wp_delete_file( $temp_file );
delete_option( $lock_key );
return;
}

$response_size = filesize( $plugin_zip_file );

// Validate ZIP package before installation
if ( ! vontmnt_validate_zip_package( $plugin_zip_file ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'failed', 'Invalid ZIP package' );
wp_delete_file( $plugin_zip_file );
delete_option( $lock_key );
return;
}

require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$upgrader = new Plugin_Upgrader();
$callback = function ( $options ) use ( $plugin_zip_file ) {
$options['package'] = $plugin_zip_file;
$options['clear_destination'] = true;
return $options;
};
add_filter( 'upgrader_package_options', $callback );
$result = $upgrader->install( $plugin_zip_file );
remove_filter( 'upgrader_package_options', $callback );

// Delete the plugin zip file using wp_delete_file.
wp_delete_file( $plugin_zip_file );

// Post-install housekeeping: refresh plugin update data
if ( function_exists( 'wp_clean_plugins_cache' ) ) {
wp_clean_plugins_cache( true );
}

// Log success or failure
if ( ! is_wp_error( $result ) && $result ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'success', 'Plugin updated successfully' );
} else {
$error_msg = is_wp_error( $result ) ? $result->get_error_message() : 'Installation failed';
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'failed', $error_msg );
}
} elseif ( 204 === $http_code ) {
$response_size = file_exists( $temp_file ) ? filesize( $temp_file ) : 0;
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'skipped', 'No update available' );
wp_delete_file( $temp_file );
} else {
$response_size = file_exists( $temp_file ) ? filesize( $temp_file ) : 0;
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, $http_code, $response_size, 'failed', 'Unexpected HTTP response' );
wp_delete_file( $temp_file );
}

// Release the lock
delete_option( $lock_key );
}
}

View file

@ -54,7 +54,7 @@ class PluginModelDbTest extends TestCase
$tmp = tempnam(sys_get_temp_dir(), 'pl');
file_put_contents($tmp, 'data');
$files = [
'name' => ['sample_1.0.zip'],
'name' => ['sample1_test_1.0.zip'],
'tmp_name' => [$tmp],
'error' => [UPLOAD_ERR_OK],
'size' => [filesize($tmp)],
@ -62,7 +62,7 @@ class PluginModelDbTest extends TestCase
$messages = PluginModel::uploadFiles($files);
$this->assertStringContainsString('uploaded successfully', $messages[0]);
$conn = DatabaseManager::getConnection();
$row = $conn->fetchAssociative('SELECT * FROM plugins WHERE slug = ?', ['sample']);
$row = $conn->fetchAssociative('SELECT * FROM plugins WHERE slug = ?', ['sample1_test']);
$this->assertSame('1.0', $row['version']);
}


View file

@ -20,16 +20,16 @@ use App\Models\Blacklist;
use App\Core\ErrorManager;
use App\Core\Controller;
use App\Core\DatabaseManager;
use App\Core\Response;

class ApiController extends Controller
{
public function handleRequest(): void
public function handleRequest(): Response
{
$ip = $_SERVER['REMOTE_ADDR'];
if (Blacklist::isBlacklisted($ip) || $_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(403);
ErrorManager::getInstance()->log('Forbidden or invalid request from ' . $ip);
exit();
return new Response(403);
}

$params = [
@ -42,9 +42,8 @@ class ApiController extends Controller
$values = [];
foreach ($params as $p) {
if (!isset($_GET[$p]) || $_GET[$p] === '' || ($p === 'type' && !in_array($_GET[$p], ['plugin', 'theme']))) {
http_response_code(400);
ErrorManager::getInstance()->log('Bad request missing parameter: ' . $p);
exit();
return new Response(400);
}
$values[] = $_GET[$p];
}
@ -69,9 +68,8 @@ class ApiController extends Controller
$invalid[] = 'version';
}
if (!empty($invalid)) {
http_response_code(400);
ErrorManager::getInstance()->log('Bad request invalid parameter: ' . implode(', ', $invalid));
exit();
return new Response(400);
}

$dir = $type === 'theme' ? THEMES_DIR : PLUGINS_DIR;
@ -88,35 +86,34 @@ class ApiController extends Controller
if (version_compare($dbVersion, $version, '>')) {
$file_path = $dir . '/' . $slug . '_' . $dbVersion . '.zip';
if (is_file($file_path)) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file_path) . '"');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
$headers = [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . basename($file_path) . '"',
'Content-Length' => (string) filesize($file_path),
];
$conn->executeStatement(
'INSERT INTO logs (domain, type, date, status) VALUES (?, ?, ?, ?)',
[$domain, $type, date('Y-m-d'), 'Success']
);
ErrorManager::getInstance()->log($domain . ' ' . date('Y-m-d') . ' Successful', 'info');
exit();
return Response::file($file_path, $headers);
}
}
http_response_code(204);
$conn->executeStatement(
'INSERT INTO logs (domain, type, date, status) VALUES (?, ?, ?, ?)',
[$domain, $type, date('Y-m-d'), 'Success']
);
ErrorManager::getInstance()->log($domain . ' ' . date('Y-m-d') . ' Successful', 'info');
exit();
return new Response(204);
}
}
}

http_response_code(403);
$conn->executeStatement(
'INSERT INTO logs (domain, type, date, status) VALUES (?, ?, ?, ?)',
[$domain, $type, date('Y-m-d'), 'Failed']
);
ErrorManager::getInstance()->log($domain . ' ' . date('Y-m-d') . ' Failed');
exit();
return new Response(403);
}
}

View file

@ -19,51 +19,44 @@ use App\Models\HostsModel;
use App\Models\Blacklist;
use App\Core\ErrorManager;
use App\Core\Controller;
use App\Core\Response;

class KeyController extends Controller
{
/**
* Handle API requests for retrieving host keys.
*/
public function handleRequest(): void
public function handleRequest(): Response
{
$ip = $_SERVER['REMOTE_ADDR'];
if (Blacklist::isBlacklisted($ip) || $_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(403);
ErrorManager::getInstance()->log('Forbidden or invalid request from ' . $ip);
return;
return new Response(403);
}

$required = ['type', 'domain'];
foreach ($required as $p) {
if (!isset($_GET[$p]) || $_GET[$p] === '') {
http_response_code(400);
ErrorManager::getInstance()->log('Bad request missing parameter: ' . $p);
return;
return new Response(400);
}
}
if ($_GET['type'] !== 'auth') {
http_response_code(400);
ErrorManager::getInstance()->log('Bad request invalid type');
return;
return new Response(400);
}

$domain = Validation::validateDomain($_GET['domain']);
if ($domain === null) {
http_response_code(400);
ErrorManager::getInstance()->log('Bad request invalid parameter: domain');
return;
return new Response(400);
}

$key = HostsModel::getKeyIfSendAuth($domain);
if ($key !== null) {
header('Content-Type: text/plain');
http_response_code(200);
echo $key;
return;
return Response::text($key);
}

http_response_code(403);
return;
return new Response(403);
}
}

View file

@ -21,20 +21,20 @@ use App\Core\ErrorManager;
use App\Helpers\MessageHelper;
use App\Core\SessionManager;
use App\Core\Csrf;
use App\Core\Response;

class LoginController extends Controller
{
public function handleRequest(): void
public function handleRequest(): Response
{
$session = SessionManager::getInstance();
if ($session->get('logged_in') === true) {
header('Location: /home');
exit();
return Response::redirect('/home');
}
$this->render('login', []);
return Response::view('login');
}

public function handleSubmission(): void
public function handleSubmission(): Response
{
$session = SessionManager::getInstance();
$token = $_POST['csrf_token'] ?? '';
@ -42,25 +42,22 @@ class LoginController extends Controller
$error = 'Invalid CSRF token.';
ErrorManager::getInstance()->log($error);
MessageHelper::addMessage($error);
header('Location: /login');
exit();
return Response::redirect('/login');
}

if (isset($_POST['logout'])) {
self::logoutUser();
return self::logoutUser();
}

$username = isset($_POST['username']) ? Validation::validateUsername($_POST['username']) : null;
$password = isset($_POST['password']) ? Validation::validatePassword($_POST['password']) : null;

if ($username === VALID_USERNAME && $password === VALID_PASSWORD) {
if ($username === VALID_USERNAME && $password !== null && password_verify($password, VALID_PASSWORD_HASH)) {
$session->set('logged_in', true);
$session->set('username', $username);
$session->set('user_agent', $_SERVER['HTTP_USER_AGENT'] ?? '');
$session->set('csrf_token', bin2hex(random_bytes(32)));
$session->regenerate();
header('Location: /home');
exit();
return Response::redirect('/home');
}

$ip = $_SERVER['REMOTE_ADDR'];
@ -74,14 +71,12 @@ class LoginController extends Controller
ErrorManager::getInstance()->log($error);
MessageHelper::addMessage($error);
}
header('Location: /login');
exit();
return Response::redirect('/login');
}

private static function logoutUser(): void
private static function logoutUser(): Response
{
SessionManager::getInstance()->destroy();
header('Location: /login');
exit();
return Response::redirect('/login');
}
}

View file

@ -38,7 +38,7 @@ class ErrorManager

public function log(string $message, string $type = 'error'): void
{
$logFile = __DIR__ . '/../../php_app.log';
$logFile = defined('LOG_FILE') ? LOG_FILE : (__DIR__ . '/../../php_app.log');
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[$timestamp] [$type]: $message\n";
error_log($logMessage, 3, $logFile);

View file

@ -0,0 +1,63 @@
<?php

namespace App\Core;

/**
* Simple HTTP response representation.
*
* @phpstan-type Headers array<string,string>
*/
class Response
{
/**
* @param int $status HTTP status code
* @param Headers $headers Response headers
* @param string $body Response body
* @param string|null $file Path to file to stream
* @param string|null $view View name to render
* @param array<string,mixed> $data Data passed to view
*/
public function __construct(
public int $status = 200,
public array $headers = [],
public string $body = '',
public ?string $file = null,
public ?string $view = null,
public array $data = []
) {
}

/**
* Create redirect response.
*/
public static function redirect(string $location, int $status = 302): self
{
return new self($status, ['Location' => $location]);
}

/**
* Create view response.
*
* @param array<string,mixed> $data
*/
public static function view(string $view, array $data = [], int $status = 200): self
{
return new self($status, [], '', null, $view, $data);
}

/**
* Create plain text response.
*/
public static function text(string $body, int $status = 200): self
{
return new self($status, ['Content-Type' => 'text/plain'], $body);
}

/**
* Create file streaming response.
*/
public static function file(string $path, array $headers = [], int $status = 200): self
{
return new self($status, $headers, '', $path);
}
}

View file

@ -18,6 +18,7 @@ use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use function FastRoute\simpleDispatcher;
use App\Core\SessionManager;
use App\Core\Response;

class Router
{
@ -28,9 +29,8 @@ class Router
{
$this->dispatcher = simpleDispatcher(function (RouteCollector $r): void {
// Redirect the root URL to the home page for convenience
$r->addRoute('GET', '/', function (): void {
header('Location: /home');
exit();
$r->addRoute('GET', '/', function (): Response {
return Response::redirect('/home');
});
$r->addRoute('GET', '/login', ['\\App\\Controllers\\LoginController', 'handleRequest']);
$r->addRoute('POST', '/login', ['\\App\\Controllers\\LoginController', 'handleSubmission']);
@ -66,11 +66,11 @@ class Router

switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
header('HTTP/1.0 404 Not Found');
http_response_code(404);
require __DIR__ . '/../Views/404.php';
break;
case Dispatcher::METHOD_NOT_ALLOWED:
header('HTTP/1.0 405 Method Not Allowed');
http_response_code(405);
break;
case Dispatcher::FOUND:
if (is_array($routeInfo[1])) {
@ -80,28 +80,56 @@ class Router
? str_starts_with($route, '/api')
: strpos($route, '/api') === 0;
if ($isApi) {
$query = parse_url($uri, PHP_URL_QUERY);
parse_str($query ?? '', $params);
$required = (function_exists('str_starts_with') ? str_starts_with($route, '/api/key') : strpos($route, '/api/key') === 0)
? ['type', 'domain']
: ['type', 'domain', 'key', 'slug', 'version'];
foreach ($required as $key) {
if (!isset($params[$key])) {
$isApi = false;
break;
$query = parse_url($uri, PHP_URL_QUERY);
parse_str($query ?? '', $params);
$required = (function_exists('str_starts_with') ? str_starts_with($route, '/api/key') : strpos($route, '/api/key') === 0)
? ['type', 'domain']
: ['type', 'domain', 'key', 'slug', 'version'];
foreach ($required as $key) {
if (!isset($params[$key])) {
$isApi = false;
break;
}
}
}
}
if ($route !== '/login' && !$isApi) {
if (!SessionManager::getInstance()->requireAuth()) {
$this->sendResponse(Response::redirect('/login'));
return;
}
}
call_user_func_array([new $class(), $action], $vars);
$response = call_user_func_array([new $class(), $action], $vars);
if ($response instanceof Response) {
$this->sendResponse($response);
}
} elseif (is_callable($routeInfo[1])) {
call_user_func($routeInfo[1]);
$response = call_user_func($routeInfo[1]);
if ($response instanceof Response) {
$this->sendResponse($response);
}
}
break;
}
}

private function sendResponse(Response $response): void
{
http_response_code($response->status);
foreach ($response->headers as $name => $value) {
header($name . ': ' . $value);
}

if ($response->file !== null) {
readfile($response->file);
return;
}

if ($response->view !== null) {
extract($response->data);
require __DIR__ . '/../Views/' . $response->view . '.php';
return;
}

echo $response->body;
}
}

View file

@ -89,8 +89,7 @@ class SessionManager
}

if (!$this->isValid()) {
header('Location: /login');
exit();
return false;
}

return true;

View file

@ -79,7 +79,7 @@ class Validation
public static function validateFilename(string $filename): ?string
{
$filename = basename(trim($filename));
return preg_match('/^[A-Za-z-]+_[0-9.]+\.zip$/', $filename) ? $filename : null;
return preg_match('/^[A-Za-z0-9_-]+_[0-9.]+\.zip$/', $filename) ? $filename : null;
}

/**

View file

@ -40,11 +40,9 @@ class HostsModel
*/
public static function addEntry(string $domain, string $key): bool
{
$safe_domain = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');
$safe_key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$encrypted = Encryption::encrypt($safe_key);
$encrypted = Encryption::encrypt($key);
$conn = DatabaseManager::getConnection();
return $conn->executeStatement('INSERT INTO hosts (domain, key, send_auth) VALUES (?, ?, 1)', [$safe_domain, $encrypted]) > 0;
return $conn->executeStatement('INSERT INTO hosts (domain, key, send_auth) VALUES (?, ?, 1)', [$domain, $encrypted]) > 0;
}

/**
@ -52,11 +50,9 @@ class HostsModel
*/
public static function updateEntry(int $line, string $domain, string $key): bool
{
$safe_domain = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');
$safe_key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
$encrypted = Encryption::encrypt($safe_key);
$encrypted = Encryption::encrypt($key);
$conn = DatabaseManager::getConnection();
return $conn->executeStatement('REPLACE INTO hosts (domain, key, send_auth) VALUES (?, ?, 1)', [$safe_domain, $encrypted]) > 0;
return $conn->executeStatement('REPLACE INTO hosts (domain, key, send_auth) VALUES (?, ?, 1)', [$domain, $encrypted]) > 0;
}

/**
@ -64,11 +60,10 @@ class HostsModel
*/
public static function deleteEntry(int $line, string $domain): bool
{
$safe_domain = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');
$conn = DatabaseManager::getConnection();
$result = $conn->executeStatement('DELETE FROM hosts WHERE domain = ?', [$safe_domain]) > 0;
$result = $conn->executeStatement('DELETE FROM hosts WHERE domain = ?', [$domain]) > 0;
if ($result) {
$conn->executeStatement('DELETE FROM logs WHERE domain = ?', [$safe_domain]);
$conn->executeStatement('DELETE FROM logs WHERE domain = ?', [$domain]);
}
return $result;
}
@ -78,9 +73,8 @@ class HostsModel
*/
public static function markSendAuth(string $domain): void
{
$safe_domain = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');
$conn = DatabaseManager::getConnection();
$conn->executeStatement('UPDATE hosts SET send_auth = 1 WHERE domain = ?', [$safe_domain]);
$conn->executeStatement('UPDATE hosts SET send_auth = 1 WHERE domain = ?', [$domain]);
}

/**
@ -88,11 +82,10 @@ class HostsModel
*/
public static function getKeyIfSendAuth(string $domain): ?string
{
$safe_domain = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8');
$conn = DatabaseManager::getConnection();
$row = $conn->fetchAssociative('SELECT key, send_auth FROM hosts WHERE domain = ?', [$safe_domain]);
$row = $conn->fetchAssociative('SELECT key, send_auth FROM hosts WHERE domain = ?', [$domain]);
if ($row && (int) $row['send_auth'] === 1) {
$conn->executeStatement('UPDATE hosts SET send_auth = 0 WHERE domain = ?', [$safe_domain]);
$conn->executeStatement('UPDATE hosts SET send_auth = 0 WHERE domain = ?', [$domain]);
return Encryption::decrypt($row['key']);
}
return null;

View file

@ -104,7 +104,7 @@ class PluginModel
continue;
}

if (preg_match('/^(.+)_([\d\.]+)\.zip$/', $file_name, $matches)) {
if (preg_match('/^([A-Za-z0-9_-]+)_([\d\.]+)\.zip$/', $file_name, $matches)) {
$slug = $matches[1];
$version = $matches[2];
if ($current && version_compare($version, $current, '<=')) {

View file

@ -12,7 +12,7 @@
*/

define('VALID_USERNAME', 'admin');
define('VALID_PASSWORD', 'password');
define('VALID_PASSWORD_HASH', '$2y$10$tYi5dWtBVRNkLqoSwV0yfuzM9Wh6A7O6oDulEGaM1lM3FsIaVvQ9e');

define('ENCRYPTION_KEY', getenv('ENCRYPTION_KEY') ?: '');

@ -23,4 +23,5 @@ define('HOSTS_ACL', BASE_DIR);
define('PLUGINS_DIR', BASE_DIR . '/storage/plugins');
define('THEMES_DIR', BASE_DIR . '/storage/themes');
define('LOG_DIR', BASE_DIR . '/storage/logs');
define('LOG_FILE', LOG_DIR . '/app.log');
define('DB_FILE', BASE_DIR . '/storage/updater.sqlite');