diff --git a/assets/modules/store/installer/instprocessor.php b/assets/modules/store/installer/instprocessor.php index 37a5ce2ae9..7437ba57fc 100644 --- a/assets/modules/store/installer/instprocessor.php +++ b/assets/modules/store/installer/instprocessor.php @@ -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} @@ -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` @@ -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}"); } } } diff --git a/core/bootstrap.php b/core/bootstrap.php index 0723925e82..010e61965b 100644 --- a/core/bootstrap.php +++ b/core/bootstrap.php @@ -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 diff --git a/core/config/app.php b/core/config/app.php index b8206e24d1..fda89d5c9a 100644 --- a/core/config/app.php +++ b/core/config/app.php @@ -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, ], @@ -138,6 +139,7 @@ */ 'global' => [ Illuminate\Session\Middleware\StartSession::class, + EvolutionCMS\Middleware\SessionProxy::class, Illuminate\Routing\Middleware\SubstituteBindings::class, Illuminate\View\Middleware\ShareErrorsFromSession::class, ], diff --git a/core/config/tracy.php b/core/config/tracy.php index 9bb67b215c..81d81e352b 100644 --- a/core/config/tracy.php +++ b/core/config/tracy.php @@ -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, diff --git a/core/functions/preload.php b/core/functions/preload.php index 075b54669a..122fd69358 100644 --- a/core/functions/preload.php +++ b/core/functions/preload.php @@ -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(''); @@ -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(); } } } diff --git a/core/functions/session_proxy.php b/core/functions/session_proxy.php new file mode 100644 index 0000000000..c873a284f4 --- /dev/null +++ b/core/functions/session_proxy.php @@ -0,0 +1,306 @@ +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; + } +} diff --git a/core/functions/utils.php b/core/functions/utils.php index e2fa0c6386..c18277ca4e 100644 --- a/core/functions/utils.php +++ b/core/functions/utils.php @@ -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); } diff --git a/core/src/Bootstrap/EnvCacheLoader.php b/core/src/Bootstrap/EnvCacheLoader.php index e0900fa312..5a456b86f9 100644 --- a/core/src/Bootstrap/EnvCacheLoader.php +++ b/core/src/Bootstrap/EnvCacheLoader.php @@ -70,15 +70,19 @@ public static function load(string $projectRoot): void private static function detectEnvPathAndMtime(string $projectRoot): array { $coreCustomEnv = $projectRoot . '/core/custom/.env'; - if (is_file($coreCustomEnv) && is_readable($coreCustomEnv)) { + if (is_file($coreCustomEnv)) { $mtime = @filemtime($coreCustomEnv); - return [$coreCustomEnv, is_int($mtime) ? $mtime : 0]; + if ($mtime !== false) { + return [$coreCustomEnv, $mtime]; + } } $rootEnv = $projectRoot . '/.env'; - if (is_file($rootEnv) && is_readable($rootEnv)) { + if (is_file($rootEnv)) { $mtime = @filemtime($rootEnv); - return [$rootEnv, is_int($mtime) ? $mtime : 0]; + if ($mtime !== false) { + return [$rootEnv, $mtime]; + } } return [null, null]; diff --git a/core/src/Console/TranslationsSyncCommand.php b/core/src/Console/TranslationsSyncCommand.php index a70ef76a35..cbce3644be 100644 --- a/core/src/Console/TranslationsSyncCommand.php +++ b/core/src/Console/TranslationsSyncCommand.php @@ -357,8 +357,7 @@ protected function writeMissingKeys(string $filePath, string $arrayName, array $ // Generate the PHP file content $content = "select('site_content.id', 'pagetitle', 'longtitle', 'description', 'introtext', 'menutitle', 'deleted', 'published', 'isfolder', 'type'); $searchfields = trim(get_by_key($_REQUEST, 'searchfields', '', 'is_scalar')); + $articul_id = []; $templateid = isset($_REQUEST['templateid']) && $_REQUEST['templateid'] !== '' ? (int)$_REQUEST['templateid'] : ''; @@ -88,7 +89,6 @@ protected function getResults() ->where('value', 'LIKE', '%' . $searchfields . '%'); if ($tvs->count() > 0) { - $articul_id = []; $i = 1; foreach ($tvs->pluck('contentid') ->toArray() as $articul) { @@ -96,29 +96,52 @@ protected function getResults() } } - if (ctype_digit($searchfields)) { - $searchQuery->orWhere('site_content.id', $searchfields); - if (strlen($searchfields) > 3) { - $searchQuery->orWhere('site_content.pagetitle', 'LIKE', '%' . $searchfields . '%'); - } - } + $searchQuery = $searchQuery->where(function ($query) use ($searchfields, $idFromAlias, $articul_id) { + $hasClause = false; - if ($idFromAlias) { - $searchQuery->orWhere('site_content.id', $idFromAlias); + if (ctype_digit($searchfields)) { + $query->where('site_content.id', $searchfields); + $hasClause = true; + if (strlen($searchfields) > 3) { + $query->orWhere('site_content.pagetitle', 'LIKE', '%' . $searchfields . '%'); + } + } - } + if ($idFromAlias) { + if ($hasClause) { + $query->orWhere('site_content.id', $idFromAlias); + } else { + $query->where('site_content.id', $idFromAlias); + $hasClause = true; + } + } - if (!ctype_digit($searchfields)) { - $searchQuery = $searchQuery->where(function ($query) use ($searchfields) { - $query->where('pagetitle', 'LIKE', '%' . $searchfields . '%') - ->orWhere('longtitle', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('introtext', 'LIKE', '%' . $searchfields . '%') - ->orWhere('menutitle', 'LIKE', '%' . $searchfields . '%') - ->orWhere('alias', 'LIKE', '%' . $searchfields . '%'); - }); + if (!ctype_digit($searchfields)) { + $condition = function ($nested) use ($searchfields) { + $nested->where('pagetitle', 'LIKE', '%' . $searchfields . '%') + ->orWhere('longtitle', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('introtext', 'LIKE', '%' . $searchfields . '%') + ->orWhere('menutitle', 'LIKE', '%' . $searchfields . '%') + ->orWhere('alias', 'LIKE', '%' . $searchfields . '%'); + }; + + if ($hasClause) { + $query->orWhere($condition); + } else { + $query->where($condition); + $hasClause = true; + } + } - } + if (!empty($articul_id)) { + if ($hasClause) { + $query->orWhereIn('site_content.id', $articul_id); + } else { + $query->whereIn('site_content.id', $articul_id); + } + } + }); } elseif ($idFromAlias) { $searchQuery = $searchQuery->where('site_content.id', $idFromAlias); } @@ -163,8 +186,10 @@ protected function getResults() 'application/vnd.ms-excel' => $this->managerTheme->getStyle('icon_excel'), ]; - if(!empty($articul_id)){ - $searchQuery = $searchQuery->orWhereIn('site_content.id', $articul_id); + if (!empty($articul_id)) { + $searchQuery = $searchQuery->orWhere(function ($query) use ($articul_id) { + $query->whereIn('site_content.id', $articul_id); + }); } $searchQuery = $searchQuery->groupBy('site_content.id'); @@ -272,10 +297,12 @@ protected function getAjaxResults() $results = SiteTemplate::query() ->select('id', 'templatename', 'locked') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('templatename', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('content', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('templatename', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('content', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); @@ -305,14 +332,16 @@ protected function getAjaxResults() $results = SiteTmplvar::query() ->select('id', 'name', 'locked') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('name', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('type', 'LIKE', '%' . $searchfields . '%') - ->orWhere('elements', 'LIKE', '%' . $searchfields . '%') - ->orWhere('display', 'LIKE', '%' . $searchfields . '%') - ->orWhere('display_params', 'LIKE', '%' . $searchfields . '%') - ->orWhere('default_text', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('name', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('type', 'LIKE', '%' . $searchfields . '%') + ->orWhere('elements', 'LIKE', '%' . $searchfields . '%') + ->orWhere('display', 'LIKE', '%' . $searchfields . '%') + ->orWhere('display_params', 'LIKE', '%' . $searchfields . '%') + ->orWhere('default_text', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); @@ -339,10 +368,12 @@ protected function getAjaxResults() $results = SiteHtmlsnippet::query() ->select('id', 'name', 'locked', 'disabled') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('name', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('snippet', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('name', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('snippet', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); @@ -369,12 +400,14 @@ protected function getAjaxResults() $results = SiteSnippet::query() ->select('id', 'name', 'locked', 'disabled') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('name', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('snippet', 'LIKE', '%' . $searchfields . '%') - ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') - ->orWhere('moduleguid', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('name', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('snippet', 'LIKE', '%' . $searchfields . '%') + ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') + ->orWhere('moduleguid', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); @@ -401,12 +434,14 @@ protected function getAjaxResults() $results = SitePlugin::query() ->select('id', 'name', 'locked', 'disabled') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('name', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('plugincode', 'LIKE', '%' . $searchfields . '%') - ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') - ->orWhere('moduleguid', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('name', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('plugincode', 'LIKE', '%' . $searchfields . '%') + ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') + ->orWhere('moduleguid', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); @@ -433,13 +468,15 @@ protected function getAjaxResults() $results = SiteModule::query() ->select('id', 'name', 'locked', 'disabled') - ->where('id', 'LIKE', '%' . $searchfields . '%') - ->orWhere('name', 'LIKE', '%' . $searchfields . '%') - ->orWhere('description', 'LIKE', '%' . $searchfields . '%') - ->orWhere('modulecode', 'LIKE', '%' . $searchfields . '%') - ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') - ->orWhere('guid', 'LIKE', '%' . $searchfields . '%') - ->orWhere('resourcefile', 'LIKE', '%' . $searchfields . '%'); + ->where(function ($query) use ($searchfields) { + $query->where('id', 'LIKE', '%' . $searchfields . '%') + ->orWhere('name', 'LIKE', '%' . $searchfields . '%') + ->orWhere('description', 'LIKE', '%' . $searchfields . '%') + ->orWhere('modulecode', 'LIKE', '%' . $searchfields . '%') + ->orWhere('properties', 'LIKE', '%' . $searchfields . '%') + ->orWhere('guid', 'LIKE', '%' . $searchfields . '%') + ->orWhere('resourcefile', 'LIKE', '%' . $searchfields . '%'); + }); $count = $results->count(); diff --git a/core/src/Database.php b/core/src/Database.php index d7ed20d6f5..a85171f7c0 100644 --- a/core/src/Database.php +++ b/core/src/Database.php @@ -26,6 +26,8 @@ class Database extends Manager public $config; + protected $sqlitePragmaApplied = false; + public function __construct(?Container $container = null) { parent::__construct($container); @@ -41,24 +43,30 @@ public function __construct(?Container $container = null) public function replaceFullTableName($tableName, $force = false) { $tableName = trim($tableName); + $connection = $this->getConnection(); + $grammar = $connection->getQueryGrammar(); + $prefix = $connection->getTablePrefix(); + $tableWithPrefix = $tableName; + if ((bool) $force === true) { - $result = $this->getConnection()->getTablePrefix() . $tableName; - } elseif (strpos($tableName, '[+prefix+]') !== false) { - $dbase = trim($this->getConfig('database'), '`'); - $prefix = $this->getConfig('prefix'); + return $grammar->wrapTable($prefix . $tableName); + } - $result = preg_replace( + if (strpos($tableName, '[+prefix+]') !== false) { + return preg_replace_callback( '@\[\+prefix\+\](\w+)@', - '`' . $dbase . '`.`' . $prefix . '$1`', + static function ($matches) use ($prefix, $grammar) { + return $grammar->wrapTable($prefix . $matches[1]); + }, $tableName ); - } else { - $result = $tableName; } - if ($this->getConfig('driver') == 'pgsql') { - $result = str_replace('"', "'", $result); + + if ($prefix !== '' && strpos($tableName, $prefix) !== 0) { + $tableWithPrefix = $prefix . $tableName; } - return $result; + + return $grammar->wrapTable($tableWithPrefix); } /** @@ -164,6 +172,8 @@ public function getConnect() if (!$this->conn->getPdo() instanceof PDO) { $this->conn->reconnect(); } + } else { + $this->applySqlitePragmas($this->conn); } return $this->conn; } @@ -173,7 +183,7 @@ public function getConnect() */ public function isConnected() { - return true; + return $this->conn instanceof Connection && $this->conn->getPdo() instanceof PDO; } public function insertFrom( @@ -458,7 +468,10 @@ public function escape($data, $safeCount = 0) */ public function connect() { - return $this->getConnection(); + $this->conn = $this->getConnection(); + $this->applySqlitePragmas($this->conn); + + return $this->conn; } /** @@ -691,6 +704,11 @@ public function getTableMetaData($table) $driver = evo()->getDatabase()->getConfig('driver'); if (!empty($table) && is_scalar($table)) { switch ($driver) { + case 'sqlite': + case 'sqlite3': + $tableName = $this->normalizeTableName($table); + $sql = 'PRAGMA table_info(' . $tableName . ')'; + break; case 'pgsql': $sql = " SELECT * FROM information_schema.columns WHERE table_name = '" . $table . "';"; break; @@ -701,6 +719,14 @@ public function getTableMetaData($table) if ($ds = $this->query($sql)) { while ($row = $this->getRow($ds)) { switch ($driver) { + case 'sqlite': + case 'sqlite3': + $fieldName = $row['name']; + $metadata[$fieldName] = [ + 'Field' => $row['name'], + 'Type' => $row['type'], + ]; + continue 2; case 'pgsql': $fieldName = $row['column_name']; break; @@ -773,6 +799,44 @@ public function rollback() public function optimize($table_name) { + $connection = DB::connection(); + $driver = $connection->getConfig('driver'); + + if (in_array($driver, ['sqlite', 'sqlite3'], true)) { + if ($connection->getPdo()->inTransaction()) { + evo()->logEvent( + 0, + 1, + 'VACUUM skipped: active transaction detected.', + 'Database::optimize' + ); + return; + } + + DB::statement('VACUUM'); + return; + } + DB::statement('OPTIMIZE TABLE ' . $table_name); } + + protected function applySqlitePragmas(Connection $connection): void + { + $driver = $connection->getConfig('driver'); + if ($this->sqlitePragmaApplied || !in_array($driver, ['sqlite', 'sqlite3'], true)) { + return; + } + + $connection->statement('PRAGMA foreign_keys = ON;'); + $connection->statement('PRAGMA busy_timeout = 5000;'); + $this->sqlitePragmaApplied = true; + } + + protected function normalizeTableName($table): string + { + $table = $this->replaceFullTableName($table); + $table = str_replace(['`', '"'], '', $table); + + return '"' . $table . '"'; + } } diff --git a/core/src/Legacy/ManagerApi.php b/core/src/Legacy/ManagerApi.php index 704d8cf64c..54099fcd70 100644 --- a/core/src/Legacy/ManagerApi.php +++ b/core/src/Legacy/ManagerApi.php @@ -370,14 +370,11 @@ public function saveLastUserSetting($settings, $val = '') } foreach ($settings as $key => $val) { - $f = []; - $f['user'] = $_SESSION['mgrInternalKey']; - $f['setting_name'] = '_LAST_' . $key; - $f['setting_value'] = $val; - $f = $modx->getDatabase()->escape($f); - $f = "(`" . implode("`, `", array_keys($f)) . "`) VALUES('" . implode("', '", array_values($f)) . "')"; - $f .= " ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"; - $modx->getDatabase()->insert($f, $modx->getDatabase()->getFullTableName('user_settings')); + $data = [ + 'user' => $_SESSION['mgrInternalKey'], + 'setting_name' => '_LAST_' . $key, + ]; + UserSetting::query()->updateOrCreate($data, ['setting_value' => $val]); } } } diff --git a/core/src/Legacy/mgrResources.php b/core/src/Legacy/mgrResources.php index 703092e360..f1c6ecd0db 100644 --- a/core/src/Legacy/mgrResources.php +++ b/core/src/Legacy/mgrResources.php @@ -108,13 +108,14 @@ public function queryResources($resourceTable, $nameField = 'name') if ($resourceTable === 'site_tmplvars') { $tvsql = 'site_tmplvars.caption, '; $tvjoin = 'LEFT JOIN ' . $modx->getDatabase()->getFullTableName('site_tmplvar_templates') . ' AS stt ON site_tmplvars.id=stt.tmplvarid GROUP BY site_tmplvars.id,reltpl'; - $sttfield = 'IF(stt.templateid,1,0) AS reltpl,'; + // SQLite-safe replacement for MySQL IF + $sttfield = 'CASE WHEN stt.templateid IS NULL THEN 0 ELSE 1 END AS reltpl,'; } else $sttfield = ''; $selectableTemplates = $resourceTable === 'site_templates' ? "{$resourceTable}.selectable, " : ""; $rs = $modx->getDatabase()->select( - "{$sttfield} {$pluginsql} {$tvsql} {$resourceTable}.{$nameField} as name, {$resourceTable}.id, {$resourceTable}.description, {$resourceTable}.locked, {$selectableTemplates}IF(isnull(categories.category),'{$_lang['no_category']}',categories.category) as category, categories.id as catid", + "{$sttfield} {$pluginsql} {$tvsql} {$resourceTable}.{$nameField} as name, {$resourceTable}.id, {$resourceTable}.description, {$resourceTable}.locked, {$selectableTemplates}CASE WHEN categories.category IS NULL THEN '{$_lang['no_category']}' ELSE categories.category END as category, categories.id as catid", $modx->getDatabase()->getFullTableName($resourceTable) . " AS {$resourceTable} LEFT JOIN " . $modx->getDatabase()->getFullTableName('categories') . " AS categories ON {$resourceTable}.category = categories.id {$tvjoin}", "", diff --git a/core/src/ManagerTheme.php b/core/src/ManagerTheme.php index b9a32ee694..51e17cbee4 100644 --- a/core/src/ManagerTheme.php +++ b/core/src/ManagerTheme.php @@ -466,22 +466,36 @@ public function getItemId() } public function getActionId() - { - // OK, let's retrieve the action directive from the request - $option = ['min_range' => 1, 'max_range' => 2000]; - if (isset($_GET['a']) && isset($_POST['a'])) { - $this->alertAndQuit('error_double_action'); - } elseif (isset($_GET['a'])) { - $action = (int)filter_input(INPUT_GET, 'a', FILTER_VALIDATE_INT, $option); - } elseif (isset($_POST['a'])) { - $action = (int)filter_input(INPUT_POST, 'a', FILTER_VALIDATE_INT, $option); - } else { - $action = null; - } +{ + // OK, let's retrieve the action directive from the request + // NOTE: Do NOT use filter_input() here. + // In embedded PHP (iOS), filter_input() may not see values + // injected into $_GET / $_POST by prelude code. + + $options = [ + 'options' => [ + 'min_range' => 1, + 'max_range' => 2000, + ], + ]; + + if (isset($_GET['a']) && isset($_POST['a'])) { + $this->alertAndQuit('error_double_action'); + } - return $action; + if (isset($_GET['a'])) { + $value = $_GET['a']; + } elseif (isset($_POST['a'])) { + $value = $_POST['a']; + } else { + return null; } + $action = filter_var($value, FILTER_VALIDATE_INT, $options); + + return ($action === false) ? 0 : (int)$action; +} + public function isAuthManager() { $out = null; @@ -510,16 +524,16 @@ public function isAuthManager() if (defined('EVO_INSTALL_TIME')) { if (isset($_SESSION['mgrValidated'])) { - if (isset($_SESSION['modx.session.created.time'])) { - if ($_SESSION['modx.session.created.time'] < EVO_INSTALL_TIME) { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - if (isset($_COOKIE[session_name()])) { - session_unset(); - @session_destroy(); - } - header('HTTP/1.0 307 Redirect'); - header('Location: ' . MODX_MANAGER_URL . 'index.php?installGoingOn=2'); + $createdKey = 'evo.session.created.time'; + $createdAt = $_SESSION[$createdKey] ?? null; + if ($createdAt !== null && $createdAt < EVO_INSTALL_TIME) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + if (isset($_COOKIE[session_name()])) { + session_unset(); + @session_destroy(); } + header('HTTP/1.0 307 Redirect'); + header('Location: ' . MODX_MANAGER_URL . 'index.php?installGoingOn=2'); } } } diff --git a/core/src/Middleware/SessionProxy.php b/core/src/Middleware/SessionProxy.php new file mode 100644 index 0000000000..2b8dc4b01f --- /dev/null +++ b/core/src/Middleware/SessionProxy.php @@ -0,0 +1,24 @@ +getParentIdColumn(); $positionColumn = $this->getPositionColumn(); @@ -1896,12 +1896,12 @@ protected function prepareTreeQueryColumns(array $columns) * Saves models from the given attributes array. * * @param array $tree - * @param SiteContent $parent + * @param SiteContent|null $parent * * @return Collection * @throws Throwable */ - public static function createFromArray(array $tree, SiteContent $parent = null) + public static function createFromArray(array $tree, ?SiteContent $parent = null) { $entities = []; @@ -2158,10 +2158,13 @@ public function scopeTvFilter($query, $filters = '', $outerSep = ';', $innerSep case ($cast == 'UNSIGNED'): case ($cast == 'SIGNED'): case (strpos($cast, 'DECIMAL') !== false): + $numericCast = (in_array(evo()->getDatabase()->getConfig('driver'), ['sqlite', 'sqlite3'], true)) + ? 'INTEGER' + : $cast; if ($type == 'tvd') { - $query = $query->whereRaw("CAST(IFNULL(`" . $prefix . "tv_" . $tvname . "`.`value`, `" . $prefix . "tvd_" . $tvname . "`.`default_text`) AS " . $cast . " ) " . $op . " " . $value); + $query = $query->whereRaw("CAST(IFNULL(`" . $prefix . "tv_" . $tvname . "`.`value`, `" . $prefix . "tvd_" . $tvname . "`.`default_text`) AS " . $numericCast . " ) " . $op . " " . $value); } else { - $query = $query->whereRaw("CAST(`" . $prefix . 'tv_' . $tvname . "`.`value` AS " . $cast . " ) " . $op . " " . $value); + $query = $query->whereRaw("CAST(`" . $prefix . 'tv_' . $tvname . "`.`value` AS " . $numericCast . " ) " . $op . " " . $value); } break; default: @@ -2182,6 +2185,11 @@ public function scopeTvOrderBy($query, $orderBy = '', $sep = ':') $tvname = $part[0]; $sortDir = !empty($part[1]) ? $part[1] : 'desc'; $cast = !empty($part[2]) ? $part[2] : ''; + $driver = evo()->getDatabase()->getConfig('driver'); + $castType = $cast; + if (in_array($driver, ['sqlite', 'sqlite3'], true) && $castType !== '') { + $castType = 'INTEGER'; + } $withDefaults = false; if (strpos($tvname, $sep) !== false) { list($tvname, $withDefaults) = explode($sep, $tvname, 2); @@ -2192,13 +2200,13 @@ public function scopeTvOrderBy($query, $orderBy = '', $sep = ':') $field = DB::Raw("IFNULL(`" . $prefix . "tv_" . $tvname . "`.`value`, `" . $prefix . "tvd_" . $tvname . "`.`default_text`)"); } switch (true) { - case ($cast == 'UNSIGNED'): - case ($cast == 'SIGNED'): - case (strpos($cast, 'DECIMAL') !== false): + case ($castType == 'UNSIGNED'): + case ($castType == 'SIGNED'): + case (strpos($castType, 'DECIMAL') !== false): if ($withDefaults === false) { - $query = $query->orderByRaw("CAST(`" . $prefix . 'tv_' . $tvname . "`.`value` AS " . $cast . ") " . $sortDir); + $query = $query->orderByRaw("CAST(`" . $prefix . 'tv_' . $tvname . "`.`value` AS " . $castType . ") " . $sortDir); } else { - $query = $query->orderByRaw("CAST(IFNULL(`" . $prefix . "tv_" . $tvname . "`.`value`, `" . $prefix . "tvd_" . $tvname . "`.`default_text`) AS " . $cast . ") " . $sortDir); + $query = $query->orderByRaw("CAST(IFNULL(`" . $prefix . "tv_" . $tvname . "`.`value`, `" . $prefix . "tvd_" . $tvname . "`.`default_text`) AS " . $castType . ") " . $sortDir); } break; default: @@ -2285,7 +2293,7 @@ public static function tvList($docs, $tvList = []) public function scopeOrderByDate($query, $sortDir = 'desc') { - return $query->orderByRaw('IF(pub_date!=0,pub_date,createdon) ' . $sortDir); + return $query->orderByRaw('CASE WHEN pub_date != 0 THEN pub_date ELSE createdon END ' . $sortDir); } public function scopeTagsData($query, $tagsData, $sep = ':', $tagSeparator = ',') diff --git a/core/src/Providers/TracyServiceProvider.php b/core/src/Providers/TracyServiceProvider.php index 35275d2f87..cdc812c38e 100644 --- a/core/src/Providers/TracyServiceProvider.php +++ b/core/src/Providers/TracyServiceProvider.php @@ -5,7 +5,7 @@ use EvolutionCMS\Interfaces\TracyPanel; use Tracy\IBarPanel; -if (session_status() == PHP_SESSION_NONE) { +if (session_status() == PHP_SESSION_NONE && (!defined('EVO_SESSION') || !EVO_SESSION)) { session_start(); } @@ -131,4 +131,4 @@ protected function injectPanel(IBarPanel $panel): void } Debugger::getBar()->addPanel($panel); } -} \ No newline at end of file +} diff --git a/core/src/Tracy/Panels/Request/Panel.php b/core/src/Tracy/Panels/Request/Panel.php index 25fa60e721..cb529bba23 100644 --- a/core/src/Tracy/Panels/Request/Panel.php +++ b/core/src/Tracy/Panels/Request/Panel.php @@ -12,10 +12,10 @@ class Panel extends AbstractPanel protected function getAttributes() { $rows = [ - 'server' => $_SERVER, - 'cookies' => $_COOKIE, - 'get' => $_GET, - 'post' => $_POST + 'server' => $_SERVER ?? [], + 'cookies' => $_COOKIE ?? [], + 'get' => $_GET ?? [], + 'post' => $_POST ?? [] ]; return compact('rows'); } diff --git a/core/src/Tracy/Panels/Routing/Panel.php b/core/src/Tracy/Panels/Routing/Panel.php index 9039cde97e..69bd51b947 100644 --- a/core/src/Tracy/Panels/Routing/Panel.php +++ b/core/src/Tracy/Panels/Routing/Panel.php @@ -20,7 +20,7 @@ protected function getAttributes() if ($this->hasEvolutionCMS() === true) { if ($this->evolution->isBackend()) { - $action = Arr::get($_REQUEST, 'a'); + $action = Arr::get($_REQUEST ?? [], 'a'); if ($action !== null) { $rows['route'] = 'action: ' . $action; } else { @@ -52,8 +52,8 @@ protected function getAttributes() } } } else { - $rows['uri'] = empty(Arr::get($_SERVER, 'HTTP_HOST')) === true ? - 404 : Arr::get($_SERVER, 'REQUEST_URI'); + $rows['uri'] = empty(Arr::get($_SERVER ?? [], 'HTTP_HOST')) === true ? + 404 : Arr::get($_SERVER ?? [], 'REQUEST_URI'); } return compact('rows'); } diff --git a/core/src/Tracy/Panels/Session/Panel.php b/core/src/Tracy/Panels/Session/Panel.php index 7cea1ac9a3..49bb27e446 100644 --- a/core/src/Tracy/Panels/Session/Panel.php +++ b/core/src/Tracy/Panels/Session/Panel.php @@ -19,7 +19,7 @@ protected function getAttributes() } if (session_status() === PHP_SESSION_ACTIVE) { $rows['sessionId'] = session_id(); - $rows['data'] = $_SESSION; + $rows['data'] = $_SESSION ?? []; } return compact('rows'); } diff --git a/index.php b/index.php index 00991f6424..a3235eedaf 100644 --- a/index.php +++ b/index.php @@ -135,6 +135,10 @@ $evo->dumpPlugins = false; $evo->mstart = $mstart; +if (defined('EVO_SESSION') && EVO_SESSION && defined('MODX_API_MODE') && MODX_API_MODE) { + \EvoSessionProxy::init(); +} + // Debugging mode: $evo->stopOnNotice = false; diff --git a/manager/index.php b/manager/index.php index 366d413b4d..796eb21022 100755 --- a/manager/index.php +++ b/manager/index.php @@ -136,6 +136,10 @@ // initiate the content manager class $modx = evo(); $modx->mstart = $mstart; +$useLaravelSession = defined('EVO_SESSION') && EVO_SESSION; +if ($useLaravelSession) { + \EvoSessionProxy::init(); +} $modx->sid = session_id(); //$settings = $modx->allConfig(); diff --git a/manager/media/style/default/css/custom.css b/manager/media/style/default/css/custom.css index 1430291725..0d87696cc2 100644 --- a/manager/media/style/default/css/custom.css +++ b/manager/media/style/default/css/custom.css @@ -210,7 +210,7 @@ ul.breadcrumbs span::after { margin-left: 2em; font-family: 'Font Awesome 5 Free /* [ SEARCHBAR ] */ .searchbar input[type=text] { max-width: 10rem } /* [ WIDGETS ] */ -.widgets .card { margin-bottom: 1rem; margin-right: 0.6rem; border-width: 0; border-radius: 0; background-color: #fff; -webkit-transition: box-shadow .35s; transition: box-shadow .35s; box-shadow: 0 0.1rem 0.3rem 0 rgba(0, 0, 0, 0.075); } +.widgets .card { margin-bottom: 1rem; margin-right: 0.6rem; border-width: 0; border-radius: 10px; background-color: #fff; -webkit-transition: box-shadow .35s; transition: box-shadow .35s; box-shadow: 0 0.1rem 0.3rem 0 rgba(0, 0, 0, 0.075); } .widgets .card:hover { box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.07); } .widgets .card-header { padding: .5rem 1rem; font-size: .9rem; color: #616a73; font-weight: 400; border-radius: 0; border: none; border-bottom: 1px solid rgba(0, 0, 0, .05); text-transform: none; background-color: transparent; border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .widgets .card-header .fa { font-size: .9rem } @@ -232,7 +232,7 @@ ul.breadcrumbs span::after { margin-left: 2em; font-family: 'Font Awesome 5 Free .widgets #welcome .wm_button a .fa { display: inline-block; } -.widgets #welcome .wm_button a .fa + span { display: block; padding: .5em 0; line-height: 1em; font-size: .7rem; word-wrap:normal; } +.widgets #welcome .wm_button a span { display: block; padding: .5em 0; line-height: 1.1; font-size: 12px; word-break: normal; overflow-wrap: normal; hyphens: none; } .widgets #recent_widget .table.data tbody tr:not(:hover) { background-color: #fff } .widgets #recent_widget .table.data tbody tr:nth-child(4n+1):not(:hover) { background-color: #f6f8f8 } @@ -348,7 +348,7 @@ ul.sortableList li.ghost { z-index: 2; box-shadow: 0 0.25rem 1.5rem rgba(0, 0, 0 .darkness .multitv .list li.element input[type="text"], .darkness .multitv .list li.element input[type="password"], .darkness .multitv .list li.element input[type="number"], .darkness .multitv .list li.element textarea { background-color: #202329 !important; border-color: #414449 !important; } .darkness #displayparams { width: calc(100% + 2.5rem); max-width: calc(100% + 2.5rem); margin: 0 -1.25rem !important; } .darkness #content_body { margin: 0 -1.5rem; } -.darkness .card .table.data, .darkness .table td[style="background-color:#ffffff;"] { background: #202329 !important; } +.darkness .card .table.data, .darkness .table td[style="background-color:#ffffff;"] { background: #202329 !important; border-radius: 10px} .darkness .table.data, .darkness .grid { background: #282c34 !important; border: none !important } .darkness .table.data thead, .darkness .grid thead { background: none } .darkness .table.data thead th, .darkness .table.data thead td, .darkness .grid th, .darkness .grid thead td { padding: 0.75rem !important; background-color: #202329; border: none; border-bottom: 1px solid #1f5994; color: #777; text-transform: uppercase; font-size: 0.675rem; } @@ -384,7 +384,7 @@ ul.sortableList li.ghost { z-index: 2; box-shadow: 0 0.25rem 1.5rem rgba(0, 0, 0 .darkness .alert-danger { background-color: #612329; border-color: #612329; color: #da706d; } .darkness .alert-danger b { color: #f7e6e8 !important } .darkness .alert-info { background-color: #193048; border-color: #193048; color: #a3b3bd; } -.darkness .alert-warning { background-color: #6c591d; border-color: #6c591d; color: #c7bd9e; } +.darkness .alert-warning { background-color: #6c591d; border-color: #6c591d; color: #c7bd9e; border-radius: 10px;} .darkness .alert-success { background-color: #3d7544; border-color: #3d7544; color: #c4efc5; } .darkness .loginbox, .darkness .loginbox form { background-color: #202329 } .darkness #mainloader { background-color: transparent !important; } @@ -509,3 +509,4 @@ ul.breadcrumbs a { color: #333; } #news .card-block ul li, #security .card-block ul li { padding: 0.75rem 1rem; border-bottom: 1px solid #ebebeb; } ul.mmTagList li { padding: 2px 8px !important; margin-bottom: 5px !important; border-radius: 3px !important; -webkit-border-radius: 3px !important; background-color: #eaeaea !important; text-decoration: none !important; } ul.mmTagList li.tagSelected { border-radius: 3px !important; -webkit-border-radius: 3px !important; background-color: #5a5e63 !important; } +