Author SHA1 Message Date
Acai ForgeandClaude Opus 5.5 8eb4caf456 perf(AcaiAPI): updateCategoryMetadata escribe solo las filas que cambian, en una transaccion
Antes reescribia la tabla entera con un UPDATE por fila en autocommit. Medido
en una copia de provincias_y_poblaciones (8.168 filas, InnoDB), resultado
identico al anterior (CRC de todas las filas): recalcular sin cambios 0,93 s
-> 0,04 s; mover un pueblo dentro de su provincia 1,39 s -> 0,13 s; subir una
provincia arriba del todo 2,37 s -> 1,28 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 18:55:16 +00:00
Jordan Diaz bd550841bd Merge fase1-credenciales: pluginLifecycle local acepta secreto_local 2026-10-19 21:06:55 +00:00
Jordan DiazandClaude Opus 5.5 5709c5c856 feat(handler_ws): pluginLifecycle en copias locales acepta secreto_local del .acai
Segunda via de autenticacion de los ganchos de ciclo de vida en copias
locales de Forge: ademas del par token+tokenHash del .acai, acepta el
campo secretoLocal del cuerpo si coincide (hash_equals) con
secreto_local del .acai. Las cadenas vacias o no escalares nunca
coinciden y mode=production sigue rechazando.

La via token+tokenHash queda como transitoria hasta que el .acai deje
de llevar el token (fase 1d).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:35:25 +00:00
Jordan Diaz dabdfa1354 Merge feat/regla-visibilidad-unica: header.php y robots consultan la regla única de builder_saas 2026-10-14 15:27:50 +00:00
2 changed files with 70 additions and 8 deletions
+45
View File
@@ -2180,13 +2180,58 @@ class AcaiAPI
$childNums[(int) $row['parentNum']][] = $row['num'];
}
$before = $byNum;
self::_rebuildCategoryBranch([
'branchParent' => 0,
'records' => &$byNum,
'childNodes' => $childNums,
]);
// Solo las filas que cambian, y todas en UNA transaccion. Antes era un
// UPDATE por fila de la tabla entera en autocommit: en InnoDB cada uno
// es un commit a disco (8.168 filas = 1,9 s; en una transaccion, 0,7 s),
// y mover un nodo deja casi siempre intacta buena parte del arbol.
// START TRANSACTION por SQL y no con PDO: `Db` no expone la conexion, y
// anadirle un metodo aqui romperia las webs con un Db.php propio (el
// sync de web-base respeta los ficheros personalizados). En MyISAM es
// inocuo.
$changed = [];
foreach ($byNum as $num => $cat) {
if (self::_categoryRowChanged($before[$num], $cat)) $changed[$num] = $cat;
}
if (!$changed) return;
$db->execute("START TRANSACTION");
try {
self::_writeCategoryRows($db, $fullTable, $changed);
$db->execute("COMMIT");
} catch (\Throwable $e) {
$db->execute("ROLLBACK");
throw $e;
}
}
/** ¿Cambia alguno de los campos que recalcula el arbol? PDO devuelve
* tipos nativos y el recalculo mezcla int y string: se compara como
* texto, salvo NULL, que solo es igual a NULL. */
private static function _categoryRowChanged(array $before, array $after)
{
foreach (['globalOrder', 'siblingOrder', 'depth', 'lineage', 'breadcrumb'] as $field) {
$a = isset($before[$field]) ? $before[$field] : null;
$b = isset($after[$field]) ? $after[$field] : null;
if ($a === null || $b === null) {
if ($a !== $b) return true;
} elseif ((string) $a !== (string) $b) {
return true;
}
}
return false;
}
private static function _writeCategoryRows($db, $fullTable, array $rows)
{
foreach ($rows as $num => $cat) {
$db->execute(
"UPDATE `$fullTable`
SET globalOrder = ?,
+25 -8
View File
@@ -406,10 +406,13 @@ function _renameFile() {
* (install/uninstall/enable/disable) quedaban inservibles ahi con un
* "Token no valido" que no tenia nada que ver con el token.
*
* No es un bypass: se compara el par token+tokenHash recibido con el que la
* propia web guarda en su .acai, que es exactamente el que Forge envia. Sin
* .acai (produccion), con mode=production, o sin coincidencia exacta, no
* autoriza nada.
* No es un bypass: se compara un secreto recibido en el cuerpo con el que la
* propia web guarda en su .acai. Sin .acai (produccion), con mode=production,
* o sin coincidencia exacta, no autoriza nada. Dos formas de coincidir:
* - secretoLocal (cuerpo) == secreto_local (.acai): el secreto de la copia,
* que no es una credencial de ninguna persona. Es la via buena.
* - token+tokenHash (cuerpo) == token+tokenHash (.acai): la de siempre.
* Las cadenas vacias nunca coinciden. Los secretos no se registran nunca.
*/
function _lifecycleTokenLocalValido($datosAuth) {
$acaiPath = __DIR__ . "/../../../.acai";
@@ -420,10 +423,24 @@ function _lifecycleTokenLocalValido($datosAuth) {
if (!is_array($acai) || @$acai["mode"] === "production") {
return false;
}
$token = (string) @$datosAuth["token"];
$hash = (string) @$datosAuth["tokenHash"];
$tokenLocal = (string) @$acai["token"];
$hashLocal = (string) @$acai["tokenHash"];
if (!is_array($datosAuth)) {
return false;
}
// Via buena: el secreto propio de la copia local.
$secretoLocal = @$acai["secreto_local"];
$secreto = @$datosAuth["secretoLocal"];
if (is_string($secretoLocal) && $secretoLocal !== ""
&& is_scalar($secreto) && (string) $secreto !== ""
&& hash_equals($secretoLocal, (string) $secreto)) {
return true;
}
// transitorio: quitar cuando el .acai ya no lleve token (fase 1d)
$token = is_scalar(@$datosAuth["token"]) ? (string) $datosAuth["token"] : "";
$hash = is_scalar(@$datosAuth["tokenHash"]) ? (string) $datosAuth["tokenHash"] : "";
$tokenLocal = is_scalar(@$acai["token"]) ? (string) $acai["token"] : "";
$hashLocal = is_scalar(@$acai["tokenHash"]) ? (string) $acai["tokenHash"] : "";
if ($token === "" || $hash === "" || $tokenLocal === "" || $hashLocal === "") {
return false;
}