Compare commits

...

7 Commits

6 changed files with 271 additions and 94 deletions

View File

@ -32,22 +32,39 @@ class Session {
global $config;
// Load session settings from config if available
self::$sessionName = self::generateRandomSessionName();
// Get session name from config or generate a random one
self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName();
if (isset($config['session']) && is_array($config['session'])) {
if (!empty($config['session']['name'])) {
self::$sessionName = $config['session']['name'];
}
// Set session name before starting the session
session_name(self::$sessionName);
if (isset($config['session']['lifetime'])) {
self::$sessionOptions['gc_maxlifetime'] = (int)$config['session']['lifetime'];
}
}
// Set session cookie parameters
$thisPath = $config['folder'] ?? '/';
$thisDomain = $config['domain'] ?? '';
$isSecure = isset($_SERVER['HTTPS']);
session_set_cookie_params([
'lifetime' => 0, // Session cookie (browser session)
'path' => $thisPath,
'domain' => $thisDomain,
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Strict'
]);
self::$initialized = true;
}
/**
* Get session name from config or generate a random one
*/
private static function getSessionNameFromConfig($config) {
if (isset($config['session']['name']) && !empty($config['session']['name'])) {
return $config['session']['name'];
}
return self::generateRandomSessionName();
}
/**
* Start or resume a session with secure options
*/
@ -55,13 +72,9 @@ class Session {
self::initialize();
if (session_status() === PHP_SESSION_NONE) {
session_name(self::$sessionName);
session_start(self::$sessionOptions);
} elseif (session_status() === PHP_SESSION_ACTIVE && session_name() !== self::$sessionName) {
// If session is active but with wrong name, destroy and restart it
session_destroy();
session_name(self::$sessionName);
session_start(self::$sessionOptions);
if (!headers_sent()) {
session_start(self::$sessionOptions);
}
}
}
@ -91,10 +104,23 @@ class Session {
/**
* Check if current session is valid
*
* @param bool $strict If true, will return false for new/unauthenticated sessions
* @return bool True if session is valid, false otherwise
*/
public static function isValidSession() {
// Check required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
public static function isValidSession($strict = true) {
// If session is not started or empty, it's not valid
if (session_status() !== PHP_SESSION_ACTIVE || empty($_SESSION)) {
return false;
}
// In non-strict mode, consider empty session as valid (for login/logout)
if (!$strict && !isset($_SESSION['user_id']) && !isset($_SESSION['username'])) {
return true;
}
// In strict mode, require user_id and username
if ($strict && (!isset($_SESSION['user_id']) || !isset($_SESSION['username']))) {
return false;
}
@ -159,26 +185,44 @@ class Session {
* Create a new authenticated session for a user
*/
public static function createAuthSession($userId, $username, $rememberMe, $config) {
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// Set cookie with secure options
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Ensure session is started
self::startSession();
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
$_SESSION['REMEMBER_ME'] = $rememberMe;
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Update session cookie with remember me setting
if (!headers_sent()) {
setcookie(
session_name(),
session_id(),
[
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]
);
// Set username cookie
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
if ($rememberMe) {
self::setRememberMe(true);
}

View File

@ -11,32 +11,47 @@ class Router {
* Returns current username if session is valid, null otherwise.
*/
public static function checkAuth(array $config, string $app_root, array $public_pages, string $page): ?string {
$validSession = Session::isValidSession();
// Always allow login page to be accessed
if ($page === 'login') {
return null;
}
// Check if this is a public page
$isPublicPage = in_array($page, $public_pages, true);
// For public pages, don't validate session
if ($isPublicPage) {
return null;
}
// For protected pages, check if we have a valid session
$validSession = Session::isValidSession(true);
// If session is valid, return the username
if ($validSession) {
return Session::getUsername();
}
if (!in_array($page, $public_pages, true)) {
// flash session timeout if needed
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
$_SESSION['session_timeout_shown'] = true;
}
// preserve flash messages
$flash_messages = $_SESSION['flash_messages'] ?? [];
Session::cleanup($config);
$_SESSION['flash_messages'] = $flash_messages;
// build login URL
$loginUrl = $app_root . '?page=login';
$trimmed = trim($page, '/?');
if (!in_array($trimmed, INVALID_REDIRECT_PAGES, true)) {
$loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']);
}
header('Location: ' . $loginUrl);
exit();
// If we get here, we need to redirect to login
// Only show timeout message if we had an active session before
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
$_SESSION['session_timeout_shown'] = true;
}
return null;
// Preserve flash messages
$flash_messages = $_SESSION['flash_messages'] ?? [];
Session::cleanup($config);
$_SESSION['flash_messages'] = $flash_messages;
// Build login URL with redirect if appropriate
$loginUrl = $app_root . '?page=login';
$trimmed = trim($page, '/?');
if (!empty($trimmed) && !in_array($trimmed, INVALID_REDIRECT_PAGES, true)) {
$loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']);
}
header('Location: ' . $loginUrl);
exit();
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Theme Asset Handler
*
* Serves theme assets (images, CSS, JS, etc.) securely by checking if the requested
* theme and asset path are valid and accessible.
*/
// Include necessary files
require_once __DIR__ . '/../config/init.php';
require_once __DIR__ . '/../core/ConfigLoader.php';
// Basic security checks
if (!isset($_GET['theme']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['theme'])) {
http_response_code(400);
exit('Invalid theme specified');
}
if (!isset($_GET['path']) || empty($_GET['path'])) {
http_response_code(400);
exit('No asset path specified');
}
$themeId = $_GET['theme'];
$assetPath = $_GET['path'];
// Validate asset path (only alphanumeric, hyphen, underscore, dot, and forward slash)
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
http_response_code(400);
exit('Invalid asset path');
}
// Prevent directory traversal
if (strpos($assetPath, '..') !== false) {
http_response_code(400);
exit('Invalid asset path');
}
// Build full path to the asset
$fullPath = __DIR__ . "/../../themes/$themeId/$assetPath";
// Check if the file exists and is readable
if (!file_exists($fullPath) || !is_readable($fullPath)) {
http_response_code(404);
exit('Asset not found');
}
// Determine content type based on file extension
$extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION));
$contentTypes = [
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'eot' => 'application/vnd.ms-fontobject',
];
$contentType = $contentTypes[$extension] ?? 'application/octet-stream';
// Set proper headers
header('Content-Type: ' . $contentType);
header('Content-Length: ' . filesize($fullPath));
// Cache for 24 hours (86400 seconds)
$expires = 86400;
header('Cache-Control: public, max-age=' . $expires);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
header('Pragma: cache');
// Output the file
readfile($fullPath);

View File

@ -193,14 +193,19 @@ class Theme
*/
public static function include($template)
{
$themeName = self::getCurrentThemeName();
$config = self::getConfig();
global $config;
$config = $config ?? [];
$themeConfig = self::getConfig();
$themeName = self::getCurrentThemeName();
// Import all global variables into local scope
// We need this here, otherwise because this helper
// between index and the views breaks the session vars
extract($GLOBALS, EXTR_SKIP | EXTR_REFS);
// Ensure config is always available in templates
$config = array_merge($config, $themeConfig);
// For non-default themes, look in the theme directory first
if ($themeName !== 'default') {
$themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php';

View File

@ -48,6 +48,19 @@ if (isset($_GET['switch_to'])) {
$themes = \App\Helpers\Theme::getAvailableThemes();
$currentTheme = \App\Helpers\Theme::getCurrentThemeName();
// Prepare theme data with screenshot URLs for the view
$themeData = [];
foreach ($themes as $id => $name) {
$themeData[$id] = [
'name' => $name,
'screenshotUrl' => \App\Helpers\Theme::getScreenshotUrl($id),
'isActive' => $id === $currentTheme
];
}
// Make theme data available to the view
$themes = $themeData;
// Generate CSRF token for the form
$csrf_token = $security->generateCsrfToken();

View File

@ -16,6 +16,28 @@
//ini_set('display_startup_errors', 1);
//error_reporting(E_ALL);
// Prepare config loader
require_once __DIR__ . '/../app/core/ConfigLoader.php';
use App\Core\ConfigLoader;
// Load configuration
$config = ConfigLoader::loadConfig([
__DIR__ . '/../app/config/jilo-web.conf.php',
__DIR__ . '/../jilo-web.conf.php',
'/srv/jilo-web/jilo-web.conf.php',
'/opt/jilo-web/jilo-web.conf.php',
]);
// Make config available globally
$GLOBALS['config'] = $config;
// Expose config file path for pages
$config_file = ConfigLoader::getConfigPath();
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
// Set app root with default
$app_root = $config['folder'] ?? '/';
// Preparing plugins and hooks
// Initialize HookDispatcher and plugin system
require_once __DIR__ . '/../app/core/HookDispatcher.php';
@ -23,10 +45,6 @@ require_once __DIR__ . '/../app/core/PluginManager.php';
use App\Core\HookDispatcher;
use App\Core\PluginManager;
// Initialize themes system
require_once __DIR__ . '/../app/helpers/theme.php';
use app\Helpers\Theme;
// Global allowed URLs registration
register_hook('filter_allowed_urls', function($urls) {
if (isset($GLOBALS['plugin_controllers']) && is_array($GLOBALS['plugin_controllers'])) {
@ -72,6 +90,11 @@ ob_start();
// Start session before any session-dependent code
require_once '../app/classes/session.php';
// Initialize themes system after session is started
require_once __DIR__ . '/../app/helpers/theme.php';
use app\Helpers\Theme;
Session::startSession();
// Define page variable early via sanitize
@ -81,6 +104,12 @@ if (!isset($page)) {
$page = 'dashboard';
}
// List of pages that don't require authentication
$public_pages = ['login', 'help', 'about'];
// Let plugins filter/extend public_pages
$public_pages = filter_public_pages($public_pages);
// Middleware pipeline for security, sanitization & CSRF
require_once __DIR__ . '/../app/core/MiddlewarePipeline.php';
$pipeline = new \App\Core\MiddlewarePipeline();
@ -90,11 +119,18 @@ $pipeline->add(function() {
return true;
});
// Check session validity
$validSession = Session::isValidSession();
// For public pages, we don't need to validate the session
// The Router will handle authentication for protected pages
$validSession = false;
$userId = null;
// Get user ID early if session is valid
$userId = $validSession ? Session::getUserId() : null;
// Only check session for non-public pages
if (!in_array($page, $public_pages)) {
$validSession = Session::isValidSession(true);
if ($validSession) {
$userId = Session::getUserId();
}
}
// Initialize feedback message system
require_once '../app/classes/feedback.php';
@ -118,28 +154,6 @@ $allowed_urls = [
// Let plugins filter/extend allowed_urls
$allowed_urls = filter_allowed_urls($allowed_urls);
require_once __DIR__ . '/../app/core/ConfigLoader.php';
use App\Core\ConfigLoader;
// Load configuration
$config = ConfigLoader::loadConfig([
__DIR__ . '/../app/config/jilo-web.conf.php',
__DIR__ . '/../jilo-web.conf.php',
'/srv/jilo-web/jilo-web.conf.php',
'/opt/jilo-web/jilo-web.conf.php',
]);
// Expose config file path for pages
$config_file = ConfigLoader::getConfigPath();
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
$app_root = $config['folder'];
// List of pages that don't require authentication
$public_pages = ['login', 'help', 'about'];
// Let plugins filter/extend public_pages
$public_pages = filter_public_pages($public_pages);
// Dispatch routing and auth
require_once __DIR__ . '/../app/core/Router.php';
use App\Core\Router;
@ -210,12 +224,19 @@ $platformDetails = $platformObject->getPlatformDetails($platform_id);
// logout is a special case, as we can't use session vars for notices
if ($page == 'logout') {
// Save config before destroying session
$savedConfig = $config;
// clean up session
Session::destroySession();
// start new session for the login page
Session::startSession();
// Restore config to global scope
$config = $savedConfig;
$GLOBALS['config'] = $config;
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
// Log successful logout
@ -307,17 +328,17 @@ if ($page == 'logout') {
} else {
include '../app/templates/error-notfound.php';
}
include '../app/templates/page-footer.php';
\App\Helpers\Theme::include('page-footer');
}
} else {
// The page is not in allowed URLs
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
\App\Helpers\Theme::include('page-header');
\App\Helpers\Theme::include('page-menu');
if ($validSession) {
include '../app/templates/page-sidebar.php';
\App\Helpers\Theme::include('page-sidebar');
}
include '../app/templates/error-notfound.php';
include '../app/templates/page-footer.php';
\App\Helpers\Theme::include('page-footer');
}
}