Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions assets/modules/store/installer/instprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ function propertiesNameValue($propertyString) {
if ($prev_id) {
$prev_id = EvolutionCms()->getDatabase()->escape($prev_id);

evo()->getDatabase()->query("INSERT IGNORE INTO `{$table_prefix}site_plugin_events` (`pluginid`, `evtid`, `priority`)
evo()->getDatabase()->query("INSERT OR IGNORE INTO `{$table_prefix}site_plugin_events` (`pluginid`, `evtid`, `priority`)
SELECT {$id} as 'pluginid', `se`.`id` AS `evtid`, COALESCE(`spe`.`priority`, MAX(`spe2`.`priority`) + 1, 0) AS `priority`
FROM `{$table_prefix}system_eventnames` `se`
LEFT JOIN `{$table_prefix}site_plugin_events` `spe` ON `spe`.`evtid` = `se`.`id` AND `spe`.`pluginid` = {$prev_id}
Expand All @@ -375,7 +375,7 @@ function propertiesNameValue($propertyString) {
GROUP BY `se`.`id`
");
} else {
evo()->getDatabase()->query("INSERT IGNORE INTO `{$table_prefix}site_plugin_events` (`pluginid`, `evtid`, `priority`)
evo()->getDatabase()->query("INSERT OR IGNORE INTO `{$table_prefix}site_plugin_events` (`pluginid`, `evtid`, `priority`)
SELECT {$id} as `pluginid`, `se`.`id` as `evtid`, COALESCE(MAX(`spe`.`priority`) + 1, 0) as `priority`
FROM `{$table_prefix}system_eventnames` `se`
LEFT JOIN `{$table_prefix}site_plugin_events` `spe` ON `spe`.`evtid` = `se`.`id`
Expand All @@ -384,7 +384,7 @@ function propertiesNameValue($propertyString) {
}

// remove existing events
evo()->getDatabase()->query("DELETE `pe` FROM `{$table_prefix}site_plugin_events` `pe` LEFT JOIN `{$table_prefix}system_eventnames` `se` ON `pe`.`evtid`=`se`.`id` AND `name` IN ('{$_events}') WHERE ISNULL(`name`) AND `pluginid` = {$id}");
evo()->getDatabase()->query("DELETE `pe` FROM `{$table_prefix}site_plugin_events` `pe` LEFT JOIN `{$table_prefix}system_eventnames` `se` ON `pe`.`evtid`=`se`.`id` AND `name` IN ('{$_events}') WHERE `name` IS NULL AND `pluginid` = {$id}");
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions core/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
}
require_once __DIR__ . '/includes/define.inc.php';

if (!defined('EVO_SESSION')) {
define('EVO_SESSION', (bool)env('EVO_SESSION', true));
}

require_once __DIR__ . '/functions/session_proxy.php';

require_once __DIR__ . '/includes/legacy.inc.php';

require_once __DIR__ . '/includes/protect.inc.php'; // harden it
Expand Down
4 changes: 3 additions & 1 deletion core/config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@

'middleware' => [
'mgr' => [
Illuminate\Session\Middleware\StartSession::class,
EvolutionCMS\Middleware\SessionProxy::class,
EvolutionCMS\Middleware\VerifyCsrfToken::class,
EvolutionCMS\Middleware\Manager::class,
Illuminate\Session\Middleware\StartSession::class,
Illuminate\Routing\Middleware\SubstituteBindings::class,
Illuminate\View\Middleware\ShareErrorsFromSession::class,
],
Expand All @@ -138,6 +139,7 @@
*/
'global' => [
Illuminate\Session\Middleware\StartSession::class,
EvolutionCMS\Middleware\SessionProxy::class,
Illuminate\Routing\Middleware\SubstituteBindings::class,
Illuminate\View\Middleware\ShareErrorsFromSession::class,
],
Expand Down
2 changes: 1 addition & 1 deletion core/config/tracy.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* IMPORTANT! Tracy ignore the "error_reporting" EvolutionCMS setting
*/
'active' => false,
'active' => true,
'panels' => [
EvolutionCMS\Tracy\Panels\Database\Panel::class,
EvolutionCMS\Tracy\Panels\Routing\Panel::class,
Expand Down
10 changes: 8 additions & 2 deletions core/functions/preload.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ function startCMSSession()
return;
}

if (defined('EVO_SESSION') && EVO_SESSION) {
EvoSessionProxy::earlyInit();
return;
}

session_name(SESSION_COOKIE_NAME);
removeInvalidCmsSessionIds(SESSION_COOKIE_NAME);
session_cache_limiter('');
Expand Down Expand Up @@ -160,8 +165,9 @@ function startCMSSession()
, true
);
}
if (!isset($_SESSION['modx.session.created.time'])) {
$_SESSION['modx.session.created.time'] = $_SERVER['REQUEST_TIME'];
$createdKey = 'evo.session.created.time';
if (!isset($_SESSION[$createdKey])) {
$_SESSION[$createdKey] = $_SERVER['REQUEST_TIME'] ?? time();
}
}
}
Expand Down
306 changes: 306 additions & 0 deletions core/functions/session_proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
<?php

class EvoSessionProxy
{
/**
* @var bool
*/
private static $initialized = false;

/**
* @var bool
*/
private static $synced = false;

/**
* @var bool
*/
private static $shutdownRegistered = false;

/**
* Early init - before Laravel middleware.
* Ensure $_SESSION is an array (do NOT overwrite if already initialized).
*/
public static function earlyInit(): void
{
if (!isset($_SESSION) || !is_array($_SESSION)) {
$_SESSION = [];
}

$createdKey = 'evo.session.created.time';
if (!isset($_SESSION[$createdKey])) {
$_SESSION[$createdKey] = $_SERVER['REQUEST_TIME'] ?? time();
}
}

/**
* Init - after Laravel StartSession middleware.
*/
public static function init(): void
{
if (self::$initialized) {
return;
}

$store = self::getLaravelSessionStore();
if ($store === null) {
return;
}

self::ensureLaravelSessionStarted($store);
self::migrateLegacySessionIfNeeded($store);

// Start PHP session with cookies disabled (Laravel owns the cookie).
if (session_status() === PHP_SESSION_NONE) {
ini_set('session.use_cookies', '0');
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80400) {
ini_set('session.use_only_cookies', '0');
}
$sessionId = $store->getId();
if (is_string($sessionId) && $sessionId !== '') {
session_id($sessionId);
}
@session_start();
}

$earlyData = (isset($_SESSION) && is_array($_SESSION)) ? $_SESSION : [];
if (!isset($_SESSION) || !is_array($_SESSION)) {
$_SESSION = [];
}

// Laravel → $_SESSION (Laravel wins on conflicts).
$laravelData = $store->all();
foreach ($laravelData as $key => $value) {
$_SESSION[$key] = $value;
}

// Merge back early data for keys not present in Laravel.
foreach ($earlyData as $key => $value) {
if (!array_key_exists($key, $laravelData)) {
$_SESSION[$key] = $value;
$store->put($key, $value);
}
}

self::$initialized = true;

if (!self::$shutdownRegistered) {
self::$shutdownRegistered = true;
register_shutdown_function([self::class, 'syncBack']);
}
}

/**
* Sync back - before response.
*/
public static function syncBack(): void
{
if (!self::$initialized || self::$synced) {
return;
}

self::$synced = true;

$store = self::getLaravelSessionStore();
if ($store === null) {
return;
}

$laravelData = $store->all();

foreach ($_SESSION as $key => $value) {
if (self::isInternalKey($key)) {
continue;
}
if (!array_key_exists($key, $laravelData) || $laravelData[$key] !== $value) {
$store->put($key, $value);
}
}

foreach ($laravelData as $key => $value) {
if (self::isInternalKey($key)) {
continue;
}
if (!array_key_exists($key, $_SESSION)) {
$store->forget($key);
}
}

$store->save();
}

/**
* @return object|null
*/
private static function getLaravelSessionStore()
{
if (!function_exists('app')) {
return null;
}

$app = app();
if (!is_object($app) || !method_exists($app, 'has') || !$app->has('session')) {
return null;
}

try {
$manager = app('session');
} catch (\Throwable $exception) {
return null;
}

if (is_object($manager) && method_exists($manager, 'driver')) {
$store = $manager->driver();
} else {
$store = $manager;
}

if (!is_object($store) || !method_exists($store, 'all') || !method_exists($store, 'getId')) {
return null;
}

return $store;
}

/**
* @param object $store
* @return void
*/
private static function ensureLaravelSessionStarted($store): void
{
if (method_exists($store, 'isStarted') && $store->isStarted()) {
return;
}

$cookieName = self::getLaravelSessionCookieName();
$cookieId = self::getCookieValue($cookieName);
if (is_string($cookieId) && $cookieId !== '') {
if (method_exists($store, 'setId')) {
$store->setId($cookieId);
}
}

if (method_exists($store, 'start')) {
$store->start();
}
}

/**
* @param object $store
* @return void
*/
private static function migrateLegacySessionIfNeeded($store): void
{
$laravelCookie = self::getLaravelSessionCookieName();
if (!empty($_COOKIE[$laravelCookie])) {
return;
}

$legacyCookie = defined('SESSION_COOKIE_NAME') ? SESSION_COOKIE_NAME : 'EVOSESSID';
$legacyId = self::getCookieValue($legacyCookie);
if (!is_string($legacyId) || $legacyId === '') {
return;
}

$payload = self::readLegacySessionPayload($legacyId);
if ($payload === null || $payload === '') {
return;
}

$legacyData = self::decodeSessionPayload($payload);
if (!is_array($legacyData) || $legacyData === []) {
return;
}

$existing = $store->all();
foreach ($legacyData as $key => $value) {
if (!array_key_exists($key, $existing)) {
$store->put($key, $value);
}
}
$store->save();

// Expire legacy cookie after successful migration.
setcookie($legacyCookie, '', time() - 3600, '/');
unset($_COOKIE[$legacyCookie]);
}

/**
* @param string $sessionId
* @return string|null
*/
private static function readLegacySessionPayload(string $sessionId): ?string
{
$savePath = session_save_path();
if (!is_string($savePath) || $savePath === '') {
$savePath = sys_get_temp_dir();
}

$parts = explode(';', $savePath);
$path = end($parts);
if (!is_string($path) || $path === '') {
return null;
}

$file = rtrim($path, "/\\") . DIRECTORY_SEPARATOR . 'sess_' . $sessionId;
if (!is_readable($file)) {
return null;
}

$payload = file_get_contents($file);
return ($payload === false) ? null : $payload;
}

/**
* @param string $payload
* @return array
*/
private static function decodeSessionPayload(string $payload): array
{
$backup = $_SESSION ?? null;
$_SESSION = [];
$ok = @session_decode($payload);
$decoded = ($ok === false) ? [] : $_SESSION;

if ($backup === null) {
unset($_SESSION);
} else {
$_SESSION = $backup;
}

return is_array($decoded) ? $decoded : [];
}

/**
* @return string
*/
private static function getLaravelSessionCookieName(): string
{
if (function_exists('config')) {
return (string)config('session.cookie', 'evo_session');
}
return 'evo_session';
}

/**
* @param string $name
* @return string|null
*/
private static function getCookieValue(string $name): ?string
{
if (!isset($_COOKIE[$name])) {
return null;
}
$value = $_COOKIE[$name];
return is_string($value) ? $value : null;
}

/**
* @param string $key
* @return bool
*/
private static function isInternalKey(string $key): bool
{
return strncmp($key, '_', 1) === 0;
}
}
Loading