Author SHA1 Message Date
Jordan Diaz 17f5d91c8d feat(core): el enlace es unico entre todas las tablas (AcaiAPI::uniqueEnlace) y slug.php registra duplicados 2026-09-24 09:34:22 +00:00
Jordan Diaz 7097b79111 fix(core): las migas no revientan con un registro sin su campo de migas (ficha en blanco) 2026-09-24 07:35:23 +00:00
Jordan Diaz 4e46322b02 feat(core): regla unica de valores por defecto en los INSERT (AcaiAPI::insertDefaults) 2026-09-24 07:21:53 +00:00
Jordan Diaz 184038b786 fix(core): una fecha ausente solo se rellena si la columna es NOT NULL sin DEFAULT 2026-09-24 00:17:04 +00:00
Jordan Diaz 14ddf1608e fix(core): altas rellenan fechas que faltan y el error de insert/update trae el mensaje de MySQL 2026-09-23 07:59:03 +00:00
Jordan DiazandClaude Opus 5.5 8659715132 Merge motor-segun-version: el motor de las tablas nuevas depende del servidor
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 17:44:27 +00:00
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
Jordan DiazandClaude Opus 5.5 bc63ebdb15 Merge corregir-comentario-innodb
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 17:28:40 +00:00
Jordan DiazandClaude Opus 5.5 3701072bb3 docs(core): el comentario del motor InnoDB daba una razon falsa
Decia que los INSERT parciales del CMS seguian colando porque el sql_mode de
Forge no es estricto. Es falso: el CMS fija su propio modo al conectar
(mysqlStrictMode(true) y TRADITIONAL en Db.php), y en un INSERT de una fila
ese modo rechaza lo mismo con MyISAM que con InnoDB. Solo cambia el
comentario.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 17:28:39 +00:00
Jordan DiazandClaude Opus 5 8d92d08a56 Merge innodb-tablas-nuevas: las tablas nuevas del CMS nacen en InnoDB
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:41:10 +00:00
6 changed files with 357 additions and 11 deletions
+291 -4
View File
@@ -87,9 +87,14 @@ class AcaiAPI
$lastId = 0; $lastId = 0;
$count = 0; $count = 0;
// Columnas que MySQL exige en un INSERT (NOT NULL sin DEFAULT): solo a
// esas se les pone valor si no vienen. null = no se pudo saber.
$requiredCols = $ignoreSchema ? [] : self::requiredColumns($db, $prefix . $table);
foreach ($records as $record) { foreach ($records as $record) {
$record = self::uniqueEnlace($db, $prefix . $table, $record, [], $options);
list($setParts, $params, $uploadFields) = self::buildSetClause( list($setParts, $params, $uploadFields) = self::buildSetClause(
$record, $schema, $functions, $ignoreSchema, true, $forceNum $record, $schema, $functions, $ignoreSchema, true, $forceNum, $requiredCols
); );
if (empty($setParts)) continue; if (empty($setParts)) continue;
@@ -102,7 +107,7 @@ class AcaiAPI
$result = $db->execute($sql, $params ? $params : null); $result = $db->execute($sql, $params ? $params : null);
if ($result === false) { if ($result === false) {
self::error(['error' => "Error al insertar en `$table`"]); self::error(['error' => "Error al insertar en `$table`" . self::dbErrorSuffix($db)]);
continue; continue;
} }
@@ -197,6 +202,22 @@ class AcaiAPI
foreach ($records as $record) { foreach ($records as $record) {
$lastNum = isset($record['num']) ? $record['num'] : null; $lastNum = isset($record['num']) ? $record['num'] : null;
if (isset($record['enlace'])) {
// Solo si el enlace CAMBIA: el formulario reenvia todos los
// campos al guardar, y un registro que ya compartia enlace
// (duplicado anterior a esta regla) no debe quedar bloqueado
// por editar otra cosa.
$targetNums = [];
$sinCambio = true;
foreach ((array) $db->executeS("SELECT num, enlace FROM `{$prefix}{$table}` WHERE " . $whereSql, $whereParams ?: null) as $row) {
if (!isset($row['num'])) continue;
$targetNums[] = (string) $row['num'];
if ((string) $row['enlace'] !== trim((string) $record['enlace'])) $sinCambio = false;
}
if ($targetNums && !$sinCambio) {
$record = self::uniqueEnlace($db, $prefix . $table, $record, $targetNums, $options);
}
}
list($setParts, $setParams, $uploadFields) = self::buildSetClause( list($setParts, $setParams, $uploadFields) = self::buildSetClause(
$record, $schema, $functions, $ignoreSchema, false, false $record, $schema, $functions, $ignoreSchema, false, false
@@ -214,7 +235,7 @@ class AcaiAPI
$result = $db->execute($sql, $allParams ? $allParams : null); $result = $db->execute($sql, $allParams ? $allParams : null);
if ($result === false) { if ($result === false) {
self::error(['error' => "Error al actualizar `$table`"]); self::error(['error' => "Error al actualizar `$table`" . self::dbErrorSuffix($db)]);
continue; continue;
} }
$count++; $count++;
@@ -1366,7 +1387,8 @@ class AcaiAPI
array $functions, array $functions,
$ignoreSchema, $ignoreSchema,
$isInsert, $isInsert,
$forceNum $forceNum,
$requiredCols = null
) { ) {
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$user = 1; $user = 1;
@@ -1438,6 +1460,7 @@ class AcaiAPI
$setParts[] = '`dragSortOrder` = ?'; $setParts[] = '`dragSortOrder` = ?';
$params[] = time(); $params[] = time();
} }
} }
// ── User-supplied fields ───────────────────────────────────────── // ── User-supplied fields ─────────────────────────────────────────
@@ -1477,9 +1500,14 @@ class AcaiAPI
// - INSERT: mismo comportamiento que el Admin -> fecha actual. // - INSERT: mismo comportamiento que el Admin -> fecha actual.
// - UPDATE: se omite la columna, para no pisar con "hoy" un valor // - UPDATE: se omite la columna, para no pisar con "hoy" un valor
// ya guardado solo porque el caller mando la clave vacia. // ya guardado solo porque el caller mando la clave vacia.
// - Pero SOLO si la columna la exige (NOT NULL sin DEFAULT). En una
// que admite NULL, vacio significa "sin fecha": se omite y queda
// NULL (antes quedaba "ahora": una nota con deleted_at vacio
// nacia borrada). Si no se sabe ($requiredCols null), como antes.
if ($schemaField && isset($schemaField['type']) && $schemaField['type'] === 'date' if ($schemaField && isset($schemaField['type']) && $schemaField['type'] === 'date'
&& ($value === null || (is_string($value) && trim($value) === ''))) { && ($value === null || (is_string($value) && trim($value) === ''))) {
if (!$isInsert) continue; if (!$isInsert) continue;
if (is_array($requiredCols) && !isset($requiredCols[$key])) continue;
$value = $now; $value = $now;
} }
@@ -1490,9 +1518,111 @@ class AcaiAPI
$params[] = $value; $params[] = $value;
} }
// ── Valores por defecto del INSERT (regla UNICA) ──────────────────
if ($isInsert && !$ignoreSchema && $schema) {
list($defParts, $defParams) = self::insertDefaults(
$setParts, $schema, $requiredCols, $now);
$setParts = array_merge($setParts, $defParts);
$params = array_merge($params, $defParams);
}
return [$setParts, $params, $uploadFields]; return [$setParts, $params, $uploadFields];
} }
/**
* Valores de las columnas que un INSERT no trae. UNICA fuente de la regla
* (antes repartida entre el Admin clasico, el server Python y aqui):
*
* 1. Default EXPLICITO del schema, siempre que el campo falte: checkbox
* marcado por defecto -> checkedValue; defaultContent (textbox,
* wysiwyg, codigo) o defaultValue. Nunca se evalua como PHP: un
* default con `<?` se ignora.
* 2. Sin default explicito, SOLO si la columna es NOT NULL sin DEFAULT
* (MySQL rechazaria el INSERT): checkbox -> uncheckedValue; si no, por
* tipo de columna (fecha -> ahora, numero -> 0, enum -> primer valor,
* resto -> ''), como el Admin clasico. Incluye columnas obligatorias
* que el schema no declara (p.ej. flags de `accounts`).
* 3. Nada mas: una columna que admite NULL o tiene DEFAULT se deja a MySQL
* (vacio puede significar algo: deleted_at = no borrada).
*
* $requiredCols null = no se pudo consultar la tabla: solo se aplica (1).
*/
private static function insertDefaults(array $setParts, array $schema, $requiredCols, $now)
{
$present = [];
foreach ($setParts as $part) {
if (preg_match('/^`([^`]+)`/', $part, $m)) $present[$m[1]] = true;
}
$parts = [];
$params = [];
$add = function ($col, $val) use (&$parts, &$params, &$present) {
$parts[] = "`$col` = ?";
$params[] = $val;
$present[$col] = true;
};
foreach ($schema as $field => $def) {
if (!is_array($def) || isset($present[$field])) continue;
$type = isset($def['type']) ? $def['type'] : '';
if (in_array($type, ['upload', 'separator', 'none'], true)) continue;
if (!empty($def['isSystemField'])) continue;
$explicit = self::schemaDefault($def);
if ($explicit !== null) {
$add($field, $explicit);
} elseif (is_array($requiredCols) && isset($requiredCols[$field])) {
$add($field, $type === 'checkbox'
? (string) (isset($def['uncheckedValue']) && $def['uncheckedValue'] !== '' ? $def['uncheckedValue'] : '0')
: self::columnFallback($requiredCols[$field], $now));
}
}
if (is_array($requiredCols)) {
foreach ($requiredCols as $col => $meta) {
if (isset($present[$col]) || $col === 'num') continue;
$add($col, self::columnFallback($meta, $now));
}
}
return [$parts, $params];
}
/** Default explicito del schema para un campo, o null si no declara uno. */
private static function schemaDefault(array $def)
{
$type = isset($def['type']) ? $def['type'] : '';
if ($type === 'checkbox') {
$checked = isset($def['checkedByDefault']) ? $def['checkedByDefault'] : 0;
if (!in_array($checked, [1, '1', true], true)) return null;
return (string) (isset($def['checkedValue']) && $def['checkedValue'] !== '' ? $def['checkedValue'] : '1');
}
$keys = in_array($type, ['textbox', 'wysiwyg', 'codigo'], true)
? ['defaultContent', 'defaultValue'] : ['defaultValue'];
foreach ($keys as $k) {
if (!isset($def[$k]) || !is_scalar($def[$k])) continue;
$v = (string) $def[$k];
if ($v === '' || strpos($v, '<?') !== false) continue;
return $v;
}
return null;
}
/** Valor por tipo de columna para una columna NOT NULL sin DEFAULT. */
private static function columnFallback(array $meta, $now)
{
$t = isset($meta['data_type']) ? $meta['data_type'] : '';
if (in_array($t, ['datetime', 'timestamp'], true)) return $now;
if ($t === 'date') return substr($now, 0, 10);
if ($t === 'time') return '00:00:00';
if ($t === 'year') return substr($now, 0, 4);
if (in_array($t, ['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint',
'decimal', 'numeric', 'float', 'double', 'real', 'bit'], true)) return 0;
if ($t === 'enum' && preg_match("/^enum\('((?:[^'\\\\]|\\\\.|'')*)'/i",
isset($meta['column_type']) ? $meta['column_type'] : '', $m)) {
return str_replace("''", "'", $m[1]);
}
return '';
}
/** /**
* Coerces a value to its correct database representation * Coerces a value to its correct database representation
* based on the schema field definition. * based on the schema field definition.
@@ -1640,6 +1770,163 @@ class AcaiAPI
// ERROR HANDLING // ERROR HANDLING
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
/**
* ": <mensaje de MySQL>" del ultimo execute() fallido, o "" si no lo hay.
* Sin el, un alta que MySQL rechaza (p.ej. "Field 'x' doesn't have a
* default value") llegaba como "Error al insertar" a secas y el agente
* reintentaba a ciegas. isset(): una web con Db.php personalizado (sin
* lastError) sigue funcionando, solo que sin el detalle.
*/
/** Cache por peticion: tablas de la BD con columna `enlace` (null = sin consultar). */
private static $enlaceTablesCache = null;
/** Tablas de la BD actual con columna `enlace`, o null si no se pudo saber. */
private static function enlaceTables($db)
{
if (self::$enlaceTablesCache !== null) return self::$enlaceTablesCache ?: null;
try {
$rows = $db->executeS(
"SELECT TABLE_NAME FROM information_schema.COLUMNS"
. " WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'enlace'"
);
} catch (\Throwable $e) {
self::$enlaceTablesCache = false;
return null;
}
$tables = [];
foreach ((array) $rows as $row) {
if (is_array($row) && !empty($row['TABLE_NAME'])) $tables[] = $row['TABLE_NAME'];
}
self::$enlaceTablesCache = $tables;
return $tables;
}
/**
* Quien usa ya `$enlace` en CUALQUIER tabla con columna enlace, sin contar
* `$fullTable` + `$excludeNums`. Devuelve [tabla, num] o null.
*/
private static function enlaceOwner($db, $enlace, $fullTable, array $excludeNums)
{
$tables = self::enlaceTables($db);
if (!$tables) return null;
$parts = [];
$params = [];
foreach ($tables as $t) {
$parts[] = "SELECT ? AS t, num FROM `$t` WHERE enlace = ?";
$params[] = $t;
$params[] = $enlace;
}
try {
$rows = $db->executeS(implode(" UNION ALL ", $parts), $params);
} catch (\Throwable $e) {
return null;
}
foreach ((array) $rows as $row) {
if ($row['t'] === $fullTable && in_array((string) $row['num'], $excludeNums, true)) continue;
return [$row['t'], $row['num']];
}
return null;
}
/**
* `enlace` UNICO entre todas las tablas: slug.php resuelve una URL
* buscandola en todas las tablas con columna enlace, asi que dos registros
* con el mismo enlace dejan uno inaccesible. Si choca:
* - options['enlaceOnConflict'] === 'error' -> error claro (enlace escrito
* a mano: no se le cambia al usuario una URL que ha elegido).
* - si no (por defecto) -> sufijo -2, -3... hasta que quede libre, para
* no romper importadores, hooks ni formularios.
* Se ignoran vacio, '#' y URLs externas. Devuelve el record (copia).
*/
private static function uniqueEnlace($db, $fullTable, array $record, array $excludeNums, array $options)
{
if (!isset($record['enlace']) || !is_string($record['enlace'])) return $record;
$enlace = trim($record['enlace']);
if ($enlace === '' || $enlace === '#' || preg_match('#^(https?:)?//#i', $enlace)) return $record;
$tables = self::enlaceTables($db);
if (!$tables || !in_array($fullTable, $tables, true)) return $record;
if (count($excludeNums) > 1) {
self::error(['error' => [
'message' => "No se puede poner el mismo enlace '$enlace' a varios registros a la vez",
'code' => 'ENLACE_DUPLICADO',
]]);
return $record;
}
$owner = self::enlaceOwner($db, $enlace, $fullTable, $excludeNums);
if (!$owner) return $record;
$prefix = defined('TABLE_PREFIX') ? TABLE_PREFIX : (isset($GLOBALS['TABLE_PREFIX']) ? $GLOBALS['TABLE_PREFIX'] : 'cms_');
$ownerName = strpos($owner[0], $prefix) === 0 ? substr($owner[0], strlen($prefix)) : $owner[0];
// La home ('/') no admite sufijo: un segundo '/' es siempre un error.
if (rtrim($enlace, '/') === '' || (isset($options['enlaceOnConflict']) && $options['enlaceOnConflict'] === 'error')) {
self::error(['error' => [
'message' => "El enlace '$enlace' ya lo usa $ownerName #{$owner[1]}: elige otro",
'code' => 'ENLACE_DUPLICADO',
]]);
return $record;
}
$slash = substr($enlace, -1) === '/' ? '/' : '';
$base = rtrim($enlace, '/');
for ($i = 2; $i <= 200; $i++) {
$candidate = $base . '-' . $i . $slash;
if (!self::enlaceOwner($db, $candidate, $fullTable, $excludeNums)) {
$record['enlace'] = $candidate;
return $record;
}
}
self::error(['error' => [
'message' => "No se encontro un enlace libre para '$enlace'",
'code' => 'ENLACE_DUPLICADO',
]]);
return $record;
}
/** Cache por peticion: tabla completa => [columna => true]. */
private static $requiredColsCache = [];
/**
* Columnas de `$fullTable` que un INSERT tiene que traer: NOT NULL, sin
* DEFAULT y no auto_increment. Devuelve [columna => [data_type, column_type]], o null si no se
* pudo consultar (el llamador no rellena nada en ese caso). Solo usa
* Db::executeS, que existe en todas las versiones de Db.php.
*/
private static function requiredColumns($db, $fullTable)
{
if (array_key_exists($fullTable, self::$requiredColsCache)) {
return self::$requiredColsCache[$fullTable];
}
try {
$rows = $db->executeS(
"SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE FROM information_schema.COLUMNS"
. " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
. " AND IS_NULLABLE = 'NO' AND COLUMN_DEFAULT IS NULL"
. " AND EXTRA NOT LIKE '%auto_increment%'",
[$fullTable]
);
} catch (\Throwable $e) {
return self::$requiredColsCache[$fullTable] = null;
}
$cols = [];
foreach ((array) $rows as $row) {
if (!is_array($row) || empty($row['COLUMN_NAME'])) continue;
$cols[$row['COLUMN_NAME']] = [
'data_type' => strtolower((string) ($row['DATA_TYPE'] ?? '')),
'column_type' => (string) ($row['COLUMN_TYPE'] ?? ''),
];
}
return self::$requiredColsCache[$fullTable] = $cols;
}
private static function dbErrorSuffix($db)
{
return (is_object($db) && isset($db->lastError) && $db->lastError !== '')
? ': ' . $db->lastError
: '';
}
static function error(array $data = []) static function error(array $data = [])
{ {
if (class_exists('API') && class_exists('ApiError')) { if (class_exists('API') && class_exists('ApiError')) {
+6
View File
@@ -8,6 +8,10 @@ class Db {
private static $instance = false; // Db Class Instance private static $instance = false; // Db Class Instance
/** Mensaje del ultimo execute() fallido (null si fue bien). AcaiAPI lo
* devuelve en su error para que el caller sepa POR QUE fallo. */
public $lastError = null;
/** /**
* @param host: DB Host * @param host: DB Host
* @param user: DB user * @param user: DB user
@@ -69,12 +73,14 @@ class Db {
* @return bool * @return bool
*/ */
public function execute($sql, $params = null) { public function execute($sql, $params = null) {
$this->lastError = null;
if ($params) { if ($params) {
$stmt = $this->link->prepare($sql); $stmt = $this->link->prepare($sql);
try { try {
$stmt->execute($params); $stmt->execute($params);
} }
catch(Exception $e) { catch(Exception $e) {
$this->lastError = $e->getMessage();
return false; return false;
} }
} }
+46 -6
View File
@@ -43,6 +43,49 @@ function connectToMySQL($returnErrors = false) {
return ''; return '';
} }
/**
* Motor de almacenamiento para las tablas que crea el CMS.
*
* InnoDB da recuperacion ante caida (MyISAM se corrompe con un apagon o un OOM
* y hay que repararlo a mano) y bloqueo por fila. Pero en servidores antiguos
* no es seguro: MariaDB 5.5 (hay un servidor de produccion con 5.5.68) no
* admite FULLTEXT en InnoDB, limita los indices a 767 bytes y usa filas
* COMPACT. Por eso el motor se decide por la version del servidor, igual que
* mysqlStrictMode decide el modo SQL:
*
* MariaDB >= 10.2 o MySQL >= 5.7 -> InnoDB (Barracuda/DYNAMIC por defecto)
* cualquier otro o desconocido -> MyISAM (lo de siempre)
*
* No cambia que INSERT se aceptan: el CMS conecta en modo estricto
* (mysqlStrictMode / TRADITIONAL en Db.php) y en un INSERT de una fila eso
* rechaza lo mismo con los dos motores. Ver docs/motor-innodb.md (repo Forge).
*/
function acaiTableEngine() {
static $engine = null;
if ($engine === null) {
$engine = acaiEngineForServerVersion(mysql_get_server_info());
}
return $engine;
}
/** Decision pura, separada para poder probarla sin servidor. */
function acaiEngineForServerVersion($serverInfo) {
$info = trim((string) $serverInfo);
// Clientes antiguos ven MariaDB 10+ como "5.5.5-10.6.25-MariaDB": el
// "5.5.5-" es un disfraz de compatibilidad, la version real va detras.
if (preg_match('/^5\.5\.5-(\d+\.\d+(?:\.\d+)?)/', $info, $m)) {
$version = $m[1];
$isMariaDb = true;
} elseif (preg_match('/^(\d+\.\d+(?:\.\d+)?)/', $info, $m)) {
$version = $m[1];
$isMariaDb = stripos($info, 'mariadb') !== false;
} else {
return 'MyISAM';
}
$minimo = $isMariaDb ? '10.2' : '5.7';
return version_compare($version, $minimo, '>=') ? 'InnoDB' : 'MyISAM';
}
function mysqlStrictMode($strictMode) { function mysqlStrictMode($strictMode) {
$currentVersion = preg_replace("/[^0-9\.]/", "", mysql_get_server_info()); $currentVersion = preg_replace("/[^0-9\.]/", "", mysql_get_server_info());
$isMySql5 = version_compare($currentVersion, '5.0.0', '>='); $isMySql5 = version_compare($currentVersion, '5.0.0', '>=');
@@ -74,12 +117,9 @@ function createMissingSchemaTablesAndFields() {
$mysqlTableName = $TABLE_PREFIX . $tableName; $mysqlTableName = $TABLE_PREFIX . $tableName;
if (!in_array($mysqlTableName, $mysqlTables)) { if (!in_array($mysqlTableName, $mysqlTables)) {
notice("Creating MySQL table for schema table: $tableName<br/>\n"); notice("Creating MySQL table for schema table: $tableName<br/>\n");
// InnoDB (antes MyISAM): las tablas nuevas nacen con recuperacion // Motor segun el servidor (ver acaiTableEngine): InnoDB donde lo
// ante caida y bloqueo por fila. El sql_mode de Forge no es // soporta bien, MyISAM en los antiguos.
// estricto, asi que los INSERT parciales del CMS (AcaiAPI no $result = mysql_query("CREATE TABLE `".mysql_real_escape_string($mysqlTableName)."` (num int(10) unsigned NOT NULL auto_increment, PRIMARY KEY (num)) ENGINE=".acaiTableEngine()." DEFAULT CHARSET=utf8mb4;");
// rellena dragSortOrder ni los checkboxes) siguen colando igual
// que con MyISAM. Ver docs/motor-innodb.md del repo de Forge.
$result = mysql_query("CREATE TABLE `".mysql_real_escape_string($mysqlTableName)."` (num int(10) unsigned NOT NULL auto_increment, PRIMARY KEY (num)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
if (!$result) { alert("Error creating MySQL table: $mysqlTableName<br/>\MySQL error was: ". htmlspecialchars(mysql_error()) . "\n"); } if (!$result) { alert("Error creating MySQL table: $mysqlTableName<br/>\MySQL error was: ". htmlspecialchars(mysql_error()) . "\n"); }
} }
+1 -1
View File
@@ -210,7 +210,7 @@ function addTablePlugin($path,$file) {
$result = mysql_query("CREATE TABLE `".mysql_real_escape_string($tableNameWithPrefix)."` ( $result = mysql_query("CREATE TABLE `".mysql_real_escape_string($tableNameWithPrefix)."` (
num int(10) unsigned NOT NULL auto_increment, num int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY (num) PRIMARY KEY (num)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;"); ) ENGINE=".acaiTableEngine()." DEFAULT CHARSET=utf8;");
if (!$result) { if (!$result) {
print "Error creating MySQL table.\n\nMySQL error was: ". htmlspecialchars(mysql_error()) . "\n"; print "Error creating MySQL table.\n\nMySQL error was: ". htmlspecialchars(mysql_error()) . "\n";
exit; exit;
+6
View File
@@ -259,6 +259,12 @@ function muestra_breadcrumb($record = array(), $previousLinks = array(),$class =
} }
if (!@$breadcrumbField || !@$breadcrumbRecord["optionsTablename"]) break; if (!@$breadcrumbField || !@$breadcrumbRecord["optionsTablename"]) break;
// Registro sin valor en su campo de migas (p.ej. producto sin
// categoria): la consulta quedaba "num=" -> error SQL -> excepcion que
// tumbaba el modulo entero y la ficha salia en blanco. Las migas se
// quedan en este nivel.
$valorMigas = @$breadcrumbRecord[$breadcrumbField];
if ($valorMigas === null || $valorMigas === '' || is_array($valorMigas)) break;
$breadcrumbRecord = dame_registros($breadcrumbRecord["optionsTablename"], $breadcrumbRecord["optionsValueField"]."=".mysql_real_escape_string($breadcrumbRecord[$breadcrumbField])); $breadcrumbRecord = dame_registros($breadcrumbRecord["optionsTablename"], $breadcrumbRecord["optionsValueField"]."=".mysql_real_escape_string($breadcrumbRecord[$breadcrumbField]));
$breadcrumbRecord = @$breadcrumbRecord[0]; $breadcrumbRecord = @$breadcrumbRecord[0];
+7
View File
@@ -300,6 +300,13 @@ if (!$routeResolved){
$encontrado = mysql_fetch_assoc(mysql_query("SELECT * FROM ".$tableName." WHERE ".$columnName."='".$enlaceEscaped."' LIMIT 1")); $encontrado = mysql_fetch_assoc(mysql_query("SELECT * FROM ".$tableName." WHERE ".$columnName."='".$enlaceEscaped."' LIMIT 1"));
// Enlace repetido entre tablas: se mantiene el comportamiento de
// siempre (gana la ULTIMA tabla recorrida; hay webs que dependen de
// ello, p.ej. apartados + servicios con el mismo enlace) pero ya no en
// silencio. Los nuevos duplicados los impide AcaiAPI::uniqueEnlace.
if (@$encontrado && @$tabla){
error_log("[slug] enlace duplicado '".$enlace."': ".$tabla."#".$registro." y ".$tableName."#".@$encontrado["num"]." (se sirve el segundo)");
}
if (@$encontrado){ if (@$encontrado){
$tabla = $tableName; $tabla = $tableName;
$registro = $encontrado["num"]; $registro = $encontrado["num"];