Files
acai-vscode-webbase/cms/lib/plugins/index.php
Jordan DiazandClaude Opus 5.5 701bc3bb8b feat(core): el motor de las tablas nuevas depende de la version del servidor
El cambio anterior (8d92d08) creaba siempre las tablas nuevas en InnoDB. Hay
un servidor de produccion con MariaDB 5.5.68, donde InnoDB no es seguro: no
admite FULLTEXT, limita los indices a 767 bytes y usa filas COMPACT. Con
«Subir core», las tablas nuevas de una web de ese servidor habrian nacido en
InnoDB sobre 5.5.

Ahora `acaiTableEngine()` decide por la version, con el mismo patron que
`mysqlStrictMode` usa para el modo SQL:

  MariaDB >= 10.2 o MySQL >= 5.7 -> InnoDB (Barracuda/DYNAMIC por defecto)
  cualquier otro o desconocido   -> MyISAM (lo de siempre)

Contempla el disfraz "5.5.5-10.x.y-MariaDB" con el que algunos clientes
antiguos ven a MariaDB 10+. La decision pura (`acaiEngineForServerVersion`)
esta separada para poder probarla sin servidor. Usada en los dos sitios que
crean tablas: createMissingSchemaTablesAndFields y la instalacion de plugins.

Comprobado: 12 versiones (Forge 10.6, webserver2 11.4, servidor antiguo 5.5,
disfraces, MySQL 5.6/5.7/8.0, vacio) y de punta a punta contra el MariaDB de
Forge a traves del shim mysql_*: InnoDB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 17:44:26 +00:00

488 lines
19 KiB
PHP

<?
$schemaPlugins = array();
function getSchemaPlugins($force = false){
global $schemaPlugins;
$pluginsDir = $GLOBALS["APP"]["pluginsdir"];
if (!$force && @$schemaPlugins) return $schemaPlugins;
foreach (scandir($pluginsDir) as $dirname) {
$fullPath = $pluginsDir."/".$dirname;
if (is_dir($fullPath)){
$schemaFile = $fullPath."/schema.ini.php";
if (file_exists($schemaFile)){
$schemaPlugins[$dirname] = loadINI($schemaFile);
}
$customSchemaFile = $fullPath."/custom-schema.ini.php";
// Sin schema.ini.php no hay plugin: un custom-schema huerfano (p.ej. tras
// un descarte del sync, que no mueve los custom-schema) se ignora en vez
// de avisar en cada peticion.
if (isset($schemaPlugins[$dirname]) && file_exists($customSchemaFile)){
$customSchema = loadINI($customSchemaFile);
$exclude = array("version", "name", "description");
foreach ($exclude as $e):
unset($customSchema[$e]);
endforeach;
foreach($schemaPlugins[$dirname] as $key => $value):
if (is_array($value)){
foreach($value as $kv => $vv):
if (isset($customSchema[$key][$kv]) && @$customSchema[$key][$kv] != "" && @$customSchema[$key][$kv] != $vv) $schemaPlugins[$dirname][$key][$kv] = $customSchema[$key][$kv];
endforeach;
}else{
if (isset($customSchema[$key]) && @$customSchema[$key] != "" && @$customSchema[$key] != $value) $schemaPlugins[$dirname][$key] = $customSchema[$key];
}
endforeach;
}
}
}
return $schemaPlugins;
}
function installSQL($path){
if (file_exists($path."/install.sql")){
try{
sqlImport($path."/install.sql");
return 1;
}catch(Exception $e){
return 0;
}
}else{
return 1;
}
}
function installSchemas($path){
if (file_exists($path."/schemaPresets")){
foreach (scandir($path."/schemaPresets") as $dirname) {
if (strpos($dirname,".ini")){
addTablePlugin($path."/schemaPresets",$dirname);
}
}
rename($path."/schemaPresets/",$path."/schemaPresets.bck/");
return 0;
}else{
return 1;
}
}
function showInstall($path){
global $TABLE_PREFIX,$menu,$action,$schema;
//
$result = 0;
showHeader();
?>
<header class="navbar navbar-default navbar-fixed-top">
<h4 class="titular"><?php echo $schema['menuName'] ?> - <span class="balance">0</span></h4>
</header>
<div id="page-content">
<h3>Instalando el plugin</h3>
<? $mensajesInstall = []; installPluginArtifacts($path, $mensajesInstall); ?>
<? foreach ($mensajesInstall as $mensajeInstall): ?><p><?=htmlspecialchars($mensajeInstall);?></p><? endforeach; ?>
<? if ($result==2){?>
<p>&nbsp;</p>
<a href="">Recarga para cargar el plugin</a>
<? }?>
</div>
<?
showFooter();
exit;
}
function addPlugins($type,&$var, $fieldtype = null){
global $CURRENT_USER,$menu,$action;
$schemaPlugins = getSchemaPlugins();
if (!@$schemaPlugins) return;
$pluginsInserted = array();
$allowedFields = array("db_type", "db_type_config");
foreach($schemaPlugins as $key => $value):
if (!@$CURRENT_USER["isAdmin"] && $value["adminOnly"]) continue;
if (!@$value["enabled"]) continue;
if (@$value["checkPoints"]){
foreach($allowedFields as $allowedField):
$value["checkPoints"][$allowedField] = "";
endforeach;
foreach($value["checkPoints"] as $checkPoint => $controller):
if ($checkPoint==$type && isset($value["checkPoints"][$type])){
$enabled = true;
$pluginsInserted[] = $key;
// DEFINIMOS LAS VARIABLES DE OPCIONES PARA EL PLUGIN
$options = array();
$options["templatePath"] = str_replace($GLOBALS["PROGRAM_DIR"]."/","",$GLOBALS["APP"]["pluginsdir"])."/".$key;
$options["pluginName"] = $key;
if (@$value["newFieldType"]){
$options["fieldType"] = $value["newFieldType"];
if (@$fieldtype){
foreach($options["fieldType"] as $kvar => $vvar):
if (isset($var["field"][$kvar])) $options["fieldType"][$kvar] = $var["field"][$kvar];
endforeach;
}
}
// SI EXISTEN FILTROS DE MENU
if (@$value["menuFilter"][$type]){
$sepMenu = array_filter(explode(",",$value["menuFilter"][$type]));
if (@$sepMenu && !in_array(@$menu,@$sepMenu)){
$enabled = false;
}
}
// SI EXISTEN FILTROS DE ACTION
if (@$value["actionFilter"][$type]){
$sepAction = array_filter(explode(",",$value["actionFilter"][$type]));
if (@$sepAction && !in_array(@$action,@$sepAction)){
$enabled = false;
}
}
// SI EXISTEN FILTROS DE TIPO DE CAMPO
if (@$value["newFieldType"]["type"] && @$fieldtype && @$value["newFieldType"]["type"] != $fieldtype) {
$enabled = false;
}
// SI EXISTE UN ARCHIVO DE INSTALACION LO EJECUTAMOS
if (file_exists($GLOBALS["APP"]["pluginsdir"]."/".$key."/install.sql") || file_exists($GLOBALS["APP"]["pluginsdir"]."/".$key."/schemaPresets")){
$enabled = false;
showInstall($GLOBALS["APP"]["pluginsdir"]."/".$key);
}
// SI NOS PIDE EL TYPE DE CAMPO Y EXISTE EN EL SCHEMA DEL PLUGIN LO AÑADIMOS
if ($enabled && @$type=="db_type" && @$value["newFieldType"]["type"]){
array_push($var, array("value" => $value["newFieldType"]["type"], "name" => $value["newFieldType"]["label"]));
continue;
}
if ($enabled && @$type=="db_type_config" && @$value["newFieldType"]){
?>
<fieldset data-plugin="<?=$options["pluginName"];?>" data-fieldtype="<?=$value["newFieldType"]["type"];?>" class="fieldPluginContainer">
<legend><b>Plugin Options</b></legend>
<? foreach ($value["newFieldType"] as $keyFT => $vFT):?>
<? if (@$keyFT=="label" || @$keyFT=="type") continue;?>
<? if (@$var[$keyFT]) $vFT = @$var[$keyFT];?>
<div class="<?=$keyFT;?>">
<div class="label"><?=$keyFT;?></div>
<input type="text" name="plugin[<?=$options["pluginName"];?>][<?=$keyFT;?>]" value="<?=$vFT;?>">
</div>
<? endforeach;?>
</fieldset>
<?
continue;
}
// SI SE PERMITE EJECUTAMOS EL PLUGIN
if ($enabled && @$value["checkPoints"][$type]) {
include $GLOBALS["APP"]["pluginsdir"]."/".$key."/".@$value["checkPoints"][$type];
}
}
endforeach;
}
endforeach;
return $pluginsInserted;
}
function addTablePlugin($path,$file) {
global $TABLE_PREFIX, $APP;
//rename($path."/".$file,$GLOBALS["APP"]["datadir"]."schemaPresets/".$file);
$presetTableName = str_replace(".ini.php","",$file);
// error checking
$errors = "";
$tableNameErrors = getTablenameErrors($presetTableName);
// create new schema data
$schemaPresetDir = $path."/";
$newSchema = loadSchema($presetTableName, $schemaPresetDir) or die("Couldn't load preset schema");
$newSchema['menuName'] = $presetTableName; // change menu name
$newSchema['menuOrder'] = time(); // use time to sort to bottom
// create mysql table
// (this isn't required but done here so we catch get mysql errors creating the table)
// createMissingSchemaTablesAndFields() creates if this doesn't.
$tableNameWithPrefix = $TABLE_PREFIX . $presetTableName;
$result = mysql_query("CREATE TABLE `".mysql_real_escape_string($tableNameWithPrefix)."` (
num int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY (num)
) ENGINE=".acaiTableEngine()." DEFAULT CHARSET=utf8;");
if (!$result) {
print "Error creating MySQL table.\n\nMySQL error was: ". htmlspecialchars(mysql_error()) . "\n";
exit;
}
// save new schema
saveSchema($presetTableName, $newSchema);
// Create schema table and fields in MySQL
createMissingSchemaTablesAndFields();
clearAlertsAndNotices(); // don't display alerts about adding new fields
}
/************************/
function sqlImport($file)
{
$delimiter = ';';
$file = fopen($file, 'r');
$isFirstRow = true;
$isMultiLineComment = false;
$sql = '';
while (!feof($file)) {
$row = fgets($file);
// remove BOM for utf-8 encoded file
if ($isFirstRow) {
$row = preg_replace('/^\x{EF}\x{BB}\x{BF}/', '', $row);
$isFirstRow = false;
}
// 1. ignore empty string and comment row
if (trim($row) == '' || preg_match('/^\s*(#|--\s)/sUi', $row)) {
continue;
}
// 2. clear comments
$row = trim(clearSQL($row, $isMultiLineComment));
// 3. parse delimiter row
if (preg_match('/^DELIMITER\s+[^ ]+/sUi', $row)) {
$delimiter = preg_replace('/^DELIMITER\s+([^ ]+)$/sUi', '$1', $row);
continue;
}
// 4. separate sql queries by delimiter
$offset = 0;
while (strpos($row, $delimiter, $offset) !== false) {
$delimiterOffset = strpos($row, $delimiter, $offset);
if (isQuoted($delimiterOffset, $row)) {
$offset = $delimiterOffset + strlen($delimiter);
} else {
$sql = trim($sql . ' ' . trim(substr($row, 0, $delimiterOffset)));
query($sql);
$row = substr($row, $delimiterOffset + strlen($delimiter));
$offset = 0;
$sql = '';
}
}
$sql = trim($sql . ' ' . $row);
}
if (strlen($sql) > 0) {
query($row);
}
fclose($file);
}
/**
* Remove comments from sql
*
* @param string sql
* @param boolean is multicomment line
* @return string
*/
function clearSQL($sql, &$isMultiComment)
{
if ($isMultiComment) {
if (preg_match('#\*/#sUi', $sql)) {
$sql = preg_replace('#^.*\*/\s*#sUi', '', $sql);
$isMultiComment = false;
} else {
$sql = '';
}
if(trim($sql) == ''){
return $sql;
}
}
$offset = 0;
while (preg_match('{--\s|#|/\*[^!]}sUi', $sql, $matched, PREG_OFFSET_CAPTURE, $offset)) {
list($comment, $foundOn) = $matched[0];
if (isQuoted($foundOn, $sql)) {
$offset = $foundOn + strlen($comment);
} else {
if (substr($comment, 0, 2) == '/*') {
$closedOn = strpos($sql, '*/', $foundOn);
if ($closedOn !== false) {
$sql = substr($sql, 0, $foundOn) . substr($sql, $closedOn + 2);
} else {
$sql = substr($sql, 0, $foundOn);
$isMultiComment = true;
}
} else {
$sql = substr($sql, 0, $foundOn);
break;
}
}
}
return $sql;
}
/**
* Check if "offset" position is quoted
*
* @param int $offset
* @param string $text
* @return boolean
*/
function isQuoted($offset, $text)
{
if ($offset > strlen($text))
$offset = strlen($text);
$isQuoted = false;
for ($i = 0; $i < $offset; $i++) {
if ($text[$i] == "'")
$isQuoted = !$isQuoted;
if ($text[$i] == "\\" && $isQuoted)
$i++;
}
return $isQuoted;
}
function query($sql)
{
//echo '#<strong>SQL CODE TO RUN:</strong><br>' . htmlspecialchars($sql) . ';<br><br>';
if (!$query = mysql_query($sql)) {
throw new Exception("Cannot execute request to the database {$sql}: " . mysql_error());
}
}
// ---------------------------------------------------------------------------
// 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.
// 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() {
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, pero si
// los mismos globales del CMS que ve un checkpoint desde addPlugins().
global $CURRENT_USER, $menu, $action, $TABLE_PREFIX, $SETTINGS;
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) {
// El detalle completo (ruta, linea) va al log del servidor; al cliente,
// el mensaje sin rutas absolutas.
error_log("[pluginLifecycle] " . $folder . "/" . $event . ".php: " . $e->getMessage()
. " en " . $e->getFile() . ":" . $e->getLine());
$raiz = realpath($GLOBALS["APP"]["pluginsdir"] . "/../../..");
$res["ok"] = false;
$res["error"] = "Excepcion en " . $event . ".php: " . ($raiz ? str_replace($raiz, "", $e->getMessage()) : $e->getMessage());
}
return $res;
}
?>