diff --git a/public/index.php b/public/index.php
index 2fa0a57..8f7f214 100644
--- a/public/index.php
+++ b/public/index.php
@@ -41,21 +41,14 @@ if (!(preg_match('/setup$/', $path))) {
}
}
-// Get a database connection
-// TODO: Change from static function.
-global $db;
+// Initialize application context with all dependencies
+global $app;
$db = Database::get();
-
-// Initialize core entities
-// Defining these as globals isn't great practice,
-// but this is a small, single-user app and this data will rarely change.
-global $config;
-global $user;
-
-$config = new ConfigModel($db);
-$config = $config->loadFromDatabase();
-$user = new UserModel($db);
-$user = $user->loadFromDatabase();
+$app = [
+ 'db' => $db,
+ 'config' => (new ConfigModel($db))->loadFromDatabase(),
+ 'user' => (new UserModel($db))->loadFromDatabase(),
+];
// Start a session and generate a CSRF Token
// if there isn't already an active session
@@ -63,8 +56,8 @@ Session::start();
Session::generateCsrfToken();
// Remove the base path from the URL
-if (strpos($path, $config->basePath) === 0) {
- $path = substr($path, strlen($config->basePath));
+if (strpos($path, $app['config']->basePath) === 0) {
+ $path = substr($path, strlen($app['config']->basePath));
}
// strip the trailing slash from the resulting route
@@ -99,7 +92,7 @@ if ($method === 'POST' && $path != 'setup') {
header('Content-Type: text/html; charset=utf-8');
// Render the requested route or throw a 404
-$router = new Router($db, $config, $user);
+$router = new Router();
if (!$router->route($path, $method)){
http_response_code(404);
echo "404 - Page Not Found";
diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php
index f2ff3a6..afccd5d 100644
--- a/src/Controller/AdminController/AdminController.php
+++ b/src/Controller/AdminController/AdminController.php
@@ -13,22 +13,26 @@ class AdminController extends Controller {
}
public function getAdminData(bool $isSetup): array {
+ global $app;
+
Log::debug("Loading admin page" . ($isSetup ? " (setup mode)" : ""));
return [
- 'user' => $this->user,
- 'config' => $this->config,
+ 'user' => $app['user'],
+ 'config' => $app['config'],
'isSetup' => $isSetup,
];
}
public function handleSave(){
+ global $app;
+
if (!Session::isLoggedIn()){
- header('Location: ' . Util::buildRelativeUrl($this->config->basePath, 'login'));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login'));
exit;
}
- $result = $this->processSettingsSave($_POST, false);
+ $result = $this->saveSettings($_POST, false);
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
@@ -36,12 +40,14 @@ class AdminController extends Controller {
public function handleSetup(){
// for setup, we don't care if they're logged in
// (because they can't be until setup is complete)
- $result = $this->processSettingsSave($_POST, true);
+ $result = $this->saveSettings($_POST, true);
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
- public function processSettingsSave(array $postData, bool $isSetup): array {
+ public function saveSettings(array $postData, bool $isSetup): array {
+ global $app;
+
$result = ['success' => false, 'errors' => []];
Log::debug("Processing settings save" . ($isSetup ? " (setup mode)" : ""));
@@ -122,30 +128,30 @@ class AdminController extends Controller {
if (empty($errors)) {
try {
// Update site settings
- $this->config->siteTitle = $siteTitle;
- $this->config->siteDescription = $siteDescription;
- $this->config->baseUrl = $baseUrl;
- $this->config->basePath = $basePath;
- $this->config->itemsPerPage = $itemsPerPage;
- $this->config->strictAccessibility = $strictAccessibility;
- $this->config->logLevel = $logLevel;
+ $app['config']->siteTitle = $siteTitle;
+ $app['config']->siteDescription = $siteDescription;
+ $app['config']->baseUrl = $baseUrl;
+ $app['config']->basePath = $basePath;
+ $app['config']->itemsPerPage = $itemsPerPage;
+ $app['config']->strictAccessibility = $strictAccessibility;
+ $app['config']->logLevel = $logLevel;
// Save site settings and reload config from database
- $this->config = $this->config->save();
+ $app['config'] = $app['config']->save();
Log::info("Site settings updated");
// Update user profile
- $this->user->username = $username;
- $this->user->displayName = $displayName;
- $this->user->website = $website;
+ $app['user']->username = $username;
+ $app['user']->displayName = $displayName;
+ $app['user']->website = $website;
// Save user profile and reload user from database
- $this->user = $this->user->save();
+ $app['user'] = $app['user']->save();
Log::info("User profile updated");
// Update the password if one was sent
if($password){
- $this->user->setPassword($password);
+ $app['user']->setPassword($password);
Log::info("User password updated");
}
diff --git a/src/Controller/AuthController/AuthController.php b/src/Controller/AuthController/AuthController.php
index 1ab8189..c5e5f1f 100644
--- a/src/Controller/AuthController/AuthController.php
+++ b/src/Controller/AuthController/AuthController.php
@@ -1,11 +1,12 @@
$config,
+ 'config' => $app['config'],
'csrf_token' => $csrf_token,
'error' => $error,
];
@@ -14,7 +15,7 @@ class AuthController extends Controller {
}
function handleLogin(){
- global $config;
+ global $app;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
@@ -22,7 +23,7 @@ class AuthController extends Controller {
Log::debug("Login attempt for user {$username}");
- $userModel = new UserModel();
+ $userModel = new UserModel($app['db']);
$user = $userModel->getByUsername($username);
//if ($user && password_verify($password, $user['password_hash'])) {
@@ -30,7 +31,7 @@ class AuthController extends Controller {
Log::info("Successful login for {$username}");
Session::newLoginSession($user);
- header('Location: ' . Util::buildRelativeUrl($config->basePath));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath));
exit;
} else {
Log::warning("Failed login for {$username}");
@@ -44,11 +45,12 @@ class AuthController extends Controller {
}
function handleLogout(){
+ global $app;
+
Log::info("Logout from user " . $_SESSION['username']);
Session::end();
- global $config;
- header('Location: ' . Util::buildRelativeUrl($config->basePath));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath));
exit;
}
}
\ No newline at end of file
diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php
index d72bd58..043acf2 100644
--- a/src/Controller/Controller.php
+++ b/src/Controller/Controller.php
@@ -1,7 +1,5 @@
$user,
- 'config' => $config,
+ 'user' => $app['user'],
+ 'config' => $app['config'],
'customCss' => $customCss,
];
@@ -49,8 +49,6 @@ class CssController extends Controller {
}
public function handlePost() {
- global $config;
-
switch ($_POST['action']) {
case 'upload':
$this->handleUpload();
@@ -69,7 +67,7 @@ class CssController extends Controller {
}
public function handleDelete(): void{
- global $config;
+ global $app;
// Don't try to delete the default theme.
if (!$_POST['selectCssFile']){
@@ -113,26 +111,26 @@ class CssController extends Controller {
}
// Set the theme back to default
- $config->cssId = null;
- $config = $config->save();
+ $app['config']->cssId = null;
+ $app['config'] = $app['config']->save();
// Set flash message
Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.');
}
private function handleSetTheme() {
- global $config;
+ global $app;
if ($_POST['selectCssFile']){
// Set custom theme
- $config->cssId = $_POST['selectCssFile'];
+ $app['config']->cssId = $_POST['selectCssFile'];
} else {
// Set default theme
- $config->cssId = null;
+ $app['config']->cssId = null;
}
// Update the site theme
- $config = $config->save();
+ $app['config'] = $app['config']->save();
// Set flash message
Session::setFlashMessage('success', 'Theme applied.');
diff --git a/src/Controller/EmojiController/EmojiController.php b/src/Controller/EmojiController/EmojiController.php
index 07520c9..9989f92 100644
--- a/src/Controller/EmojiController/EmojiController.php
+++ b/src/Controller/EmojiController/EmojiController.php
@@ -2,11 +2,12 @@
class EmojiController extends Controller {
// Shows the custom emoji management page
public function index(){
- global $config;
+ global $app;
+
$emojiList = EmojiModel::loadAll();
$vars = [
- 'config' => $config,
+ 'config' => $app['config'],
'emojiList' => $emojiList,
];
@@ -14,7 +15,7 @@
}
public function handlePost(): void {
- global $config;
+ global $app;
switch ($_POST['action']) {
case 'add':
@@ -29,7 +30,7 @@
break;
}
- header('Location: ' . Util::buildRelativeUrl($config->basePath, 'admin/emoji'));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'admin/emoji'));
exit;
}
diff --git a/src/Controller/FeedController/FeedController.php b/src/Controller/FeedController/FeedController.php
index 77eb8ae..b20b687 100644
--- a/src/Controller/FeedController/FeedController.php
+++ b/src/Controller/FeedController/FeedController.php
@@ -2,17 +2,19 @@
class FeedController extends Controller {
private $ticks;
- public function __construct(PDO $db, ConfigModel $config, UserModel $user){
- parent::__construct($db, $config, $user);
+ public function __construct() {
+ global $app;
- $tickModel = new TickModel($db, $config);
- $this->ticks = $tickModel->getPage($config->itemsPerPage);
+ $tickModel = new TickModel($app['db'], $app['config']);
+ $this->ticks = $tickModel->getPage($app['config']->itemsPerPage);
Log::debug("Loaded " . count($this->ticks) . " ticks for feeds");
}
public function rss(){
- $generator = new RssGenerator($this->config, $this->ticks);
+ global $app;
+
+ $generator = new RssGenerator($app['config'], $this->ticks);
Log::debug("Generating RSS feed with " . count($this->ticks) . " ticks");
header('Content-Type: ' . $generator->getContentType());
@@ -20,7 +22,9 @@ class FeedController extends Controller {
}
public function atom(){
- $generator = new AtomGenerator($this->config, $this->ticks);
+ global $app;
+
+ $generator = new AtomGenerator($app['config'], $this->ticks);
Log::debug("Generating Atom feed with " . count($this->ticks) . " ticks");
header('Content-Type: ' . $generator->getContentType());
diff --git a/src/Controller/HomeController/HomeController.php b/src/Controller/HomeController/HomeController.php
index 06e5097..820a82b 100644
--- a/src/Controller/HomeController/HomeController.php
+++ b/src/Controller/HomeController/HomeController.php
@@ -9,21 +9,23 @@ class HomeController extends Controller {
}
public function getHomeData(int $page): array {
+ global $app;
+
Log::debug("Loading home page $page");
- $tickModel = new TickModel($this->db, $this->config);
- $limit = $this->config->itemsPerPage;
+ $tickModel = new TickModel($app['db'], $app['config']);
+ $limit = $app['config']->itemsPerPage;
$offset = ($page - 1) * $limit;
$ticks = $tickModel->getPage($limit, $offset);
- $view = new TicksView($this->config, $ticks, $page);
+ $view = new TicksView($app['config'], $ticks, $page);
$tickList = $view->getHtml();
Log::info("Home page loaded with " . count($ticks) . " ticks");
return [
- 'config' => $this->config,
- 'user' => $this->user,
+ 'config' => $app['config'],
+ 'user' => $app['user'],
'tickList' => $tickList,
];
}
@@ -31,14 +33,18 @@ class HomeController extends Controller {
// POST handler
// Saves the tick and reloads the homepage
public function handleTick(){
+ global $app;
+
$result = $this->processTick($_POST);
// redirect to the index (will show the latest tick if one was sent)
- header('Location: ' . Util::buildRelativeUrl($this->config->basePath));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath));
exit;
}
public function processTick(array $postData): array {
+ global $app;
+
$result = ['success' => false, 'message' => ''];
if (!isset($postData['new_tick'])) {
@@ -55,7 +61,7 @@ class HomeController extends Controller {
}
try {
- $tickModel = new TickModel($this->db, $this->config);
+ $tickModel = new TickModel($app['db'], $app['config']);
$tickModel->insert($tickContent);
Log::info("New tick created: " . substr($tickContent, 0, 50) . (strlen($tickContent) > 50 ? '...' : ''));
$result['success'] = true;
diff --git a/src/Controller/LogController/LogController.php b/src/Controller/LogController/LogController.php
index f9fe2fb..3bccfd5 100644
--- a/src/Controller/LogController/LogController.php
+++ b/src/Controller/LogController/LogController.php
@@ -2,16 +2,16 @@
class LogController extends Controller {
private string $storageDir;
- public function __construct(PDO $db, ConfigModel $config, UserModel $user, ?string $storageDir = null) {
- parent::__construct($db, $config, $user);
+ public function __construct(?string $storageDir = null) {
$this->storageDir = $storageDir ?? STORAGE_DIR;
}
public function index() {
+ global $app;
+
// Ensure user is logged in
if (!Session::isLoggedIn()) {
- global $config;
- header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login'));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login'));
exit;
}
@@ -26,7 +26,7 @@ class LogController extends Controller {
}
public function getLogData(string $levelFilter = '', string $routeFilter = ''): array {
- global $config;
+ global $app;
$limit = 300; // Show last 300 log entries
@@ -38,7 +38,7 @@ class LogController extends Controller {
$availableLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR'];
return [
- 'config' => $config,
+ 'config' => $app['config'],
'logEntries' => $logEntries,
'availableRoutes' => $availableRoutes,
'availableLevels' => $availableLevels,
diff --git a/src/Controller/MoodController/MoodController.php b/src/Controller/MoodController/MoodController.php
index f586ae2..450df86 100644
--- a/src/Controller/MoodController/MoodController.php
+++ b/src/Controller/MoodController/MoodController.php
@@ -1,14 +1,14 @@
render_mood_picker(self::getEmojisWithLabels(), $user->mood);
+ $moodPicker = $view->render_mood_picker(self::getEmojisWithLabels(), $app['user']->mood);
$vars = [
- 'config' => $config,
+ 'config' => $app['config'],
'moodPicker' => $moodPicker,
];
@@ -16,11 +16,9 @@
}
public function handlePost(){
+ global $app;
+
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- // Get the data we need
- global $config;
- global $user;
-
switch ($_POST['action']){
case 'set':
$mood = $_POST['mood'];
@@ -31,11 +29,11 @@
}
// set or clear the mood
- $user->mood = $mood;
- $user = $user->save();
+ $app['user']->mood = $mood;
+ $app['user'] = $app['user']->save();
// go back to the index and show the updated mood
- header('Location: ' . Util::buildRelativeUrl($config->basePath));
+ header('Location: ' . Util::buildRelativeUrl($app['config']->basePath));
exit;
}
}
diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php
index 7b1f2f3..ab2e887 100644
--- a/src/Controller/TickController/TickController.php
+++ b/src/Controller/TickController/TickController.php
@@ -3,7 +3,9 @@
class TickController extends Controller{
//public function index(string $year, string $month, string $day, string $hour, string $minute, string $second){
public function index(int $id){
- $tickModel = new TickModel();
+ global $app;
+
+ $tickModel = new TickModel($app['db'], $app['config']);
$vars = $tickModel->get($id);
$this->render('tick.php', $vars);
}
diff --git a/src/Framework/Log/Log.php b/src/Framework/Log/Log.php
index 5c41fdc..08a818b 100644
--- a/src/Framework/Log/Log.php
+++ b/src/Framework/Log/Log.php
@@ -44,8 +44,8 @@ class Log {
}
private static function write($level, $message) {
- global $config;
- $logLevel = $config->logLevel ?? self::LEVELS['INFO'];
+ global $app;
+ $logLevel = $app['config']->logLevel ?? self::LEVELS['INFO'];
// Only log messages if they're at or above the configured log level.
if (self::LEVELS[$level] < $logLevel){
diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php
index 3cea851..28d3dcb 100644
--- a/src/Framework/Router/Router.php
+++ b/src/Framework/Router/Router.php
@@ -1,8 +1,6 @@
db, $this->config, $this->user);
+ $instance = new $controllerName();
call_user_func_array([$instance, $functionName], $matches);
return true;
}
diff --git a/src/Framework/Util/Util.php b/src/Framework/Util/Util.php
index 9456cb3..2741a42 100644
--- a/src/Framework/Util/Util.php
+++ b/src/Framework/Util/Util.php
@@ -25,12 +25,12 @@ class Util {
return preg_replace_callback(
'~(https?://[^\s<>"\'()]+)~i',
function($matches) use ($link_attrs) {
- global $config;
+ global $app;
$escaped_url = rtrim($matches[1], '.,!?;:)]}>');
$clean_url = html_entity_decode($escaped_url, ENT_QUOTES, 'UTF-8');
- $tabIndex = $config->strictAccessibility ? ' tabindex="0" ' : ' ';
+ $tabIndex = $app['config']->strictAccessibility ? ' tabindex="0"' : '';
- return '' . $escaped_url . '';
+ return '' . $escaped_url . '';
},
$text
);
@@ -66,41 +66,41 @@ class Util {
public static function buildUrl(string $baseUrl, string $basePath, string $path = ''): string {
// Normalize baseUrl (remove trailing slash)
$baseUrl = rtrim($baseUrl, '/');
-
+
// Normalize basePath (ensure leading slash, remove trailing slash unless it's just '/')
if ($basePath === '' || $basePath === '/') {
$basePath = '/';
} else {
$basePath = '/' . trim($basePath, '/') . '/';
}
-
+
// Normalize path (remove leading slash if present)
$path = ltrim($path, '/');
-
+
return $baseUrl . $basePath . $path;
}
public static function buildRelativeUrl(string $basePath, string $path = ''): string {
// Ensure basePath starts with / for relative URLs
$basePath = '/' . ltrim($basePath, '/');
-
+
// Remove trailing slash unless it's just '/'
if ($basePath !== '/') {
$basePath = rtrim($basePath, '/');
}
-
+
// Add path
$path = ltrim($path, '/');
-
+
if ($path === '') {
return $basePath;
}
-
+
// If basePath is root, don't add extra slash
if ($basePath === '/') {
return '/' . $path;
}
-
+
return $basePath . '/' . $path;
}
}
\ No newline at end of file
diff --git a/tests/Controller/AdminController/AdminControllerTest.php b/tests/Controller/AdminController/AdminControllerTest.php
index 1725369..5618419 100644
--- a/tests/Controller/AdminController/AdminControllerTest.php
+++ b/tests/Controller/AdminController/AdminControllerTest.php
@@ -15,13 +15,8 @@ class AdminControllerTest extends TestCase
$this->tempLogDir = sys_get_temp_dir() . '/tkr_test_logs_' . uniqid();
mkdir($this->tempLogDir . '/logs', 0777, true);
Log::init($this->tempLogDir . '/logs/tkr.log');
-
- // Set up global config for logging level (DEBUG = 1)
- global $config;
- $config = new stdClass();
- $config->logLevel = 1; // Allow DEBUG level logs
- // Create mock PDO (needed for base constructor)
+ // Create mock PDO
$this->mockPdo = $this->createMock(PDO::class);
// Create real config and user objects with mocked PDO
@@ -36,6 +31,17 @@ class AdminControllerTest extends TestCase
$this->user->username = 'testuser';
$this->user->displayName = 'Test User';
$this->user->website = 'https://example.com';
+
+ // Set up global $app for simplified dependency access
+ global $app;
+ $app = [
+ 'db' => $this->mockPdo,
+ 'config' => $this->config,
+ 'user' => $this->user,
+ ];
+
+ // Set log level on config for Log class
+ $this->config->logLevel = 1; // Allow DEBUG level logs
}
protected function tearDown(): void
@@ -60,7 +66,7 @@ class AdminControllerTest extends TestCase
public function testGetAdminDataRegularMode(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
$data = $controller->getAdminData(false);
// Should return proper structure
@@ -76,7 +82,7 @@ class AdminControllerTest extends TestCase
public function testGetAdminDataSetupMode(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
$data = $controller->getAdminData(true);
// Should return proper structure
@@ -92,8 +98,8 @@ class AdminControllerTest extends TestCase
public function testProcessSettingsSaveWithEmptyData(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
- $result = $controller->processSettingsSave([], false);
+ $controller = new AdminController();
+ $result = $controller->saveSettings([], false);
$this->assertFalse($result['success']);
$this->assertContains('No data provided', $result['errors']);
@@ -101,7 +107,7 @@ class AdminControllerTest extends TestCase
public function testProcessSettingsSaveValidationErrors(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
// Test data with multiple validation errors
$postData = [
@@ -116,7 +122,7 @@ class AdminControllerTest extends TestCase
'confirm_password' => 'different' // Passwords don't match
];
- $result = $controller->processSettingsSave($postData, false);
+ $result = $controller->saveSettings($postData, false);
$this->assertFalse($result['success']);
$this->assertNotEmpty($result['errors']);
@@ -157,7 +163,12 @@ class AdminControllerTest extends TestCase
$config = new ConfigModel($this->mockPdo);
$user = new UserModel($this->mockPdo);
- $controller = new AdminController($this->mockPdo, $config, $user);
+ // Update global $app with test models
+ global $app;
+ $app['config'] = $config;
+ $app['user'] = $user;
+
+ $controller = new AdminController();
$postData = [
'username' => 'newuser',
@@ -172,7 +183,7 @@ class AdminControllerTest extends TestCase
'log_level' => 2
];
- $result = $controller->processSettingsSave($postData, false);
+ $result = $controller->saveSettings($postData, false);
$this->assertTrue($result['success']);
$this->assertEmpty($result['errors']);
@@ -214,7 +225,12 @@ class AdminControllerTest extends TestCase
$config = new ConfigModel($this->mockPdo);
$user = new UserModel($this->mockPdo);
- $controller = new AdminController($this->mockPdo, $config, $user);
+ // Update global $app with test models
+ global $app;
+ $app['config'] = $config;
+ $app['user'] = $user;
+
+ $controller = new AdminController();
$postData = [
'username' => 'testuser',
@@ -228,7 +244,7 @@ class AdminControllerTest extends TestCase
'confirm_password' => 'newpassword'
];
- $result = $controller->processSettingsSave($postData, false);
+ $result = $controller->saveSettings($postData, false);
$this->assertTrue($result['success']);
}
@@ -242,7 +258,12 @@ class AdminControllerTest extends TestCase
$config = new ConfigModel($this->mockPdo);
$user = new UserModel($this->mockPdo);
- $controller = new AdminController($this->mockPdo, $config, $user);
+ // Update global $app with test models
+ global $app;
+ $app['config'] = $config;
+ $app['user'] = $user;
+
+ $controller = new AdminController();
$postData = [
'username' => 'testuser',
@@ -254,7 +275,7 @@ class AdminControllerTest extends TestCase
'items_per_page' => 10
];
- $result = $controller->processSettingsSave($postData, false);
+ $result = $controller->saveSettings($postData, false);
$this->assertFalse($result['success']);
$this->assertContains('Failed to save settings', $result['errors']);
@@ -262,7 +283,7 @@ class AdminControllerTest extends TestCase
public function testLoggingOnAdminPageLoad(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
$controller->getAdminData(false);
// Check that logs were written
@@ -275,7 +296,7 @@ class AdminControllerTest extends TestCase
public function testLoggingOnSetupPageLoad(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
$controller->getAdminData(true);
// Check that logs were written
@@ -288,7 +309,7 @@ class AdminControllerTest extends TestCase
public function testLoggingOnValidationErrors(): void
{
- $controller = new AdminController($this->mockPdo, $this->config, $this->user);
+ $controller = new AdminController();
$postData = [
'username' => '', // Will cause validation error
@@ -299,7 +320,7 @@ class AdminControllerTest extends TestCase
'items_per_page' => 10
];
- $controller->processSettingsSave($postData, false);
+ $controller->saveSettings($postData, false);
// Check that logs were written
$logFile = $this->tempLogDir . '/logs/tkr.log';
@@ -341,7 +362,12 @@ class AdminControllerTest extends TestCase
$config = new ConfigModel($this->mockPdo);
$user = new UserModel($this->mockPdo);
- $controller = new AdminController($this->mockPdo, $config, $user);
+ // Update global $app with test models
+ global $app;
+ $app['config'] = $config;
+ $app['user'] = $user;
+
+ $controller = new AdminController();
$postData = [
'username' => 'testuser',
@@ -353,7 +379,7 @@ class AdminControllerTest extends TestCase
'items_per_page' => 10
];
- $controller->processSettingsSave($postData, false);
+ $controller->saveSettings($postData, false);
// Check that logs were written
$logFile = $this->tempLogDir . '/logs/tkr.log';
diff --git a/tests/Controller/FeedController/FeedControllerTest.php b/tests/Controller/FeedController/FeedControllerTest.php
index 4f8ddd1..3cfe81f 100644
--- a/tests/Controller/FeedController/FeedControllerTest.php
+++ b/tests/Controller/FeedController/FeedControllerTest.php
@@ -16,11 +16,6 @@ class FeedControllerTest extends TestCase
mkdir($this->tempLogDir . '/logs', 0777, true);
Log::init($this->tempLogDir . '/logs/tkr.log');
- // Set up global config for logging level (DEBUG = 1)
- global $config;
- $config = new stdClass();
- $config->logLevel = 1; // Allow DEBUG level logs
-
// Create mock PDO and PDOStatement
$this->mockStatement = $this->createMock(PDOStatement::class);
$this->mockPdo = $this->createMock(PDO::class);
@@ -36,6 +31,17 @@ class FeedControllerTest extends TestCase
// Mock user
$this->mockUser = new UserModel($this->mockPdo);
$this->mockUser->displayName = 'Test User';
+
+ // Set up global $app for simplified dependency access
+ global $app;
+ $app = [
+ 'db' => $this->mockPdo,
+ 'config' => $this->mockConfig,
+ 'user' => $this->mockUser,
+ ];
+
+ // Set log level on config for Log class
+ $this->mockConfig->logLevel = 1; // Allow DEBUG level logs
}
protected function tearDown(): void
@@ -77,7 +83,7 @@ class FeedControllerTest extends TestCase
{
$this->setupMockDatabase([]);
- $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new FeedController();
// Verify it was created successfully
$this->assertInstanceOf(FeedController::class, $controller);
@@ -99,7 +105,7 @@ class FeedControllerTest extends TestCase
$this->setupMockDatabase($testTicks);
- $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new FeedController();
// Verify it was created successfully
$this->assertInstanceOf(FeedController::class, $controller);
@@ -127,7 +133,7 @@ class FeedControllerTest extends TestCase
->method('execute')
->with([10, 0]); // itemsPerPage=10, page 1 = offset 0
- new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ new FeedController();
}
public function testRssMethodLogsCorrectly(): void
@@ -138,7 +144,7 @@ class FeedControllerTest extends TestCase
$this->setupMockDatabase($testTicks);
- $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new FeedController();
// Capture output to prevent headers/content from affecting test
ob_start();
@@ -160,7 +166,7 @@ class FeedControllerTest extends TestCase
$this->setupMockDatabase($testTicks);
- $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new FeedController();
// Capture output to prevent headers/content from affecting test
ob_start();
diff --git a/tests/Controller/HomeController/HomeControllerTest.php b/tests/Controller/HomeController/HomeControllerTest.php
index 647e94f..6bfbf26 100644
--- a/tests/Controller/HomeController/HomeControllerTest.php
+++ b/tests/Controller/HomeController/HomeControllerTest.php
@@ -16,11 +16,6 @@ class HomeControllerTest extends TestCase
mkdir($this->tempLogDir . '/logs', 0777, true);
Log::init($this->tempLogDir . '/logs/tkr.log');
- // Set up global config for logging level (DEBUG = 1)
- global $config;
- $config = new stdClass();
- $config->logLevel = 1; // Allow DEBUG level logs
-
// Create mock PDO and PDOStatement
$this->mockStatement = $this->createMock(PDOStatement::class);
$this->mockPdo = $this->createMock(PDO::class);
@@ -34,6 +29,17 @@ class HomeControllerTest extends TestCase
$this->mockUser = new UserModel($this->mockPdo);
$this->mockUser->displayName = 'Test User';
$this->mockUser->mood = '😊';
+
+ // Set up global $app for simplified dependency access
+ global $app;
+ $app = [
+ 'db' => $this->mockPdo,
+ 'config' => $this->mockConfig,
+ 'user' => $this->mockUser,
+ ];
+
+ // Set log level on config for Log class
+ $this->mockConfig->logLevel = 1; // Allow DEBUG level logs
}
protected function tearDown(): void
@@ -91,7 +97,7 @@ class HomeControllerTest extends TestCase
{
$this->setupMockDatabase([]); // Empty array = no ticks
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$data = $controller->getHomeData(1);
// Should return proper structure
@@ -118,7 +124,7 @@ class HomeControllerTest extends TestCase
$this->setupMockDatabase($testTicks);
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$data = $controller->getHomeData(1);
// Should return proper structure
@@ -147,7 +153,7 @@ class HomeControllerTest extends TestCase
->method('execute')
->with([10, 10]); // itemsPerPage=10, page 2 = offset 10
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$controller->getHomeData(2); // Page 2
}
@@ -171,7 +177,7 @@ class HomeControllerTest extends TestCase
&& $params[1] === 'This is a test tick';
}));
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => 'This is a test tick'];
$result = $controller->processTick($postData);
@@ -185,7 +191,7 @@ class HomeControllerTest extends TestCase
// PDO shouldn't be called at all for empty content
$this->mockPdo->expects($this->never())->method('prepare');
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => ' ']; // Just whitespace
$result = $controller->processTick($postData);
@@ -199,7 +205,7 @@ class HomeControllerTest extends TestCase
// PDO shouldn't be called at all for missing field
$this->mockPdo->expects($this->never())->method('prepare');
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = []; // No new_tick field
$result = $controller->processTick($postData);
@@ -219,7 +225,7 @@ class HomeControllerTest extends TestCase
return $params[1] === 'This has whitespace'; // Should be trimmed
}));
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => ' This has whitespace '];
$result = $controller->processTick($postData);
@@ -231,7 +237,7 @@ class HomeControllerTest extends TestCase
{
$this->setupMockDatabaseForInsert(false); // Will throw exception
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => 'This will fail'];
$result = $controller->processTick($postData);
@@ -247,7 +253,7 @@ class HomeControllerTest extends TestCase
];
$this->setupMockDatabase($testTicks);
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$controller->getHomeData(1);
// Check that logs were written
@@ -263,7 +269,7 @@ class HomeControllerTest extends TestCase
{
$this->setupMockDatabaseForInsert(true);
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => 'Test tick for logging'];
$controller->processTick($postData);
@@ -278,7 +284,7 @@ class HomeControllerTest extends TestCase
public function testLoggingOnEmptyTick(): void
{
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => ''];
$controller->processTick($postData);
@@ -297,7 +303,7 @@ class HomeControllerTest extends TestCase
{
$this->setupMockDatabaseForInsert(false);
- $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
+ $controller = new HomeController();
$postData = ['new_tick' => 'This will fail'];
$controller->processTick($postData);
diff --git a/tests/Controller/LogController/LogControllerTest.php b/tests/Controller/LogController/LogControllerTest.php
index 6c90c22..684e028 100644
--- a/tests/Controller/LogController/LogControllerTest.php
+++ b/tests/Controller/LogController/LogControllerTest.php
@@ -19,12 +19,20 @@ class LogControllerTest extends TestCase
$this->originalGet = $_GET;
$_GET = [];
- // Mock global config
- global $config;
+ // Set up global $app for simplified dependency access
$mockPdo = $this->createMock(PDO::class);
- $config = new ConfigModel($mockPdo);
- $config->baseUrl = 'https://example.com';
- $config->basePath = '/tkr/';
+ $mockConfig = new ConfigModel($mockPdo);
+ $mockConfig->baseUrl = 'https://example.com';
+ $mockConfig->basePath = '/tkr/';
+
+ $mockUser = new UserModel($mockPdo);
+
+ global $app;
+ $app = [
+ 'db' => $mockPdo,
+ 'config' => $mockConfig,
+ 'user' => $mockUser,
+ ];
}
protected function tearDown(): void
@@ -51,10 +59,8 @@ class LogControllerTest extends TestCase
public function testGetLogDataWithNoLogFiles(): void
{
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData();
// Should return empty log entries but valid structure
@@ -85,10 +91,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData();
// Should parse all valid entries and ignore invalid ones
@@ -129,10 +133,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData('ERROR');
// Should only include ERROR entries
@@ -152,10 +154,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData('', 'GET /admin');
// Should only include GET /admin entries
@@ -175,10 +175,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData('ERROR', 'GET /admin');
// Should only include entries matching both filters
@@ -201,10 +199,8 @@ class LogControllerTest extends TestCase
$rotatedLog2 = '[2025-01-31 12:00:00] WARNING: 127.0.0.1 - Rotated log entry 2';
file_put_contents($this->testLogFile . '.2', $rotatedLog2);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData();
// Should read from all log files, newest first
@@ -226,10 +222,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData();
// Should extract unique routes, sorted
@@ -248,10 +242,8 @@ class LogControllerTest extends TestCase
file_put_contents($this->testLogFile, $logContent);
- $mockPdo = $this->createMock(PDO::class);
- $mockConfig = new ConfigModel($mockPdo);
- $mockUser = new UserModel($mockPdo);
- $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir);
+ // Uses global $app set up in setUp()
+ $controller = new LogController($this->tempLogDir);
$data = $controller->getLogData();
// Should only include valid entries, ignore invalid ones
diff --git a/tests/Framework/Log/LogTest.php b/tests/Framework/Log/LogTest.php
index 0f9e4d0..2b9fc56 100644
--- a/tests/Framework/Log/LogTest.php
+++ b/tests/Framework/Log/LogTest.php
@@ -43,10 +43,11 @@ class LogTest extends TestCase
{
Log::setRouteContext('GET /admin');
- // Create a mock config for log level
- global $config;
- $config = new stdClass();
- $config->logLevel = 1; // DEBUG level
+ // Create a mock app config for log level
+ global $app;
+ $app = [
+ 'config' => (object)['logLevel' => 1] // DEBUG level
+ ];
Log::debug('Test message');
@@ -61,9 +62,10 @@ class LogTest extends TestCase
{
Log::setRouteContext('');
- global $config;
- $config = new stdClass();
- $config->logLevel = 1;
+ global $app;
+ $app = [
+ 'config' => (object)['logLevel' => 1]
+ ];
Log::info('Test without route');
@@ -78,9 +80,10 @@ class LogTest extends TestCase
public function testLogLevelFiltering(): void
{
- global $config;
- $config = new stdClass();
- $config->logLevel = 3; // WARNING level
+ global $app;
+ $app = [
+ 'config' => (object)['logLevel' => 3] // WARNING level
+ ];
Log::debug('Debug message'); // Should be filtered out
Log::info('Info message'); // Should be filtered out
@@ -99,9 +102,10 @@ class LogTest extends TestCase
{
Log::setRouteContext('POST /admin');
- global $config;
- $config = new stdClass();
- $config->logLevel = 1;
+ global $app;
+ $app = [
+ 'config' => (object)['logLevel' => 1]
+ ];
Log::error('Test error message');
@@ -129,9 +133,10 @@ class LogTest extends TestCase
public function testLogRotation(): void
{
- global $config;
- $config = new stdClass();
- $config->logLevel = 1;
+ global $app;
+ $app = [
+ 'config' => (object)['logLevel' => 1]
+ ];
// Create a log file with exactly 1000 lines (the rotation threshold)
$logLines = str_repeat("[2025-01-31 12:00:00] INFO: 127.0.0.1 - Test line\n", 1000);
@@ -154,9 +159,11 @@ class LogTest extends TestCase
public function testDefaultLogLevelWhenConfigMissing(): void
{
- // Clear global config
- global $config;
- $config = null;
+ // Set up config without logLevel property (simulates missing config value)
+ global $app;
+ $app = [
+ 'config' => (object)[] // Empty config object, no logLevel property
+ ];
// Should not throw errors and should default to INFO level
Log::debug('Debug message'); // Should be filtered out (default INFO level = 2)
diff --git a/tests/Framework/Util/UtilTest.php b/tests/Framework/Util/UtilTest.php
index e4bdfb5..5e60d77 100644
--- a/tests/Framework/Util/UtilTest.php
+++ b/tests/Framework/Util/UtilTest.php
@@ -73,4 +73,134 @@ final class UtilTest extends TestCase
$this->assertEquals($expected, $result);
}
+ // Test data for escape_html function
+ public static function escapeHtmlProvider(): array {
+ return [
+ 'basic HTML' => ['', '<script>alert("xss")</script>'],
+ 'quotes' => ['He said "Hello" & she said \'Hi\'', 'He said "Hello" & she said 'Hi''],
+ 'empty string' => ['', ''],
+ 'normal text' => ['Hello World', 'Hello World'],
+ 'ampersand' => ['Tom & Jerry', 'Tom & Jerry'],
+ 'unicode' => ['🚀 emoji & text', '🚀 emoji & text'],
+ ];
+ }
+
+ #[DataProvider('escapeHtmlProvider')]
+ public function testEscapeHtml(string $input, string $expected): void {
+ $result = Util::escape_html($input);
+ $this->assertEquals($expected, $result);
+ }
+
+ // Test data for escape_xml function
+ public static function escapeXmlProvider(): array {
+ return [
+ 'basic XML' => ['content', '<tag attr="value">content</tag>'],
+ 'quotes and ampersand' => ['Title & "Subtitle"', 'Title & "Subtitle"'],
+ 'empty string' => ['', ''],
+ 'normal text' => ['Hello World', 'Hello World'],
+ 'unicode' => ['🎵 music & notes', '🎵 music & notes'],
+ ];
+ }
+
+ #[DataProvider('escapeXmlProvider')]
+ public function testEscapeXml(string $input, string $expected): void {
+ $result = Util::escape_xml($input);
+ $this->assertEquals($expected, $result);
+ }
+
+ // Test data for linkify function
+ public static function linkifyProvider(): array {
+ return [
+ 'simple URL' => [
+ 'Check out https://example.com for more info',
+ 'Check out https://example.com for more info',
+ false // not strict accessibility
+ ],
+ 'URL with path' => [
+ 'Visit https://example.com/path/to/page',
+ 'Visit https://example.com/path/to/page',
+ false
+ ],
+ 'multiple URLs' => [
+ 'See https://example.com and https://other.com',
+ 'See https://example.com and https://other.com',
+ false
+ ],
+ 'URL with punctuation' => [
+ 'Check https://example.com.',
+ 'Check https://example.com',
+ false
+ ],
+ 'no URL' => [
+ 'Just some regular text',
+ 'Just some regular text',
+ false
+ ],
+ 'strict accessibility mode' => [
+ 'Visit https://example.com now',
+ 'Visit https://example.com now',
+ true // strict accessibility
+ ],
+ ];
+ }
+
+ #[DataProvider('linkifyProvider')]
+ public function testLinkify(string $input, string $expected, bool $strictAccessibility): void {
+ // Set up global $app with config
+ global $app;
+ $app = [
+ 'config' => (object)['strictAccessibility' => $strictAccessibility]
+ ];
+
+ $result = Util::linkify($input);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testLinkifyNoNewWindow(): void {
+ // Test linkify without new window
+ global $app;
+ $app = [
+ 'config' => (object)['strictAccessibility' => false]
+ ];
+
+ $input = 'Visit https://example.com';
+ $expected = 'Visit https://example.com';
+
+ $result = Util::linkify($input, false); // no new window
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetClientIp(): void {
+ // Test basic case with REMOTE_ADDR
+ $_SERVER['REMOTE_ADDR'] = '192.168.1.100';
+ unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP']);
+
+ $result = Util::getClientIp();
+ $this->assertEquals('192.168.1.100', $result);
+ }
+
+ public function testGetClientIpWithForwardedHeaders(): void {
+ // Test precedence: HTTP_CLIENT_IP > HTTP_X_FORWARDED_FOR > HTTP_X_REAL_IP > REMOTE_ADDR
+ $_SERVER['HTTP_CLIENT_IP'] = '10.0.0.1';
+ $_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.2';
+ $_SERVER['HTTP_X_REAL_IP'] = '10.0.0.3';
+ $_SERVER['REMOTE_ADDR'] = '10.0.0.4';
+
+ $result = Util::getClientIp();
+ $this->assertEquals('10.0.0.1', $result); // Should use HTTP_CLIENT_IP
+ }
+
+ public function testGetClientIpUnknown(): void {
+ // Test when no IP is available
+ unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP'], $_SERVER['REMOTE_ADDR']);
+
+ $result = Util::getClientIp();
+ $this->assertEquals('unknown', $result);
+ }
+
+ protected function tearDown(): void {
+ // Clean up $_SERVER after IP tests
+ unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP'], $_SERVER['REMOTE_ADDR']);
+ }
+
}
\ No newline at end of file