From 46c4af7f77adac264ab200203d59227274f05f92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:04:20 +0000 Subject: [PATCH 01/17] Add .github/copilot-instructions.md for Copilot coding agent Co-authored-by: fiammybe <3736946+fiammybe@users.noreply.github.com> --- .github/copilot-instructions.md | 143 ++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..45dcfd92337a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,143 @@ +# ImpressCMS - Copilot Coding Agent Instructions + +## Repository Overview + +ImpressCMS is a community-developed PHP Content Management System (CMS) focused on speed, multi-language support, and security. It is built on PHP (requires 7.4+, supports up to 8.4) and MySQL/MariaDB. + +- **Repository Size**: ~82MB with ~1,535 PHP files +- **Language**: PHP (primary), JavaScript, CSS +- **Framework**: Custom MVC-style architecture with modules, plugins, and themes +- **Database**: MySQL/MariaDB (PDO connection only) +- **License**: GPL 2.0 + +## Project Structure + +``` +/ +├── .github/workflows/ # GitHub Actions (apigen.yml, generate-diff-zip.yml) +├── docs/ # Documentation (changelog, license) +├── extras/plugins/ # Extra plugin components +├── htdocs/ # Web root - MAIN APPLICATION CODE +│ ├── editors/ # Rich text editors (CKEditor, etc.) +│ ├── images/ # System images and icons +│ ├── include/ # Core includes and functions +│ │ ├── common.php # Main bootstrap file +│ │ ├── constants.php # System constants +│ │ ├── functions.php # Helper functions +│ │ └── version.php # Version info (ICMS_VERSION_NAME, BUILD) +│ ├── install/ # Installation wizard +│ ├── language/ # Language files (english, etc.) +│ ├── libraries/ # Core libraries +│ │ ├── icms/ # ImpressCMS core classes +│ │ ├── icms.php # Main kernel/services manager class +│ │ ├── smarty/ # Template engine +│ │ ├── phpmailer/ # Email library +│ │ └── [others] # Various libraries +│ ├── modules/ # Installable modules +│ │ └── system/ # Core system module +│ │ ├── icms_version.php # Module version config +│ │ └── include/update.php # Database upgrades +│ ├── plugins/ # System plugins +│ │ └── preloads/ # Preload event handlers +│ ├── themes/ # Site themes (iTheme, reflex) +│ ├── index.php # Site entry point +│ └── mainfile.php # Configuration bootstrap +└── upgrade/ # Version upgrade scripts +``` + +## Key Files to Know + +| File | Purpose | +|------|---------| +| `htdocs/mainfile.php` | Site configuration (redirects to install if not configured) | +| `htdocs/include/common.php` | Core bootstrap - loads kernel and services | +| `htdocs/include/version.php` | Version constants (ICMS_VERSION_NAME, ICMS_VERSION_BUILD) | +| `htdocs/libraries/icms.php` | Main kernel class - services and autoloading | +| `htdocs/modules/system/icms_version.php` | System module version definition | +| `htdocs/modules/system/include/update.php` | Database migration scripts | + +## Development Guidelines + +### Code Style +- **Indentation**: Tabs (4 spaces wide) +- **Line endings**: LF (Unix-style) +- **Charset**: UTF-8 +- Follow existing patterns in the codebase +- See `.editorconfig` for detailed formatting rules + +### PHP Standards +- **Minimum PHP**: 7.4 +- **Maximum PHP**: 8.4 +- Use `icms::handler()` for service handlers +- Use `icms::$module`, `icms::$user`, `icms::$db` for global services +- Language constants are defined with `define()` in `htdocs/language/*/` files + +### Important Patterns +```php +// Get a handler +$handler = icms::handler('icms_member'); + +// Access global services +icms::$db // Database connection (PDO) +icms::$user // Current user object +icms::$module // Current module +icms::$config // Configuration service +``` + +## Validation and Quality Checks + +### Code Quality Tools (External CI) +The repository uses external services for code analysis. These run automatically on PR: +- **Code Climate**: Style checking, complexity analysis (`.codeclimate.yml`) +- **Scrutinizer CI**: PHP static analysis, duplication detection (`.scrutinizer.yml`) + +### Manual Validation Steps +1. **Syntax Check**: `php -l htdocs/path/to/file.php` +2. **Test in Browser**: Access the site via web server after changes +3. **Check Admin Panel**: Admin changes require testing at `/admin.php` + +### No Local Test Suite +There is **no PHPUnit or automated test suite** in this repository. Validation is done through: +- PHP syntax checking +- Manual browser testing +- External CI services on pull requests + +## Making Changes + +### Module Changes +- Module configs are in `htdocs/modules/[module]/icms_version.php` +- Database migrations go in `htdocs/modules/[module]/include/update.php` +- Admin pages are in `htdocs/modules/[module]/admin/` + +### Language/Translation Changes +- English is the base language at `htdocs/language/english/` +- Translations managed via Crowdin (`crowdin.yml`) +- Define constants with `define('_CONSTANT_NAME', 'value');` + +### Theme Changes +- Themes are in `htdocs/themes/` +- Use Smarty template syntax (`.html` files) + +### Adding New Features +1. Check if a preload event exists in `htdocs/plugins/preloads/` +2. Consider adding as a module if feature is substantial +3. Update version constants in `htdocs/include/version.php` if needed + +## Common Gotchas + +1. **mainfile.php**: The default mainfile.php redirects to install wizard. A configured site will have database credentials here. + +2. **Database migrations**: Always increment `ICMS_SYSTEM_DBVERSION` in `htdocs/include/version.php` when adding DB changes. + +3. **Global variables**: The codebase uses `icms::$module` (not `$icmsModule` which is deprecated). + +4. **Libraries**: Most libraries in `htdocs/libraries/` are bundled (not via Composer). Check specific library versions before updating. + +5. **Excluded paths**: Code Climate and Scrutinizer exclude many paths (see config files). Changes to excluded paths won't trigger CI issues. + +## Branch Strategy +- Active development branches: `MAJOR.MINOR.x` format (e.g., `2.0.x`) +- Branch from the most similar version branch for fixes/features + +## Trust These Instructions +These instructions are accurate for the current state of the repository. Only search for additional information if you encounter errors or find the instructions incomplete. From 7b576ac73a69d7c4a1ce65f50807e1aae80d9236 Mon Sep 17 00:00:00 2001 From: David Janssens Date: Tue, 25 Nov 2025 14:16:37 +0100 Subject: [PATCH 02/17] Revise development guidelines and PHP version limits Updated development guidelines for code style and PHP standards. --- .github/copilot-instructions.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 45dcfd92337a..5d7534af0d0a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -44,7 +44,7 @@ ImpressCMS is a community-developed PHP Content Management System (CMS) focused │ └── mainfile.php # Configuration bootstrap └── upgrade/ # Version upgrade scripts ``` - +All the subfolders in the /htdocs/libraries folder are external includes, and should not be altered. The only exception is the 'icms' folder, the content of that folder are part of the project ## Key Files to Know | File | Purpose | @@ -59,15 +59,11 @@ ImpressCMS is a community-developed PHP Content Management System (CMS) focused ## Development Guidelines ### Code Style -- **Indentation**: Tabs (4 spaces wide) -- **Line endings**: LF (Unix-style) -- **Charset**: UTF-8 -- Follow existing patterns in the codebase -- See `.editorconfig` for detailed formatting rules +- new and updated code should follow PSR-12 styling rules ### PHP Standards - **Minimum PHP**: 7.4 -- **Maximum PHP**: 8.4 +- **Maximum PHP**: 8.5 - Use `icms::handler()` for service handlers - Use `icms::$module`, `icms::$user`, `icms::$db` for global services - Language constants are defined with `define()` in `htdocs/language/*/` files From 80cf3d1b3a40a3da21dc4c1ecc21a5506241be09 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 17:19:53 +0100 Subject: [PATCH 03/17] Initial commit of keepalive functionality --- htdocs/assets/js/keepalive.js | 54 +++++++++++++++++++++++++++ htdocs/keepalive.php | 19 ++++++++++ htdocs/plugins/preloads/keepalive.php | 30 +++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 htdocs/assets/js/keepalive.js create mode 100644 htdocs/keepalive.php create mode 100644 htdocs/plugins/preloads/keepalive.php diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js new file mode 100644 index 000000000000..35638f5fcac5 --- /dev/null +++ b/htdocs/assets/js/keepalive.js @@ -0,0 +1,54 @@ +/*------------------------------------------------------------- + * keepalive.js – Keeps an ImpressCMS session alive while the + * user is interacting with the page. + *-------------------------------------------------------------*/ + +document.addEventListener("DOMContentLoaded", function () { + /* 5 minutes in milliseconds */ + const KEEPALIVE_INTERVAL = 5 * 60 * 1000; + + /* URL of the keep‑alive endpoint (root‑relative) */ + const KEEPALIVE_URL = "/keepalive.php"; + + /* Timestamp of the last detected user activity */ + let lastActivity = Date.now(); + + /*--- 1️⃣ Initial ping -----------------------------------*/ + sendKeepAlive(); + + /*--- 2️⃣ Periodic ping – only after the idle interval */ + setInterval(function () { + if (Date.now() - lastActivity > KEEPALIVE_INTERVAL) { + sendKeepAlive(); + lastActivity = Date.now(); // reset the counter + } + }, KEEPALIVE_INTERVAL); + + /*--- 3️⃣ Send a GET request to the server -------------*/ + function sendKeepAlive() { + fetch(KEEPALIVE_URL, { + method: "GET", + credentials: "include", + headers: { "X-Requested-With": "XMLHttpRequest" }, + }) + .then((r) => r.json()) + .then((data) => console.log("Keepalive:", data)) + .catch((err) => console.error("Keepalive error:", err)); + } + + /*--- 4️⃣ Detect fetch activity ------------------------*/ + const originalFetch = window.fetch; + window.fetch = function (...args) { + lastActivity = Date.now(); + return originalFetch.apply(this, args); + }; + + /*--- 5️⃣ Detect XHR activity --------------------------*/ + const originalXHROpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (...args) { + this.addEventListener("loadstart", function () { + lastActivity = Date.now(); + }); + return originalXHROpen.apply(this, args); + }; +}); diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php new file mode 100644 index 000000000000..b948bb855283 --- /dev/null +++ b/htdocs/keepalive.php @@ -0,0 +1,19 @@ +isGuest()) { + http_response_code(403); + echo json_encode(["error" => "Not authenticated"]); + exit(); +} + +// --- Send the keep‑alive response ---------------------------- +header("Content-Type: application/json"); +echo json_encode(["status" => "ok"]); +exit(); // Stop here – prevents any debug or other output diff --git a/htdocs/plugins/preloads/keepalive.php b/htdocs/plugins/preloads/keepalive.php new file mode 100644 index 000000000000..f748ac681ec3 --- /dev/null +++ b/htdocs/plugins/preloads/keepalive.php @@ -0,0 +1,30 @@ +isGuest() // guest users are ignored + ) { + return; + } + + /* ------------------------------------------------------------------ + * 2️⃣ Register the external script. + * ------------------------------------------------------------------*/ + // No inline JS – just enqueue the file. + $xoTheme->addScript(ICMS_URL . "/assets/js/keepalive.js"); + } +} From 551515049a16a8810660a16d33f069822c83a56a Mon Sep 17 00:00:00 2001 From: David Janssens Date: Wed, 25 Feb 2026 17:31:41 +0100 Subject: [PATCH 04/17] Delete .github/copilot-instructions.md --- .github/copilot-instructions.md | 139 -------------------------------- 1 file changed, 139 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 5d7534af0d0a..000000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,139 +0,0 @@ -# ImpressCMS - Copilot Coding Agent Instructions - -## Repository Overview - -ImpressCMS is a community-developed PHP Content Management System (CMS) focused on speed, multi-language support, and security. It is built on PHP (requires 7.4+, supports up to 8.4) and MySQL/MariaDB. - -- **Repository Size**: ~82MB with ~1,535 PHP files -- **Language**: PHP (primary), JavaScript, CSS -- **Framework**: Custom MVC-style architecture with modules, plugins, and themes -- **Database**: MySQL/MariaDB (PDO connection only) -- **License**: GPL 2.0 - -## Project Structure - -``` -/ -├── .github/workflows/ # GitHub Actions (apigen.yml, generate-diff-zip.yml) -├── docs/ # Documentation (changelog, license) -├── extras/plugins/ # Extra plugin components -├── htdocs/ # Web root - MAIN APPLICATION CODE -│ ├── editors/ # Rich text editors (CKEditor, etc.) -│ ├── images/ # System images and icons -│ ├── include/ # Core includes and functions -│ │ ├── common.php # Main bootstrap file -│ │ ├── constants.php # System constants -│ │ ├── functions.php # Helper functions -│ │ └── version.php # Version info (ICMS_VERSION_NAME, BUILD) -│ ├── install/ # Installation wizard -│ ├── language/ # Language files (english, etc.) -│ ├── libraries/ # Core libraries -│ │ ├── icms/ # ImpressCMS core classes -│ │ ├── icms.php # Main kernel/services manager class -│ │ ├── smarty/ # Template engine -│ │ ├── phpmailer/ # Email library -│ │ └── [others] # Various libraries -│ ├── modules/ # Installable modules -│ │ └── system/ # Core system module -│ │ ├── icms_version.php # Module version config -│ │ └── include/update.php # Database upgrades -│ ├── plugins/ # System plugins -│ │ └── preloads/ # Preload event handlers -│ ├── themes/ # Site themes (iTheme, reflex) -│ ├── index.php # Site entry point -│ └── mainfile.php # Configuration bootstrap -└── upgrade/ # Version upgrade scripts -``` -All the subfolders in the /htdocs/libraries folder are external includes, and should not be altered. The only exception is the 'icms' folder, the content of that folder are part of the project -## Key Files to Know - -| File | Purpose | -|------|---------| -| `htdocs/mainfile.php` | Site configuration (redirects to install if not configured) | -| `htdocs/include/common.php` | Core bootstrap - loads kernel and services | -| `htdocs/include/version.php` | Version constants (ICMS_VERSION_NAME, ICMS_VERSION_BUILD) | -| `htdocs/libraries/icms.php` | Main kernel class - services and autoloading | -| `htdocs/modules/system/icms_version.php` | System module version definition | -| `htdocs/modules/system/include/update.php` | Database migration scripts | - -## Development Guidelines - -### Code Style -- new and updated code should follow PSR-12 styling rules - -### PHP Standards -- **Minimum PHP**: 7.4 -- **Maximum PHP**: 8.5 -- Use `icms::handler()` for service handlers -- Use `icms::$module`, `icms::$user`, `icms::$db` for global services -- Language constants are defined with `define()` in `htdocs/language/*/` files - -### Important Patterns -```php -// Get a handler -$handler = icms::handler('icms_member'); - -// Access global services -icms::$db // Database connection (PDO) -icms::$user // Current user object -icms::$module // Current module -icms::$config // Configuration service -``` - -## Validation and Quality Checks - -### Code Quality Tools (External CI) -The repository uses external services for code analysis. These run automatically on PR: -- **Code Climate**: Style checking, complexity analysis (`.codeclimate.yml`) -- **Scrutinizer CI**: PHP static analysis, duplication detection (`.scrutinizer.yml`) - -### Manual Validation Steps -1. **Syntax Check**: `php -l htdocs/path/to/file.php` -2. **Test in Browser**: Access the site via web server after changes -3. **Check Admin Panel**: Admin changes require testing at `/admin.php` - -### No Local Test Suite -There is **no PHPUnit or automated test suite** in this repository. Validation is done through: -- PHP syntax checking -- Manual browser testing -- External CI services on pull requests - -## Making Changes - -### Module Changes -- Module configs are in `htdocs/modules/[module]/icms_version.php` -- Database migrations go in `htdocs/modules/[module]/include/update.php` -- Admin pages are in `htdocs/modules/[module]/admin/` - -### Language/Translation Changes -- English is the base language at `htdocs/language/english/` -- Translations managed via Crowdin (`crowdin.yml`) -- Define constants with `define('_CONSTANT_NAME', 'value');` - -### Theme Changes -- Themes are in `htdocs/themes/` -- Use Smarty template syntax (`.html` files) - -### Adding New Features -1. Check if a preload event exists in `htdocs/plugins/preloads/` -2. Consider adding as a module if feature is substantial -3. Update version constants in `htdocs/include/version.php` if needed - -## Common Gotchas - -1. **mainfile.php**: The default mainfile.php redirects to install wizard. A configured site will have database credentials here. - -2. **Database migrations**: Always increment `ICMS_SYSTEM_DBVERSION` in `htdocs/include/version.php` when adding DB changes. - -3. **Global variables**: The codebase uses `icms::$module` (not `$icmsModule` which is deprecated). - -4. **Libraries**: Most libraries in `htdocs/libraries/` are bundled (not via Composer). Check specific library versions before updating. - -5. **Excluded paths**: Code Climate and Scrutinizer exclude many paths (see config files). Changes to excluded paths won't trigger CI issues. - -## Branch Strategy -- Active development branches: `MAJOR.MINOR.x` format (e.g., `2.0.x`) -- Branch from the most similar version branch for fixes/features - -## Trust These Instructions -These instructions are accurate for the current state of the repository. Only search for additional information if you encounter errors or find the instructions incomplete. From cda8977b5ff90a566d155de7bc23e131e499d42c Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 20:50:47 +0100 Subject: [PATCH 05/17] implment no-cache headers --- htdocs/keepalive.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php index b948bb855283..681ca9929c87 100644 --- a/htdocs/keepalive.php +++ b/htdocs/keepalive.php @@ -1,19 +1,31 @@ "Not authenticated"]); + exit(); +} -// --- Optional: make sure the user is logged in ---------------- +// Reject guests – they don’t need a keep‑alive if (icms::$user->isGuest()) { http_response_code(403); + header("Content-Type: application/json"); echo json_encode(["error" => "Not authenticated"]); exit(); } -// --- Send the keep‑alive response ---------------------------- +/* ----- 2 Send explicit no‑cache headers ------------- */ +header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); +header("Pragma: no-cache"); +header("Expires: 0"); + +/* ----- 3 Return the stable JSON payload ------------- */ header("Content-Type: application/json"); echo json_encode(["status" => "ok"]); -exit(); // Stop here – prevents any debug or other output +exit(); From 0249de9df5d66b3f5a38e5e16f0ddebfd92c7684 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 22:48:18 +0100 Subject: [PATCH 06/17] fix intervals not happening at 5-minute intervals in some cases --- htdocs/assets/js/keepalive.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index 35638f5fcac5..b002a6d84a9f 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -7,43 +7,52 @@ document.addEventListener("DOMContentLoaded", function () { /* 5 minutes in milliseconds */ const KEEPALIVE_INTERVAL = 5 * 60 * 1000; - /* URL of the keep‑alive endpoint (root‑relative) */ - const KEEPALIVE_URL = "/keepalive.php"; + /* ----- Grab the URL that the preload injected ---------------*/ + const scriptTag = document.getElementById("keepalive-script"); + const KEEPALIVE_URL = + scriptTag && scriptTag.dataset.keepaliveUrl + ? scriptTag.dataset.keepaliveUrl + : "/keepalive.php"; // graceful fallback for older installations /* Timestamp of the last detected user activity */ let lastActivity = Date.now(); - /*--- 1️⃣ Initial ping -----------------------------------*/ + /*--- Initial ping -----------------------------------*/ sendKeepAlive(); - /*--- 2️⃣ Periodic ping – only after the idle interval */ + /*--- Periodic ping – only after the idle interval */ setInterval(function () { - if (Date.now() - lastActivity > KEEPALIVE_INTERVAL) { + if (Date.now() - lastActivity >= KEEPALIVE_INTERVAL) { sendKeepAlive(); lastActivity = Date.now(); // reset the counter } }, KEEPALIVE_INTERVAL); - /*--- 3️⃣ Send a GET request to the server -------------*/ + /*--- Send a GET request to the server -------------*/ function sendKeepAlive() { fetch(KEEPALIVE_URL, { method: "GET", credentials: "include", - headers: { "X-Requested-With": "XMLHttpRequest" }, + /* ----- 4️⃣ Tell the browser not to cache the request -----*/ + cache: "no-store", + headers: { + "X-Requested-With": "XMLHttpRequest", + "Cache-Control": "no-store", // extra safety for HTTP‑level caching + }, }) .then((r) => r.json()) .then((data) => console.log("Keepalive:", data)) .catch((err) => console.error("Keepalive error:", err)); } - /*--- 4️⃣ Detect fetch activity ------------------------*/ + /*--- Detect fetch activity ------------------------*/ const originalFetch = window.fetch; window.fetch = function (...args) { lastActivity = Date.now(); return originalFetch.apply(this, args); }; - /*--- 5️⃣ Detect XHR activity --------------------------*/ + /*--- Detect XHR activity --------------------------*/ const originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...args) { this.addEventListener("loadstart", function () { From 6aca7a7f8a931b444ff169fcca54fe89bed7c07e Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 22:59:05 +0100 Subject: [PATCH 07/17] Add DOM listeners for keepalive activity --- htdocs/assets/js/keepalive.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index b002a6d84a9f..cdba3bc5fdcb 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -44,13 +44,21 @@ document.addEventListener("DOMContentLoaded", function () { .then((data) => console.log("Keepalive:", data)) .catch((err) => console.error("Keepalive error:", err)); } - - /*--- Detect fetch activity ------------------------*/ - const originalFetch = window.fetch; - window.fetch = function (...args) { + function markActivity() { lastActivity = Date.now(); - return originalFetch.apply(this, args); - }; + } + + /*--- Detect user activity via DOM events ---------*/ + document.addEventListener("pointerdown", markActivity, { passive: true }); + document.addEventListener("keydown", markActivity); + document.addEventListener("scroll", markActivity, { passive: true }); + document.addEventListener("focus", markActivity); + /*--- Question : do we want the keepalive to pauze when the page is hidden? ---------*/ + document.addEventListener("visibilitychange", function () { + if (!document.hidden) { + markActivity(); + } + }); /*--- Detect XHR activity --------------------------*/ const originalXHROpen = XMLHttpRequest.prototype.open; From 38602c0caabc381eed343c442117a418c817d45e Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:01:39 +0100 Subject: [PATCH 08/17] remove emoji numerals --- htdocs/plugins/preloads/keepalive.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/htdocs/plugins/preloads/keepalive.php b/htdocs/plugins/preloads/keepalive.php index f748ac681ec3..89087cf4cee1 100644 --- a/htdocs/plugins/preloads/keepalive.php +++ b/htdocs/plugins/preloads/keepalive.php @@ -9,7 +9,7 @@ public function eventBeforeFooter() { global $xoTheme; /* ------------------------------------------------------------------ - * 1️⃣ Make sure a user object exists and is not a guest. + * Make sure a user object exists and is not a guest. * ------------------------------------------------------------------*/ // The core may still be booting, so icms::$user can be null. // We guard against that before calling isGuest(). @@ -22,9 +22,20 @@ public function eventBeforeFooter() } /* ------------------------------------------------------------------ - * 2️⃣ Register the external script. - * ------------------------------------------------------------------*/ - // No inline JS – just enqueue the file. - $xoTheme->addScript(ICMS_URL . "/assets/js/keepalive.js"); + * Register the external script, attaching a unique id + * and the keep‑alive endpoint as a data attribute. + */ + $keepaliveUrl = ICMS_URL . "/keepalive.php"; + + $xoTheme->addScript( + "assets/js/keepalive.js", + [ + "id" => "keepalive-script", + "data-keepalive-url" => $keepaliveUrl, + ], + "", // no inline content + "module", + 0, // default weight + ); } } From 41105f668642eeedeb54e31b63832e86817d9fe2 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:03:05 +0100 Subject: [PATCH 09/17] remove double code --- htdocs/keepalive.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php index 681ca9929c87..4381fba75acb 100644 --- a/htdocs/keepalive.php +++ b/htdocs/keepalive.php @@ -5,15 +5,7 @@ require_once __DIR__ . "/mainfile.php"; /* ----- 1 Ensure icms::$user is set and not null ------------- */ -if (!isset(icms::$user) || !is_object(icms::$user)) { - http_response_code(403); - header("Content-Type: application/json"); - echo json_encode(["error" => "Not authenticated"]); - exit(); -} - -// Reject guests – they don’t need a keep‑alive -if (icms::$user->isGuest()) { +if (!is_object(icms::$user) || icms::$user->isGuest()) { http_response_code(403); header("Content-Type: application/json"); echo json_encode(["error" => "Not authenticated"]); From f77c7ced89169f98a82112b523bd3dea066bb17e Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:04:12 +0100 Subject: [PATCH 10/17] we really, really, really don't want to be cached --- htdocs/keepalive.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php index 4381fba75acb..0550eb294a6c 100644 --- a/htdocs/keepalive.php +++ b/htdocs/keepalive.php @@ -14,6 +14,7 @@ /* ----- 2 Send explicit no‑cache headers ------------- */ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); +header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Expires: 0"); From 084016790c48df2df9786dfd58edcf231c2ae247 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:06:40 +0100 Subject: [PATCH 11/17] ensure the listener is only attached once per XHR instance --- htdocs/assets/js/keepalive.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index cdba3bc5fdcb..a7aa52751df8 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -47,7 +47,7 @@ document.addEventListener("DOMContentLoaded", function () { function markActivity() { lastActivity = Date.now(); } - + /*--- Detect user activity via DOM events ---------*/ document.addEventListener("pointerdown", markActivity, { passive: true }); document.addEventListener("keydown", markActivity); @@ -63,9 +63,13 @@ document.addEventListener("DOMContentLoaded", function () { /*--- Detect XHR activity --------------------------*/ const originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...args) { - this.addEventListener("loadstart", function () { - lastActivity = Date.now(); - }); + // Attach the listener only once per XHR instance to avoid accumulation + if (!this.__keepaliveLoadstartAttached) { + this.__keepaliveLoadstartAttached = true; + this.addEventListener("loadstart", function () { + lastActivity = Date.now(); + }); + } return originalXHROpen.apply(this, args); }; }); From d850d6bb9f85a491bb554a11f8f96af941762798 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:16:38 +0100 Subject: [PATCH 12/17] make the keepalive work also in the ACP --- htdocs/plugins/preloads/keepalive.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/htdocs/plugins/preloads/keepalive.php b/htdocs/plugins/preloads/keepalive.php index 89087cf4cee1..9962189be311 100644 --- a/htdocs/plugins/preloads/keepalive.php +++ b/htdocs/plugins/preloads/keepalive.php @@ -24,7 +24,7 @@ public function eventBeforeFooter() /* ------------------------------------------------------------------ * Register the external script, attaching a unique id * and the keep‑alive endpoint as a data attribute. - */ + */ $keepaliveUrl = ICMS_URL . "/keepalive.php"; $xoTheme->addScript( @@ -38,4 +38,9 @@ public function eventBeforeFooter() 0, // default weight ); } + + public function eventAdminBeforeFooter() + { + $this->eventBeforeFooter(); + } } From 215c7ec5a5dd0abc205ee38a9e684d66f191d245 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:19:28 +0100 Subject: [PATCH 13/17] change to eventAdminHeader for ACP inclusion --- htdocs/plugins/preloads/keepalive.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/plugins/preloads/keepalive.php b/htdocs/plugins/preloads/keepalive.php index 9962189be311..73f597e9ca25 100644 --- a/htdocs/plugins/preloads/keepalive.php +++ b/htdocs/plugins/preloads/keepalive.php @@ -38,9 +38,9 @@ public function eventBeforeFooter() 0, // default weight ); } - - public function eventAdminBeforeFooter() + + public function eventAdminHeader() { - $this->eventBeforeFooter(); + $this->eventBeforeFooter(); } } From b6e927a64642601deffc2d6e81426a7a135b6576 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 25 Feb 2026 23:32:04 +0100 Subject: [PATCH 14/17] handle errors with the XMLHTTPRequest gracefully --- htdocs/assets/js/keepalive.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index a7aa52751df8..7bfb674837c9 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -33,17 +33,26 @@ document.addEventListener("DOMContentLoaded", function () { fetch(KEEPALIVE_URL, { method: "GET", credentials: "include", - /* ----- 4️⃣ Tell the browser not to cache the request -----*/ cache: "no-store", headers: { "X-Requested-With": "XMLHttpRequest", "Cache-Control": "no-store", // extra safety for HTTP‑level caching }, }) - .then((r) => r.json()) - .then((data) => console.log("Keepalive:", data)) - .catch((err) => console.error("Keepalive error:", err)); + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) + .then((data) => { + console.log("Keepalive:", data); + }) + .catch((error) => { + console.error("Keepalive error:", error); + }); } + function markActivity() { lastActivity = Date.now(); } From 2dfc449978ee7ee3bf39fdc14938ce553138bfd2 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Wed, 4 Mar 2026 09:56:41 +0100 Subject: [PATCH 15/17] security hardening --- htdocs/assets/js/keepalive.js | 29 +++++++++++---- htdocs/keepalive.php | 70 +++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index 7bfb674837c9..0566fecb4b70 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -17,39 +17,54 @@ document.addEventListener("DOMContentLoaded", function () { /* Timestamp of the last detected user activity */ let lastActivity = Date.now(); + /* Guard against concurrent in-flight requests */ + let inflightController = null; + /*--- Initial ping -----------------------------------*/ sendKeepAlive(); - /*--- Periodic ping – only after the idle interval */ + /*--- Periodic ping – only when the user has been idle for a full interval. + * Active users naturally refresh the session via normal page requests; + * the keepalive is only needed when the tab is open but untouched. */ setInterval(function () { if (Date.now() - lastActivity >= KEEPALIVE_INTERVAL) { sendKeepAlive(); - lastActivity = Date.now(); // reset the counter + lastActivity = Date.now(); // reset so the next interval checks cleanly } }, KEEPALIVE_INTERVAL); /*--- Send a GET request to the server -------------*/ function sendKeepAlive() { + // Abort any previous in-flight request to prevent accumulation + if (inflightController) { + inflightController.abort(); + } + const controller = new AbortController(); + inflightController = controller; + fetch(KEEPALIVE_URL, { method: "GET", credentials: "include", cache: "no-store", + signal: controller.signal, headers: { "X-Requested-With": "XMLHttpRequest", - "Cache-Control": "no-store", // extra safety for HTTP‑level caching + "Cache-Control": "no-store", }, }) .then((response) => { + inflightController = null; if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + throw new Error("HTTP " + response.status); } return response.json(); }) - .then((data) => { - console.log("Keepalive:", data); + .then(function () { + // Response consumed silently; no data exposed to the browser console. }) .catch((error) => { - console.error("Keepalive error:", error); + inflightController = null; + // Suppress AbortError – it is expected when a prior request is cancelled. }); } diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php index 0550eb294a6c..78e96c62938d 100644 --- a/htdocs/keepalive.php +++ b/htdocs/keepalive.php @@ -4,7 +4,50 @@ // Bootstrap ImpressCMS so the session and user object are available require_once __DIR__ . "/mainfile.php"; -/* ----- 1 Ensure icms::$user is set and not null ------------- */ +/* ----- 1 Restrict to GET requests only --------------------------- + * POST, PUT, DELETE, etc. have no purpose here and a simple + * cross-origin HTML form can issue a POST without a CORS preflight. + * ------------------------------------------------------------------ */ +if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + http_response_code(405); + header("Allow: GET"); + header("Content-Type: application/json"); + echo json_encode(["error" => "Method not allowed"]); + exit(); +} + +/* ----- 2 Enforce XHR-only access --------------------------------- + * X-Requested-With is a non-simple CORS header. A cross-origin page + * must pass an OPTIONS preflight to include it; since we do not + * advertise Access-Control-Allow-Headers, the preflight is refused + * and the actual request is never sent by the browser. Checking the + * header server-side makes this protection explicit (OWASP "Custom + * Request Headers" CSRF pattern) and independent of browser CORS. + * ------------------------------------------------------------------ */ +if (xoops_getenv('HTTP_X_REQUESTED_WITH') !== 'XMLHttpRequest') { + http_response_code(400); + header("Content-Type: application/json"); + echo json_encode(["error" => "Invalid request"]); + exit(); +} + +/* ----- 3 Validate referer when present --------------------------- + * We use icms::$security->checkReferer() as the reference, but + * intentionally allow an empty referer to avoid blocking privacy- + * oriented browsers or strict Referrer-Policy configurations. + * We only reject when a referer IS present but comes from a + * different origin. + * ------------------------------------------------------------------ */ +$_keepaliveReferer = xoops_getenv('HTTP_REFERER'); +if ($_keepaliveReferer !== '' && strpos($_keepaliveReferer, ICMS_URL) !== 0) { + http_response_code(403); + header("Content-Type: application/json"); + echo json_encode(["error" => "Invalid request"]); + exit(); +} +unset($_keepaliveReferer); + +/* ----- 4 Ensure icms::$user is set and not a guest -------------- */ if (!is_object(icms::$user) || icms::$user->isGuest()) { http_response_code(403); header("Content-Type: application/json"); @@ -12,13 +55,32 @@ exit(); } -/* ----- 2 Send explicit no‑cache headers ------------- */ +/* ----- 5 Session-based rate limiting ----------------------------- + * Require at least 60 seconds between keepalive calls from the same + * session. This prevents a stolen-cookie holder from artificially + * preventing session expiry and limits DB session-table write load. + * ------------------------------------------------------------------ */ +define('KEEPALIVE_MIN_INTERVAL', 60); +if ( + isset($_SESSION['keepalive_last']) && + (time() - (int) $_SESSION['keepalive_last']) < KEEPALIVE_MIN_INTERVAL +) { + $_keepaliveRetryAfter = KEEPALIVE_MIN_INTERVAL - (time() - (int) $_SESSION['keepalive_last']); + http_response_code(429); + header("Content-Type: application/json"); + header("Retry-After: " . $_keepaliveRetryAfter); + echo json_encode(["error" => "Too many requests"]); + exit(); +} +$_SESSION['keepalive_last'] = time(); + +/* ----- 6 Send security and no-cache headers --------------------- */ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); -header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); header("Expires: 0"); +header("X-Content-Type-Options: nosniff"); -/* ----- 3 Return the stable JSON payload ------------- */ +/* ----- 7 Return the stable JSON payload ------------------------- */ header("Content-Type: application/json"); echo json_encode(["status" => "ok"]); exit(); From 13b66e84645685ec3d54233fa402b192f53712da Mon Sep 17 00:00:00 2001 From: fiammybe Date: Thu, 12 Mar 2026 00:36:27 +0100 Subject: [PATCH 16/17] No activity tracking, just session extension every X interval. Simplify the JS file by removing all that unnecessary complexity --- htdocs/assets/js/keepalive.js | 82 ++++++++++------------------------- 1 file changed, 22 insertions(+), 60 deletions(-) diff --git a/htdocs/assets/js/keepalive.js b/htdocs/assets/js/keepalive.js index 0566fecb4b70..0beb4296fb67 100644 --- a/htdocs/assets/js/keepalive.js +++ b/htdocs/assets/js/keepalive.js @@ -1,41 +1,28 @@ -/*------------------------------------------------------------- - * keepalive.js – Keeps an ImpressCMS session alive while the - * user is interacting with the page. - *-------------------------------------------------------------*/ +/* keepalive.js */ document.addEventListener("DOMContentLoaded", function () { - /* 5 minutes in milliseconds */ - const KEEPALIVE_INTERVAL = 5 * 60 * 1000; + /* 1 minute in milliseconds */ + const KEEPALIVE_INTERVAL = 1 * 60 * 1000; - /* ----- Grab the URL that the preload injected ---------------*/ + /* Keepalive URL */ const scriptTag = document.getElementById("keepalive-script"); - const KEEPALIVE_URL = - scriptTag && scriptTag.dataset.keepaliveUrl - ? scriptTag.dataset.keepaliveUrl - : "/keepalive.php"; // graceful fallback for older installations + const KEEPALIVE_URL = scriptTag ? scriptTag.dataset.keepaliveUrl : ""; - /* Timestamp of the last detected user activity */ - let lastActivity = Date.now(); + if (!KEEPALIVE_URL) { + return; + } - /* Guard against concurrent in-flight requests */ + /* Active request controller */ let inflightController = null; - /*--- Initial ping -----------------------------------*/ - sendKeepAlive(); - - /*--- Periodic ping – only when the user has been idle for a full interval. - * Active users naturally refresh the session via normal page requests; - * the keepalive is only needed when the tab is open but untouched. */ + /* Periodic keepalive */ setInterval(function () { - if (Date.now() - lastActivity >= KEEPALIVE_INTERVAL) { - sendKeepAlive(); - lastActivity = Date.now(); // reset so the next interval checks cleanly - } + sendKeepAlive(); }, KEEPALIVE_INTERVAL); - /*--- Send a GET request to the server -------------*/ + /* Send keepalive request */ function sendKeepAlive() { - // Abort any previous in-flight request to prevent accumulation + // Abort previous request if (inflightController) { inflightController.abort(); } @@ -53,47 +40,22 @@ document.addEventListener("DOMContentLoaded", function () { }, }) .then((response) => { - inflightController = null; + if (inflightController === controller) { + inflightController = null; + } if (!response.ok) { throw new Error("HTTP " + response.status); } return response.json(); }) .then(function () { - // Response consumed silently; no data exposed to the browser console. + // Consume response }) - .catch((error) => { - inflightController = null; - // Suppress AbortError – it is expected when a prior request is cancelled. + .catch(() => { + if (inflightController === controller) { + inflightController = null; + } + // Ignore errors }); } - - function markActivity() { - lastActivity = Date.now(); - } - - /*--- Detect user activity via DOM events ---------*/ - document.addEventListener("pointerdown", markActivity, { passive: true }); - document.addEventListener("keydown", markActivity); - document.addEventListener("scroll", markActivity, { passive: true }); - document.addEventListener("focus", markActivity); - /*--- Question : do we want the keepalive to pauze when the page is hidden? ---------*/ - document.addEventListener("visibilitychange", function () { - if (!document.hidden) { - markActivity(); - } - }); - - /*--- Detect XHR activity --------------------------*/ - const originalXHROpen = XMLHttpRequest.prototype.open; - XMLHttpRequest.prototype.open = function (...args) { - // Attach the listener only once per XHR instance to avoid accumulation - if (!this.__keepaliveLoadstartAttached) { - this.__keepaliveLoadstartAttached = true; - this.addEventListener("loadstart", function () { - lastActivity = Date.now(); - }); - } - return originalXHROpen.apply(this, args); - }; }); From 1fcc9c6bd917c25431c8a642c007ab27b8eb9d89 Mon Sep 17 00:00:00 2001 From: fiammybe Date: Thu, 12 Mar 2026 00:36:56 +0100 Subject: [PATCH 17/17] shortening the documentation --- htdocs/keepalive.php | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/htdocs/keepalive.php b/htdocs/keepalive.php index 78e96c62938d..c81abc0644ef 100644 --- a/htdocs/keepalive.php +++ b/htdocs/keepalive.php @@ -1,13 +1,10 @@ checkReferer() as the reference, but - * intentionally allow an empty referer to avoid blocking privacy- - * oriented browsers or strict Referrer-Policy configurations. - * We only reject when a referer IS present but comes from a - * different origin. - * ------------------------------------------------------------------ */ +/* Referer validation */ $_keepaliveReferer = xoops_getenv('HTTP_REFERER'); if ($_keepaliveReferer !== '' && strpos($_keepaliveReferer, ICMS_URL) !== 0) { http_response_code(403); @@ -47,7 +31,7 @@ } unset($_keepaliveReferer); -/* ----- 4 Ensure icms::$user is set and not a guest -------------- */ +/* Authenticated user only */ if (!is_object(icms::$user) || icms::$user->isGuest()) { http_response_code(403); header("Content-Type: application/json"); @@ -55,11 +39,7 @@ exit(); } -/* ----- 5 Session-based rate limiting ----------------------------- - * Require at least 60 seconds between keepalive calls from the same - * session. This prevents a stolen-cookie holder from artificially - * preventing session expiry and limits DB session-table write load. - * ------------------------------------------------------------------ */ +/* Session rate limit */ define('KEEPALIVE_MIN_INTERVAL', 60); if ( isset($_SESSION['keepalive_last']) && @@ -74,13 +54,13 @@ } $_SESSION['keepalive_last'] = time(); -/* ----- 6 Send security and no-cache headers --------------------- */ +/* Response headers */ header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); header("Pragma: no-cache"); header("Expires: 0"); header("X-Content-Type-Options: nosniff"); -/* ----- 7 Return the stable JSON payload ------------------------- */ +/* JSON response */ header("Content-Type: application/json"); echo json_encode(["status" => "ok"]); exit();