El proxy no era prescindible: el container de las webs de Forge NO tiene MTA.
`which sendmail mail postfix ssmtp msmtp exim4` no devuelve nada y
`sendmail_path` apunta a /usr/sbin/sendmail, que no existe. Con $smtp = false
PHPMailer cae a mail() de PHP y ahi no sale nada, en silencio.
Medido sobre las webs de Forge:
26 configuran CocoEmail::$smtp = true propio -> enviarian igual
57 NO configuran SMTP -> se quedaban SIN correo
Entre esas 57 esta cdn-ecommerce, que es justo la web de pruebas. Se vuelve a
la version condicional: proxy cuando hay .acai (Forge/test) y directo cuando no
(legacy de produccion). Cubre los tres casos, porque el proxy reenvia con el
smtp_data que le llega en el payload, asi que las 26 con SMTP propio siguen
saliendo por su servidor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El commit anterior dejaba el envio condicionado al entorno (proxy en local,
directo en produccion) para poder unificar el fichero entre los dos canales sin
cambiarle el comportamiento a nadie. Decision posterior: el proxy no hace falta,
asi que se adopta la version de core_client tal cual y se envia SIEMPRE directo.
Se van dos funciones con el, y ninguna deja huerfanos:
send_email_coco_proxy() POST a cocosolution.com/?sendQuantumEmail=1
encrypt() solo la usaba el proxy, y en una linea que ya estaba
comentada dentro de esa misma funcion
Comprobado antes de quitarlas: cero llamadas fuera de la clase en web-base, en
las 131 webs de Forge, en el CMS central y en core_client.
Con esto core_client y web-base quedan identicos: 0 ficheros de deriva fuera de
plugins/, que actualizacion.php se salta a proposito.
OJO al alcance: las webs de Forge pasan de enviar por el proxy a enviar
directo. Es el cambio que se buscaba, pero conviene confirmarlo con un envio
real desde una web local antes de darlo por bueno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El core viaja por dos canales que deberian ser iguales y no lo eran:
core_client -> webs legacy via el CMS, y web-base -> webs de Forge. Nadie los
sincroniza, asi que el mismo arreglo se hacia dos veces, por separado y con
implementaciones distintas.
Medida la deriva: 5 ficheros de classes/ (el resto eran plugins/, que
actualizacion.php se salta a proposito).
CocoDB.php, CocoDB_alias.php <- de core_client
Traen los guards `if (file_exists(...))`: si falta el alias o AcaiAPI,
CocoDB se declara igual con la implementacion legacy. Es degradacion
real para webs viejas y aqui no la teniamos. Se pierde la declaracion de
CocoDB_old, que no referencia NADIE en ninguno de los cuatro arboles.
AcaiAPI.php, CocoParser.php -> ya iban a core_client desde aqui
AcaiAPI resulto ser el MISMO codigo: 9252 tokens en ambos y 0
diferencias al ordenarlos. El bloque de auditoria de updatedDate estaba
colocado antes o despues del if($isInsert), y como ese bloque no escribe
en $record, las dos posiciones son equivalentes.
CocoEmail.php <- fusion, y la unica divergencia REAL
web-base mandaba SIEMPRE por el proxy y core_client SIEMPRE directo, asi
que el fichero no se podia unificar sin cambiarle el comportamiento a
media plataforma. Ahora el camino se elige por entorno con el mismo
criterio que header.php:81 (un .acai en la raiz = local), en linea y sin
depender de otro fichero.
Para las webs de Forge NO cambia nada: las 131 tienen .acai salvo dos
directorios abandonados de abril sin index.php ni cms/lib/classes, que no
llegan a ser webs. Verificado en villagrancanaria: home, listado y ficha en
200 sin un solo error PHP, listado renderizando y hook en 54 ms.
Con los dos arboles ya identicos, el paso siguiente es que el CMS legacy se
nutra de web-base y dejar de mantener dos copias a mano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1) Un campo `date` con valor vacio rompia TODA insercion en la tabla.
MySQL rechaza la cadena vacia en una columna datetime ("Incorrect datetime
value") tanto si es NOT NULL como si es nullable, y bajo el sql_mode
TRADITIONAL que fuerza Db.php no hay ningun valor "vacio" valido: '' falla,
NULL falla si la columna es NOT NULL, '0000-00-00' falla por NO_ZERO_DATE y
omitir la columna falla por no tener default. Bastaba UN campo fecha para que
dejara de entrar cualquier registro, con un 400 "Error al insertar" sin
detalle.
El Admin clasico nunca se topa con esto porque ya lo resuelve:
menus/default/save.php:287 pone la fecha actual cuando el campo no viene
("default date fields to current date"). AcaiAPI nunca replico esa regla —
coerceValue tiene casos para checkbox, list y multitext, pero no para date.
Ahora en INSERT hace lo mismo que el Admin; en UPDATE omite la columna, para
no pisar con "hoy" un valor ya guardado solo porque venga la clave vacia.
Se descarto la alternativa de crear la columna como NULL DEFAULT NULL: no
arregla ninguna de las tablas ya existentes (el sync copia ficheros, no
ejecuta ALTER) y ademas '' seguiria fallando igual.
2) Auditoria: `updatedDate`/`updatedByUserNum` pasan a respetar el valor que
traiga el caller y generarse solo si no viene. Antes se pisaban siempre
("not overridable by the caller"). Alineado con la intencion del cambio de
Anael en core_client, pero con los guards FUERA del if ($isInsert): dentro,
un UPDATE se quedaba sin sellar la fecha porque no actuaba ninguna rama.
No hay doble escritura: cuando el caller lo trae, lo escribe el bucle de
campos y el guard evita que el bloque de auditoria lo repita en el SET.
3) Seguridad: los ids de las relaciones se escapan antes de interpolarlos en el
IN (...). Salen del valor guardado de un campo `list`, que es un mediumtext
sin validacion, asi que era una inyeccion SQL de segundo orden. Ya estaba
corregido en core_client y faltaba aqui.
Ref: docs/escalados/estado.md 3.1 (repo acai-vscode-plugin)
Los hooks legacy llaman CocoDB::updateRecords(..., null) y el nuevo
buildSetClause tipa $functions como array estricto: TypeError fatal en
PHP 8 (p.ej. pago por transferencia via IPNGenerica). Se normaliza en la
frontera publica (insert/update/get/delete).
PDO con prepares nativos devuelve tipos reales (depth=int 0) mientras que
mysql_* devolvia siempre strings. Los templates y codigo Acai comparan
estrictamente contra strings (is same as('0'), === '1'...) y con tipos
nativos esos filtros fallan en silencio — p.ej. el menu del header vacio
en webs importadas (impulse) porque el filtro depth is same as('0') no
matcheaba int(0). stringifyRows normaliza todos los escalares no-NULL a
string en los 5 paths de lectura que devuelven datos hacia fuera: get(),
uploads, cache de queries de relaciones, traducciones y pluginsConfig.
buildSetClause añadia siempre updatedDate/updatedByUserNum (y en inserts
createdDate/createdByUserNum), pero las tablas raw que se escriben con
ignoreSchema (aux_plg_config, uploads, traducciones...) no tienen esas
columnas y el INSERT/UPDATE moria con Unknown column — p.ej. guardar la
config de un plugin desde el dashboard fallaba con 'No se pudo guardar la
configuracion en la base de datos'. Se replica la semantica del CocoDB
legacy: prepareBaseSQL solo añadia auditoria y defaults cuando habia
schema cargado.
The batched translation preload requires the (tableName, recordNum,
prefix) index; without it the per-record query scans and is slower
than the legacy per-field lookup. t() now checks once per PHP process
(static) whether idx_tabla_registro_prefijo exists, creates it on the
fly when the DB user has ALTER rights (re-checking after a lost race),
and otherwise falls back to the legacy per-field query — never slower
than the original behavior on webs where the index cannot be created.
Verified live: dropped the index on villagrancanaria local, first
request recreated it automatically and subsequent renders stayed fast.
t() issued one SELECT per translated field (1,300+ queries on a typical
multilanguage home). It now preloads all translations of a (table,
record, language) in a single query and serves subsequent fields from
memory, preserving the previous LIMIT 1 semantics (first row per
fieldName). Measured on villagrancanaria: cold render -60% (en home)
/ -67% (es home), byte-identical output.
REQUIRES index (tableName, recordNum, prefix) on cms_traducciones —
without it the batched query scans and is SLOWER than before:
ALTER TABLE cms_traducciones ADD INDEX idx_tabla_registro_prefijo (tableName, recordNum, prefix);
AcaiAPI: PDO-based data access layer (prepared statements, JOIN relation
loading, batch upload fetching) developed on villagrancanaria.
CocoDB_alias declares the CocoDB facade delegating to AcaiAPI; the
legacy implementation stays available as CocoDB_old.