fix(plugins): red de seguridad ante exit() en un gancho y globales del CMS

Tras la revision de codigo de 273c3dd:
- Un exit()/die() dentro de un gancho no es una excepcion y dejaba un cuerpo
  que no era JSON. register_shutdown_function en _pluginLifecycle responde
  {ok:false, error, messages:[lo impreso]} si el gancho no llego al final.
- event/plugin/origin se castean solo si son escalares (un array producia un
  Warning delante del JSON).
- Los ganchos ven CURRENT_USER, menu, action, TABLE_PREFIX y SETTINGS como
  un checkpoint desde addPlugins(); documentado en el contrato.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPL66q9F33YZxbLPUT7KW8
This commit is contained in:
Jordan Diaz
2026-09-12 22:51:56 +00:00
co-authored by Claude Fable 5.1
parent 3565abdd88
commit ea8eb6dbcb
2 changed files with 28 additions and 5 deletions
+21 -4
View File
@@ -412,15 +412,32 @@ function _pluginLifecycle() {
// respeta ficheros personalizados y puede haber entregado este y no aquel. // respeta ficheros personalizados y puede haber entregado este y no aquel.
die(json_encode(["ok" => false, "error" => "El cargador de plugins de esta web no soporta ganchos (cms/lib/plugins/index.php desactualizado)"])); die(json_encode(["ok" => false, "error" => "El cargador de plugins de esta web no soporta ganchos (cms/lib/plugins/index.php desactualizado)"]));
} }
$event = (string) @$datos["event"]; // Red de seguridad: exit()/die() dentro de un gancho no es una excepcion y
$plugin = (string) @$datos["plugin"]; // se saltaria el die(json_encode(...)) de abajo dejando un cuerpo que no es
// JSON. Si al cerrar no hemos respondido, respondemos nosotros.
$GLOBALS["_pluginLifecycleRespondido"] = false;
register_shutdown_function(function () {
if (!empty($GLOBALS["_pluginLifecycleRespondido"])) return;
$salida = "";
while (ob_get_level() > 0) { $salida .= (string) ob_get_clean(); }
echo json_encode(array(
"ok" => false,
"error" => "El gancho ha terminado con exit()/die() sin devolver resultado",
"messages" => $salida !== "" ? array(trim($salida)) : array(),
));
});
$event = is_scalar(@$datos["event"]) ? (string) $datos["event"] : "";
$plugin = is_scalar(@$datos["plugin"]) ? (string) $datos["plugin"] : "";
$origin = is_scalar(@$datos["origin"]) ? (string) $datos["origin"] : "";
$context = array( $context = array(
"origin" => in_array(@$datos["origin"], array("forge", "legacy"), true) ? $datos["origin"] : "desconocido", "origin" => in_array($origin, array("forge", "legacy"), true) ? $origin : "desconocido",
"dropData" => filter_var(@$datos["dropData"], FILTER_VALIDATE_BOOLEAN), "dropData" => filter_var(@$datos["dropData"], FILTER_VALIDATE_BOOLEAN),
// Misma señal de entorno que header.php: el .acai solo existe en las copias locales. // Misma señal de entorno que header.php: el .acai solo existe en las copias locales.
"mode" => file_exists(__DIR__ . "/../../../.acai") ? "local" : "production", "mode" => file_exists(__DIR__ . "/../../../.acai") ? "local" : "production",
); );
die(json_encode(runPluginLifecycle($plugin, $event, $context))); $resultado = runPluginLifecycle($plugin, $event, $context);
$GLOBALS["_pluginLifecycleRespondido"] = true;
die(json_encode($resultado));
} }
function _removeFileRutaSegura($ruta, $esPlugin) { function _removeFileRutaSegura($ruta, $esPlugin) {
+7 -1
View File
@@ -369,6 +369,10 @@ function query($sql)
// simplemente terminar. status.php devuelve semaforos: // simplemente terminar. status.php devuelve semaforos:
// [["clave" => "...", "estado" => "ok|warn|error", "texto" => "..."], ...]. // [["clave" => "...", "estado" => "ok|warn|error", "texto" => "..."], ...].
// Lo que el gancho imprima se recoge como mensaje, nunca sale por la respuesta. // Lo que el gancho imprima se recoge como mensaje, nunca sale por la respuesta.
// El gancho ve $CURRENT_USER, $menu, $action, $TABLE_PREFIX y $SETTINGS como un
// checkpoint; cualquier otro global lo declara el. NUNCA exit()/die(): no es una
// excepcion, se salta el resultado y solo la red de seguridad de handler_ws.php
// evita una respuesta que no sea JSON.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function pluginLifecycleEvents() { function pluginLifecycleEvents() {
return array("install", "uninstall", "enable", "disable", "status"); return array("install", "uninstall", "enable", "disable", "status");
@@ -410,7 +414,9 @@ function installPluginArtifacts($path, &$messages) {
} }
function _includePluginHook($hookFile, $plugin, $pluginDir, $event, $context, $dropData) { function _includePluginHook($hookFile, $plugin, $pluginDir, $event, $context, $dropData) {
// Ambito propio: el gancho no ve las variables de quien lo llama. // Ambito propio: el gancho no ve las variables de quien lo llama, pero si
// los mismos globales del CMS que ve un checkpoint desde addPlugins().
global $CURRENT_USER, $menu, $action, $TABLE_PREFIX, $SETTINGS;
ob_start(); ob_start();
$ret = include $hookFile; $ret = include $hookFile;
$salida = trim((string) ob_get_clean()); $salida = trim((string) ob_get_clean());