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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions htdocs/assets/js/keepalive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* keepalive.js */

document.addEventListener("DOMContentLoaded", function () {
/* 1 minute in milliseconds */
const KEEPALIVE_INTERVAL = 1 * 60 * 1000;

/* Keepalive URL */
const scriptTag = document.getElementById("keepalive-script");
const KEEPALIVE_URL = scriptTag ? scriptTag.dataset.keepaliveUrl : "";

if (!KEEPALIVE_URL) {
return;
}

/* Active request controller */
let inflightController = null;

/* Periodic keepalive */
setInterval(function () {
sendKeepAlive();
}, KEEPALIVE_INTERVAL);

/* Send keepalive request */
function sendKeepAlive() {
// Abort previous request
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",
},
})
.then((response) => {
if (inflightController === controller) {
inflightController = null;
}
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
return response.json();
})
.then(function () {
// Consume response
})
.catch(() => {
if (inflightController === controller) {
inflightController = null;
}
// Ignore errors
});
}
});
66 changes: 66 additions & 0 deletions htdocs/keepalive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
// htdocs/keepalive.php

// Bootstrap ImpressCMS
require_once __DIR__ . "/mainfile.php";

/* GET requests only */
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();
}

/* X-Requested-With header check */
if (xoops_getenv('HTTP_X_REQUESTED_WITH') !== 'XMLHttpRequest') {
http_response_code(400);
header("Content-Type: application/json");
echo json_encode(["error" => "Invalid request"]);
exit();
}

/* Referer validation */
$_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);

/* Authenticated user only */
if (!is_object(icms::$user) || icms::$user->isGuest()) {
http_response_code(403);
header("Content-Type: application/json");
echo json_encode(["error" => "Not authenticated"]);
exit();
}

/* Session rate limit */
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();

/* 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");

/* JSON response */
header("Content-Type: application/json");
echo json_encode(["status" => "ok"]);
exit();
46 changes: 46 additions & 0 deletions htdocs/plugins/preloads/keepalive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
// preload/keepalive.php

defined("ICMS_ROOT_PATH") || die("ImpressCMS root path not defined");

class IcmsPreloadKeepalive extends icms_preload_Item
{
public function eventBeforeFooter()
{
global $xoTheme;
/* ------------------------------------------------------------------
* 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().
if (
Comment thread
fiammybe marked this conversation as resolved.
!isset(icms::$user) || // no user object yet
!is_object(icms::$user) || // defensive – just in case
icms::$user->isGuest() // guest users are ignored
) {
return;
}

/* ------------------------------------------------------------------
* 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
);
}

public function eventAdminHeader()
{
$this->eventBeforeFooter();
}
}