diff --git a/cms/lib/handlers/handler_ws.php b/cms/lib/handlers/handler_ws.php index cdb16c7..4c42433 100755 --- a/cms/lib/handlers/handler_ws.php +++ b/cms/lib/handlers/handler_ws.php @@ -30,6 +30,7 @@ if (isset($_REQUEST["action_ws"])){ case "coreUpdate": _coreUpdate(realpath($GLOBALS["APP"]["pluginsdir"]."/../../lib")); break; case "pluginsUpdate": _coreUpdate(realpath($GLOBALS["APP"]["pluginsdir"]),true); break; case "removeFile": _removeFile(); break; + case "pluginLifecycle": _pluginLifecycle(); break; case "clearCache": _clearCache(); break; case "renameFile": _renameFile(); default : die(json_encode(["result" => 0,"error" => "Método no permitido"])); @@ -386,3 +387,29 @@ function _renameFile() { die(json_encode(['success' => file_exists($newFile)])); } + + +function _pluginLifecycle() { + // Fase 0 (2026-09-12): ejecuta los ganchos de ciclo de vida de un plugin + // (install/uninstall/enable/disable/status) DENTRO de la web. Unico punto + // de entrada para Forge y para el CMS legacy. Misma autenticacion que coreUpdate. + CocoWS::setToken(@$_REQUEST["token"]); + if (!CocoWS::validateUploadToken(@$_REQUEST["tokenHash"])) { + die(json_encode(['error' => ['message' => 'Token no válido', 'code' => 403]])); + } + header('Content-Type: application/json'); + if (!function_exists("runPluginLifecycle")) { + // El cargador de plugins de esta web es anterior a los ganchos: el sync + // 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)"])); + } + $event = (string) @$_REQUEST["event"]; + $plugin = (string) @$_REQUEST["plugin"]; + $context = array( + "origin" => in_array(@$_REQUEST["origin"], array("forge", "legacy"), true) ? $_REQUEST["origin"] : "desconocido", + "dropData" => filter_var(@$_REQUEST["dropData"], FILTER_VALIDATE_BOOLEAN), + // Misma señal de entorno que header.php: el .acai solo existe en las copias locales. + "mode" => file_exists(__DIR__ . "/../../../.acai") ? "local" : "production", + ); + die(json_encode(runPluginLifecycle($plugin, $event, $context))); +} diff --git a/cms/lib/init.php b/cms/lib/init.php index a96bb50..109a358 100755 --- a/cms/lib/init.php +++ b/cms/lib/init.php @@ -27,6 +27,14 @@ displaySocketErrors(); if ($SETTINGS['isInstalled']) { connectToMySQL(); } +// Checkpoint `boot_plugins` (2026-09-12): el CMS esta cargado y la BD conectada. +// Los plugins que lo declaren en [checkPoints] se inicializan aqui en cada +// peticion (instalar/reconciliar lo suyo, autodiagnostico). Ningun plugin del +// catalogo lo declaraba al crearlo: no cambia nada hasta que uno lo use. +if (function_exists('addPlugins')) { + $bootPluginsVar = array(); + addPlugins('boot_plugins', $bootPluginsVar); +} function enableErrorReporting() { if (!defined('E_STRICT')) { define('E_STRICT', 2048); } // define E_STRICT for PHP < 5 error_reporting(E_ALL | E_STRICT); // display all errors diff --git a/cms/lib/plugins/index.php b/cms/lib/plugins/index.php index 6d13a90..d91a3dc 100644 --- a/cms/lib/plugins/index.php +++ b/cms/lib/plugins/index.php @@ -75,9 +75,8 @@ function showInstall($path){
Instalando tablas necesarias
}else{?>Error instalando la base de datos
}?> - if (installSchemas($path)){ if ($borro) rename($path."/schemaPresets",$path."/schemaPresets.bck"); ?>Instalando Schemas
}else{?>Error instalando los esquemas
}?> + $mensajesInstall = []; installPluginArtifacts($path, $mensajesInstall); ?> + foreach ($mensajesInstall as $mensajeInstall): ?>=htmlspecialchars($mensajeInstall);?>
endforeach; ?> if ($result==2){?>Recarga para cargar el plugin @@ -356,4 +355,119 @@ function query($sql) } } -?> \ No newline at end of file +// --------------------------------------------------------------------------- +// Ciclo de vida de plugins (fase 0, 2026-09-12). +// +// Ganchos OPCIONALES en la carpeta del plugin: install.php, uninstall.php, +// enable.php, disable.php y status.php. Los ejecuta runPluginLifecycle() +// DENTRO de la web, instale quien instale (Forge o el CMS legacy), a traves de +// action_ws=pluginLifecycle (cms/lib/handlers/handler_ws.php). +// +// Contrato de un gancho: se incluye en un ambito propio con $plugin, $pluginDir, +// $event, $context y $dropData; debe ser IDEMPOTENTE (puede ejecutarse dos veces); +// puede devolver ["ok" => bool, "messages" => [..], "status" => [..]] o +// simplemente terminar. status.php devuelve semaforos: +// [["clave" => "...", "estado" => "ok|warn|error", "texto" => "..."], ...]. +// Lo que el gancho imprima se recoge como mensaje, nunca sale por la respuesta. +// --------------------------------------------------------------------------- +function pluginLifecycleEvents() { + return array("install", "uninstall", "enable", "disable", "status"); +} + +function pluginFolderIsValid($folder) { + return is_string($folder) && $folder !== "" && $folder !== "." && $folder !== ".." + && preg_match('/^[a-zA-Z0-9._-]+$/', $folder) === 1; +} + +function pluginDirResolved($folder) { + // Ruta real del plugin, o false si no existe o se sale de pluginsdir. + $pluginsDir = realpath($GLOBALS["APP"]["pluginsdir"]); + if (!$pluginsDir || !pluginFolderIsValid($folder)) return false; + $dir = realpath($pluginsDir . "/" . $folder); + if (!$dir || !is_dir($dir)) return false; + if (strpos($dir, $pluginsDir . DIRECTORY_SEPARATOR) !== 0) return false; + return $dir; +} + +function installPluginArtifacts($path, &$messages) { + // Lo que hacia showInstall() sin el HTML: install.sql y schemaPresets/ una + // sola vez (se renombran a .bck, como siempre). false si el SQL fallo. + $ok = true; + if (file_exists($path . "/install.sql")) { + if (installSQL($path)) { + @rename($path . "/install.sql", $path . "/install.sql.bck"); + $messages[] = "install.sql ejecutado"; + } else { + $ok = false; + $messages[] = "install.sql ha fallado"; + } + } + if (file_exists($path . "/schemaPresets")) { + installSchemas($path); // renombra schemaPresets -> schemaPresets.bck + $messages[] = "schemaPresets instalados"; + } + return $ok; +} + +function _includePluginHook($hookFile, $plugin, $pluginDir, $event, $context, $dropData) { + // Ambito propio: el gancho no ve las variables de quien lo llama. + ob_start(); + $ret = include $hookFile; + $salida = trim((string) ob_get_clean()); + return array("ret" => $ret, "output" => $salida); +} + +function _pluginCoreStatus($folder, $pluginDir) { + $schemas = getSchemaPlugins(true); + $enabled = !empty($schemas[$folder]["enabled"]); + $pendiente = file_exists($pluginDir . "/install.sql") || file_exists($pluginDir . "/schemaPresets"); + $status = array( + array("clave" => "instalado", "estado" => "ok", "texto" => "Plugin instalado en la web"), + array("clave" => "activado", "estado" => $enabled ? "ok" : "warn", + "texto" => $enabled ? "Plugin activado" : "Plugin desactivado"), + array("clave" => "core_ganchos", "estado" => "ok", "texto" => "El core de esta web soporta ganchos de plugins"), + ); + if ($pendiente) { + $status[] = array("clave" => "instalacion_pendiente", "estado" => "warn", + "texto" => "install.sql o schemaPresets pendientes de ejecutar"); + } + return $status; +} + +function runPluginLifecycle($folder, $event, $context = array()) { + $res = array("ok" => false, "event" => $event, "plugin" => $folder, "ran" => false, + "messages" => array(), "status" => array(), "error" => null); + if (!pluginFolderIsValid($folder)) { $res["error"] = "Nombre de plugin no valido"; return $res; } + if (!in_array($event, pluginLifecycleEvents(), true)) { $res["error"] = "Evento no valido"; return $res; } + $pluginDir = pluginDirResolved($folder); + if (!$pluginDir) { $res["error"] = "El plugin no existe en esta web"; return $res; } + if (!file_exists($pluginDir . "/schema.ini.php")) { $res["error"] = "El plugin no tiene schema.ini.php"; return $res; } + $dropData = !empty($context["dropData"]); + try { + if ($event === "install" && !installPluginArtifacts($pluginDir, $res["messages"])) { + $res["error"] = "install.sql ha fallado"; + return $res; + } + if ($event === "status") $res["status"] = _pluginCoreStatus($folder, $pluginDir); + $hookOk = true; + $hookFile = $pluginDir . "/" . $event . ".php"; + if (file_exists($hookFile)) { + $res["ran"] = true; + $hook = _includePluginHook($hookFile, $folder, $pluginDir, $event, $context, $dropData); + if ($hook["output"] !== "") $res["messages"][] = $hook["output"]; + if (is_array($hook["ret"])) { + if (array_key_exists("ok", $hook["ret"]) && !$hook["ret"]["ok"]) $hookOk = false; + foreach ((array) @$hook["ret"]["messages"] as $m) $res["messages"][] = (string) $m; + foreach ((array) @$hook["ret"]["status"] as $st) if (is_array($st)) $res["status"][] = $st; + if (!empty($hook["ret"]["error"])) $res["error"] = (string) $hook["ret"]["error"]; + } + } + $res["ok"] = $hookOk; + if (!$hookOk && !$res["error"]) $res["error"] = "El gancho " . $event . ".php ha devuelto ok=false"; + } catch (Throwable $e) { + $res["ok"] = false; + $res["error"] = "Excepcion en " . $event . ".php: " . $e->getMessage(); + } + return $res; +} +?> diff --git a/lib/recursos.php b/lib/recursos.php index 75adf09..c654670 100755 --- a/lib/recursos.php +++ b/lib/recursos.php @@ -1,5 +1,13 @@ require_once "variables.php"; +// Checkpoint `post_variables` (2026-09-12): variables.php ya ha corrido. Un plugin +// puede pisar aqui lo que decida la web (p.ej. la cache) antes de resolver la +// pagina. Una sola vez por peticion aunque se cargue desde varios sitios. +if (function_exists("addPlugins") && !defined("POST_VARIABLES_DISPARADO")) { + define("POST_VARIABLES_DISPARADO", true); + $postVariablesVar = array(); + addPlugins("post_variables", $postVariablesVar); +} require_once "FileJoiner.php"; $fileType = "css"; diff --git a/sesion.php b/sesion.php index ff0be4f..74b9596 100755 --- a/sesion.php +++ b/sesion.php @@ -4,6 +4,14 @@ if (session_status() == PHP_SESSION_NONE) { session_start(); } require_once(dirname(__FILE__)."/lib/Module.php"); require_once(dirname(__FILE__)."/lib/Resource.class.php"); require_once(dirname(__FILE__)."/lib/variables.php"); +// Checkpoint `post_variables` (2026-09-12): variables.php ya ha corrido. Un plugin +// puede pisar aqui lo que decida la web (p.ej. la cache) antes de resolver la +// pagina. Una sola vez por peticion aunque se cargue desde varios sitios. +if (function_exists("addPlugins") && !defined("POST_VARIABLES_DISPARADO")) { + define("POST_VARIABLES_DISPARADO", true); + $postVariablesVar = array(); + addPlugins("post_variables", $postVariablesVar); +} require_once(dirname(__FILE__)."/lib/minifier.php"); if (file_exists(dirname(__FILE__)."/hooks/autorequire-files")){ diff --git a/slug.php b/slug.php index 211453d..4991129 100755 --- a/slug.php +++ b/slug.php @@ -5,6 +5,14 @@ if (@$sep[1]){ header("Location: http://".$_SERVER["HTTP_HOST"].$sep[0].""); } require_once "lib/variables.php"; +// Checkpoint `post_variables` (2026-09-12): variables.php ya ha corrido. Un plugin +// puede pisar aqui lo que decida la web (p.ej. la cache) antes de resolver la +// pagina. Una sola vez por peticion aunque se cargue desde varios sitios. +if (function_exists("addPlugins") && !defined("POST_VARIABLES_DISPARADO")) { + define("POST_VARIABLES_DISPARADO", true); + $postVariablesVar = array(); + addPlugins("post_variables", $postVariablesVar); +} $sufijo = ".html"; if (@$_REQUEST["tipo"]=="barra") $sufijo="";