Compare commits
7 Commits
47875289a8
...
37566b5122
Author | SHA1 | Date |
---|---|---|
|
37566b5122 | |
|
8203c10f37 | |
|
b1dae54aac | |
|
d65b7bcc55 | |
|
a0f3e84432 | |
|
c9490cf149 | |
|
ad8c833862 |
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue