feat(core): el enlace es unico entre todas las tablas (AcaiAPI::uniqueEnlace) y slug.php registra duplicados
This commit is contained in:
@@ -92,6 +92,7 @@ class AcaiAPI
|
|||||||
$requiredCols = $ignoreSchema ? [] : self::requiredColumns($db, $prefix . $table);
|
$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, $requiredCols
|
$record, $schema, $functions, $ignoreSchema, true, $forceNum, $requiredCols
|
||||||
);
|
);
|
||||||
@@ -201,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
|
||||||
@@ -1760,6 +1777,113 @@ class AcaiAPI
|
|||||||
* reintentaba a ciegas. isset(): una web con Db.php personalizado (sin
|
* reintentaba a ciegas. isset(): una web con Db.php personalizado (sin
|
||||||
* lastError) sigue funcionando, solo que sin el detalle.
|
* 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]. */
|
/** Cache por peticion: tabla completa => [columna => true]. */
|
||||||
private static $requiredColsCache = [];
|
private static $requiredColsCache = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|||||||
Reference in New Issue
Block a user