Selaa lähdekoodia

Require observed integer checkbox value for Backup diario

feature/client-backups-summary
psarria 13 tuntia sitten
vanhempi
commit
31c9d92bcc
5 muutettua tiedostoa jossa 30 lisäystä ja 34 poistoa
  1. +1
    -3
      config.example.php
  2. +7
    -7
      docs/BACKUPS-REGRESSION.md
  3. +4
    -4
      docs/BACKUPS.md
  4. +7
    -9
      lib/Backups.php
  5. +11
    -11
      tests/backups.php

+ 1
- 3
config.example.php Näytä tiedosto

@@ -2,9 +2,7 @@
return [
'backups' => [
'enabled' => false,
// Confirm exact runtime types/values on contracted AND uncontracted services first.
'option_values_confirmed' => false,
'active_values' => ['Backup diario' => [], 'Backup diario VPS' => []], // Strict PHP identity (===), no truthiness or coercion.
// Entitlement is fixed in code: applicable Backup diario must be integer 1.
'endpoint' => '', // Fixed HTTPS CGI path without query, fragment or embedded credentials.
'username' => '', // HTTP Basic over verified TLS; confirm account permissions.
'password' => '',


+ 7
- 7
docs/BACKUPS-REGRESSION.md Näytä tiedosto

@@ -1,6 +1,6 @@
# Informe de regresión — 19/09/2026

Implementación local, desactivada. Falta observar la contratación real; este informe no es una aceptación de producción.
Implementación local, desactivada. Contrato de activación observado en la instalación real; backend y página cliente completa siguen pendientes. Este informe no es una aceptación de producción.

## Base y cambios

@@ -18,15 +18,15 @@ Runtime: PHP CLI 8.1.2-1ubuntu2.26, DOM/libxml del paquete Ubuntu `php8.1-xml`,
|---|---|
| Antes del cambio: `php -n tests/run.php` | PASS: 192 aserciones, HTTP simulado |
| Antes del cambio: `node tests/frontend.cjs` | PASS: 24 aserciones |
| Después: `php -n -d extension=/ruta/dom.so tests/backups.php` | PASS: 192 existentes + 137 nuevas |
| Después: `php -n -d extension=/ruta/dom.so tests/backups.php` | PASS: 192 existentes + 142 nuevas |
| Después: `node tests/frontend.cjs` | PASS: 24 aserciones |
| Diagnóstico separado: `php -n test-diagnostico.php /ruta/checkout` | PASS: 35 aserciones |
| Diagnóstico separado: `php -n test-diagnostico.php /ruta/checkout` | PASS: 51 aserciones |
| `php -l` en los 22 PHP del checkout y del diagnóstico | PASS |
| `node --check assets/metrics.js` | PASS, JS sin cambios |
| `git diff --check` | PASS |
| `python3 tools/audit.py` | PASS heurístico; revisión manual descrita abajo |

Cobertura nueva ejecutada: claves exactas y grupos aplicables, no herencia de opciones de otros grupos, duplicados ambiguos, conflicto de ambas claves/tipos con incidencia segura, lista estricta sintética, valores ausentes/nulos/desconocidos, host y límites; detalle correcto/equivocado/resumen/login; tablas y filas reordenadas/unión por ID; contadores/tamaños ausentes; cinco/seis/cero filas; Filled, idle y estado desconocido; fechas y DST ambiguo/inexistente; HTTP 200/301/302/401/403/404/500, retorno falso cURL para fallo TLS/timeout y aborto por tamaño; URL fija y política TLS/redirect; propietario/sin sesión/permiso delegado denegado y permitido con mocks; alteración de ID desde callback real; no consultas denegadas; caché aislada por servicio/host/configuración, revocación, TTL, stale y caducidad; escape y sentinelas públicos.
Cobertura nueva ejecutada: claves exactas y grupos aplicables, no herencia de opciones de otros grupos, duplicados ambiguos, conflicto de ambas claves/tipos con incidencia segura, regla estricta con valores observados integer 1/0, valores ausentes/nulos/desconocidos, host y límites; detalle correcto/equivocado/resumen/login; tablas y filas reordenadas/unión por ID; contadores/tamaños ausentes; cinco/seis/cero filas; Filled, idle y estado desconocido; fechas y DST ambiguo/inexistente; HTTP 200/301/302/401/403/404/500, retorno falso cURL para fallo TLS/timeout y aborto por tamaño; URL fija y política TLS/redirect; propietario/sin sesión/permiso delegado denegado y permitido con mocks; alteración de ID desde callback real; no consultas denegadas; caché aislada por servicio/host/configuración, revocación, TTL, stale y caducidad; escape y sentinelas públicos.

El caso TLS simula el retorno de error de cURL y verifica sus opciones; no realiza un handshake real. El caso de delegado simula el contrato de CurrentUser/permiso; no ejecuta WHMCS. La prueba de integración llama al callback de producción con la frontera de sesión y consultas Capsule simuladas, incluyendo servicio ajeno: no se limita a pasar parámetros que ya se suponen autorizados, pero no ejecuta el router cifrado real.

@@ -38,11 +38,11 @@ Los tres fixtures se copiaron exclusivamente del ZIP saneado proporcionado. Revi

## Pendientes reales — no son PASS

- Los dos IDs de servicio autorizados para diagnóstico y las observaciones de presencia/tipo/valor en contratado y no contratado. Los IDs de definición de opciones y `optiontype=3` no cubren esta verificación.
- Fijar la regla exacta observada, con tests, y retirar el diagnóstico temporal de la instalación si se utiliza. Mientras tanto listas activas vacías y confirmación desactivada.
- Ya ejecutado (no pendiente): observación administrativa de casilla marcada/desmarcada, ambas con clave `Backup diario` presente, tipo `integer`, valores `1` y `0`. Regla exacta y tests incorporados. Diagnóstico deshabilitado y retirado; checksum del módulo original restaurado: `7bc076e93636f2126b28ddf79e24759e88b20da7abe4c700d581c2a13bfc1f5d`.
- Ya ejecutado: WHMCS 8.13.3-release.1, PHP real 8.1.22, autenticación administrativa real en el callback, ausencia de tabla con enabled=false, enlaces administrativos conservados y log PHP sin nuevas entradas durante las comprobaciones. No equivale a validar la autorización cliente/delegada.
- WHMCS/tema/Smarty reales: firma efectiva del callback de botones, permiso `products`, CurrentUser, enrutamiento, propietario/delegados e IDOR, formato horario y semántica del campo exclusivamente administrativo, ausencia en emails/pedido/factura/página completa.
- Cuenta del origen, permisos de solo lectura efectivos, mecanismo de autenticación, endpoint, TLS real, zona horaria y estructura de detalle de un host realmente sin copias.
- Caché fuera de todos los alias HTTP, permisos del usuario PHP, exclusión del CDN/caché de página y verificación entre sesiones.
- Regresión operativa en staging: Graphite/panel/snapshots/firewall/reiniciar/detener/iniciar/suspensión/reactivación. El código existente se conserva y sus pruebas simuladas pasan, pero no se han ejecutado órdenes reales ni se garantiza desde este entorno la instalación productiva.

Instalación y rollback: `BACKUPS.md`. Diagnóstico temporal separado y no integrado en runtime: carpeta `diagnostico` de la entrega, con instrucciones para revisión. La implementación no está habilitada para clientes y no se considera terminada la validación de contratación.
Instalación y rollback: `BACKUPS.md`. Diagnóstico temporal separado y no integrado en runtime: carpeta `diagnostico` de la entrega, con instrucciones para revisión. La implementación no está habilitada para clientes. La pareja activo/inactivo queda confirmada; permanecen pendientes las verificaciones reales enumeradas del backend, permisos y superficie cliente.

+ 4
- 4
docs/BACKUPS.md Näytä tiedosto

@@ -5,11 +5,11 @@ Base: `1174c120dec0982b4c628a353c66cdaeab894b1e`, rama original `main`, árbol o

## Contratación y autorización

Se admiten exclusivamente las claves exactas `Backup diario` y `Backup diario VPS`. Las definiciones comunicadas son Sí/No (`optiontype=3`), pero no se ha observado todavía el tipo PHP/valor de contratación que entrega el módulo. No se han usado ni IDs de opción ni el tipo de definición como indicadores de activación.
El propietario ha unificado las opciones en el nombre exacto `Backup diario`. En WHMCS 8.13.3-release.1 se observaron desde `AdminServicesTabFields` dos casos: marcado → clave presente, tipo PHP `integer`, valor original `1`; desmarcado → clave presente, tipo PHP `integer`, valor original `0`. Los IDs y evidencias particulares se mantienen fuera del repositorio público.

`BackupOption::applicable()` resuelve los grupos vinculados al producto del servicio autorizado a través de `tblhosting.packageid` → `tblproductconfiglinks.pid/gid` → `tblproductconfigoptions.gid`. No usa LIKE ni una selección global del primer resultado. Las definiciones duplicadas de una clave o un tipo no confirmado cierran el acceso. Si las dos claves están presentes con valores no idénticos (`!==`), se deniega y se registra `OPTION_CONFLICT` con el ID del servicio.
La regla fija exige integración habilitada con `enabled === true`, resolución aplicable exactamente `['Backup diario']`, presencia mediante `array_key_exists` y valor `=== 1`. No hay lista configurable de valores activos ni conversiones. Se deniegan `0`, `"1"`, `true`, `1.0`, null, arrays, claves ausentes y cualquier valor desconocido. El ejemplo conserva `enabled=false`: la confirmación de valores no autoriza un despliegue ni valida el origen de copias.

La regla provisional es cerrada: `enabled` y `option_values_confirmed` deben ser exactamente `true`; cada clave aplicable debe existir mediante `array_key_exists`, contener string, entero o booleano y coincidir con identidad estricta con un elemento de su propia lista `active_values`. Ambas listas se entregan vacías. Las pruebas usan `SYNTHETIC_ACTIVE`, nunca como supuesto valor real. Las claves ausentes, `null`, arrays, floats y textos desconocidos no conceden acceso. Se fijará la regla observada y sus tests tras el diagnóstico administrativo separado; no extrapolar automáticamente entre nombres sin evidencia.
`BackupOption::applicable()` resuelve únicamente los grupos vinculados al producto del servicio autorizado mediante `tblhosting.packageid` → `tblproductconfiglinks.pid/gid` → `tblproductconfigoptions.gid`. No usa LIKE, ni el primer resultado global, ni IDs de opciones para conceder acceso. Las definiciones duplicadas o no Sí/No fallan cerradas. El nombre antiguo `Backup diario VPS` se conserva solo en la detección de definiciones/conflictos: no tiene una regla de activación validada y no concede acceso. Si ambas claves están presentes con valores no idénticos (`!==`), se registra `OPTION_CONFLICT` con el ID y se deniega, sin elegir arbitrariamente.

La entrada es una función de módulo invocada por WHMCS. El registro `ClientAreaCustomButtonArray` conserva los cinco botones existentes y añade Backups solo si es elegible. La función `vpsmanager_Backups` vuelve a comprobarlo. Antes de caché/HTTP, `BackupAccess` comprueba CurrentUser, cuenta de cliente, permiso `products`, y pertenencia del servicio en la base de datos. No confunde el usuario delegado con el ID de la cuenta cliente. Versiones sin estas interfaces fallan de forma cerrada. La autorización efectiva del router WHMCS y sus accesos delegados debe comprobarse en staging de la versión instalada.

@@ -21,7 +21,7 @@ La entrada es una función de módulo invocada por WHMCS. El registro `ClientAre
4. Configurar el endpoint HTTPS fijo del CGI, sin query/fragmento/credenciales embebidas, y autenticación HTTP Basic sobre TLS verificado. Si la instalación usa otro mecanismo, no activar hasta adaptar/probarlo. Confirmar permisos efectivos de la cuenta: no ser administrador no demuestra solo lectura. Si la interfaz concede acciones, restringir adicionalmente el acceso de integración a la ruta y consulta de host previstas mediante un proxy/política revisados; no se ha instalado tal restricción.
5. Confirmar la zona horaria IANA del origen. Crear un directorio de caché privado, modo 0700, accesible al usuario PHP y fuera de todas las raíces/alias web; configurar `cache_directory`. Los archivos usan 0600. Directorios públicos o permisivos se rechazan; un error de caché no impide la consulta, pero registra `CACHE` y elimina la ventaja de caché entre peticiones. Programar limpieza administrativa de JSON antiguos en ese directorio si cambian muchos servicios/mapeos: no se incluye un cron automático.
6. Por producto, comprobar si ya existe `backuppc_host`. Crear solo si falta, sin duplicados ni migraciones de dominio: texto, exclusivamente administrativo, no pedido, no factura, no campo de comunicaciones. Guardar un mapeo explícito. El código exige `adminonly='on'`, `showorder=''`, `showinvoice=''`, tipo `text`; confirmar estos valores de esquema en la versión instalada. No modificar campos automáticamente. La plantilla completa y los emails pueden tener personalizaciones: verificar que nunca exponen el nombre ni el valor.
7. Ejecutar el diagnóstico temporal administrativo revisado para un servicio contratado y otro no contratado, limitado a sus dos IDs. Retirarlo al obtener resultados. Registrar solo clave, presencia, tipo y valor; completar las listas exactas y sus tests antes de confirmar/activar la integración.
7. La pareja activo/inactivo ya se observó en esta instalación y el diagnóstico fue deshabilitado y retirado, restaurando el módulo original con checksum verificado. Antes de activar, comprobar que no ha cambiado el nombre, tipo de opción ni versión/comportamiento de WHMCS. Si cambia el contrato, mantener desactivado y repetir una observación administrativa acotada; no ampliar la regla por aproximación.
8. En staging, comprobar contratado/no contratado, servicio ajeno cambiando ID, sin sesión, delegado permitido y denegado, revocación, cambio de mapeo y conflicto de nombres. Comprobar la página completa, DOM, HTML, emails y solicitudes del navegador: ninguna referencia al proveedor, host o credenciales; solo rutas WHMCS para esta función.
9. Probar fallo del backend y aislamiento respecto a los controles existentes. Desactivar cualquier caché HTTP/CDN de páginas privadas y confirmar que nunca comparte respuestas entre cuentas. El callback añade `Cache-Control: private, no-store`.
10. Activar únicamente tras superar esas comprobaciones y la revisión operativa. No iniciar, borrar, parar, descargar ni restaurar copias para validar esta lectura.


+ 7
- 9
lib/Backups.php Näytä tiedosto

@@ -24,15 +24,13 @@ final class Backups
self::diagnostic('OPTION_CONFLICT', (int) ($params['serviceid'] ?? 0));
return false;
}
if (($config['enabled'] ?? false) !== true || ($config['option_values_confirmed'] ?? false) !== true || !$keys) return false;
foreach ($keys as $key) {
if (!in_array($key, BackupOption::NAMES, true) || !array_key_exists($key, $options)) return false;
$value = $options[$key];
if (!is_string($value) && !is_int($value) && !is_bool($value)) return false;
$allowed = $config['active_values'][$key] ?? [];
if (!is_array($allowed) || !in_array($value, $allowed, true)) return false;
}
return true;
// Observed in WHMCS 8.13.3: checked => integer 1, unchecked => integer 0.
// The installation now uses the single exact name Backup diario.
// Legacy aliases remain detectable for conflicts, but do not grant access.
return ($config['enabled'] ?? false) === true
&& $keys === ['Backup diario']
&& array_key_exists('Backup diario', $options)
&& $options['Backup diario'] === 1;
}
public static function diagnostic(string $code, int $service): void
{


+ 11
- 11
tests/backups.php Näytä tiedosto

@@ -14,10 +14,10 @@ $dir = sys_get_temp_dir() . '/copies-test-' . bin2hex(random_bytes(8));
mkdir($dir, 0700);
$temp = tempnam(sys_get_temp_dir(), 'copies-config-');
putenv('O6H_VPSMANAGER_CONFIG=' . $temp);
$config = ['enabled' => true, 'option_values_confirmed' => true, 'active_values' => ['Backup diario' => ['SYNTHETIC_ACTIVE'], 'Backup diario VPS' => ['SYNTHETIC_ACTIVE']],
$config = ['enabled' => true,
'endpoint' => 'https://PRIVATE-ENDPOINT.invalid/cgi', 'username' => 'FAKE_USER', 'password' => 'FAKE_SECRET_SENTINEL',
'source_timezone' => 'Europe/Madrid', 'cache_directory' => $dir];
$params = ['serviceid' => 42, 'configoptions' => ['Backup diario' => 'SYNTHETIC_ACTIVE'],
$params = ['serviceid' => 42, 'configoptions' => ['Backup diario' => 1],
'customfields' => ['backuppc_host' => 'UNTRUSTED_PARAMS.invalid']];
$GLOBALS['actor'] = new BackupTestClient(7);
$GLOBALS['db'] = [
@@ -39,14 +39,14 @@ function tableWith(DOMXPath $xp, $header) {
}
try {
writeConfig($temp, ['backups' => $config]);
check(Backups::entitled($params, $config, ['Backup diario']), 'explicit synthetic active string');
foreach ([null, false, true, 0, 1, 1.0, '1', 'on', 'yes', 'No', [], new stdClass(), 'unknown'] as $value) {
check(Backups::entitled($params, $config, ['Backup diario']), 'observed active integer 1');
foreach ([null, false, true, 0, 2, -1, 1.0, '1', '0', 'on', 'yes', 'No', [], new stdClass(), 'unknown'] as $value) {
$p = $params; $p['configoptions']['Backup diario'] = $value;
check(!Backups::entitled($p, $config, ['Backup diario']), 'unknown type/value denied');
}
check(!Backups::entitled([], $config, ['Backup diario']), 'missing option');
check(!Backups::entitled($params, [], ['Backup diario']), 'default closed');
check(!Backups::entitled($params, array_replace($config, ['option_values_confirmed' => false]), ['Backup diario']), 'confirmation gate');
check(!Backups::entitled($params, array_replace($config, ['enabled' => false]), ['Backup diario']), 'integration disabled gate');
foreach (['', '.', '..', '/etc', 'abc/def', 'a%2fb', 'a?host=b', 'a&action=x', "a\n", ' a', str_repeat('a', 65), [], null] as $value) check(!Backups::validHost($value), 'invalid host');
foreach ([$host, 'a', 'a_B-9.example', str_repeat('a', 64)] as $value) check(Backups::validHost($value), 'valid host');
// Exact names and product/group resolution: no global LIKE or first-row choice.
@@ -56,14 +56,14 @@ try {
check(\VPSManager\BackupOption::applicable(999) === [], 'no linked group');
$GLOBALS['db']['tblproductconfigoptions'][0]->optionname = 'Backup diario VPS';
check(\VPSManager\BackupOption::applicable(3) === ['Backup diario VPS'], 'VPS exact name');
$p = $params; $p['configoptions'] = ['Backup diario VPS' => 'SYNTHETIC_ACTIVE'];
check(Backups::entitled($p, $config, ['Backup diario VPS']), 'VPS synthetic active');
$p = $params; $p['configoptions'] = ['Backup diario VPS' => 1];
check(!Backups::entitled($p, $config, ['Backup diario VPS']), 'legacy name not confirmed and no longer applicable');
check(!Backups::entitled($params, $config, ['Backup diario VPS']), 'inapplicable key cannot grant');
$p['configoptions']['Backup diario'] = 'SYNTHETIC_INACTIVE';
check(!Backups::entitled($p, $config, ['Backup diario VPS']), 'conflicting aliases denied');
check(strpos(end($GLOBALS['safeLogs']), 'OPTION_CONFLICT') !== false, 'conflict safe admin diagnostic');
$p['configoptions']['Backup diario'] = 'SYNTHETIC_ACTIVE';
check(Backups::entitled($p, $config, ['Backup diario', 'Backup diario VPS']), 'both require explicit same active value');
$p['configoptions']['Backup diario'] = 1;
check(!Backups::entitled($p, $config, ['Backup diario', 'Backup diario VPS']), 'multiple names cannot override canonical resolution');
$p['configoptions']['Backup diario'] = 1;
$p['configoptions']['Backup diario VPS'] = '1';
check(!Backups::entitled($p, $config, ['Backup diario VPS']), 'type conflict denied');
@@ -129,7 +129,7 @@ try {
$page = vpsmanager_Backups($params);
check(strpos($page['vars']['copiesHtml'], '40,68 GiB') !== false, 'controller renders');
$requests = count($GLOBALS['calls']);
foreach ([[], ['Backup diario' => 'UNKNOWN']] as $option) { $p = $params; $p['configoptions'] = $option; check(strpos(vpsmanager_Backups($p)['vars']['copiesHtml'], 'temporalmente') !== false, 'revocation denies cached data'); }
foreach ([[], ['Backup diario' => 'UNKNOWN'], ['Backup diario' => 0], ['Backup diario' => '1'], ['Backup diario' => true]] as $option) { $p = $params; $p['configoptions'] = $option; check(strpos(vpsmanager_Backups($p)['vars']['copiesHtml'], 'temporalmente') !== false, 'revocation denies cached data'); }
$p = $params; $p['serviceid'] = 43;
check(strpos(vpsmanager_Backups($p)['vars']['copiesHtml'], 'temporalmente') !== false, 'changing service id denied by owner DB check');
$GLOBALS['actor'] = null;
@@ -167,7 +167,7 @@ try {
check((fileperms($f) & 0077) === 0, 'cache file private');
}
if ($path = getenv('BACKUPS_RENDER_OUTPUT')) file_put_contents($path, $render);
echo 'PASS: ' . ($count - $before) . " backup assertions; synthetic entitlement, WHMCS/HTTP mocks.\n";
echo 'PASS: ' . ($count - $before) . " backup assertions; observed integer entitlement values; WHMCS/HTTP mocks.\n";
} finally {
foreach (glob($dir . '/*') as $f) unlink($f);
rmdir($dir); unlink($temp); putenv('O6H_VPSMANAGER_CONFIG');


Loading…
Peruuta
Tallenna