## 31/08/2026 — Fix campi API username/password non editabili in prod (solo UI)
- **Sintomo utente**: installato il plugin sul server produttivo, nel form Server non si vede il campo API username e il campo API password è bloccato (asterischi fissi non editabili), anche per l'utente che ha installato il plugin.
- **Causa**: divergenza nel controllo diritti. I front usavano `Profile::canCurrentUser()` (legge i diritti da `glpi_profilerights` su DB), ma `Server::showFormFields()` (form server) usava `Session::haveRight(self::$rightname, UPDATE)` per calcolare `$canEdit` (e idem `rawSearchOptions()` riga 308 e `showUnlinkedClientsTab()`/`showMissingClientsTab()` righe 1155/1222). `Session::haveRight` dipende dalla **cache dei diritti in sessione** (`$_SESSION['glpiactiveprofile']['rights']`), popolata al login. Dopo l'installazione/aggiornamento del plugin i diritti sono scritti in DB ma la **sessione corrente non viene ricostruita**, quindi `haveRight` resta false → ramo read-only: username stampato come testo (invisibile se vuoto), password `******` fissa.
- **Fix**: sostituito `Session::haveRight(self::$rightname, ...)` con `Profile::canCurrentUser(...)` in Server.php (righe ~308, ~447-449, ~1155, ~1222) e AssetTab.php (riga ~446, tab Asset), coerente col resto del plugin, legge dal DB e aggira la cache di sessione stantia.
- Nessun bump versione (nessuna modifica DB, regola 6); nessuna nuova stringa i18n.
- Verifica IA: `php -l` OK; git diff autorevisione OK. **Verifica UI utente OBBLIGATORIA**: aggiornare il file su prod, poi testare il form Server (API username editabile + API password campo input con placeholder).
## 25/08/2026 — Blocco "This asset hosts the UrBackup server" in evidenza (solo UI)
- **Richiesta utente**: mettere in evidenza con riquadro colorato il blocco host + nome del server UrBackup.
- **Modifiche**: `AssetTab.php::showHostServerBlock()` — classi dedicate `plugin-urbackup-host-card` / `-card-header` sul card Bootstrap e `plugin-urbackup-host-server-link` sui link server; `public/css/urbackup.css` — card con bordo 2px #1e6091, header blu #1e6091 testo bianco, body #eaf3fa, link server bold 1.05rem.
- Nessun bump versione (nessuna modifica DB, regola 6); nessuna nuova stringa i18n.
- **Sintomo utente**: scelto il tipo host (es. Computer) nel form server, il dropdown degli elementi non compariva.
- **Causa 1 (405)**: `Ajax::updateItemOnSelectEvent` genera `$("#target").load(url, {params})` — jQuery `.load()` con data **oggetto** invia **POST**, mentre `front/dropdown_host.ajax.php` era GET-only (405) → div mai popolato. (Nota: il CSRF non era il blocco per `.load()`: il global `$(document).ajaxSend` in public/js/common.js aggiunge `X-Glpi-Csrf-Token` a OGNI POST AJAX, letto da `getAjaxCsrfToken()` sul `<meta property="glpi:csrf_token">`.)
- **Causa 2 (500)**: `Session::getMatchingActiveEntities()` richiede esattamente 1 argomento (è un **filtro** di entity, `@since 10.0.13`) → usare `Session::getActiveEntities()` (Session.php:2194).
- **Causa 3 (parametro sbagliato)**: in `Dropdown::show` il parametro per limitare le entità è **`entity`** (default -1, riga 133), NON `entity_restrict` (che è solo l'output serializzato del config, riga 287).
- **Causa 4 (scriptBlock NON emesso)**: `Html::scriptBlock()` in GLPI 11 **ritorna** la stringa (`return sprintf(...)`, Html.php:5145) — serve `echo Html::scriptBlock(...)`; senza echo lo script sparisce silenziosamente.
- **Fix**: JS inline nel form via `echo Html::scriptBlock(...)`: `$(document).on('change', '#dropdown_host_itemtype$rand', ...)` → `$.get(PLUGIN_URBACKUP_WEB_DIR.'/front/dropdown_host.ajax.php', {itemtype, value:0})` → `.html()` del div `urbackup_host_items$rand` (GET = bodyless → nessun check CSRF; jQuery esegue gli script inline iniettati, incluso il `$(function(){})` del config select2). Rimosso `use Ajax;` (non più usato).
- **Lezione GLPI 11 — i dropdown sono LAZY**: `Dropdown::show()` NON renderizza più le `<option>`: genera un componente `Html::jsAjaxDropdown` (select2) con `params` JSON + `_idor_token`; i valori arrivano via **POST** a `/ajax/getDropdownValue.php` (`setupAjaxDropdown` in public/js/common.js: `type:"POST"` + `ajaxSend` globale per il CSRF). Il markup contiene solo l'option vuota finché l'utente non apre/cerca.
- **Lezione IDOR**: `Session::getNewIDORToken` NON ruota (accumula token validi 2h, `cleanIDORTokens`); `validateIDOR` confronta le chiavi salvate (itemtype, entity_restrict, displaywith, condition) con la POST. Il config JS serializzato da jQuery manda `entity_restrict` con la stessa forma salvata nel token → match. **Non è possibile testare via curl** la chiamata a getDropdownValue (la serializzazione jQuery degli array `[]` → `chiave=` diverge da `-d @file.json` → validateIDOR false) → test e2e in CLI: generare il dropdown con `Dropdown::show` reale, estrarre il config dal markup, ricostruire la POST → `validateIDOR: true` (provato con `/tmp/opencode/idor_e2e.php`; il "No active session" dopo è limite del bootstrap CLI, non del flusso).
- **Stato**: `php -l` OK; form contiene lo script `$.get` (verificato su server.form.php?id=1: 200); endpoint GET 200 con select+config+IDOR; validateIDOR true via `/tmp/opencode/idor_e2e.php`; verifica visiva in browser = **utente** (regola 7).
- **Richiesta utente**: collegare il "UrBackup Server" del plugin con il Computer / custom Asset dell'inventario che lo ospita (analogo a computer↔monitor) per trovare l'hardware fisico su cui gira il server.
- **Design (decisioni utente)**: (1) itemtype host = SOLO tipi con capacità attiva (`Config::getEnabledItemtypes()`); (2) blocco "Questo asset ospita il server UrBackup" SEMPRE visibile sull'asset host, anche se l'asset è anche client; (3) niente search option in questo giro (MVP).
- **Modifica DB**: `glpi_plugin_urbackup_servers` + `host_itemtype VARCHAR(255) DEFAULT NULL` (after comment) + `host_items_id INT UNSIGNED NOT NULL DEFAULT 0` (after host_itemtype) + KEY `host_asset` (host_itemtype,host_items_id) — NIENTE unique (più server per host ammessi, es. VM). Migrazione idempotente `plugin_urbackup_install_update_servers_table()` in install.php + empty.sql. `glpi_plugin_urbackup_serverassets` NON toccata (semantica CLIENT). → **bump 0.7.2 → 0.7.3** (regola 6).
- **Server.php**: riga "Hardware host" in `showFormFields()` dopo la riga Comments (`Dropdown::showItemTypes('host_itemtype', Config::getEnabledItemtypes(), ...)` + `echo Html::scriptBlock(...)` con `$.get` su `dropdown_host.ajax.php` al `change` del select tipo — vedi sezione Fix + div `urbackup_host_items$rand` pre-popolato con `Dropdown::show` con `entity => Session::getActiveEntities()` + link all'host); validazione in `prepareInputForUpdate()` (itemtype vuoto/items_id ≤0/class mancante/`!isItemtypeEnabled`/item inesistente → azzera entrambi); `getHostAsset(): ?CommonDBTM`; `getServersHostingAsset(string, int): array` con guard `$DB->fieldExists(self::getTable(),'host_itemtype')` (robustezza pre-migrazione — pattern di Config::getEnableComputer).
- **AssetTab.php**: `showHostServerBlock(CommonDBTM $item)` chiamata per prima in `displayTabContentForItem()` — card "This asset hosts the UrBackup server" con lista link (`Server::getFormURLWithID`) dai server che puntano all'asset; sempre visibile se c'è un host.
- Locales: +2 msgid per lingua (182→184): `Hardware host` (it "Host hardware", de "Hardware-Host", en "Hardware host"), `This asset hosts the UrBackup server` (it "Questo asset ospita il server UrBackup", de "Dieses Asset hostet den UrBackup-Server", en identica); header .po → 0.7.3; `.mo` ricompilati (msgfmt --check OK, warning header pre-esistenti) + **cache traduzioni svuotata** (`files/_cache/*/translations/`).
- **Verifiche IA**: `php -l` OK (setup.php, install/install.php, Server.php, AssetTab.php, dropdown_host.ajax.php); test CLI `/tmp/opencode/urbackup_host_test.php` 10/10 PASS (prepareInputForUpdate: host non inviato/items_id=0/itemtype vuoto/Monitor non abilitato/Computer valido/items_id inesistente; getHostAsset null; getServersHostingAsset SKIP in attesa migrazione).
- **Checklist UI utente 0.7.3 (verifica finale)**: (1) *Admin → Server* → aprire un server → "Hardware host": tipo → elemento (dropdown selezionabile) → Salva; (2) ricaricare il server: link host sotto la riga; (3) aprire l'asset host → card "Questo asset ospita il server UrBackup" con link al server; (4) verificare traduzioni it_IT/DE ("Host hardware", "Questo asset ospita il server UrBackup"); (5) smoke HTTP dropdown: GET `front/dropdown_host.ajax.php?itemtype=Computer` (sessione) → 200 select; itemtype=Monitor → rifiutato; senza sessione → 302/403.
- **Doc aggiornate**: README.md Changelog 0.7.3; GLPIDEV.md (schema v0.7.3 + caveat 21 con le API caveat dropdown lazy/IDOR + nota api_password cifrata); MEMORY.md (questo file).
## 06/08/2026 — Linguette interne tab sempre su una riga (desktop)
- **Richiesta utente**: le linguette "Stato Client / Azioni / Info-Log" risultavano a capo una sopra l'altra anche su monitor normale → fix CSS in `public/css/urbackup.css` (nessun bump: solo UI).
- **Indagine**: markup già `ul.nav.nav-tabs#urbackupTabs` (row di default); test headless chromium con CSS reali (tabler.min.css + css_glpi.min.css + urbackup.css) e wrapper reale (`card-tabs` + `#tabspanel` + `tab-content p-2 flex-grow-1 card`) mostrava affiancate → la causa nel browser utente non è riproducibile (probabile flex non applicato/cache/zoom); fix deterministico con regola dedicata.
- **Fix**: `.plugin-urbackup-inner-tabs .nav-tabs { display:flex; flex-direction:row; flex-wrap:nowrap; overflow-x:auto }` + `.nav-item { display:inline-flex; flex:0 0 auto }`; media query `max-width: 575.98px` → `flex-wrap:wrap` (comportamento mobile invariato, come richiesto dall'utente).
- **Iterazione 2 (sintomo: linguette su una riga ma estese per tutta la pagina, 3 bottoni oltre lo schermo = width:100%/flex-grow esterni)**: override deterministico con `!important` su `.nav-tabs` (display/flex-direction/flex-wrap) + `.nav-item` e `.nav-link` (`flex:0 0 auto !important`, `width:auto !important`, `max-width:none !important`, `white-space:nowrap`) — nessuna regola esterna può più allargarli. Verifica headless: w=118/82/103, flex=0 0 auto, xs=8/129/214, nessun overflow.
- **⚠️ Lezione test**: una pagina GLPI scaricata via curl e riaperta come `file://` NON applica i CSS (href relativi) → rendering con soli user-agent defaults (font "Times New Roman", ul block, li list-item, a inline) → test INVALIDO. Usare sempre pagine di test con href CSS assoluti (es. http://localhost/...).
## 06/08/2026 — RISOLTO: nuove chiavi i18n non tradotte nel browser (cache catalogo traduzioni GLPI)
- **Sintomo**: "API status" e "Client State" (chiavi nuove) restavano in inglese nel browser; le chiavi pre-esistenti ("Stato client", "Stato UrBackup") erano tradotte — i `.mo` erano corretti (msgunfmt/msgfmt --check OK).
- **Causa radice**: GLPI 11 `Session::loadLanguage()` (src/Session.php:816+) crea `$TRANSLATE` (anonima class che estende `Laminas\I18n\Translator\Translator`) con cache catalogo via `I18nCache` (src/Glpi/Cache/I18nCache.php) → `CacheManager::getTranslationsCacheInstance()` (CONTEXT_TRANSLATIONS) → `FilesystemAdapter` su `files/_cache/<env-hash>/translations/` con **TTL 0 = infinito e NIENTE invalidazione su mtime**: dopo la ricompilazione dei `.mo`, il vecchio catalogo continua a essere servito finché la cache non viene svuotata. (Il path contiene l'hash della versione GLPI, es. `11.0.8-53eff695-production` — cambia a ogni bump di GLPI.)
- **Fix**: eliminare i file in `files/_cache/<env-hash>/translations/` (12 file) → la cache si rigenera al primo caricamento con i `.mo` nuovi.
- **Regola operativa**: dopo OGNI ricompilazione dei `.mo` del plugin, svuotare `files/_cache/*/translations/` (non è un'azione UI — la regola 7 NON si applica; è manutenzione cache).
- **Verifica HTTP (login 06/08)**: GET `/ajax/common.tabs.php?_glpi_tab=GlpiPlugin%5CUrbackup%5CAssetTab%241&_itemtype=Computer&id=1` (sessione glpi/glpi) → "Stato Client" (tab nav), "Stato client" (header sezione), "Stato API" + badge "Connessione API OK". Nota: il parametro è `_itemtype` (non `_glpi_itemtype`); il token CSRF di login in GLPI 11.0.8 sta in `<meta property="glpi:csrf_token">` e i campi form sono `login_name`/`login_password`; il POST va a `/front/login.php`.
## REGOLA PERMANENTE — Verifica UI da parte dell'utente
- **Tutte le azioni che l'utente normalmente esegue dalla UI di GLPI** (update/attivazione plugin, toggle di configurazione, link/unlink asset, test connessione, azioni backup) DEVONO essere eseguite dall'**utente** per verificarne il funzionamento reale — l'IA NON le esegue al suo posto (né via console `glpi:plugin:*`, né via HTTP/curl con sessione).
- **Per i bump di versione (regola 6)**: l'IA consegna codice + migrazioni idempotenti + bump `PLUGIN_URBACKUP_VERSION`, poi **l'utente** esegue l'update dalla UI: il plugin viene marcato `NOTUPDATED` e deattivato → *Configurazione → Plugin* → pulsante **"Aggiorna"** → **"Attiva"** → verifica della feature con la checklist fornita dall'IA.
- Verifiche che NON passano dalla UI (es. `php -l`, bootstrap CLI, query DB) restano compito dell'IA.
- ⚠️ Contesto storico: l'update 0.7.1 e 0.7.2 fu eseguito dall'IA via console (`glpi:plugin:install`/`activate`) — comportamento NON più ammesso da questa regola (istituita il 05/08/2026).
## REGOLA PERMANENTE — Gestione versione per modifiche DB (standard GLPI)
- **Da ora in poi**: OGNI modifica che tocca il database (nuove tabelle/colonne/indici, migrazioni di dati, cambi di default) DEVE essere accompagnata da un **incremento di `PLUGIN_URBACKUP_VERSION`** in `setup.php` (regola 6 di AGENTS.md).
- **Meccanismo GLPI verificato** (`src/Plugin.php`): `checkPluginState()` (~righe 909-933) confronta `plugin_version_urbackup()['version']` con `glpi_plugins.version`; se diverse → `state = NOTUPDATED` e plugin DEATTIVATO ("update process has to be launched").
- **Procedura update**: meccanismo standard GLPI `php bin/console glpi:plugin:install urbackup` (da `/var/www/glpi`) → `plugin_urbackup_install()` con migrazioni idempotenti (`new Migration(PLUGIN_URBACKUP_VERSION)`), poi `php bin/console glpi:plugin:activate urbackup` (l'install imposta `NOTACTIVATED`) — **MA l'esecuzione è dell'utente via UI (regola 7)**: *Configurazione → Plugin* → "Aggiorna" → "Attiva". I comandi console restano solo riferimento documentale.
- Aggiornare sempre anche: `README.md` (Changelog) e header `Project-Id-Version` dei `.po` (+ ricompilazione `.mo` + svuotamento cache traduzioni).
- **Bugfix (regressione fix XSS 05/08)**: `showStateSection()` — `$internetModeDisplay` (HTML badge) finiva nell'array `$rows` e il loop generico lo passava da `htmlspecialchars()` → si vedeva il testo letterale `<span class="badge bg-secondary">No</span>`. Fix: riga dedicata per "Internet mode" fuori dal loop (badge raw, label escapata, contenuto escapato in costruzione) — pattern già usato per l'authkey; la protezione XSS sui valori API resta intatta. Audit: unico caso nel plugin (Server.php/Config.php echo direttamente i badge).
- **Opzione A — card Bootstrap** in `showServerLinkedBlock()`: la tabella 2×4 "UrBackup status" è ora una `card` con header + 4 card (row-cols-1/2/4): Linked server (link + `protocol://ip:port`), **API status** (badge verde/rosso da `last_api_status` + messaggio + `last_api_check`), server version (dash se vuoto), Client name. Valori `fw-bold`, label `text-uppercase text-muted small`; nessun CSS custom, solo utility Bootstrap 5.
- **Rename tab interna**: `__('State', 'urbackup')` → `__('Client State', 'urbackup')` (it "Stato Client", de "Client-Status", en "Client State"); msgid orfana `State` rimossa dai 3 `.po`.
- **Modifica DB**: nuova riga `enable_computer` ('1') in `glpi_plugin_urbackup_configs` (migrazione idempotente `plugin_urbackup_install_add_enable_computer_config()` in install.php + INSERT in empty.sql) → bump `PLUGIN_URBACKUP_VERSION``0.7.1` → `0.7.2` (regola 6 AGENTS.md).
- **Perché prima "Computer sempre abilitato"**: `Config::isItemtypeEnabled()` hardcoded `return true` per `Computer` + `registerClass(AssetTab, addtabon Computer)` incondizionato in setup.php + badge statico in `showForm()`.
- **Config.php**: nuovo `getEnableComputer()` (cache statica, guard `TableExists`, fallback `true` se riga/tabella assente), `isItemtypeEnabled('Computer')` e `getEnabledItemtypes()` ora rispettano il toggle; nuovo `getEnabledAssetDefinitions()` — lista Asset Definition con capacità UrBackup via `hasCapacityEnabled()` con istanza da `getAvailableCapacities()` (fallback: decode raw JSON `capacities` → `array_column('name')`).
- **setup.php**: `registerClass(AssetTab, addtabon Computer)` solo se `Config::getEnableComputer()`; per le Asset Definition la registrazione tab resta in `UrBackupCapacity::onClassBootstrap()` (non toccata).
- **front/config.form.php**: reintrodotto handler POST `update` (CSRF gestito dal listener globale GLPI 11 — niente `Session::checkCSRF()`): upsert `enable_computer`, `Session::addMessageAfterRedirect`, redirect a `Config::getFormURL()`; form con `Dropdown::showYesNo` + `Html::hidden('_glpi_csrf_token', ...)` + `Html::submit(__('Save'))`.
- **Nuova sezione config page**: "Custom assets with \"Urbackup\" capacity enabled" (it: `Asset custom con Capacità "Urbackup" attivata`, de: `Benutzerdefinierte Assets mit aktivierter Kapazität "Urbackup"`) — colonne Name / System name / Active (badge Yes/No); su DB locale: definizioni **Server** e **NAS** con capacità attiva.
- **Comportamento a Computer disattivato**: tab nascosto (doppia protezione: registrazione condizionale + gate in `AssetTab::getTabNameForItem/displayTabContentForItem`), MassiveActions bloccate (hook.php:47), `front/asset.form.php` rifiuta, unlinked/missing clients escludono Computer; i link esistenti in `glpi_plugin_urbackup_serverassets` restano intatti (solo visibilità UI).
- Locales: +5 msgid (180 tradotti + header), header `Project-Id-Version` → 0.7.2, `.mo` ricompilati con `msgfmt --check` (OK per it/de/en).
### 1. Claim "flussi API non testabili in locale" ERRATO — verificato
-`urbackupsrv` (pid ~1282) è attivo su `0.0.0.0:55414`; endpoint `/x?a=salt` e **login 2-fasi verificati funzionanti** (`admin`/`12345678`): PBKDF2 hash calcolato e `"success":true`.
- La frase "nessun server UrBackup reale raggiungibile in locale" era copiata dalle docs senza riverifica → corretta in MEMORY.md, AGENTS.md, GLPIDEV.md e `urbackup-api-actions.md`.
### 2. Cifratura `api_password` con GLPIKey (punto ⚠️ 1 risolto)
- **API core corretta**: `(new GLPIKey())->encrypt()/decrypt()` — `GLPIKey::getInstance()`**NON esiste** in GLPI 11.0.8 (pattern usato dal core: `APIRest.php:623`, `ClientRepository.php:90`).
-`src/Server.php`: nuovo `getApiPassword()` (decrypt on-the-fly con fallback legacy in chiaro) + `isApiPasswordEncrypted()` (rileva formato base64+nonce ≥ 24B, evita warning `trigger_error` di `decrypt()` su valori legacy).
-`prepareInputForUpdate()`: campo vuoto → `unset` (mantiene password, fix di un wipe involontario della password ad ogni save); campo valorizzato → cifrato con `(new GLPIKey())->encrypt()`.
-`src/UrbackupApiClient.php`: `__construct()` usa `$server->getApiPassword()` quando `$server instanceof Server`.
-`install/install.php`: nuova migrazione idempotente `plugin_urbackup_install_encrypt_api_passwords()` che cifra le password legacy in chiaro (salta valori già cifrati; in caso di chiave non disponibile lascia il valore invariato per non perdere dati).
- Nota: le righe già in chiaro rimangono usabili finché non avviene il save o l'install/update del plugin (fallback in `getApiPassword()`).
### 3. Test connessione API solo a parametri cambiati (punto ⚠️ 2 risolto)
-`prepareInputForUpdate()` ora esegue `testConnection()` SOLO se cambiano `ip_address|port|protocol|api_username|api_password|ignore_ssl`; salvataggi "neutri" non innescano più richieste API da 30s.
### 4. Loop opcache solo in development (punto ⚠️ 3 risolto)
-`setup.php`: loop `opcache_invalidate()` attivo solo se `defined('GLPI_ENVIRONMENT_TYPE') && GLPI_ENVIRONMENT_TYPE === 'development'` (enum `Glpi\Application\Environment`, valori `production|staging|testing|e2e_testing|development`; default produzione).
### 5. Fix N+1 in `showUnlinkedClientsTab()` (punto ⚠️ 4 risolto)
- Batch loading nomi asset: 1 query per itemtype (map `itemtype-id`), sostituisce `ServerAsset::getAssetName()` per riga (2 query/riga prima).
- I 3 `.po` erano fermi alla v0.4.0 (69 msgid vs 176 chiavi nel codice). Aggiunte **115 msgid mancanti** con traduzioni complete (it/de/en), rimosse 9 orfane (es. "Checking...", "Root location ID", "Server unreachable", "UrBackup rights"), header `Project-Id-Version` → 0.7.0.
-`.mo` ricompilati con `msgfmt --check` (tutti "175 messaggi tradotti").
- Audit hardcoded: nessuna stringa utente fuori da `__()/_n()/_x()` (solo tag HTML nei raw echo); `_x('button', 'Save')` nel Twig gestito correttamente.
### Verifiche
-`php -l` OK su tutti i file toccati (Server.php, UrbackupApiClient.php, setup.php, install/install.php).
- Roundtrip cifratura/decifratura verificato via bootstrap GLPI CLI.
- Smoke test HTTP post-modifica: pagine server/config caricate senza errori.
### Da fare residuo
- (nessuno dei 4 punti ⚠️ precedenti) — i test REALI su server semi-produttivo restano consigliati, ma i flussi API sono ora verificabili anche in locale (localhost:55414).
## 05/08/2026 — Verifica codice, rimozione codice morto e audit sicurezza
### Codice morto rimosso (verificato con grep su tutto il progetto)
1.**`src/Server.php`**: rimossi `testApiConnection()` + `isNetworkError()` (nessun chiamante; l'endpoint `front/server_test.ajax.php` implementa il test inline), `getAssetGroupName()` (mai chiamato), import `use CommonGLPI;` inutilizzato.
2.**`src/UrbackupApiClient.php`**: rimossi `changeClientSetting()` (duplicato di `updateClientSettings()`, mai chiamato) e `extractSettingValue()` (duplicato della copia privata di `AssetTab`, mai chiamato).
3.**`src/Config.php`**: rimossi `ensureDefaultConfiguration()` e `saveConfiguration()` (no-op con commento "kept for backward compatibility" ma senza alcun chiamante utile) + import inutilizzati `DBmysql`, `Html`.
4.**`install/install.php`**: rimossa funzione `plugin_urbackup_install_update_assettypes_table()` (mai chiamata da `install_process()`); rimosso blocco `hasCapacity()`/`enableCapacity()` con API inesistente in GLPI 11.0.8 (caveat 18, ora documentato nel codice: l'enable passa dal form Capacities UI); rimossa chiamata `Config::ensureDefaultConfiguration()`; rimossi import `AssetDefinitionManager`, `UrBackupCapacity`.
5.**`front/asset.form.php`**: rimossi handler POST `start_file_backup`/`start_image_backup` (nessun form del plugin li emette; i bottoni usano `execute` + `urbackup_action`).
6.**`front/config.form.php`**: rimosso handler POST `update` (il form non esiste più; `Config::saveConfiguration()` rimosso); aggiunto bootstrap `include_once inc/includes.php` mancante; rimosso `global $CFG_GLPI;` inutilizzato.
7.**`public/js/urbackup.js`**: FILE ELIMINATO (cercava `#plugin-urbackup-api-status` e `.plugin-urbackup-test-api`, elementi mai renderizzati da nessun file PHP/Twig) + rimosso hook `ADD_JAVASCRIPT` in `setup.php`.
8.**`hook.php`**: rimossi `require_once install/install.php` e `install/uninstall.php` (caricati ad ogni richiesta inutilmente; il lifecycle li carica da `setup.php`).
9.**`src/MassiveAction.php`**: rimosso `use Session;` inutilizzato; corrette due parentesi di chiusura con indentazione errata.
10.**`src/AssetTab.php`**: rimossi parametri inutilizzati `$server`/`$link` da `showStateSection()`/`showActionsSection()` e `$link` da `showInternalTabs()` (privati).
### Fix di sicurezza
1.**XSS (stored) — `AssetTab::showStateSection()`**: `$displayValue` (valori status/settings dall'API UrBackup, potenzialmente ostili) era stampato SENZA `htmlspecialchars()` → ora escapato (`htmlspecialchars((string) $displayValue)`).
- **POST-only** (prima accettava GET con scrittura DB → state change via GET, CSRF-friendly);
- diritto richiesto per la scrittura DB: **UPDATE** (prima READ poteva sovrascrivere `last_api_status/message/check`);
- import espliciti `use` al posto dei FQCN inline.
3.**Password API non più esposta nel page source** (`Server::showFormFields`): campo `api_password` ora con `value=''` + placeholder `******` + hint "leave empty to keep current"; `prepareInputForUpdate()` mantiene la password esistente quando il campo è vuoto. (Risolto il 05/08/2026: `api_password` ora cifrata con GLPIKey — vedi sezione aggiornata.)
4.**Validazione input server**: `protocol` whitelistato `http|https` (prima qualsiasi stringa finiva in `getWebInterfaceUrl()` → href e URL cURL) e `port` clampato `1..65535` in `prepareInputForAdd`/`prepareInputForUpdate`.
5.**`front/asset.form.php` disconnect**: condizione diritti allineata a `ServerAsset::disconnectAsset()` (solo UPDATE; prima UPDATE|DELETE incoerente).
6.**`front/server.form.php`**: `$_GET['id']` castato a int.
### Verifiche
-`php -l` OK su tutti i file PHP del plugin.
- Smoke test HTTP (ambiente locale, senza auth): `server_test.ajax.php` GET→302 (login), POST→403 (denied), `config.form.php` e `server.php` → 200 senza fatal error. (Nota: la verifica del 05/08/2026 ha confermato che il server UrBackup locale è attivo su `localhost:55414` e il login API 2-fasi funziona — il claim "flussi API non testabili in locale" era errato, vedi sezione aggiornata.)
### Da fare (non bloccanti, segnalati nel report) — TUTTI RISOLTI il 05/08/2026
- ~~`api_password` in chiaro su `glpi_plugin_urbackup_servers` → cifrare con `GLPIKey`~~ → fatto: `(new GLPIKey())->encrypt()` in `prepareInputForAdd/Update` + migrazione `plugin_urbackup_install_encrypt_api_passwords()` + `getApiPassword()` con fallback legacy.
- ~~`setup.php` top-level: loop `opcache_invalidate()` ad OGNI richiesta web~~ → fatto: solo se `GLPI_ENVIRONMENT_TYPE === 'development'`.
- ~~`Server::prepareInputForUpdate()`: test connessione API ad ogni save~~ → fatto: solo se i parametri di connessione cambiano.
- ~~`Server::showUnlinkedClientsTab()`: N+1 su `ServerAsset::getAssetName()`~~ → fatto: batch loading per itemtype.
Generati i 3 file operativi del progetto a partire dai modelli `*_netbackup` (copie dei file del plugin netbackup presenti in cartella), adattandoli alla realtà verificata del codice:
### File prodotti
1.**`AGENTS.md`** (sovrascritto — prima era identico a `AGENTS.md_netbackup`): ruolo Senior GLPI Plugin Architect, regole assolute (Git rule con autorizzazione esplicita, strict types PHP 8.3/8.4, namespace `GlpiPlugin\Urbackup\`), CSRF GLPI 11 (CheckCsrfListener globale, niente `Session::checkCSRF()` nei front), limiti ambiente locale (server UrBackup locale verificato attivo: `localhost:55414`), struttura v0.7.0 e hook essenziali.
2.**`SKILL.md`** (nuovo): competenze UrBackup Web API (login salt/PBKDF2, azioni, struct setting, versioni ≥ 2.4), Capacity system GLPI 11, location-aware matching, network resilience, security zero-trust.
3.**`GLPIDEV.md`** (nuovo): reference API GLPI 11.0.8 verificata sul core `/var/www/glpi` — DB layer, tabelle plugin, Migration, CSRF, Capacity system, UrBackupApiClient (tabella azioni), LocationHelper/batch loading, 18 caveat verificati.
### Fatti ri-verificati durante la stesura (da codice reale)
-`$DB->runFile()` è deprecato ma usato SOLO per schema iniziale in `install/install.php` (mai in upgrade/uninstall).
-`Html::hidden()` in GLPI 11 usa la firma `(name, options)` — pattern `Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()])`.
- Hook `plugin_urbackup_MassiveActions($type)` riceve l'itemtype come **stringa**.
-`api_password` in chiaro: limite noto → **risolto il 05/08/2026** con cifratura `(new GLPIKey())->encrypt()/decrypt()` (non esiste `GLPIKey::getInstance()`).
### Server UrBackup per test in locale (VERIFICATO ATTIVO)
- **Scopo**: eseguire API semplici (login, status, backup) in locale per debugging e validazione rapida.
- **Stato**: processo `urbackupsrv` attivo (porta 55414), endpoint `/x?a=salt` e login 2-fasi (PBKDF2) verificati funzionanti il 05/08/2026.
- **Risultati test API**: documentato in `/var/www/glpi/plugins/urbackup/urbackup-api-actions.md`.
### ⚠️ BUG latente trovato: hasCapacity()/enableCapacity() inesistenti in GLPI 11.0.8
-`install/install.php:367-368` chiama `$definition->hasCapacity(UrBackupCapacity::class)` e `$definition->enableCapacity(UrBackupCapacity::class)` — in GLPI 11.0.8 su `AssetDefinition` esistono SOLO `hasCapacityEnabled(CapacityInterface $capacity)` (oggetto, non stringa), `getEnabledCapacities()`, `getCapacityConfiguration()` (`src/Glpi/Asset/AssetDefinition.php:627-652`).
- Impatto attuale: la chiamata è in try/catch → durante l'install si mostra il messaggio "Error enabling UrBackup capacity on definitions: ..." ma la migrazione continua; le capacità NON vengono auto-abilitate.
- Impatto funzionale nullo oggi perché `Config::isItemtypeEnabled()` ritorna true per tutte le sottoclassi di `Glpi\Asset\Asset` (tab sempre visibile).
- **TODO**: correggere con l'API core corretta: check con `hasCapacityEnabled(new UrBackupCapacity())` (richiede un oggetto `CapacityInterface`); l'abilitazione/disabilitazione in GLPI 11.0.8 avviene via input `capacities` del form (array di spec) processato in `AssetDefinition::post_updateItem()` (AssetDefinition.php:316-440) — NON esistono metodi pubblici `enableCapacity/saveCapacity/disableCapacity`. Documentato in GLPIDEV.md §2.6 e caveat 18. → NOTA: rimosso come dead code il 05/08/2026 (il blocco non è più in install.php).
In GLPI 11, `Session::checkCSRF()` richiede il primo argomento `$data` (i dati POST da validare). In GLPI ≤10 accettava zero argomenti.
Il listener globale `CheckCsrfListener` gestisce già il CSRF per tutte le richieste POST, ma il plugin chiamava `Session::checkCSRF()` senza argomenti causando errore fatale.
### Fix applicati (tutti i file)
- RIMOSSE tutte le chiamate esplicite `Session::checkCSRF()` dai 4 file front — GLPI 11 le gestisce già globalmente via `CheckCsrfListener`
- Il listener globale consuma il token CSRF, quindi una seconda chiamata dal plugin fallisce perché il token non è più valido
-`public/js/urbackup.js` — aggiunto header `X-Glpi-Csrf-Token` con `getAjaxCsrfToken()` per le richieste AJAX (NOTA 05/08/2026: urbackup.js è stato ELIMINATO come dead code — il pattern CSRF AJAX resta documentato per riferimento)
Gli Asset Definition custom (GLPI 11) sono 2-5x più lenti dei Computer nativi nel caricamento del tab UrBackup.
### Causa
L'overhead è in GLPI 11 core, non nel plugin:
1.**`Asset::__construct()`** — itera 30+ Capacity classi per ogni istanza
2.**`Asset::post_getFromDB()`** — decodifica JSON custom fields e li processa
3.**`eval()` autoloading** — le classi concrete sono definite via `eval()` a runtime
### Ottimizzazioni applicate
1.**`AssetTab.php::loadApiData()`** — letto `$item->fields['name']` direttamente invece di chiamare `ServerAsset::getAssetName()` che faceva una seconda `getFromDB()` ridondante
2.**`AssetTab.php::startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`, `showServerLinkedBlock()`** — stesso pattern, `$item->fields['name']` al posto di `getAssetName()`
3.**`Server.php::showMissingClientsTab()`** — batch loading IP e gruppi: `batchLoadIps()` (1 query per itemtype vs 1 per riga) + `batchLoadGroups()` (1 query per itemtype vs 1 per riga)
## Architettura
-`src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
## Bugfix: Campi API username/password non visibili in nuova installazione
### Problema
In una nuova installazione del plugin, i campi "API username" e "API password" non venivano renderizzati come input — si vedeva solo la label ma non il campo editabile.
### Cause (2 bug distinti)
1.**`src/Server.php:436`** — `$canUpdate` controllava solo `Session::haveRight(self::$rightname, UPDATE)`. In fase di creazione di un nuovo server, l'utente ha diritto CREATE ma non necessariamente UPDATE, quindi il campo non veniva mostrato.
2.**`src/Profile.php:73-77`** — `installRights()` usava `$_SESSION['glpiactiveprofile']['id']` per assegnare i diritti completi al profilo corrente. In installazione via CLI (`php bin/console glpi:plugin:install`), non c'è sessione, quindi `$profiles_id = 0` e nessun profilo riceveva i diritti completi.
### Fix applicati
1.**`Server.php::showFormFields()`** — Sostituito `$canUpdate` con `$canEdit`: per nuovi server (`$ID <= 0`) usa `CREATE`, per server esistenti usa `UPDATE`.
2.**`Profile.php::installRights()`** — In assenza di sessione attiva (CLI), cerca il profilo "Super-Admin" tramite query diretta e gli assegna tutti i diritti.
Su server produttivo, `Group::showItems()` generava un errore MySQL 1054 (Unknown column 'users_id') quando GLPI iterava sugli itemtype registrati con `linkgroup_types => true`. La query includeva:
La colonna `users_id` non esisteva nella tabella `glpi_plugin_urbackup_servers`.
### Causa
`Server` è registrato in `setup.php:57` con `linkgroup_types => true`, il che fa sì che GLPI lo includa nelle query di `Group::getDataItems()`. Il metodo aggiunge automaticamente una condizione `users_id IN (...)` assumendo che ogni itemtype abbia tale colonna.
### Fix
1.**`install/mysql/plugin_urbackup-empty.sql`** — Aggiunta colonna `users_id INT UNSIGNED NOT NULL DEFAULT 0` dopo `locations_id` + indice
2.**`install/install.php`** — Aggiunto `$migration->addField('users_id', ...)` e `$migration->addKey('users_id')` nella migrazione del server
3.**`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
2.**Form server**: riga "Hardware host" con dropdown tipo (`Dropdown::showItemTypes`) + dropdown elementi AJAX (`$.get` su `front/dropdown_host.ajax.php`, vedi sezione Fix del 07/08/2026).
3.**AssetTab**: blocco "This asset hosts the UrBackup server" (sempre visibile, anche se client).
4.**Validazione** in `prepareInputForUpdate()` + helper `getHostAsset()` / `getServersHostingAsset()` (guard `fieldExists`).
## 0.7.2 — Toggle Computer configurabile + lista Asset custom con capacità attiva
1.**`enable_computer` in `glpi_plugin_urbackup_configs`** (default '1') letto da `Config::getEnableComputer()`; registrazione tab su Computer condizionale in setup.php; `isItemtypeEnabled`/`getEnabledItemtypes` rispettano il toggle.
2.**Config page**: dropdown Sì/No per Computer + lista Asset Definition con capacità UrBackup attivata (`getEnabledAssetDefinitions()`).
3.**front/config.form.php**: handler POST `update` ripristinato (upsert, redirect, messaggio).
## 0.7.1 — Cifratura api_password (GLPIKey), ottimizzazioni e localizzazioni
1.**`api_password` cifrata con `(new GLPIKey())->encrypt()`** on save (`Server::prepareInputForAdd/Update`), `Server::getApiPassword()` con fallback legacy, migrazione idempotente `plugin_urbackup_install_encrypt_api_passwords()` (install.php) — applicata al DB (server id=1).
2.**Test connessione API solo a parametri cambiati** in `prepareInputForUpdate()` (niente più chiamate da 30s ad ogni save).
3.**Loop `opcache_invalidate()` solo in development** (`GLPI_ENVIRONMENT_TYPE === 'development'`).
4.**Fix N+1** in `Server::showUnlinkedClientsTab()` (batch loading per itemtype).
5.**Localizzazioni**: 115 nuove traduzioni it/de/en, 9 orfane rimosse, `.mo` ricompilati.
1.**CSRF hardening**: `Session::checkCSRF()` aggiunto su `asset.form.php`, `server.form.php`, `server_test.ajax.php`, `config.form.php` (NOTA: poi RIMOSSO il 05/08/2026 — in GLPI 11 il listener globale lo gestisce)
4.**DB migration**: DROP `glpi_plugin_urbackup_profiles` e `glpi_plugin_urbackup_assettypes`; add index `location_active` su servers; add `date_creation`/`date_mod` su serverassets
10.**Session::isDebugActive()**: Metodo inesistente in GLPI 11; sostituito in `AssetTab.php:150` con `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`