Skip to content
Open
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_LARAVEL_SESSION')) {
define('EVO_LARAVEL_SESSION', (bool)env('EVO_LARAVEL_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
5 changes: 5 additions & 0 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_LARAVEL_SESSION') && EVO_LARAVEL_SESSION) {
EvoSessionProxy::earlyInit();
return;
}

session_name(SESSION_COOKIE_NAME);
removeInvalidCmsSessionIds(SESSION_COOKIE_NAME);
session_cache_limiter('');
Expand Down
305 changes: 305 additions & 0 deletions core/functions/session_proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
<?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 = [];
}

if (!isset($_SESSION['modx.session.created.time'])) {
$_SESSION['modx.session.created.time'] = $_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;
}
}
2 changes: 1 addition & 1 deletion core/functions/utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function evo_role(string $role = ''): bool
* @param array $options
* @return mixed variable itself
*/
function var_debug($var, $title = null, array $options = null)
function var_debug($var, $title = null, ?array $options = null)
{
return EvolutionCMS\Tracy\Debugger::barDump($var, $title, $options);
}
Expand Down
Loading