diff --git a/public/index.php b/public/index.php index 9a92b7a..8214dee 100644 --- a/public/index.php +++ b/public/index.php @@ -46,11 +46,11 @@ $db = $prerequisites->getDatabase(); if (!(preg_match('/tkr-setup$/', $path))) { try { $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); - $config = (new ConfigModel($db))->get(); - + $settings = (new SettingsModel($db))->get(); + $hasUser = $user_count > 0; - $hasUrl = !empty($config->baseUrl) && !empty($config->basePath); - + $hasUrl = !empty($settings->baseUrl) && !empty($settings->basePath); + if (!$hasUser || !$hasUrl) { // Redirect to setup with auto-detected URL $autodetected = Util::getAutodetectedUrl(); @@ -77,7 +77,7 @@ global $app; $app = [ 'db' => $db, - 'config' => (new ConfigModel($db))->get(), + 'settings' => (new SettingsModel($db))->get(), 'user' => (new UserModel($db))->get(), ]; @@ -86,8 +86,8 @@ Session::start(); Session::generateCsrfToken(); // Remove the base path from the URL -if (strpos($path, $app['config']->basePath) === 0) { - $path = substr($path, strlen($app['config']->basePath)); +if (strpos($path, $app['settings']->basePath) === 0) { + $path = substr($path, strlen($app['settings']->basePath)); } // strip the trailing slash from the resulting route @@ -105,7 +105,7 @@ if ($method === 'POST' && $path != 'setup') { if (!Session::isValid($_POST['csrf_token'])) { // Invalid session - redirect to /login Log::info('Attempt to POST with invalid session. Redirecting to login.'); - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login')); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login')); exit; } } else { diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php index dc6587e..72ada00 100644 --- a/src/Controller/AdminController/AdminController.php +++ b/src/Controller/AdminController/AdminController.php @@ -11,41 +11,41 @@ class AdminController extends Controller { public function showSetup(){ $data = $this->getAdminData(true); - + // Auto-detect URL and pre-fill if not already configured - if (empty($data['config']->baseUrl) || empty($data['config']->basePath)) { + if (empty($data['settings']->baseUrl) || empty($data['settings']->basePath)) { $autodetected = Util::getAutodetectedUrl(); $data['autodetectedUrl'] = $autodetected; - + // Pre-fill empty values with auto-detected ones - if (empty($data['config']->baseUrl)) { - $data['config']->baseUrl = $autodetected['baseUrl']; + if (empty($data['settings']->baseUrl)) { + $data['settings']->baseUrl = $autodetected['baseUrl']; } - if (empty($data['config']->basePath)) { - $data['config']->basePath = $autodetected['basePath']; + if (empty($data['settings']->basePath)) { + $data['settings']->basePath = $autodetected['basePath']; } } - + $this->render("admin.php", $data); } - + public function getAdminData(bool $isSetup): array { global $app; - + Log::debug("Loading admin page" . ($isSetup ? " (setup mode)" : "")); - + return [ 'user' => $app['user'], - 'config' => $app['config'], + 'settings' => $app['settings'], 'isSetup' => $isSetup, ]; } public function handleSave(){ global $app; - + if (!Session::isLoggedIn()){ - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login')); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login')); exit; } @@ -64,9 +64,9 @@ class AdminController extends Controller { public function saveSettings(array $postData, bool $isSetup): array { global $app; - + $result = ['success' => false, 'errors' => []]; - + Log::debug("Processing settings save" . ($isSetup ? " (setup mode)" : "")); // handle form submission @@ -95,7 +95,7 @@ class AdminController extends Controller { // Password $password = $postData['password'] ?? ''; $confirmPassword = $postData['confirm_password'] ?? ''; - + Log::info("Processing settings for user: $username"); // Validate user profile @@ -145,16 +145,16 @@ class AdminController extends Controller { if (empty($errors)) { try { // Update site settings - $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; + $app['settings']->siteTitle = $siteTitle; + $app['settings']->siteDescription = $siteDescription; + $app['settings']->baseUrl = $baseUrl; + $app['settings']->basePath = $basePath; + $app['settings']->itemsPerPage = $itemsPerPage; + $app['settings']->strictAccessibility = $strictAccessibility; + $app['settings']->logLevel = $logLevel; // Save site settings and reload config from database - $app['config'] = $app['config']->save(); + $app['settings'] = $app['settings']->save(); Log::info("Site settings updated"); // Update user profile @@ -174,7 +174,7 @@ class AdminController extends Controller { Session::setFlashMessage('success', 'Settings updated'); $result['success'] = true; - + } catch (Exception $e) { Log::error("Failed to save settings: " . $e->getMessage()); Session::setFlashMessage('error', 'Failed to save settings'); @@ -186,7 +186,7 @@ class AdminController extends Controller { } $result['errors'] = $errors; } - + return $result; } } diff --git a/src/Controller/AuthController/AuthController.php b/src/Controller/AuthController/AuthController.php index 6a30a95..83644ce 100644 --- a/src/Controller/AuthController/AuthController.php +++ b/src/Controller/AuthController/AuthController.php @@ -4,11 +4,11 @@ declare(strict_types=1); class AuthController extends Controller { function showLogin(?string $error = null){ global $app; - + $csrf_token = Session::getCsrfToken(); $vars = [ - 'config' => $app['config'], + 'settings' => $app['settings'], 'csrf_token' => $csrf_token, 'error' => $error, ]; @@ -34,7 +34,7 @@ class AuthController extends Controller { try { Session::newLoginSession($user); - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath)); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath)); exit; } catch (Exception $e) { Log::error("Failed to create login session for {$username}: " . $e->getMessage()); @@ -61,11 +61,11 @@ class AuthController extends Controller { function handleLogout(){ global $app; - + Log::info("Logout from user " . $_SESSION['username']); Session::end(); - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath)); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath)); exit; } } \ No newline at end of file diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php index 2e6254b..49420a4 100644 --- a/src/Controller/Controller.php +++ b/src/Controller/Controller.php @@ -17,9 +17,9 @@ class Controller { // Add custom CSS filename if needed global $app; - if ($app['config']->cssId) { + if ($app['settings']->cssId) { $cssModel = new CssModel($app['db']); - $cssFile = $cssModel->getById($app['config']->cssId); + $cssFile = $cssModel->getById($app['settings']->cssId); $vars['customCssFilename'] = $cssFile['filename'] ?? null; } else { $vars['customCssFilename'] = null; diff --git a/src/Controller/CssController/CssController.php b/src/Controller/CssController/CssController.php index ad73202..090ffee 100644 --- a/src/Controller/CssController/CssController.php +++ b/src/Controller/CssController/CssController.php @@ -9,7 +9,7 @@ class CssController extends Controller { $vars = [ 'user' => $app['user'], - 'config' => $app['config'], + 'settings' => $app['settings'], 'customCss' => $customCss, ]; @@ -114,8 +114,8 @@ class CssController extends Controller { // Set the theme back to default try { - $app['config']->cssId = null; - $app['config'] = $app['config']->save(); + $app['settings']->cssId = null; + $app['settings'] = $app['settings']->save(); Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.'); } catch (Exception $e) { Log::error("Failed to update config after deleting theme: " . $e->getMessage()); @@ -129,14 +129,14 @@ class CssController extends Controller { try { if ($_POST['selectCssFile']){ // Set custom theme - $app['config']->cssId = $_POST['selectCssFile']; + $app['settings']->cssId = $_POST['selectCssFile']; } else { // Set default theme - $app['config']->cssId = null; + $app['settings']->cssId = null; } // Update the site theme - $app['config'] = $app['config']->save(); + $app['settings'] = $app['settings']->save(); Session::setFlashMessage('success', 'Theme applied.'); } catch (Exception $e) { Log::error("Failed to save theme setting: " . $e->getMessage()); diff --git a/src/Controller/EmojiController/EmojiController.php b/src/Controller/EmojiController/EmojiController.php index 684e139..bc889de 100644 --- a/src/Controller/EmojiController/EmojiController.php +++ b/src/Controller/EmojiController/EmojiController.php @@ -16,7 +16,7 @@ declare(strict_types=1); } $vars = [ - 'config' => $app['config'], + 'settings' => $app['settings'], 'emojiList' => $emojiList, ]; @@ -39,7 +39,7 @@ declare(strict_types=1); break; } - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'admin/emoji')); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'admin/emoji')); exit; } diff --git a/src/Controller/FeedController/FeedController.php b/src/Controller/FeedController/FeedController.php index c948253..23aaebc 100644 --- a/src/Controller/FeedController/FeedController.php +++ b/src/Controller/FeedController/FeedController.php @@ -6,10 +6,10 @@ class FeedController extends Controller { public function __construct() { global $app; - + try { - $tickModel = new TickModel($app['db'], $app['config']); - $this->ticks = $tickModel->getPage($app['config']->itemsPerPage); + $tickModel = new TickModel($app['db'], $app['settings']); + $this->ticks = $tickModel->getPage($app['settings']->itemsPerPage); Log::debug("Loaded " . count($this->ticks) . " ticks for feeds"); } catch (Exception $e) { Log::error("Failed to load ticks for feed: " . $e->getMessage()); @@ -20,8 +20,8 @@ class FeedController extends Controller { public function rss(){ global $app; - - $generator = new RssGenerator($app['config'], $this->ticks); + + $generator = new RssGenerator($app['settings'], $this->ticks); Log::debug("Generating RSS feed with " . count($this->ticks) . " ticks"); header('Content-Type: ' . $generator->getContentType()); @@ -30,8 +30,8 @@ class FeedController extends Controller { public function atom(){ global $app; - - $generator = new AtomGenerator($app['config'], $this->ticks); + + $generator = new AtomGenerator($app['settings'], $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 15554c4..0736df4 100644 --- a/src/Controller/HomeController/HomeController.php +++ b/src/Controller/HomeController/HomeController.php @@ -9,24 +9,24 @@ class HomeController extends Controller { $data = $this->getHomeData($page); $this->render("home.php", $data); } - + public function getHomeData(int $page): array { global $app; - + Log::debug("Loading home page $page"); - $tickModel = new TickModel($app['db'], $app['config']); - $limit = $app['config']->itemsPerPage; + $tickModel = new TickModel($app['db'], $app['settings']); + $limit = $app['settings']->itemsPerPage; $offset = ($page - 1) * $limit; $ticks = $tickModel->getPage($limit, $offset); - $view = new TicksView($app['config'], $ticks, $page); + $view = new TicksView($app['settings'], $ticks, $page); $tickList = $view->getHtml(); Log::info("Home page loaded with " . count($ticks) . " ticks"); return [ - 'config' => $app['config'], + 'settings' => $app['settings'], 'user' => $app['user'], 'tickList' => $tickList, ]; @@ -36,34 +36,34 @@ class HomeController extends Controller { // 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($app['config']->basePath)); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath)); exit; } - + public function processTick(array $postData): array { global $app; - + $result = ['success' => false, 'message' => '']; - + if (!isset($postData['new_tick'])) { Log::warning("Tick submission without new_tick field"); $result['message'] = 'No tick content provided'; return $result; } - + $tickContent = trim($postData['new_tick']); if (empty($tickContent)) { Log::debug("Empty tick submission ignored"); $result['message'] = 'Empty tick ignored'; return $result; } - + try { - $tickModel = new TickModel($app['db'], $app['config']); + $tickModel = new TickModel($app['db'], $app['settings']); $tickModel->insert($tickContent); Log::info("New tick created: " . substr($tickContent, 0, 50) . (strlen($tickContent) > 50 ? '...' : '')); $result['success'] = true; @@ -72,7 +72,7 @@ class HomeController extends Controller { Log::error("Failed to save tick: " . $e->getMessage()); $result['message'] = 'Failed to save tick'; } - + return $result; } diff --git a/src/Controller/LogController/LogController.php b/src/Controller/LogController/LogController.php index c8b12f4..1a907f7 100644 --- a/src/Controller/LogController/LogController.php +++ b/src/Controller/LogController/LogController.php @@ -10,10 +10,10 @@ class LogController extends Controller { public function index() { global $app; - + // Ensure user is logged in if (!Session::isLoggedIn()) { - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath, 'login')); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, 'login')); exit; } @@ -49,7 +49,7 @@ class LogController extends Controller { } return [ - 'config' => $app['config'], + 'settings' => $app['settings'], 'logEntries' => $logEntries, 'availableRoutes' => $availableRoutes, 'availableLevels' => $availableLevels, @@ -80,7 +80,7 @@ class LogController extends Controller { Log::warning("Failed to read log file: $file"); continue; } - + foreach (array_reverse($lines) as $line) { if (count($entries) >= $limit) break 2; diff --git a/src/Controller/MoodController/MoodController.php b/src/Controller/MoodController/MoodController.php index fc4eb1e..30c5f97 100644 --- a/src/Controller/MoodController/MoodController.php +++ b/src/Controller/MoodController/MoodController.php @@ -10,7 +10,7 @@ declare(strict_types=1); $moodPicker = $view->renderMoodPicker(self::getEmojisWithLabels(), $app['user']->mood); $vars = [ - 'config' => $app['config'], + 'settings' => $app['settings'], 'moodPicker' => $moodPicker, ]; @@ -41,7 +41,7 @@ declare(strict_types=1); } // go back to the index and show the updated mood - header('Location: ' . Util::buildRelativeUrl($app['config']->basePath)); + header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath)); exit; } } diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php index 84a12e9..fb8d17a 100644 --- a/src/Controller/TickController/TickController.php +++ b/src/Controller/TickController/TickController.php @@ -4,23 +4,23 @@ declare(strict_types=1); class TickController extends Controller{ public function index(int $id){ global $app; - + Log::debug("Fetching tick with ID: {$id}"); - + try { - $tickModel = new TickModel($app['db'], $app['config']); + $tickModel = new TickModel($app['db'], $app['settings']); $vars = $tickModel->get($id); - + if (empty($vars) || !isset($vars['tick'])) { Log::warning("Tick not found for ID: {$id}"); http_response_code(404); echo '

404 - Tick Not Found

'; return; } - + Log::info("Successfully loaded tick {$id}: " . substr($vars['tick'], 0, 50) . (strlen($vars['tick']) > 50 ? '...' : '')); $this->render('tick.php', $vars); - + } catch (Exception $e) { Log::error("Failed to load tick {$id}: " . $e->getMessage()); http_response_code(500); diff --git a/src/Feed/AtomGenerator.php b/src/Feed/AtomGenerator.php index f29c330..f3f0fa5 100644 --- a/src/Feed/AtomGenerator.php +++ b/src/Feed/AtomGenerator.php @@ -15,10 +15,10 @@ class AtomGenerator extends FeedGenerator { } private function buildFeed(): string { - Log::debug("Building Atom feed for " . $this->config->siteTitle); - $feedTitle = Util::escape_xml($this->config->siteTitle . " Atom Feed"); - $siteUrl = Util::escape_xml(Util::buildUrl($this->config->baseUrl, $this->config->basePath)); - $feedUrl = Util::escape_xml(Util::buildUrl($this->config->baseUrl, $this->config->basePath, 'feed/atom')); + Log::debug("Building Atom feed for " . $this->settings->siteTitle); + $feedTitle = Util::escape_xml($this->settings->siteTitle . " Atom Feed"); + $siteUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath)); + $feedUrl = Util::escape_xml(Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, 'feed/atom')); $updated = date(DATE_ATOM, strtotime($this->ticks[0]['timestamp'] ?? 'now')); ob_start(); @@ -33,7 +33,7 @@ class AtomGenerator extends FeedGenerator { - config->siteTitle) ?> + settings->siteTitle) ?> ticks as $tick): // build the tick entry components diff --git a/src/Feed/FeedGenerator.php b/src/Feed/FeedGenerator.php index dcebe55..3d3cdd3 100644 --- a/src/Feed/FeedGenerator.php +++ b/src/Feed/FeedGenerator.php @@ -5,11 +5,11 @@ declare(strict_types=1); // Specific feeds (RSS, Atom, etc.) will inherit from this. // This will wrap the basic generator functionality. abstract class FeedGenerator { - protected $config; + protected $settings; protected $ticks; - public function __construct(ConfigModel $config, array $ticks) { - $this->config = $config; + public function __construct(SettingsModel $settings, array $ticks) { + $this->settings = $settings; $this->ticks = $ticks; } @@ -17,10 +17,10 @@ abstract class FeedGenerator { abstract public function getContentType(): string; protected function buildTickUrl(int $tickId): string { - return Util::buildUrl($this->config->baseUrl, $this->config->basePath, "tick/{$tickId}"); + return Util::buildUrl($this->settings->baseUrl, $this->settings->basePath, "tick/{$tickId}"); } protected function getSiteUrl(): string { - return Util::buildUrl($this->config->baseUrl, $this->config->basePath); + return Util::buildUrl($this->settings->baseUrl, $this->settings->basePath); } } \ No newline at end of file diff --git a/src/Feed/RssGenerator.php b/src/Feed/RssGenerator.php index f8739e3..c8597b6 100644 --- a/src/Feed/RssGenerator.php +++ b/src/Feed/RssGenerator.php @@ -17,16 +17,16 @@ class RssGenerator extends FeedGenerator { } private function buildChannel(): string { - Log::debug("Building RSS channel for " . $this->config->siteTitle); + Log::debug("Building RSS channel for " . $this->settings->siteTitle); ob_start(); ?> - <?php echo Util::escape_xml($this->config->siteTitle . ' RSS Feed') ?> - config->baseUrl, $this->config->basePath))?> - settings->siteTitle . ' RSS Feed') ?> + settings->baseUrl, $this->settings->basePath))?> + - config->siteDescription) ?> + settings->siteDescription) ?> en-us ticks as $tick): diff --git a/src/Framework/Log/Log.php b/src/Framework/Log/Log.php index 533111c..5e8b4ef 100644 --- a/src/Framework/Log/Log.php +++ b/src/Framework/Log/Log.php @@ -52,7 +52,7 @@ class Log { private static function write($level, $message) { global $app; - $logLevel = $app['config']->logLevel ?? self::LEVELS['INFO']; + $logLevel = $app['settings']->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/Util/Util.php b/src/Framework/Util/Util.php index b085fd0..0af9f5d 100644 --- a/src/Framework/Util/Util.php +++ b/src/Framework/Util/Util.php @@ -30,7 +30,7 @@ class Util { global $app; $escaped_url = rtrim($matches[1], '.,!?;:)]}>'); $clean_url = html_entity_decode($escaped_url, ENT_QUOTES, 'UTF-8'); - $tabIndex = $app['config']->strictAccessibility ? ' tabindex="0"' : ''; + $tabIndex = $app['settings']->strictAccessibility ? ' tabindex="0"' : ''; return '' . $escaped_url . ''; }, @@ -114,29 +114,29 @@ class Util { // Detect base URL $baseUrl = ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://'; $baseUrl .= $_SERVER['HTTP_HOST'] ?? 'localhost'; - + // Don't include standard ports in URL $port = $_SERVER['SERVER_PORT'] ?? null; if ($port && $port != 80 && $port != 443) { $baseUrl .= ':' . $port; } - + // Detect base path from script location $scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php'; $basePath = dirname($scriptName); - + if ($basePath === '/' || $basePath === '.' || $basePath === '') { $basePath = '/'; } else { $basePath = '/' . trim($basePath, '/') . '/'; } - + // Construct full URL $fullUrl = $baseUrl; if ($basePath !== '/') { $fullUrl .= ltrim($basePath, '/'); } - + return [ 'baseUrl' => $baseUrl, 'basePath' => $basePath, diff --git a/src/Model/ConfigModel/ConfigModel.php b/src/Model/SettingsModel/SettingsModel.php similarity index 99% rename from src/Model/ConfigModel/ConfigModel.php rename to src/Model/SettingsModel/SettingsModel.php index 86d3ee1..539332d 100644 --- a/src/Model/ConfigModel/ConfigModel.php +++ b/src/Model/SettingsModel/SettingsModel.php @@ -1,7 +1,7 @@ db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); $stmt->execute([$limit, $offset]); @@ -32,7 +32,7 @@ class TickModel { return [ 'tickTime' => $row['timestamp'], 'tick' => $row['tick'], - 'config' => $this->config, + 'settings' => $this->settings, ]; } } diff --git a/src/View/TicksView/TicksView.php b/src/View/TicksView/TicksView.php index c2f52b7..1e434a6 100644 --- a/src/View/TicksView/TicksView.php +++ b/src/View/TicksView/TicksView.php @@ -4,15 +4,15 @@ declare(strict_types=1); class TicksView { private $html; - public function __construct(ConfigModel $config, array $ticks, int $page){ - $this->html = $this->render($config, $ticks, $page); + public function __construct(SettingsModel $settings, array $ticks, int $page){ + $this->html = $this->render($settings, $ticks, $page); } public function getHtml(): string { return $this->html; } - private function render(ConfigModel $config, array $ticks, int $page): string{ + private function render(SettingsModel $settings, array $ticks, int $page): string{ ob_start(); ?> @@ -23,18 +23,18 @@ class TicksView { $relativeTime = Util::relative_time($tick['timestamp']); ?>
  • - + 🗑️
  • diff --git a/templates/main.php b/templates/main.php index c98d36d..c8ef32f 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,5 +1,5 @@ - + @@ -7,23 +7,23 @@ - <?= $config->siteTitle ?> + <?= $settings->siteTitle ?> -cssId)): ?> + href="basePath, 'css/default.css')) ?>"> +cssId)): ?> + href="basePath, 'css/custom/' . $customCssFilename)) ?>"> + title="siteTitle) ?> RSS Feed" + href="baseUrl . $settings->basePath)?>feed/rss/"> + title="siteTitle) ?> Atom Feed" + href="baseUrl . $settings->basePath)?>feed/atom/"> diff --git a/templates/partials/admin.php b/templates/partials/admin.php index e478d00..557470b 100644 --- a/templates/partials/admin.php +++ b/templates/partials/admin.php @@ -1,10 +1,10 @@ - +

    SetupAdmin

    @@ -36,43 +36,43 @@ + value="siteDescription) ?>"> strictAccessibility): ?> checked > + strictAccessibility): ?> checked >
    diff --git a/templates/partials/css.php b/templates/partials/css.php index 75b1084..94b62ac 100644 --- a/templates/partials/css.php +++ b/templates/partials/css.php @@ -1,18 +1,18 @@ - +

    CSS Management

    - +
    Manage
    Add Emoji @@ -24,7 +24,7 @@
    -
    +
    Delete Emoji diff --git a/templates/partials/home.php b/templates/partials/home.php index ec9b52a..c5efb0d 100644 --- a/templates/partials/home.php +++ b/templates/partials/home.php @@ -1,5 +1,5 @@ - + diff --git a/templates/partials/login.php b/templates/partials/login.php index 32d2f1f..8d57824 100644 --- a/templates/partials/login.php +++ b/templates/partials/login.php @@ -1,8 +1,8 @@ - +

    Login

    - +
    diff --git a/templates/partials/logs.php b/templates/partials/logs.php index 327612c..2dcc5ed 100644 --- a/templates/partials/logs.php +++ b/templates/partials/logs.php @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@
    - +
    Filter Logs
    @@ -35,7 +35,7 @@
    -
    Clear +
    Clear
    diff --git a/templates/partials/navbar.php b/templates/partials/navbar.php index 4659bc1..2c9a6f2 100644 --- a/templates/partials/navbar.php +++ b/templates/partials/navbar.php @@ -1,35 +1,35 @@ - + \ No newline at end of file diff --git a/tests/Controller/AdminController/AdminControllerTest.php b/tests/Controller/AdminController/AdminControllerTest.php index 714440f..95494a8 100644 --- a/tests/Controller/AdminController/AdminControllerTest.php +++ b/tests/Controller/AdminController/AdminControllerTest.php @@ -7,32 +7,32 @@ use PHPUnit\Framework\TestCase; class AdminControllerTest extends TestCase { private PDO $mockPdo; - private ConfigModel $config; + private SettingsModel $settings; private UserModel $user; protected function setUp(): void { // Create mock PDO $this->mockPdo = $this->createMock(PDO::class); - + // Create real config and user objects with mocked PDO - $this->config = new ConfigModel($this->mockPdo); - $this->config->siteTitle = 'Test Site'; - $this->config->siteDescription = 'Test Description'; - $this->config->baseUrl = 'https://example.com'; - $this->config->basePath = '/tkr'; - $this->config->itemsPerPage = 10; - + $this->settings = new SettingsModel($this->mockPdo); + $this->settings->siteTitle = 'Test Site'; + $this->settings->siteDescription = 'Test Description'; + $this->settings->baseUrl = 'https://example.com'; + $this->settings->basePath = '/tkr'; + $this->settings->itemsPerPage = 10; + $this->user = new UserModel($this->mockPdo); $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, + 'settings' => $this->settings, 'user' => $this->user, ]; } @@ -41,14 +41,14 @@ class AdminControllerTest extends TestCase { $controller = new AdminController(); $data = $controller->getAdminData(false); - + // Should return proper structure - $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('settings', $data); $this->assertArrayHasKey('user', $data); $this->assertArrayHasKey('isSetup', $data); - + // Should be the injected instances - $this->assertSame($this->config, $data['config']); + $this->assertSame($this->settings, $data['settings']); $this->assertSame($this->user, $data['user']); $this->assertFalse($data['isSetup']); } @@ -57,14 +57,14 @@ class AdminControllerTest extends TestCase { $controller = new AdminController(); $data = $controller->getAdminData(true); - + // Should return proper structure - $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('settings', $data); $this->assertArrayHasKey('user', $data); $this->assertArrayHasKey('isSetup', $data); - + // Should be the injected instances - $this->assertSame($this->config, $data['config']); + $this->assertSame($this->settings, $data['settings']); $this->assertSame($this->user, $data['user']); $this->assertTrue($data['isSetup']); } @@ -73,7 +73,7 @@ class AdminControllerTest extends TestCase { $controller = new AdminController(); $result = $controller->saveSettings([], false); - + $this->assertFalse($result['success']); $this->assertContains('No data provided', $result['errors']); } @@ -81,7 +81,7 @@ class AdminControllerTest extends TestCase public function testProcessSettingsSaveValidationErrors(): void { $controller = new AdminController(); - + // Test data with multiple validation errors $postData = [ 'username' => '', // Missing username @@ -94,12 +94,12 @@ class AdminControllerTest extends TestCase 'password' => 'test123', 'confirm_password' => 'different' // Passwords don't match ]; - + $result = $controller->saveSettings($postData, false); - + $this->assertFalse($result['success']); $this->assertNotEmpty($result['errors']); - + // Should have multiple validation errors $this->assertGreaterThan(5, count($result['errors'])); } @@ -133,16 +133,16 @@ class AdminControllerTest extends TestCase $this->mockPdo->method('query')->willReturn($mockStatement); // Create models with mocked PDO - $config = new ConfigModel($this->mockPdo); + $settings = new SettingsModel($this->mockPdo); $user = new UserModel($this->mockPdo); - + // Update global $app with test models global $app; - $app['config'] = $config; + $app['settings'] = $settings; $app['user'] = $user; - + $controller = new AdminController(); - + $postData = [ 'username' => 'newuser', 'display_name' => 'New User', @@ -155,9 +155,9 @@ class AdminControllerTest extends TestCase 'strict_accessibility' => 'on', 'log_level' => 2 ]; - + $result = $controller->saveSettings($postData, false); - + $this->assertTrue($result['success']); $this->assertEmpty($result['errors']); } @@ -191,20 +191,20 @@ class AdminControllerTest extends TestCase $this->mockPdo->expects($this->atLeastOnce()) ->method('prepare') ->willReturn($mockStatement); - + $this->mockPdo->method('query')->willReturn($mockStatement); // Create models with mocked PDO - $config = new ConfigModel($this->mockPdo); + $settings = new SettingsModel($this->mockPdo); $user = new UserModel($this->mockPdo); - + // Update global $app with test models global $app; - $app['config'] = $config; + $app['settings'] = $settings; $app['user'] = $user; - + $controller = new AdminController(); - + $postData = [ 'username' => 'testuser', 'display_name' => 'Test User', @@ -216,9 +216,9 @@ class AdminControllerTest extends TestCase 'password' => 'newpassword', 'confirm_password' => 'newpassword' ]; - + $result = $controller->saveSettings($postData, false); - + $this->assertTrue($result['success']); } @@ -228,16 +228,16 @@ class AdminControllerTest extends TestCase $this->mockPdo->method('query') ->willThrowException(new PDOException("Database error")); - $config = new ConfigModel($this->mockPdo); + $settings = new SettingsModel($this->mockPdo); $user = new UserModel($this->mockPdo); - + // Update global $app with test models global $app; - $app['config'] = $config; + $app['settings'] = $settings; $app['user'] = $user; - + $controller = new AdminController(); - + $postData = [ 'username' => 'testuser', 'display_name' => 'Test User', @@ -247,9 +247,9 @@ class AdminControllerTest extends TestCase 'base_path' => '/tkr', 'items_per_page' => 10 ]; - + $result = $controller->saveSettings($postData, false); - + $this->assertFalse($result['success']); $this->assertContains('Failed to save settings', $result['errors']); } diff --git a/tests/Controller/FeedController/FeedControllerTest.php b/tests/Controller/FeedController/FeedControllerTest.php index ba1e124..6d50840 100644 --- a/tests/Controller/FeedController/FeedControllerTest.php +++ b/tests/Controller/FeedController/FeedControllerTest.php @@ -7,7 +7,7 @@ class FeedControllerTest extends TestCase { private PDO $mockPdo; private PDOStatement $mockStatement; - private ConfigModel $mockConfig; + private SettingsModel $mockConfig; private UserModel $mockUser; private string $tempLogDir; @@ -17,31 +17,31 @@ class FeedControllerTest 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'); - + // Create mock PDO and PDOStatement $this->mockStatement = $this->createMock(PDOStatement::class); $this->mockPdo = $this->createMock(PDO::class); - + // Mock config with feed-relevant properties - $this->mockConfig = new ConfigModel($this->mockPdo); + $this->mockConfig = new SettingsModel($this->mockPdo); $this->mockConfig->itemsPerPage = 10; $this->mockConfig->basePath = '/tkr'; $this->mockConfig->siteTitle = 'Test Site'; $this->mockConfig->siteDescription = 'Test Description'; $this->mockConfig->baseUrl = 'https://test.example.com'; - + // 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, + 'settings' => $this->mockConfig, 'user' => $this->mockUser, ]; - + // Set log level on config for Log class $this->mockConfig->logLevel = 1; // Allow DEBUG level logs } @@ -57,7 +57,7 @@ class FeedControllerTest extends TestCase private function deleteDirectory(string $dir): void { if (!is_dir($dir)) return; - + $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; @@ -71,11 +71,11 @@ class FeedControllerTest extends TestCase // Mock PDO prepare method to return our mock statement $this->mockPdo->method('prepare') ->willReturn($this->mockStatement); - + // Mock statement execute method $this->mockStatement->method('execute') ->willReturn(true); - + // Mock statement fetchAll to return our test data $this->mockStatement->method('fetchAll') ->willReturn($tickData); @@ -84,16 +84,16 @@ class FeedControllerTest extends TestCase public function testControllerInstantiationWithNoTicks(): void { $this->setupMockDatabase([]); - + $controller = new FeedController(); - + // Verify it was created successfully $this->assertInstanceOf(FeedController::class, $controller); - + // Check logs $logFile = $this->tempLogDir . '/logs/tkr.log'; $this->assertFileExists($logFile); - + $logContent = file_get_contents($logFile); $this->assertStringContainsString('Loaded 0 ticks for feeds', $logContent); } @@ -104,18 +104,18 @@ class FeedControllerTest extends TestCase ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'First tick'], ['id' => 2, 'timestamp' => '2025-01-31 13:00:00', 'tick' => 'Second tick'], ]; - + $this->setupMockDatabase($testTicks); - + $controller = new FeedController(); - + // Verify it was created successfully $this->assertInstanceOf(FeedController::class, $controller); - + // Check logs $logFile = $this->tempLogDir . '/logs/tkr.log'; $this->assertFileExists($logFile); - + $logContent = file_get_contents($logFile); $this->assertStringContainsString('Loaded 2 ticks for feeds', $logContent); } @@ -123,18 +123,18 @@ class FeedControllerTest extends TestCase public function testControllerCallsDatabaseCorrectly(): void { $this->setupMockDatabase([]); - + // Verify that PDO prepare is called with the correct SQL for tick loading $this->mockPdo->expects($this->once()) ->method('prepare') ->with('SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?') ->willReturn($this->mockStatement); - + // Verify that execute is called with correct parameters (page 1, offset 0) $this->mockStatement->expects($this->once()) ->method('execute') ->with([10, 0]); // itemsPerPage=10, page 1 = offset 0 - + new FeedController(); } @@ -143,16 +143,16 @@ class FeedControllerTest extends TestCase $testTicks = [ ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'Test tick'] ]; - + $this->setupMockDatabase($testTicks); - + $controller = new FeedController(); - + // Capture output to prevent headers/content from affecting test ob_start(); $controller->rss(); ob_end_clean(); - + // Check logs for RSS generation $logFile = $this->tempLogDir . '/logs/tkr.log'; $logContent = file_get_contents($logFile); @@ -165,16 +165,16 @@ class FeedControllerTest extends TestCase ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'Test tick'], ['id' => 2, 'timestamp' => '2025-01-31 13:00:00', 'tick' => 'Another tick'] ]; - + $this->setupMockDatabase($testTicks); - + $controller = new FeedController(); - + // Capture output to prevent headers/content from affecting test ob_start(); $controller->atom(); ob_end_clean(); - + // Check logs for Atom generation $logFile = $this->tempLogDir . '/logs/tkr.log'; $logContent = file_get_contents($logFile); diff --git a/tests/Controller/HomeController/HomeControllerTest.php b/tests/Controller/HomeController/HomeControllerTest.php index f3bec85..a8460b3 100644 --- a/tests/Controller/HomeController/HomeControllerTest.php +++ b/tests/Controller/HomeController/HomeControllerTest.php @@ -7,33 +7,33 @@ class HomeControllerTest extends TestCase { private PDO $mockPdo; private PDOStatement $mockStatement; - private ConfigModel $mockConfig; + private SettingsModel $mockConfig; private UserModel $mockUser; protected function setUp(): void { // Reset Log state to prevent test pollution Log::init(sys_get_temp_dir() . '/tkr_controller_test.log'); - + // Create mock PDO and PDOStatement $this->mockStatement = $this->createMock(PDOStatement::class); $this->mockPdo = $this->createMock(PDO::class); - + // Mock config - $this->mockConfig = new ConfigModel($this->mockPdo); + $this->mockConfig = new SettingsModel($this->mockPdo); $this->mockConfig->itemsPerPage = 10; $this->mockConfig->basePath = '/tkr'; - + // Mock user $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, + 'settings' => $this->mockConfig, 'user' => $this->mockUser, ]; } @@ -43,11 +43,11 @@ class HomeControllerTest extends TestCase // Mock PDO prepare method to return our mock statement $this->mockPdo->method('prepare') ->willReturn($this->mockStatement); - + // Mock statement execute method $this->mockStatement->method('execute') ->willReturn(true); - + // Mock statement fetchAll to return our test data $this->mockStatement->method('fetchAll') ->willReturn($tickData); @@ -59,7 +59,7 @@ class HomeControllerTest extends TestCase // Mock successful insert $this->mockPdo->method('prepare') ->willReturn($this->mockStatement); - + $this->mockStatement->method('execute') ->willReturn(true); } else { @@ -72,19 +72,19 @@ class HomeControllerTest extends TestCase public function testGetHomeDataWithNoTicks(): void { $this->setupMockDatabase([]); // Empty array = no ticks - + $controller = new HomeController(); $data = $controller->getHomeData(1); - + // Should return proper structure - $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('settings', $data); $this->assertArrayHasKey('user', $data); $this->assertArrayHasKey('tickList', $data); - + // Config and user should be the injected instances - $this->assertSame($this->mockConfig, $data['config']); + $this->assertSame($this->mockConfig, $data['settings']); $this->assertSame($this->mockUser, $data['user']); - + // Should have tick list HTML (even if empty) $this->assertIsString($data['tickList']); } @@ -97,17 +97,17 @@ class HomeControllerTest extends TestCase ['id' => 2, 'timestamp' => '2025-01-31 13:00:00', 'tick' => 'Second tick'], ['id' => 3, 'timestamp' => '2025-01-31 14:00:00', 'tick' => 'Third tick'], ]; - + $this->setupMockDatabase($testTicks); - + $controller = new HomeController(); $data = $controller->getHomeData(1); - + // Should return proper structure - $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('settings', $data); $this->assertArrayHasKey('user', $data); $this->assertArrayHasKey('tickList', $data); - + // Should contain tick content in HTML $this->assertStringContainsString('First tick', $data['tickList']); $this->assertStringContainsString('Second tick', $data['tickList']); @@ -117,18 +117,18 @@ class HomeControllerTest extends TestCase public function testGetHomeDataCallsDatabaseCorrectly(): void { $this->setupMockDatabase([]); - + // Verify that PDO prepare is called with the correct SQL $this->mockPdo->expects($this->once()) ->method('prepare') ->with('SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?') ->willReturn($this->mockStatement); - + // Verify that execute is called with correct parameters for page 2 $this->mockStatement->expects($this->once()) ->method('execute') ->with([10, 10]); // itemsPerPage=10, page 2 = offset 10 - + $controller = new HomeController(); $controller->getHomeData(2); // Page 2 } @@ -136,28 +136,28 @@ class HomeControllerTest extends TestCase public function testProcessTickSuccess(): void { $this->setupMockDatabaseForInsert(true); - + // Verify the INSERT SQL is called correctly $this->mockPdo->expects($this->once()) ->method('prepare') ->with('INSERT INTO tick(timestamp, tick) values (?, ?)') ->willReturn($this->mockStatement); - + // Verify execute is called with timestamp and content $this->mockStatement->expects($this->once()) ->method('execute') ->with($this->callback(function($params) { // First param should be a timestamp, second should be the tick content - return count($params) === 2 - && is_string($params[0]) + return count($params) === 2 + && is_string($params[0]) && $params[1] === 'This is a test tick'; })); - + $controller = new HomeController(); $postData = ['new_tick' => 'This is a test tick']; - + $result = $controller->processTick($postData); - + $this->assertTrue($result['success']); $this->assertEquals('Tick saved successfully', $result['message']); } @@ -166,12 +166,12 @@ class HomeControllerTest extends TestCase { // PDO shouldn't be called at all for empty content $this->mockPdo->expects($this->never())->method('prepare'); - + $controller = new HomeController(); $postData = ['new_tick' => ' ']; // Just whitespace - + $result = $controller->processTick($postData); - + $this->assertFalse($result['success']); $this->assertEquals('Empty tick ignored', $result['message']); } @@ -180,12 +180,12 @@ class HomeControllerTest extends TestCase { // PDO shouldn't be called at all for missing field $this->mockPdo->expects($this->never())->method('prepare'); - + $controller = new HomeController(); $postData = []; // No new_tick field - + $result = $controller->processTick($postData); - + $this->assertFalse($result['success']); $this->assertEquals('No tick content provided', $result['message']); } @@ -193,31 +193,31 @@ class HomeControllerTest extends TestCase public function testProcessTickTrimsWhitespace(): void { $this->setupMockDatabaseForInsert(true); - + // Verify execute is called with trimmed content $this->mockStatement->expects($this->once()) ->method('execute') ->with($this->callback(function($params) { return $params[1] === 'This has whitespace'; // Should be trimmed })); - + $controller = new HomeController(); $postData = ['new_tick' => ' This has whitespace ']; - + $result = $controller->processTick($postData); - + $this->assertTrue($result['success']); } public function testProcessTickHandlesDatabaseError(): void { $this->setupMockDatabaseForInsert(false); // Will throw exception - + $controller = new HomeController(); $postData = ['new_tick' => 'This will fail']; - + $result = $controller->processTick($postData); - + $this->assertFalse($result['success']); $this->assertEquals('Failed to save tick', $result['message']); } diff --git a/tests/Controller/LogController/LogControllerTest.php b/tests/Controller/LogController/LogControllerTest.php index 662bb56..92b2446 100644 --- a/tests/Controller/LogController/LogControllerTest.php +++ b/tests/Controller/LogController/LogControllerTest.php @@ -13,7 +13,7 @@ class LogControllerTest extends TestCase { $this->tempLogDir = sys_get_temp_dir() . '/tkr_test_logs_' . uniqid(); mkdir($this->tempLogDir, 0777, true); - + $this->testLogFile = $this->tempLogDir . '/logs/tkr.log'; mkdir(dirname($this->testLogFile), 0777, true); @@ -23,16 +23,16 @@ class LogControllerTest extends TestCase // Set up global $app for simplified dependency access $mockPdo = $this->createMock(PDO::class); - $mockConfig = new ConfigModel($mockPdo); + $mockConfig = new SettingsModel($mockPdo); $mockConfig->baseUrl = 'https://example.com'; $mockConfig->basePath = '/tkr/'; - + $mockUser = new UserModel($mockPdo); - + global $app; $app = [ 'db' => $mockPdo, - 'config' => $mockConfig, + 'settings' => $mockConfig, 'user' => $mockUser, ]; } @@ -50,7 +50,7 @@ class LogControllerTest extends TestCase private function deleteDirectory(string $dir): void { if (!is_dir($dir)) return; - + $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; @@ -64,14 +64,14 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData(); - + // Should return empty log entries but valid structure $this->assertArrayHasKey('logEntries', $data); $this->assertArrayHasKey('availableRoutes', $data); $this->assertArrayHasKey('availableLevels', $data); $this->assertArrayHasKey('currentLevelFilter', $data); $this->assertArrayHasKey('currentRouteFilter', $data); - + $this->assertEmpty($data['logEntries']); $this->assertEmpty($data['availableRoutes']); $this->assertEquals(['DEBUG', 'INFO', 'WARNING', 'ERROR'], $data['availableLevels']); @@ -96,15 +96,15 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData(); - + // Should parse all valid entries and ignore invalid ones $this->assertCount(5, $data['logEntries']); - + // Verify entries are in reverse chronological order (newest first) $entries = $data['logEntries']; $this->assertEquals('Info without route', $entries[0]['message']); $this->assertEquals('Debug home page', $entries[4]['message']); - + // Verify entry structure $firstEntry = $entries[0]; $this->assertArrayHasKey('timestamp', $firstEntry); @@ -112,13 +112,13 @@ class LogControllerTest extends TestCase $this->assertArrayHasKey('ip', $firstEntry); $this->assertArrayHasKey('route', $firstEntry); $this->assertArrayHasKey('message', $firstEntry); - + // Test route extraction $adminEntry = array_filter($entries, fn($e) => $e['message'] === 'Info admin page'); $adminEntry = array_values($adminEntry)[0]; $this->assertEquals('GET /admin', $adminEntry['route']); $this->assertEquals('INFO', $adminEntry['level']); - + // Test entry without route $noRouteEntry = array_filter($entries, fn($e) => $e['message'] === 'Info without route'); $noRouteEntry = array_values($noRouteEntry)[0]; @@ -138,7 +138,7 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData('ERROR'); - + // Should only include ERROR entries $this->assertCount(1, $data['logEntries']); $this->assertEquals('ERROR', $data['logEntries'][0]['level']); @@ -159,7 +159,7 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData('', 'GET /admin'); - + // Should only include GET /admin entries $this->assertCount(1, $data['logEntries']); $this->assertEquals('GET /admin', $data['logEntries'][0]['route']); @@ -171,7 +171,7 @@ class LogControllerTest extends TestCase { $logContent = implode("\n", [ '[2025-01-31 12:00:00] ERROR: 127.0.0.1 [GET /admin] - Admin error', - '[2025-01-31 12:01:00] INFO: 127.0.0.1 [GET /admin] - Admin info', + '[2025-01-31 12:01:00] INFO: 127.0.0.1 [GET /admin] - Admin info', '[2025-01-31 12:02:00] ERROR: 127.0.0.1 [GET /] - Home error' ]); @@ -180,7 +180,7 @@ class LogControllerTest extends TestCase // 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 $this->assertCount(1, $data['logEntries']); $this->assertEquals('ERROR', $data['logEntries'][0]['level']); @@ -204,7 +204,7 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData(); - + // Should read from all log files, newest first $this->assertCount(3, $data['logEntries']); $this->assertEquals('Current log entry', $data['logEntries'][0]['message']); @@ -227,7 +227,7 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData(); - + // Should extract unique routes, sorted $expectedRoutes = ['GET /', 'GET /admin', 'POST /admin']; $this->assertEquals($expectedRoutes, $data['availableRoutes']); @@ -247,7 +247,7 @@ class LogControllerTest extends TestCase // Uses global $app set up in setUp() $controller = new LogController($this->tempLogDir); $data = $controller->getLogData(); - + // Should only include valid entries, ignore invalid ones $this->assertCount(2, $data['logEntries']); $this->assertEquals('Another valid entry', $data['logEntries'][0]['message']); diff --git a/tests/Controller/TickController/TickControllerTest.php b/tests/Controller/TickController/TickControllerTest.php index 6f7b4d0..0f90d8f 100644 --- a/tests/Controller/TickController/TickControllerTest.php +++ b/tests/Controller/TickController/TickControllerTest.php @@ -6,29 +6,29 @@ use PHPUnit\Framework\TestCase; class TickControllerTest extends TestCase { private $mockPdo; - private $config; + private $settings; private $user; protected function setUp(): void { // Reset Log state to prevent test pollution Log::init(sys_get_temp_dir() . '/tkr_controller_test.log'); - + // Set up mocks $this->mockPdo = $this->createMock(PDO::class); - - $this->config = new ConfigModel($this->mockPdo); - $this->config->baseUrl = 'https://example.com'; - $this->config->basePath = '/tkr/'; - $this->config->itemsPerPage = 10; - + + $this->settings = new SettingsModel($this->mockPdo); + $this->settings->baseUrl = 'https://example.com'; + $this->settings->basePath = '/tkr/'; + $this->settings->itemsPerPage = 10; + $this->user = new UserModel($this->mockPdo); // Set up global $app for simplified dependency access global $app; $app = [ 'db' => $this->mockPdo, - 'config' => $this->config, + 'settings' => $this->settings, 'user' => $this->user, ]; } @@ -60,10 +60,10 @@ class TickControllerTest extends TestCase // Capture output since render() outputs directly ob_start(); - + $controller = new TickController(); $controller->index(123); - + $output = ob_get_clean(); // Should not be a 404 or 500 error @@ -94,10 +94,10 @@ class TickControllerTest extends TestCase // Capture output ob_start(); - + $controller = new TickController(); $controller->index(999); - + $output = ob_get_clean(); // Should return 404 error @@ -123,10 +123,10 @@ class TickControllerTest extends TestCase // Capture output ob_start(); - + $controller = new TickController(); $controller->index(456); - + $output = ob_get_clean(); // Should return 404 error for empty data @@ -143,10 +143,10 @@ class TickControllerTest extends TestCase // Capture output ob_start(); - + $controller = new TickController(); $controller->index(123); - + $output = ob_get_clean(); // Should return 500 error diff --git a/tests/Feed/AtomGeneratorTest.php b/tests/Feed/AtomGeneratorTest.php index cbe89c9..fcaf02d 100644 --- a/tests/Feed/AtomGeneratorTest.php +++ b/tests/Feed/AtomGeneratorTest.php @@ -7,12 +7,12 @@ class AtomGeneratorTest extends TestCase { private function createMockConfig() { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->siteDescription = 'Test Description'; - $config->baseUrl = 'https://example.com'; - $config->basePath = '/tkr/'; - return $config; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->siteDescription = 'Test Description'; + $settings->baseUrl = 'https://example.com'; + $settings->basePath = '/tkr/'; + return $settings; } private function createSampleTicks() { @@ -23,10 +23,10 @@ class AtomGeneratorTest extends TestCase } public function testCanGenerateValidAtom() { - $config = $this->createMockConfig(); + $settings = $this->createMockConfig(); $ticks = $this->createSampleTicks(); - $generator = new AtomGenerator($config, $ticks); + $generator = new AtomGenerator($settings, $ticks); $xml = $generator->generate(); // Test XML structure @@ -58,8 +58,8 @@ class AtomGeneratorTest extends TestCase } public function testCanHandleEmptyTickList() { - $config = $this->createMockConfig(); - $generator = new AtomGenerator($config, []); + $settings = $this->createMockConfig(); + $generator = new AtomGenerator($settings, []); $xml = $generator->generate(); // Should still be valid Atom with no entries @@ -85,7 +85,7 @@ class AtomGeneratorTest extends TestCase } public function testCanHandleSpecialCharactersAndUnicode() { - $config = $this->createMockConfig(); + $settings = $this->createMockConfig(); // Test various challenging characters $ticks = [ @@ -111,7 +111,7 @@ class AtomGeneratorTest extends TestCase ] ]; - $generator = new AtomGenerator($config, $ticks); + $generator = new AtomGenerator($settings, $ticks); $xml = $generator->generate(); // Test that emojis are preserved diff --git a/tests/Feed/FeedGeneratorTest.php b/tests/Feed/FeedGeneratorTest.php index 8fabfcf..c6e9eed 100644 --- a/tests/Feed/FeedGeneratorTest.php +++ b/tests/Feed/FeedGeneratorTest.php @@ -7,12 +7,12 @@ class FeedGeneratorTest extends TestCase { private function createMockConfig() { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->siteDescription = 'Test Description'; - $config->baseUrl = 'https://example.com'; - $config->basePath = '/tkr/'; - return $config; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->siteDescription = 'Test Description'; + $settings->baseUrl = 'https://example.com'; + $settings->basePath = '/tkr/'; + return $settings; } private function createSampleTicks() { @@ -22,11 +22,11 @@ class FeedGeneratorTest extends TestCase ]; } - private function createTestGenerator($config = null, $ticks = null) { - $config = $config ?? $this->createMockConfig(); + private function createTestGenerator($settings = null, $ticks = null) { + $settings = $settings ?? $this->createMockConfig(); $ticks = $ticks ?? $this->createSampleTicks(); - return new class($config, $ticks) extends FeedGenerator { + return new class($settings, $ticks) extends FeedGenerator { public function generate(): string { return 'content'; } @@ -69,12 +69,12 @@ class FeedGeneratorTest extends TestCase public function testUrlMethodsHandleSubdomainConfiguration() { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->baseUrl = 'https://tkr.example.com'; - $config->basePath = '/'; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->baseUrl = 'https://tkr.example.com'; + $settings->basePath = '/'; - $generator = $this->createTestGenerator($config, []); + $generator = $this->createTestGenerator($settings, []); $this->assertEquals('https://tkr.example.com/', $generator->testGetSiteUrl()); $this->assertEquals('https://tkr.example.com/tick/456', $generator->testBuildTickUrl(456)); @@ -82,12 +82,12 @@ class FeedGeneratorTest extends TestCase public function testUrlMethodsHandleEmptyBasePath() { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->baseUrl = 'https://example.com'; - $config->basePath = ''; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->baseUrl = 'https://example.com'; + $settings->basePath = ''; - $generator = $this->createTestGenerator($config, []); + $generator = $this->createTestGenerator($settings, []); $this->assertEquals('https://example.com/', $generator->testGetSiteUrl()); $this->assertEquals('https://example.com/tick/789', $generator->testBuildTickUrl(789)); @@ -106,12 +106,12 @@ class FeedGeneratorTest extends TestCase foreach ($testCases as [$basePath, $expectedSiteUrl, $expectedTickUrl]) { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->baseUrl = 'https://example.com'; - $config->basePath = $basePath; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->baseUrl = 'https://example.com'; + $settings->basePath = $basePath; - $generator = $this->createTestGenerator($config, []); + $generator = $this->createTestGenerator($settings, []); $this->assertEquals($expectedSiteUrl, $generator->testGetSiteUrl(), "Failed for basePath: '$basePath'"); $this->assertEquals($expectedTickUrl, $generator->testBuildTickUrl(123), "Failed for basePath: '$basePath'"); diff --git a/tests/Feed/RssGeneratorTest.php b/tests/Feed/RssGeneratorTest.php index b90a2c0..270728c 100644 --- a/tests/Feed/RssGeneratorTest.php +++ b/tests/Feed/RssGeneratorTest.php @@ -7,12 +7,12 @@ class RssGeneratorTest extends TestCase { private function createMockConfig() { $mockPdo = $this->createMock(PDO::class); - $config = new ConfigModel($mockPdo); - $config->siteTitle = 'Test Site'; - $config->siteDescription = 'Test Description'; - $config->baseUrl = 'https://example.com'; - $config->basePath = '/tkr/'; - return $config; + $settings = new SettingsModel($mockPdo); + $settings->siteTitle = 'Test Site'; + $settings->siteDescription = 'Test Description'; + $settings->baseUrl = 'https://example.com'; + $settings->basePath = '/tkr/'; + return $settings; } private function createSampleTicks() { @@ -23,10 +23,10 @@ class RssGeneratorTest extends TestCase } public function testCanGenerateValidRss() { - $config = $this->createMockConfig(); + $settings = $this->createMockConfig(); $ticks = $this->createSampleTicks(); - $generator = new RssGenerator($config, $ticks); + $generator = new RssGenerator($settings, $ticks); $xml = $generator->generate(); // Test XML structure @@ -56,8 +56,8 @@ class RssGeneratorTest extends TestCase } public function testCanHandleEmptyTickList() { - $config = $this->createMockConfig(); - $generator = new RssGenerator($config, []); + $settings = $this->createMockConfig(); + $generator = new RssGenerator($settings, []); $xml = $generator->generate(); // Should still be valid RSS with no items @@ -81,7 +81,7 @@ class RssGeneratorTest extends TestCase } public function testCanHandleSpecialCharactersAndUnicode() { - $config = $this->createMockConfig(); + $settings = $this->createMockConfig(); // Test various challenging characters $ticks = [ @@ -107,7 +107,7 @@ class RssGeneratorTest extends TestCase ] ]; - $generator = new RssGenerator($config, $ticks); + $generator = new RssGenerator($settings, $ticks); $xml = $generator->generate(); // Test that emojis are preserved diff --git a/tests/Framework/Log/LogTest.php b/tests/Framework/Log/LogTest.php index 9047827..ab225af 100644 --- a/tests/Framework/Log/LogTest.php +++ b/tests/Framework/Log/LogTest.php @@ -13,9 +13,9 @@ class LogTest extends TestCase // Create a temporary directory for test logs $this->tempLogDir = sys_get_temp_dir() . '/tkr_test_logs_' . uniqid(); mkdir($this->tempLogDir, 0777, true); - + $this->testLogFile = $this->tempLogDir . '/tkr.log'; - + // Initialize Log with test file and reset route context Log::init($this->testLogFile); Log::setRouteContext(''); @@ -32,31 +32,31 @@ class LogTest extends TestCase private function deleteDirectory(string $dir): void { if (!is_dir($dir)) return; - + $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); - + foreach ($iterator as $path) { $path->isDir() ? rmdir($path->getRealPath()) : unlink($path->getRealPath()); } rmdir($dir); } - + private function setLogLevel(int $level): void { global $app; - $app = ['config' => (object)['logLevel' => $level]]; + $app = ['settings' => (object)['logLevel' => $level]]; } - + private function assertLogContains(string $message): void { $this->assertFileExists($this->testLogFile); $logContent = file_get_contents($this->testLogFile); $this->assertStringContainsString($message, $logContent); } - + private function assertLogDoesNotContain(string $message): void { $this->assertFileExists($this->testLogFile); @@ -68,9 +68,9 @@ class LogTest extends TestCase { Log::setRouteContext('GET /admin'); $this->setLogLevel(1); // DEBUG level - + Log::debug('Test message'); - + $logContent = file_get_contents($this->testLogFile); $this->assertStringContainsString('[GET /admin]', $logContent); $this->assertStringContainsString('Test message', $logContent); @@ -80,11 +80,11 @@ class LogTest extends TestCase { Log::setRouteContext(''); $this->setLogLevel(1); - + Log::info('Test without route'); - + $logContent = file_get_contents($this->testLogFile); - + // Should match format without route context: [timestamp] LEVEL: IP - message $this->assertMatchesRegularExpression( '/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] INFO: .+ - Test without route/', @@ -95,12 +95,12 @@ class LogTest extends TestCase public function testLogLevelFiltering(): void { $this->setLogLevel(3); // WARNING level - + Log::debug('Debug message'); // Should be filtered out - Log::info('Info message'); // Should be filtered out + Log::info('Info message'); // Should be filtered out Log::warning('Warning message'); // Should be logged Log::error('Error message'); // Should be logged - + $this->assertLogDoesNotContain('Debug message'); $this->assertLogDoesNotContain('Info message'); $this->assertLogContains('Warning message'); @@ -111,11 +111,11 @@ class LogTest extends TestCase { Log::setRouteContext('POST /admin'); $this->setLogLevel(1); - + Log::error('Test error message'); - + $logContent = file_get_contents($this->testLogFile); - + // Check log format: [timestamp] LEVEL: IP [route] - message $this->assertMatchesRegularExpression( '/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] ERROR: .+ \[POST \/admin\] - Test error message/', @@ -126,15 +126,15 @@ class LogTest extends TestCase public function testInitCreatesLogDirectory(): void { $newLogFile = $this->tempLogDir . '/nested/logs/test.log'; - + // Directory doesn't exist yet $this->assertDirectoryDoesNotExist(dirname($newLogFile)); - + Log::init($newLogFile); - + // init() should create the directory $this->assertDirectoryExists(dirname($newLogFile)); - + // Verify we can actually write to it $this->setLogLevel(1); Log::info('Test directory creation'); @@ -144,35 +144,35 @@ class LogTest extends TestCase public function testLogRotation(): void { $this->setLogLevel(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); file_put_contents($this->testLogFile, $logLines); - + // This should trigger rotation Log::info('This should trigger rotation'); - + // Verify rotation happened $this->assertFileExists($this->testLogFile . '.1'); $this->assertLogContains('This should trigger rotation'); } - + public function testLogRotationLimitsFileCount(): void { $this->setLogLevel(1); - + // Create 5 existing rotated log files (.1 through .5) for ($i = 1; $i <= 5; $i++) { file_put_contents($this->testLogFile . '.' . $i, "Old log file $i\n"); } - + // Create main log file at rotation threshold $logLines = str_repeat("[2025-01-31 12:00:00] INFO: 127.0.0.1 - Test line\n", 1000); file_put_contents($this->testLogFile, $logLines); - + // This should trigger rotation and delete the oldest file (.5) Log::info('Trigger rotation with max files'); - + // Verify rotation happened and file count is limited $this->assertFileExists($this->testLogFile . '.1'); // New rotated file $this->assertFileExists($this->testLogFile . '.2'); // Old .1 became .2 @@ -180,7 +180,7 @@ class LogTest extends TestCase $this->assertFileExists($this->testLogFile . '.4'); // Old .3 became .4 $this->assertFileExists($this->testLogFile . '.5'); // Old .4 became .5 $this->assertFileDoesNotExist($this->testLogFile . '.6'); // Old .5 was deleted - + $this->assertLogContains('Trigger rotation with max files'); } @@ -188,12 +188,12 @@ class LogTest extends TestCase { // Set up config without logLevel property (simulates missing config value) global $app; - $app = ['config' => (object)[]]; - + $app = ['settings' => (object)[]]; + // Should not throw errors and should default to INFO level Log::debug('Debug message'); // Should be filtered out (default INFO level = 2) Log::info('Info message'); // Should be logged - + $this->assertLogDoesNotContain('Debug message'); $this->assertLogContains('Info message'); } diff --git a/tests/Framework/Util/UtilTest.php b/tests/Framework/Util/UtilTest.php index cc43f39..eacda25 100644 --- a/tests/Framework/Util/UtilTest.php +++ b/tests/Framework/Util/UtilTest.php @@ -151,7 +151,7 @@ final class UtilTest extends TestCase // Set up global $app with config global $app; $app = [ - 'config' => (object)['strictAccessibility' => $strictAccessibility] + 'settings' => (object)['strictAccessibility' => $strictAccessibility] ]; $result = Util::linkify($input); @@ -162,12 +162,12 @@ final class UtilTest extends TestCase // Test linkify without new window global $app; $app = [ - 'config' => (object)['strictAccessibility' => false] + 'settings' => (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); } @@ -176,7 +176,7 @@ final class UtilTest extends TestCase // 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); } @@ -187,7 +187,7 @@ final class UtilTest extends TestCase $_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 } @@ -195,7 +195,7 @@ final class UtilTest extends TestCase 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); } diff --git a/tkr-setup.php b/tkr-setup.php index df63295..676af17 100644 --- a/tkr-setup.php +++ b/tkr-setup.php @@ -175,11 +175,11 @@ try { echo "💾 Saving configuration...\n"; // Create/update settings - $configModel = new ConfigModel($db); - $configModel->siteTitle = $siteTitle; - $configModel->baseUrl = $baseUrl; - $configModel->basePath = $basePath; - $config = $configModel->save(); + $settingsModel = new SettingsModel($db); + $settingsModel->siteTitle = $siteTitle; + $settingsModel->baseUrl = $baseUrl; + $settingsModel->basePath = $basePath; + $settings = $settingsModel->save(); // Create admin user $userModel = new UserModel($db);