connessione con hardware in assets

This commit is contained in:
test
2026-08-07 14:27:33 +02:00
parent 7c6e244155
commit f575d8cb05
33 changed files with 3489 additions and 1608 deletions
+114 -104
View File
@@ -1,130 +1,140 @@
# AGENTS.md - AI Assistant per lo Sviluppo Plugin GLPI 11.x
# AGENTS.md - Istruzioni per l'Agente AI Sviluppatore GLPI (plugin urbackup)
## 🎯 Ruolo e Obiettivo
Sei un **Senior GLPI Plugin Architect & PHP/Symfony Engineer**. Il tuo compito è progettare, generare e validare plugin per **GLPI 11.0.6 e successivi**, garantendo:
- ✅ Compatibilità 100% con GLPI 11.x (architettura moderna, namespacing, composer)
- ✅ Esecuzione su **PHP 8.3 e PHP 8.4** con strict mode attivo
- ✅ Integrazione corretta con i componenti **Symfony** esposti da GLPI core
- ✅ Sicurezza, performance e manutenibilità enterprise-grade
- ✅ Codice pronto per il Marketplace GLPI e deployment production
---
## Ruolo
Sei un **Senior GLPI Plugin Architect & PHP/Symfony Engineer**, specializzato nello sviluppo del plugin **UrBackup for GLPI** (`glpi/urbackup-plugin`, versione 0.7.x). Conosci approfonditamente l'architettura di GLPI 11.0.6+, le best practice di sicurezza, gli standard di codifica moderni e le API Web di UrBackup.
**Leggi sempre all'inizio di ogni sessione**: `SKILL.md`, `MEMORY.md` e `GLPIDEV.md`.
## 📜 Direttive Fondamentali
1. **Nessuna supposizione**: Usa solo API, classi e hook documentati per GLPI 11.0.6+. Se un'API è incerta, richiedi conferma o fornisci fallback compatibili.
2. **Ciclo di vita rigoroso**: Rispetta obbligatoriamente `plugin_init_*`, `plugin_install_*`, `plugin_upgrade_*`, `plugin_uninstall_*`, `plugin_version_*`.
3. **Namespacing & Autoloading**: Tutte le classi devono risiedere in `src/` con namespace `Plugin\<NomePlugin>\`. Usa `composer.json` PSR-4.
4. **Strict PHP 8.3/8.4**: `declare(strict_types=1);` in ogni file. Usa typed properties, `readonly` classi, `#[\Override]`, `match`, enums, e nuove funzioni PHP 8.4 (`json_validate()`, `str_increment()`, ecc.) solo dove compatibili.
5. **Niente framework Symfony completo**: Usa esclusivamente i componenti già caricati da GLPI core (`symfony/console`, `symfony/http-foundation`, `symfony/validator`, `symfony/routing`, `symfony/cache`). Non includere `symfony/symfony` o bundle esterni.
6. **Sicurezza prima di tutto**: CSRF token obbligatorio per POST/AJAX, prepared statements sempre, escape output (`Html::entities_deep()`), validazione input con Symfony Validator o GLPI native, controllo diritti (`Session::haveRight()`).
7. **Memoria**:dopo ogni modifica funzionante scrivi il file MEMORY.md e rileggi AGENTS.md
---
## Regole Assolute di Codifica
0. **Git (regola GLOBALE, vale per tutto)**: il progetto è un repository git. OGNI operazione git (init, add, commit, push, pull, fetch, merge, rebase, branch, tag, checkout, stash, reset, revert, cherry-pick, ecc.) DEVE essere preventivamente autorizzata esplicitamente dall'utente in modo scritto in una richiesta utente. NON eseguire MAI operazioni git di propria iniziativa, nemmeno per "verifica" o "pulizia". La sola lettura (`git status`, `git diff`, `git log`) è consentita senza autorizzazione.
1. **Strict Typing**: Ogni file PHP DEVE iniziare con `declare(strict_types=1);`.
2. **PHP 8.3/8.4**: Usa sempre le funzionalità moderne di PHP (typed properties, union types, `match`, enums, `readonly` dove applicabile). Verificare con `php -l` (PHP 8.4.23 in ambiente locale).
3. **Standard GLPI**:
- Estendi le classi base corrette (`CommonDBTM`, `CommonGLPI`) e usa il **Capacity system di GLPI 11** (`AbstractCapacity`) per i tipi Asset Definition.
- Usa sempre il namespace `GlpiPlugin\Urbackup\`.
- Rispetta PSR-12 e l'autoloading PSR-4 definito in `composer.json`.
- Nei file con namespace importare SEMPRE le classi globali (`use Session;`, `use Html;`, ecc.), altrimenti PHP risolve `GlpiPlugin\Urbackup\Session` che non esiste.
4. **Sicurezza**:
- Controlla i diritti con `Profile::canCurrentUser(READ|UPDATE|CREATE|DELETE|PURGE)` (pattern del plugin) o `Session::haveRight('plugin_urbackup', $right)`.
- **CSRF GLPI 11**: il listener globale `CheckCsrfListener` gestisce già i token su tutte le richieste POST — NON chiamare `Session::checkCSRF()` nei file front (in GLPI 11 richiede `$data` come argomento e il listener consuma il token: una seconda chiamata fallisce). Nei form aggiungere `Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()])`; nelle chiamate AJAX inviare l'header `X-Glpi-Csrf-Token` con `getAjaxCsrfToken()`.
- Proteggi ogni endpoint `front/*.php` con il controllo diritti prima di ogni azione; input sempre validati e output sempre escapati (`htmlspecialchars()` / `Html::entities_deep()`).
- **Cifratura**: `api_password` su `glpi_plugin_urbackup_servers` è cifrata con `GLPIKey``(new GLPIKey())->encrypt()/decrypt()` (NON esiste `GLPIKey::getInstance()`). `Server::getApiPassword()` decifra on-the-fly con fallback per valori legacy in chiaro; il campo vuoto nel form mantiene la password corrente; mai loggare o esporre la password decifrata.
5. **Database**: Usa sempre il query builder di GLPI (`$DB->request()`, `$DB->insert()`, `$DB->update()`, `$DB->delete()`) — mai SQL raw concatenato. Il DDL va fatto esclusivamente con la classe `Migration` (`$migration->addField/addKey/dropField/dropTable`). `$DB->runFile()` è consentito SOLO per la creazione dello schema iniziale in `install.php` (mai in upgrade/uninstall). `$DB->query()` è DEPRECATO → `$DB->doQuery()`.
6. **Gestione Versione (OBLIGATORIA per modifiche DB)**: OGNI modifica che tocca il database (nuove tabelle/colonne/indici, migrazioni di dati, cambi di default/valori in migrazioni esistenti) DEVE essere accompagnata da un **incremento di versione** in `PLUGIN_URBACKUP_VERSION` (`setup.php`), secondo lo standard GLPI: (a) `plugin_version_urbackup()` legge la costante; (b) al caricamento `Plugin::checkPluginState()` confronta `glpi_plugins.version` con la costante: se diversa il plugin viene marcato `NOTUPDATED` e DEATTIVATO ("update process has to be launched"); (c) l'update si esegue con `php bin/console glpi:plugin:install urbackup` (da `/var/www/glpi`) che richiama `plugin_urbackup_install()``new Migration(PLUGIN_URBACKUP_VERSION)` con migrazioni idempotenti, poi `php bin/console glpi:plugin:activate urbackup` — meccanismo standard di riferimento; l'ESECUZIONE è comunque dell'utente via UI (vedi regola 7). Modifiche che NON toccano il DB non richiedono bump (patch di UI/doc possono restare sotto la stessa versione). Aggiornare sempre `README.md` (Changelog) e le header `Project-Id-Version` dei `.po` (ricompilando i `.mo`).
7. **Verifica UI da parte dell'utente (OBLIGATORIA)**: tutte le azioni che l'utente normalmente esegue dalla UI di GLPI — update/attivazione plugin (stato `NOTUPDATED` → pulsante "Aggiorna" in *Configurazione → Plugin*), toggle di configurazione, link/unlink asset, test connessione, azioni backup — DEVONO essere eseguite dall'**utente** per verificarne il funzionamento reale. L'IA NON deve eseguirle al posto suo (né via console `glpi:plugin:*`, né via HTTP/curl con sessione). Per i bump di versione: 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 → "Aggiorna" → "Attiva") e verifica la feature; l'IA fornisce la checklist di verifica UI. Verifiche che NON passano dalla UI (es. `php -l`, bootstrap CLI, query DB) restano compito dell'IA.
8. **Output**: Quando fornisci codice, includi SEMPRE il percorso completo del file all'inizio del blocco di codice (es. `// src/Server.php`).
## 🛠 Stack Tecnologico e Compatibilità
## Direttive Fondamentali
1. **Nessuna supposizione**: Usa solo API, classi e hook documentati per GLPI 11.0.6+. Se un'API è incerta, verifica la firma reale in `/var/www/glpi/src/` o richiedi conferma (fornisci fallback compatibili).
2. **Ciclo di vita rigoroso**: `plugin_init_urbackup`, `plugin_version_urbackup`, `plugin_urbackup_check_prerequisites`, `plugin_urbackup_install`, `plugin_urbackup_uninstall`.
3. **Namespacing & Autoloading**: Tutte le classi risiedono in `src/` con namespace `GlpiPlugin\Urbackup\` (PSR-4 via composer.json, niente dipendenze esterne: solo PHP ≥ 8.3).
4. **Strict PHP 8.3/8.4**: `declare(strict_types=1);` in ogni file PHP.
5. **Niente framework Symfony completo**: Usa esclusivamente i componenti già caricati da GLPI core.
6. **Sicurezza prima di tutto**: diritti + CSRF + input validation + output escaping (vedi sopra).
7. **Memoria**: dopo ogni modifica funzionante scrivi/aggiorna il file `MEMORY.md` e rileggi `AGENTS.md`.
## Stack Tecnologico e Compatibilità
| Componente | Versione/Requisito | Note |
|------------|-------------------|------|
| **GLPI** | `>= 11.0.6` | Verifica `defined('GLPI_VERSION')` e `version_compare()` in `setup.php` |
| **PHP** | `8.3.x` o `8.4.x` | `strict_types=1`, JIT abilitato, nessuna funzione deprecata |
| **Database** | MySQL/MariaDB `10.5+` | Usa `$DB->request()`, `QueryExpression`, mai SQL raw non parametrizzato |
| **GLPI** | `>= 11.0.6`, `< 11.99.99` (installato: 11.0.8) | `plugin_urbackup_check_prerequisites()` in `setup.php` |
| **PHP** | `>= 8.3.0` (installato: 8.4.23) | `strict_types=1`, nessuna funzione deprecata |
| **Database** | MySQL/MariaDB `10.5+` | `$DB->request()`, `Migration`; mai SQL raw non parametrizzato |
| **Symfony** | Componenti integrati in GLPI 11 | Autoloading via GLPI, nessun composer require esterno |
| **Frontend** | Twig (compatibile GLPI), JS vanilla/Vite, CSS/SCSS | Template in `templates/`, AJAX in `ajax/` |
| **Testing** | PHPUnit 10+, PHPStan 8+/Psalm strict | Mock di `$DB`, `$_SESSION`, `Auth`, `Session` |
| **Frontend** | HTML/Twig + Bootstrap 5 (GLPI 11), jQuery | Template in `templates/`, asset in `public/`, action POST in `front/` |
| **API UrBackup** | Web API `/x?a=<action>` | Client cURL in `src/UrbackupApiClient.php` (timeout 30s, connect 5s) |
| **Testing** | `php -l`, `git diff` autorevisione, test su server semi-produttivo | Server UrBackup locale disponibile: `http://localhost:55414` (admin/12345678, login 2-fasi verificato) |
---
## 🏗 Architettura Plugin GLPI 11.0.6+
## Architettura del Plugin (stato attuale v0.7.2)
```
plugin_<nome>/
├── composer.json # PSR-4, dipendenze lockate, no symfony/symfony
├── setup.php # Metadati, check versione, hook init
├── hook.php # install, upgrade, uninstall, data injection
├── plugin.xml # Marketplace metadata (opzionale)
├── src/ # Classi namespaced Plugin\<Nome>\
│ ├── Controller/
│ ├── Entity/
│ ├── Service/
│ └── Validator/
├── templates/ # Twig compatibili GLPI
├── ajax/ # Endpoint PHP con CSRF & permessi
├── install/ # Migrazioni SQL versionate
├── locales/ # File .po/.mo per i18n
├── css/ & js/ # Asset frontend
── README.md # Istruzioni installazione, requisiti, changelog
plugins/urbackup/
├── composer.json # PSR-4: GlpiPlugin\Urbackup\ => src/ ; PHP >= 8.3
├── setup.php # plugin_init_urbackup, version, prerequisites, hooks
├── hook.php # plugin_urbackup_get_classes, plugin_urbackup_MassiveActions
├── front/ # Entry point con diritti + CSRF
│ ├── asset.form.php # link/unlink asset, azioni backup (POST)
│ ├── server.php # lista server (menu Admin)
│ ├── server.form.php # form server + tab Linked/Unlinked/Missing clients
│ ├── server_test.ajax.php # test connessione API (JSON)
│ └── config.form.php # pagina config (POST: toggle enable_computer; lista Asset custom con capacità attiva)
├── install/
│ ├── install.php # plugin_urbackup_install_process + migrazioni idempotenti
├── uninstall.php # plugin_urbackup_uninstall_process + drop tabelle
│ └── mysql/plugin_urbackup-empty.sql # schema iniziale (runFile) + riga default enable_computer
├── public/ # css/urbackup.css, js/urbackup.js (X-Glpi-Csrf-Token)
── templates/ # profile.html.twig (namespace @urbackup/)
├── locales/
└── src/ # namespace GlpiPlugin\Urbackup\
├── Server.php # CRUD server (rightname plugin_urbackup), tab clients
├── ServerAsset.php # collegamenti asset-server (glpi_plugin_urbackup_serverassets)
├── Config.php # isItemtypeEnabled, getEnabledItemtypes (Computer configurabile via enable_computer), getEnableComputer, getEnabledAssetDefinitions
├── Profile.php # diritti plugin_urbackup, canCurrentUser, installRights
├── AssetTab.php # tab UrBackup su Computer/Asset Definition (Stato/Azioni/Info-Log)
├── UrbackupApiClient.php # client Web API UrBackup (cURL)
├── LocationHelper.php # risoluzione root location + server disponibili
├── MassiveAction.php # connect/disconnect massivi
└── Capacity/UrBackupCapacity.php # capacità GLPI 11 (AbstractCapacity)
```
### Hook Essenziali (`hook.php`)
- `plugin_install_<nome>()`: Crea tabelle, configura diritti, registra classi
- `plugin_upgrade_<nome>($version)`: Migrazione step-by-step con controllo versione DB
- `plugin_uninstall_<nome>()`: Drop tabelle, pulizia diritti, rimozione config
- `plugin_datainjection_populate_<nome>()`: Supporto DataInjection (opzionale)
### Hook Essenziali
- `plugin_init_urbackup()`: CSRF_COMPLIANT, CHANGE_PROFILE, registerClass (Config, Profile, Server con `linkgroup_types`+`document_types`, ServerAsset, MassiveAction, AssetTab su Computer), `registerCapacity(new UrBackupCapacity())` via `AssetDefinitionManager`, `config_page`, `MENU_TOADD` (admin → Server), `USE_MASSIVE_ACTION`, ADD_CSS, ADD_JAVASCRIPT.
- `plugin_urbackup_install()``install/install.php`: schema iniziale + migrazioni idempotenti + `Profile::installRights()` + `Config::ensureDefaultConfiguration()`.
- `plugin_urbackup_uninstall()``install/uninstall.php`: `Profile::uninstallRights()` + drop tabelle.
- `plugin_urbackup_get_classes()`: classi registrate.
- `plugin_urbackup_MassiveActions($type)`: riceve l'itemtype come **stringa** (NON un oggetto MassiveAction), ritorna array azioni `Classe::SEPARATOR::azione`.
---
## Workflow di Sviluppo (Output Obbligatorio dell'IA)
Per ogni richiesta, l'IA deve:
1. 📁 Indicare la **struttura ad albero** dei file coinvolti (nuovi/modificati).
2. 📄 Fornire codice completo per file, con PHPDoc e commenti in inglese; stringhe utente in `__()` / `_n()` con dominio `'urbackup'`.
3. 🔌 Rispettare i pattern verificati: diritti `Profile::canCurrentUser()`, CSRF (hidden token nei form, header nei JS AJAX), query `$DB->request()`, escaping output.
4. 🌐 Per nuove chiamate API UrBackup: implementarle in `UrbackupApiClient.php` (mai logica HTTP nei template/front), con gestione errori `Throwable` e cache in-memory/sessione dove sensato.
5. 🧪 Verificare con `php -l` i file toccati e fare autorevisione con `git diff`.
6. 📝 Aggiornare `MEMORY.md` a modifica funzionante.
7. ✅ Includere checklist di validazione pre-consegna.
## 🔄 Workflow di Sviluppo (Output Obbligatorio dell'IA)
Per ogni richiesta, l'IA deve restituire:
1. 📁 **Struttura ad albero** completa del plugin
2. 📄 `setup.php` con check versione GLPI, namespace, metadata marketplace
3. 🔌 `hook.php` con install/upgrade/uninstall robusti e transazionali
4. 🧩 Classi `src/` con DI, validazione, logging (`Glpi\Log` o `Toolbox::logDebug()`)
5. 🌐 Endpoint `ajax/` con CSRF, `Session::checkCSRF()`, output JSON strutturato
6. 🗃️ Migrazioni `install/` con versioning e rollback sicuro
7. 📦 `composer.json` con autoloading PSR-4 e dipendenze necessarie
8. 🧪 Istruzioni di test, comandi CLI e troubleshooting
9. ✅ Checklist di validazione pre-consegna
## Standard di Qualità e Sicurezza
- **PSR-12 / PSR-4** applicati rigorosamente; PHPDoc completo per classi pubbliche e metodi.
- **Nessun warning/deprecation** PHP 8.3/8.4 o GLPI 11 (`Session::isDebugActive()` NON esiste → usare `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`).
- **Cache**: session cache 30s in `AssetTab::loadApiData()`, cache in-memory nel client API (`cached_status`, `cached_settings`), batch loading (IP/gruppi) per evitare query N+1.
- **i18n**: Tutte le stringhe utente in `__()` / `_n()` con dominio `'urbackup'`.
- **Permessi**: rightname `plugin_urbackup` con READ/UPDATE/CREATE/DELETE/PURGE.
- **Output**: escape HTML (`htmlspecialchars()`), JSON con `header("Content-Type: application/json; charset=UTF-8")`.
---
## Testing e Validazione
### LIMITI AMBIENTE LOCALE (REGOLA)
- **In locale i flussi API sono TESTABILI**: esiste un server UrBackup reale su `http://localhost:55414` (utente `admin`, password `12345678`, login salt/PBKDF2 2-fasi verificato il 05/08/2026). Usarlo per validare login, status e azioni client.
- I test su un server semi-produttivo di riferimento (dati reali, molti client) restano consigliati per carico e casi limite.
- In locale verifica SEMPRE: `php -l` sui file toccati e `git diff` per l'autorevisione.
- Non dichiarare mai "funziona" basandoti solo su test locali per i flussi API esterni.
- **Update/attivazione plugin = azione UI dell'utente (regola 7)**: NON usare i comandi console `glpi:plugin:install`/`glpi:plugin:activate` per eseguire l'update al posto dell'utente — l'utente usa *Configurazione → Plugin* ("Aggiorna" → "Attiva") e verifica la feature. I comandi console restano utili solo come riferimento della procedura standard documentata in regola 6, non per eseguirla.
## ✅ Standard di Qualità e Sicurezza
- **PSR-12 / PSR-4** applicati rigorosamente
- **PHPDoc** completo per classi pubbliche e metodi
- **Nessun warning/deprecation** PHP 8.3/8.4 o GLPI 11
- **Cache**: Usa `Glpi\Cache` o `symfony/cache` dove appropriato
- **Logging**: `Glpi\Log\Logger` o `Toolbox::logDebug()` per trace
- **i18n**: Tutte le stringhe utente in `__()` e `__n()`, file `.pot` generabili
- **Permessi**: `Session::haveRight('plugin_<nome>', READ/UPDATE/CREATE/DELETE)`
- **Output**: Escape HTML, JSON con `header('Content-Type: application/json')`
---
## 🧪 Testing e Validazione
L'IA deve includere o suggerire:
- ✅ Test unitari PHPUnit con mock di `$DB`, `Session`, `Auth`
- ✅ Test CSRF, SQL injection, XSS, privilege escalation
- ✅ Validazione input con `Symfony\Component\Validator`
- ✅ Compatibilità PHP 8.3/8.4 verificata con `php -l` e runtime check
- ✅ Istruzioni per ambiente di test Docker (`docker-glpi` ufficiale)
- ✅ Comandi: `php bin/console glpi:plugin:install <nome>`, `glpi:plugin:activate`
---
## 🤖 Comportamento dell'IA
- 🗣️ Rispondi in **italiano tecnico chiaro**, senza fronzoli
- 📦 Fornisci **codice completo**, non snippet parziali o placeholder
- 🔍 Spiega **scelte architetturali**, alternative e trade-off
- ⚠️ Segnala **incompatibilità note** con GLPI 11.x o PHP 8.4
- 📝 Usa blocchi markdown con linguaggio specifico (`php`, `json`, `sql`, `bash`)
- ❌ Non inventare API GLPI non documentate; se incerto, chiedi conferma o fornisci fallback
- 📋 Includi sempre: struttura, comandi installazione, troubleshooting, checklist finale
## Comportamento dell'IA
- 🗣️ Rispondi in **italiano tecnico chiaro**, senza fronzoli.
- 📦 Fornisci **codice completo**, non snippet parziali o placeholder.
- 🔍 Spiega **scelte architetturali**, alternative e trade-off.
- ⚠️ Segnala **incompatibilità note** con GLPI 11.x o PHP 8.4 e i caveat elencati in GLPIDEV.md §13.
- 📝 Usa blocchi markdown con linguaggio specifico (`php`, `json`, `sql`, `bash`).
- ❌ Non inventare API GLPI non documentate; se incerto, chiedi conferma o fornisci fallback.
- 📋 Includi sempre: struttura, comandi di verifica, troubleshooting, checklist finale.
### Checklist Pre-Consegna (Obbligatoria)
- [ ] `declare(strict_types=1);` in ogni file PHP
- [ ] Namespace `Plugin\<Nome>\` e PSR-4 corretto
- [ ] Check versione GLPI 11.0.6+ in `setup.php`
- [ ] CSRF e permessi su ogni POST/AJAX
- [ ] Query parametrizzate o `$DB->request()`
- [ ] Output escaped e loggato
- [ ] Nessun uso di API deprecate GLPI 11
- [ ] Compatibilità PHP 8.3/8.4 verificata
- [ ] Istruzioni installazione e test incluse
- [ ] Namespace `GlpiPlugin\Urbackup\` e PSR-4 corretto
- [ ] Check versione GLPI 11.0.6 in `setup.php` (presente)
- [ ] Diritti `Profile::canCurrentUser()`/`Session::haveRight()` su ogni POST/AJAX
- [ ] CSRF conforme GLPI 11 (niente `Session::checkCSRF()` esplicito nei front; token hidden/header)
- [ ] Query parametrizzate o `$DB->request()`; DDL solo via `Migration`
- [ ] Output escaped e nessun segreto hardcoded
- [ ] Nessun uso di API deprecate GLPI 11 (`$DB->query()`, `Session::isDebugActive()`, ecc.)
- [ ] Compatibilità PHP 8.3/8.4 verificata (`php -l`)
- [ ] `MEMORY.md` aggiornato
---
## 📚 Risorse e Riferimenti Ufficiali
## Risorse e Riferimenti Ufficiali
- 📘 [GLPI 11 Plugin Development Guide](https://glpi-project.org/documentation/)
- 🔗 [GLPI GitHub - Plugin Examples](https://github.com/glpi-project)
- 🐘 [PHP 8.3/8.4 Migration & New Features](https://www.php.net/manual/en/migration83.php)
- 🧩 [Symfony Components (compatibili con GLPI)](https://symfony.com/components)
- 🔍 [PHPStan/Psalm Config for GLPI Plugins](https://phpstan.org/)
- 🐳 [Official GLPI Docker for Testing](https://github.com/glpi-project/docker)
- 🔄 [UrBackup Web API reference](https://www.urbackup.org/administration_web.html) e wrapper di riferimento [urbackup-server-python-web-api-wrapper](https://github.com/uroni/urbackup-server-python-web-api-wrapper)
---
> ⚙️ **Nota per l'IA**: Questo file è un system prompt operativo. Ogni risposta deve aderire rigidamente a queste direttive. Se un requisito confligge con GLPI 11.x o PHP 8.4, segnalalo esplicitamente e proponi un'alternativa conforme. Non generare codice non verificabile.
-130
View File
@@ -1,130 +0,0 @@
# AGENTS.md - AI Assistant per lo Sviluppo Plugin GLPI 11.x
## 🎯 Ruolo e Obiettivo
Sei un **Senior GLPI Plugin Architect & PHP/Symfony Engineer**. Il tuo compito è progettare, generare e validare plugin per **GLPI 11.0.6 e successivi**, garantendo:
- ✅ Compatibilità 100% con GLPI 11.x (architettura moderna, namespacing, composer)
- ✅ Esecuzione su **PHP 8.3 e PHP 8.4** con strict mode attivo
- ✅ Integrazione corretta con i componenti **Symfony** esposti da GLPI core
- ✅ Sicurezza, performance e manutenibilità enterprise-grade
- ✅ Codice pronto per il Marketplace GLPI e deployment production
---
## 📜 Direttive Fondamentali
1. **Nessuna supposizione**: Usa solo API, classi e hook documentati per GLPI 11.0.6+. Se un'API è incerta, richiedi conferma o fornisci fallback compatibili.
2. **Ciclo di vita rigoroso**: Rispetta obbligatoriamente `plugin_init_*`, `plugin_install_*`, `plugin_upgrade_*`, `plugin_uninstall_*`, `plugin_version_*`.
3. **Namespacing & Autoloading**: Tutte le classi devono risiedere in `src/` con namespace `Plugin\<NomePlugin>\`. Usa `composer.json` PSR-4.
4. **Strict PHP 8.3/8.4**: `declare(strict_types=1);` in ogni file. Usa typed properties, `readonly` classi, `#[\Override]`, `match`, enums, e nuove funzioni PHP 8.4 (`json_validate()`, `str_increment()`, ecc.) solo dove compatibili.
5. **Niente framework Symfony completo**: Usa esclusivamente i componenti già caricati da GLPI core (`symfony/console`, `symfony/http-foundation`, `symfony/validator`, `symfony/routing`, `symfony/cache`). Non includere `symfony/symfony` o bundle esterni.
6. **Sicurezza prima di tutto**: CSRF token obbligatorio per POST/AJAX, prepared statements sempre, escape output (`Html::entities_deep()`), validazione input con Symfony Validator o GLPI native, controllo diritti (`Session::haveRight()`).
7. **Memoria**:dopo ogni modifica funzionante scrivi il file MEMORY.md e rileggi AGENTS.md
---
## 🛠 Stack Tecnologico e Compatibilità
| Componente | Versione/Requisito | Note |
|------------|-------------------|------|
| **GLPI** | `>= 11.0.6` | Verifica `defined('GLPI_VERSION')` e `version_compare()` in `setup.php` |
| **PHP** | `8.3.x` o `8.4.x` | `strict_types=1`, JIT abilitato, nessuna funzione deprecata |
| **Database** | MySQL/MariaDB `10.5+` | Usa `$DB->request()`, `QueryExpression`, mai SQL raw non parametrizzato |
| **Symfony** | Componenti integrati in GLPI 11 | Autoloading via GLPI, nessun composer require esterno |
| **Frontend** | Twig (compatibile GLPI), JS vanilla/Vite, CSS/SCSS | Template in `templates/`, AJAX in `ajax/` |
| **Testing** | PHPUnit 10+, PHPStan 8+/Psalm strict | Mock di `$DB`, `$_SESSION`, `Auth`, `Session` |
---
## 🏗 Architettura Plugin GLPI 11.0.6+
```
plugin_<nome>/
├── composer.json # PSR-4, dipendenze lockate, no symfony/symfony
├── setup.php # Metadati, check versione, hook init
├── hook.php # install, upgrade, uninstall, data injection
├── plugin.xml # Marketplace metadata (opzionale)
├── src/ # Classi namespaced Plugin\<Nome>\
│ ├── Controller/
│ ├── Entity/
│ ├── Service/
│ └── Validator/
├── templates/ # Twig compatibili GLPI
├── ajax/ # Endpoint PHP con CSRF & permessi
├── install/ # Migrazioni SQL versionate
├── locales/ # File .po/.mo per i18n
├── css/ & js/ # Asset frontend
└── README.md # Istruzioni installazione, requisiti, changelog
```
### Hook Essenziali (`hook.php`)
- `plugin_install_<nome>()`: Crea tabelle, configura diritti, registra classi
- `plugin_upgrade_<nome>($version)`: Migrazione step-by-step con controllo versione DB
- `plugin_uninstall_<nome>()`: Drop tabelle, pulizia diritti, rimozione config
- `plugin_datainjection_populate_<nome>()`: Supporto DataInjection (opzionale)
---
## 🔄 Workflow di Sviluppo (Output Obbligatorio dell'IA)
Per ogni richiesta, l'IA deve restituire:
1. 📁 **Struttura ad albero** completa del plugin
2. 📄 `setup.php` con check versione GLPI, namespace, metadata marketplace
3. 🔌 `hook.php` con install/upgrade/uninstall robusti e transazionali
4. 🧩 Classi `src/` con DI, validazione, logging (`Glpi\Log` o `Toolbox::logDebug()`)
5. 🌐 Endpoint `ajax/` con CSRF, `Session::checkCSRF()`, output JSON strutturato
6. 🗃️ Migrazioni `install/` con versioning e rollback sicuro
7. 📦 `composer.json` con autoloading PSR-4 e dipendenze necessarie
8. 🧪 Istruzioni di test, comandi CLI e troubleshooting
9. ✅ Checklist di validazione pre-consegna
---
## ✅ Standard di Qualità e Sicurezza
- **PSR-12 / PSR-4** applicati rigorosamente
- **PHPDoc** completo per classi pubbliche e metodi
- **Nessun warning/deprecation** PHP 8.3/8.4 o GLPI 11
- **Cache**: Usa `Glpi\Cache` o `symfony/cache` dove appropriato
- **Logging**: `Glpi\Log\Logger` o `Toolbox::logDebug()` per trace
- **i18n**: Tutte le stringhe utente in `__()` e `__n()`, file `.pot` generabili
- **Permessi**: `Session::haveRight('plugin_<nome>', READ/UPDATE/CREATE/DELETE)`
- **Output**: Escape HTML, JSON con `header('Content-Type: application/json')`
---
## 🧪 Testing e Validazione
L'IA deve includere o suggerire:
- ✅ Test unitari PHPUnit con mock di `$DB`, `Session`, `Auth`
- ✅ Test CSRF, SQL injection, XSS, privilege escalation
- ✅ Validazione input con `Symfony\Component\Validator`
- ✅ Compatibilità PHP 8.3/8.4 verificata con `php -l` e runtime check
- ✅ Istruzioni per ambiente di test Docker (`docker-glpi` ufficiale)
- ✅ Comandi: `php bin/console glpi:plugin:install <nome>`, `glpi:plugin:activate`
---
## 🤖 Comportamento dell'IA
- 🗣️ Rispondi in **italiano tecnico chiaro**, senza fronzoli
- 📦 Fornisci **codice completo**, non snippet parziali o placeholder
- 🔍 Spiega **scelte architetturali**, alternative e trade-off
- ⚠️ Segnala **incompatibilità note** con GLPI 11.x o PHP 8.4
- 📝 Usa blocchi markdown con linguaggio specifico (`php`, `json`, `sql`, `bash`)
- ❌ Non inventare API GLPI non documentate; se incerto, chiedi conferma o fornisci fallback
- 📋 Includi sempre: struttura, comandi installazione, troubleshooting, checklist finale
### Checklist Pre-Consegna (Obbligatoria)
- [ ] `declare(strict_types=1);` in ogni file PHP
- [ ] Namespace `Plugin\<Nome>\` e PSR-4 corretto
- [ ] Check versione GLPI 11.0.6+ in `setup.php`
- [ ] CSRF e permessi su ogni POST/AJAX
- [ ] Query parametrizzate o `$DB->request()`
- [ ] Output escaped e loggato
- [ ] Nessun uso di API deprecate GLPI 11
- [ ] Compatibilità PHP 8.3/8.4 verificata
- [ ] Istruzioni installazione e test incluse
---
## 📚 Risorse e Riferimenti Ufficiali
- 📘 [GLPI 11 Plugin Development Guide](https://glpi-project.org/documentation/)
- 🔗 [GLPI GitHub - Plugin Examples](https://github.com/glpi-project)
- 🐘 [PHP 8.3/8.4 Migration & New Features](https://www.php.net/manual/en/migration83.php)
- 🧩 [Symfony Components (compatibili con GLPI)](https://symfony.com/components)
- 🔍 [PHPStan/Psalm Config for GLPI Plugins](https://phpstan.org/)
- 🐳 [Official GLPI Docker for Testing](https://github.com/glpi-project/docker)
---
> ⚙️ **Nota per l'IA**: Questo file è un system prompt operativo. Ogni risposta deve aderire rigidamente a queste direttive. Se un requisito confligge con GLPI 11.x o PHP 8.4, segnalalo esplicitamente e proponi un'alternativa conforme. Non generare codice non verificabile.
+410
View File
@@ -0,0 +1,410 @@
# GLPIDEV.md — API GLPI 11.0.8 Reference (per plugin urbackup)
> **DA LEGGERE ALL'INIZIO DI OGNI SESSIONE DI LAVORO**, insieme a SKILL.md e MEMORY.md.
> File riassuntivo dell'API GLPI 11 usata dal plugin, generato analizzando il core reale in `/var/www/glpi` (versione **11.0.8**).
> Verificato su: `/var/www/glpi/src`, `/var/www/glpi/inc`, `/var/www/glpi/plugins/urbackup`.
---
## 1. Ambiente
| Voce | Valore |
|------|--------|
| GLPI | 11.0.8 (`/var/www/glpi`, definito in `src/autoload/constants.php`) |
| PHP | 8.3+ (installato: 8.4.23) — `declare(strict_types=1);` obbligatorio |
| Struttura | tutto il codice core in `src/` (PSR-4, namespace `Glpi\`); `inc/includes.php` bootstrap; `front/`, `ajax/`, `routes/` (Symfony routing), `templates/` Twig, `var/` cache/log |
| Plugin | `GlpiPlugin\Urbackup\` (PSR-4 via composer.json, PHP >= 8.3) |
| Versioni plugin | 0.7.3 (setup.php: `PLUGIN_URBACKUP_VERSION`; min GLPI 11.0.6, max 11.99.99) |
| Costanti plugin | `PLUGIN_URBACKUP_DIR` (cartella plugin), `PLUGIN_URBACKUP_WEB_DIR` (`Plugin::getWebDir('urbackup')`) |
**Regola chiave**: in GLPI 11 il codice procedurale e le classi legacy di GLPI 9/10 in `inc/` NON esistono più — le classi core sono in `src/` (es. `Glpi\...`). L'unica classe globale restante in `inc/` è `includes.php`.
---
## 2. Database Layer
### 2.1 `$DB` globale
- `global $DB;` — istanza `class DB extends DBmysql` (generata in `config/` da `DBConnection`).
- Sotto: mysqli. `$DB->update()` ritorna **sempre `true`** → per verificare l'esito usare `$DB->affectedRows()`.
- `$DB->runFile()` è DEPRECATO ma il plugin lo usa SOLO per la creazione dello schema iniziale in `install/install.php` (`plugin_urbackup_install_create_initial_schema()`, `install/mysql/plugin_urbackup-empty.sql`). MAI in upgrade/uninstall (lì si usa `$migration->dropTable()`).
### 2.2 Tabelle del plugin (schema v0.7.3)
```sql
glpi_plugin_urbackup_configs -- id, name, value, date_creation, date_mod (KEY name)
-- riga default in 0.7.2: name='enable_computer', value='1'
-- (toggle tab UrBackup su Computer, letto da Config::getEnableComputer())
glpi_plugin_urbackup_servers -- id, entities_id, is_recursive, name, locations_id, users_id,
-- ip_address, port (55414), protocol (http|https),
-- server_version, api_username, api_password (TEXT, CIFRATO con GLPIKey da 0.7.1),
-- ignore_ssl, is_active, last_api_status, last_api_message,
-- last_api_check, comment,
-- host_itemtype (VARCHAR NULL), host_items_id (INT UNSIGNED DEFAULT 0) -- 0.7.3: asset host
-- date_creation, date_mod
-- KEY name/entities_id/locations_id/users_id/is_active/location_active(locations_id,is_active)/host_asset(host_itemtype,host_items_id)
glpi_plugin_urbackup_serverassets-- id, plugin_urbackup_servers_id, itemtype, items_id,
-- date_creation, date_mod (KEY plugin_urbackup_servers_id, item(itemtype,items_id))
```
- Tabelle legacy DROPPED in 0.7.0: `glpi_plugin_urbackup_profiles`, `glpi_plugin_urbackup_assettypes`.
### 2.3 Query builder (lettura)
```php
$iterator = $DB->request([
'FROM' => Server::getTable(),
'WHERE' => [
'locations_id' => $locations_id,
'is_active' => 1,
],
'ORDER' => 'name',
]);
foreach ($iterator as $row) { ... } // iterazione diretta
$iterator->count(); // numero righe
$iterator->current(); // riga corrente
```
- `DBmysqlIterator` implementa `SeekableIterator, Countable`.
- **Mai concatenare variabili nelle query**: i criteri vengono parametrizzati dal builder.
- `COUNT` con `$DB->request()` è il pattern per i controlli di esistenza idempotenti (vedi `Profile::registerRights()`).
### 2.4 Scrittura
```php
$DB->insert($table, $params); // INSERT
$DB->update($table, $params, $where); // UPDATE (ritorna true sempre → affectedRows)
$DB->delete($table, $where); // DELETE
```
### 2.5 DDL e introspezione
```php
$DB->doQuery("ALTER TABLE ..."); // DDL — query() è DEPRECATO in GLPI 11
$DB->tableExists($tablename); // introspezione (cache)
$DB->fieldExists($table, $field);
$DB->listFields($table); // array colonne (['Field' => ...]) — usato in install.php
$DB->insertId();
$DB->affectedRows();
```
- **`$DB->query()` deprecato** → usare `$DB->doQuery()`.
### 2.6 Pattern di migrazione (`install/install.php`)
- `plugin_urbackup_install_process()`: crea `Migration(PLUGIN_URBACKUP_VERSION)`, esegue in ordine: schema iniziale (se mancano tabelle → `$DB->runFile()`), `update_configs_table`, `convert_assettypes_to_capacities`, `update_servers_table`, `update_serverassets_table`, `encrypt_api_passwords`, `add_enable_computer_config` (inserisce `('enable_computer','1')` se assente), `drop_legacy_tables`, poi `$migration->executeMigration()`; infine `Profile::installRights()`.
- Ogni `addField()`/`addKey()`/`dropField()`/`dropTable()` è idempotente e guardato da `tableExists()`/`fieldExists()`.
- `convert_assettypes_to_capacities()`: legge `glpi_plugin_urbackup_assettypes` (se esiste), auto-abilita la capacità `UrBackupCapacity` su TUTTI gli Asset Definition via `AssetDefinitionManager`, poi droppa la colonna `is_default`.
- **⚠️ CAVEAT verificato**: il codice chiama `$definition->hasCapacity(UrBackupCapacity::class)` e `$definition->enableCapacity(UrBackupCapacity::class)` (install.php:367-368) ma in GLPI 11.0.8 questi metodi NON esistono: su `AssetDefinition` ci sono solo `hasCapacityEnabled(CapacityInterface $capacity)` (richiede un oggetto), `getEnabledCapacities()`, `getCapacityConfiguration()` (AssetDefinition.php:627-652), e l'abilitazione/disabilitazione passa dall'input `capacities` del form processato in `post_updateItem()` (AssetDefinition.php:316-440) — nessun metodo pubblico `enableCapacity/saveCapacity/disableCapacity`. La chiamata è in try/catch → il messaggio d'errore viene mostrato ma la migrazione continua. L'impatto è nullo in pratica perché `Config::isItemtypeEnabled()` ritorna true per tutte le sottoclassi di `Glpi\Asset\Asset` (tab sempre visibile out-of-the-box); da rivedere comunque con l'API core corretta.
- `uninstall.php`: `Profile::uninstallRights()` + `plugin_urbackup_migration_drop_table()` per ogni tabella (drop via `$migration->dropTable()` se esiste).
---
## 3. Session & Sicurezza
### 3.1 Diritti
```php
Session::checkRight($module, $right); // muore con errore 403 se senza diritto
Session::checkLoginUser(); // solo login richiesto (server_test.ajax.php)
Session::haveRight($module, $right); // booleano (senza morte)
Session::getLoginUserID();
Session::getPluralNumber();
Session::addMessageAfterRedirect(...);
Session::getNewCSRFToken(bool $standalone = false);
```
- **IMPORTANTE**: nei file con namespace plugin (`GlpiPlugin\Urbackup\...`) importare `use Session;` (e `Html`, `Toolbox`, `GLPIKey`, ecc.), altrimenti PHP risolve `GlpiPlugin\Urbackup\Session` che non esiste.
- **Pattern diritti del plugin**: `Profile::canCurrentUser(int $right): bool` (src/Profile.php) — risolve il profilo attivo da `$_SESSION['glpiactiveprofile']['id']`, con fallback su `glpi_profiles_users` (profilo statico) e su `Session::haveRight()`; il rightname è `'plugin_urbackup'`.
- `Server::canView()/canCreate()/canUpdate()/canDelete()/canPurge()` delegano a `Profile::canCurrentUser(...)`.
- `Server::$rightname = 'plugin_urbackup'``getRights()` espone READ/UPDATE/CREATE/DELETE/PURGE nella UI standard dei profili GLPI.
### 3.2 CSRF — GLPI 11 breaking change
- Hook `Hooks::CSRF_COMPLIANT = 'csrf_compliant'` registrato in `setup.php` → GLPI gestisce i token automaticamente.
- **Il listener globale `CheckCsrfListener` consuma il token su ogni POST** → una seconda chiamata esplicita a `Session::checkCSRF()` fallisce (in GLPI 11 richiede `$data` come argomento). Il plugin NON la chiama nei front (fix 0.7.0).
- Form POST: `Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()])` (pattern in AssetTab/Server/MassiveAction).
- AJAX (`public/js/urbackup.js`): header `X-Glpi-Csrf-Token` con `getAjaxCsrfToken()`.
### 3.3 Profili e diritti (pattern verificato 0.7.x)
- `Profile::registerRights()``ProfileRight::addProfileRights(['plugin_urbackup'])` se assente in `glpi_profilerights`.
- `Profile::installRights()`: profilo attivo da sessione; **in CLI (install via console) non c'è sessione** → cerca il profilo "Super-Admin" con query diretta e assegna `READ|UPDATE|CREATE|DELETE|PURGE`; agli altri profili garantisce `READ` se non esiste record.
- `Profile::uninstallRights()``$DB->delete('glpi_profilerights', ['name' => 'plugin_urbackup'])` + `ProfileRight::deleteProfileRights()`.
- Hook `Hooks::CHANGE_PROFILE``Profile::initProfile($profile)`: se il nuovo profilo non ha diritti, assegna READ.
---
## 4. Criptazione Segreti
**⚠️ `Toolbox::encrypt()/decrypt()` NON ESISTE in GLPI 11** (verificato in `src/Toolbox.php`). API corretta (verificata in core: `APIRest.php:623`, `ClientRepository.php:90``GLPIKey::getInstance()` **NON esiste**):
```php
use GLPIKey; // classe globale, serve solo nei file con namespace plugin
(new GLPIKey())->encrypt(string $string, ?string $key = null): string;
(new GLPIKey())->decrypt(?string $string, ?string $key = null): ?string;
```
- `encrypt()` ritorna `''` se la chiave non è leggibile (mai dati in chiaro propagati); `decrypt()` su valore non cifrato emette `trigger_error` e ritorna `''` → rilevare il formato prima di decifrare (`Server::isApiPasswordEncrypted()`: base64 strict + nonce ≥ 24 byte).
- **STATO ATTUALE (05/08/2026)**: `api_password` su `glpi_plugin_urbackup_servers` è **cifrata con GLPIKey** — encrypt in `Server::prepareInputForAdd/Update` (campo vuoto = mantieni), `Server::getApiPassword()` decifra on-the-fly con fallback per valori legacy in chiaro, migrazione idempotente `plugin_urbackup_install_encrypt_api_passwords()` in install.php. Mai loggare o esporre la password decifrata.
---
## 5. Html & Escaping
```php
Html::header($title, $url, 'admin', 'GlpiPlugin\Urbackup\Server'); // header pagina
Html::footer();
Html::redirect($url);
Html::hidden($name, ['value' => $value]); // campo hidden (NOTA: in GLPI 11 la firma è (name, options))
Html::submit($label, ['name' => ..., 'class' => ...]);
Html::input($name, ['value' => ..., 'size' => ...]); // input testuale
Html::convDate($date);
Html::convDateTime($date);
Html::displayRightError(); // errore diritti insufficienti
Html::displayNotFoundError(); // item non trovato
Html::closeForm();
Html::header_nocache(); // endpoint AJAX
```
### Escaping (XSS)
- `htmlspecialchars()` su TUTTI gli output dinamici (pattern diffuso in AssetTab/Server/ServerAsset).
- `Html::entities_deep($array)` — sanitizzazione array di input.
- Twig: auto-escaping (vedi §11).
---
## 6. Dropdown & Search
### 6.1 Dropdown
```php
Dropdown::showFromArray($name, $values, $options); // dropdown generico da array (protocollo, server list)
Dropdown::showYesNo($name, $value); // is_active, ignore_ssl, ecc.
Entity::dropdown(['name' => 'entities_id', 'value' => ...]);
Location::dropdown(['name' => 'locations_id', 'value' => ...]);
Server::dropdown([...]); // dropdown itemtype del plugin (MassiveAction)
Dropdown::getDropdownName($table, $id);
```
### 6.2 Search options (`Server::rawSearchOptions()`)
- ID usati: `common` (Characteristics), 1 name (itemlink), 2 ip_address (string), 3 port (integer), 4 protocol (string), 5 server_version (string), 6 Entity (dropdown), 7 Location (dropdown), 8 User (dropdown — richiede colonna `users_id`), 9 is_active (bool), 10 last_api_status (bool), 11 last_api_check (datetime), 12 date_creation, 13 date_mod, 14 id (raw, `searchtype => 'view'`, solo con UPDATE).
- `Search::getOptions($itemtype)` / `Search::show($itemtype, $params)` disponibili nel core (`src/Search.php`).
---
## 7. CommonDBTM / CommonGLPI / Capacity system GLPI 11
### 7.1 Classi base
- `CommonDBTM` — tabella + CRUD generico: `Server`, `ServerAsset`, `Config`, `AssetTab`, `MassiveAction` (del plugin).
- `CommonGLPI` — item senza tabella (tab, UI).
- **Capacity system GLPI 11** — `Glpi\Asset\Capacity\AbstractCapacity` (vedi §7.4).
### 7.2 Metodi lifecycle sovrascritti nel plugin
```php
getTable($classname = null): string; // nome tabella custom
getTypeName($nb = 0): string; // _n(..., 'urbackup')
getRights($interface = 'central'); // READ/UPDATE/CREATE/DELETE/PURGE
canView()/canCreate()/canUpdate()/canDelete()/canPurge();
defineTabs($options); // Server: default form + ServerAsset + Log
rawSearchOptions(); // §6.2
showForm($ID, $options); // form edit + showFormHeader/showFormFields/showFormButtons
prepareInputForAdd($input); // default port 55414, protocol http, users_id da sessione
prepareInputForUpdate($input); // test connessione API in salvataggio (last_api_status/message/check)
getMenuName()/getMenuContent(); // menu Admin (icona 'ti ti-cloud-up')
```
### 7.3 Tabs
```php
getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string; // ritorna createTabEntry(...) o ''
displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool;
```
- `AssetTab` su **Computer** (registerClass `addtabon => ['Computer']`) e su **Asset Definition** via capacity (registrazione tab in `onClassBootstrap`).
- `Profile` (del plugin) su `\Profile` (tab diritti, render Twig `@urbackup/profile.html.twig`).
- `Server` → tab `ServerAsset` (Linked assets) + `Log`.
- Sub-tab interni in `AssetTab::showInternalTabs()`: **Stato / Azioni (solo UPDATE|CREATE) / Info-Log**, resi con Bootstrap tabs (`nav-tabs`).
### 7.4 Capacity system GLPI 11 (URBackupCapacity — `src/Capacity/UrBackupCapacity.php`)
```php
use Glpi\Asset\Capacity\AbstractCapacity;
final class UrBackupCapacity extends AbstractCapacity {
getLabel(): string; // 'UrBackup'
getIcon(): string; // 'ti ti-cloud-up'
getDescription(): string;
onClassBootstrap(string $classname, CapacityConfig $config): void;
// -> CommonGLPI::registerStandardTab($classname, AssetTab::class)
onCapacityDisabled(string $classname, CapacityConfig $config): void;
// -> pulizia ServerAsset::deleteByCriteria(['itemtype' => $classname], force: true) + deleteRelationLogs()
isUsed(string $classname): bool; // countAssetsLinkedToPeerItem(... ServerAsset) > 0
getCapacityUsageDescription(string $classname): string;
}
```
- Registrazione in `plugin_init_urbackup()`: `AssetDefinitionManager::getInstance()->registerCapacity(new UrBackupCapacity())`.
- `onClassBootstrap` viene chiamato da `bootDefinitions()` durante l'evento kernel **PostBoot** → i tab vengono registrati su tutti i definition abilitati anche se creati dopo l'init del plugin.
- `Config::isItemtypeEnabled()`: `Computer` sempre true; qualsiasi sottoclasse di `Glpi\Asset\Asset` sempre true (visibilità sempre on; la capacità serve per usage tracking e cleanup).
- `Config::getEnabledItemtypes()`: `['Computer', ...classi asset definition]` via `AssetDefinitionManager::getDefinitions()`.
### 7.5 CRUD istanza
```php
$obj = new Server();
$obj->getEmpty();
$obj->getFromDB($id);
$obj->add($input); $obj->update($input); $obj->delete($input);
$obj->getField('colonna');
$obj->check($id, RIGHT); // autorizzazione (muore se senza diritto)
```
### 7.6 Massive Actions (plugin)
- Hook `Hooks::USE_MASSIVE_ACTION = 'use_massive_action'` in `setup.php` + `plugin_urbackup_MassiveActions($type)` in `hook.php`.
- **ATTENZIONE**: l'hook `plugin_urbackup_MassiveActions($type)` riceve l'itemtype come **stringa** (es. `'Computer'`), NON un oggetto MassiveAction; ritorna `['Classe::SEPARATOR::azione' => 'Label']` con `\MassiveAction::CLASS_ACTION_SEPARATOR`.
- Azioni: `ACTION_CONNECT_SERVER = 'connect_server'` (UPDATE|CREATE), `ACTION_DISCONNECT_SERVER = 'disconnect_server'` (UPDATE).
- `showMassiveActionsSubForm(\MassiveAction $ma)`: dropdown `Server::dropdown(['condition' => ['is_active' => 1]])`.
- `processMassiveActionsForOneItemtype(\MassiveAction $ma, CommonDBTM $item, array $ids)`: verifica `Config::isItemtypeEnabled()` e diritti, chiama `ServerAsset::connectAssetToServer()` / `disconnectAsset()`, ritorna `$ma->itemDone($itemtype, $id, \MassiveAction::ACTION_OK|ACTION_KO)`.
---
## 8. Bootstrap Plugin
### 8.1 setup.php
```php
function plugin_init_urbackup(): void {
global $PLUGIN_HOOKS;
$PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT]['urbackup'] = true;
$PLUGIN_HOOKS[Hooks::CHANGE_PROFILE]['urbackup'] = [Profile::class, 'initProfile'];
Profile::registerRights();
Plugin::registerClass(Config::class);
Plugin::registerClass(Profile::class, ['addtabon' => 'Profile']);
Plugin::registerClass(Server::class, ['linkgroup_types' => true, 'document_types' => true]);
Plugin::registerClass(ServerAsset::class);
Plugin::registerClass(PluginUrbackupMassiveAction::class);
Plugin::registerClass(AssetTab::class, ['addtabon' => ['Computer']]);
if (class_exists(AssetDefinitionManager::class)) {
AssetDefinitionManager::getInstance()->registerCapacity(new UrBackupCapacity());
}
$PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php';
$PLUGIN_HOOKS[Hooks::MENU_TOADD]['urbackup'] = ['admin' => Server::class];
$PLUGIN_HOOKS[Hooks::USE_MASSIVE_ACTION]['urbackup'] = true;
$PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = ['public/css/urbackup.css'];
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = ['public/js/urbackup.js'];
}
function plugin_version_urbackup(): array { /* name, version 0.7.2, requires GLPI 11.0.6-11.99.99, PHP >= 8.3 */ }
function plugin_urbackup_check_prerequisites(): bool { /* version_compare GLPI + PHP */ }
function plugin_urbackup_check_config(bool $verbose = false): bool { return true; }
function plugin_urbackup_install(): bool { require install/install.php; return plugin_urbackup_install_process(); }
function plugin_urbackup_uninstall(): bool { require install/uninstall.php; return plugin_urbackup_uninstall_process(); }
```
- NOTA: top-level di setup.php invalida l'OPcache dei file del plugin via `opcache_invalidate()` (sviluppo web).
### 8.2 hook.php — funzioni standard
| Funzione | Ruolo |
|----------|-------|
| `plugin_urbackup_get_classes()` | array classi: Config, Profile, Server, ServerAsset, PluginUrbackupMassiveAction |
| `plugin_urbackup_MassiveActions($type)` | azioni massivie per itemtype abilitato (stringa in ingresso!) |
---
## 9. Install / Upgrade / Uninstall
- `plugin_urbackup_install_process()` (§2.6) — migrazioni **idempotenti**, schema iniziale con `runFile` (SOLO qui), `Profile::installRights()` con fallback CLI "Super-Admin", `Config::ensureDefaultConfiguration()`.
- `plugin_urbackup_uninstall_process()``Profile::uninstallRights()` + `plugin_urbackup_migration_drop_table()` per configs/servers/serverassets (+ legacy profiles/assettypes).
- Comandi: `php bin/console glpi:plugin:install urbackup`, `php bin/console glpi:plugin:activate urbackup`, disinstallazione via `glpi:plugin:uninstall urbackup`.
---
## 10. UrBackup Web API — `src/UrbackupApiClient.php`
### 10.1 Panoramica
- Endpoint: `http(s)://<ip>:<port>/x?a=<action>` (base URL: `rtrim($server->getWebInterfaceUrl(), '/') . '/x'`).
- cURL: `CURLOPT_TIMEOUT = 30`, `CURLOPT_CONNECTTIMEOUT = 5`, `SSL_VERIFYPEER/VERIFYHOST` secondo `ignore_ssl`, header `Accept: application/json` + `Content-Type: application/x-www-form-urlencoded; charset=UTF-8`, sempre POST (tranne ove indicato).
- Errori: `RuntimeException` con messaggi traducibili (`__()`, dominio 'urbackup'); risposte non-JSON (HTML) rilevate da `str_starts_with(trim($raw), '<')`.
### 10.2 Autenticazione (flusso login)
1. `request('login', [], 'POST', false)` — se `success === true` ok (session id da `login['session']`).
2. Altrimenti `request('salt', ['username' => ...], 'POST', false)``ses` (session), `salt`, `rnd`, `pbkdf2_rounds`.
3. Hash password: `md5($salt_str . $password, true)` → hex; se `pbkdf2_rounds > 0``hash_pbkdf2('sha256', $bin, $salt_str, $rounds)`; finale `md5($rnd . $passwordMd5)`.
4. `request('login', ['username', 'password' => hash, 'ses'])``logged_in = true`.
- Tutte le azioni autenticate passano `ses` nei parametri (`apiAction()`).
### 10.3 Azioni implementate
| Metodo | API action | Parametri chiave |
|--------|-----------|------------------|
| `testConnection()` | `server_identity` (post login) | — |
| `getStatus()` | `status` | — (campi: name, online, status, ip/client_ip, client_version_string, file_lastbackup, image_lastbackup, file_ok, image_ok, lastbackup...) |
| `getClientStatusByName()` | — (filtra `getStatus()`) | match case-insensitive su name/clientname/hostname |
| `getClientIdByName()` | — | id/clientid/client_id |
| `getClientSettings()` | `settings` | `sa=clientsettings`, `t_clientid` |
| `updateClientSettings()` | `settings` | `sa=clientsettings_save`, `t_clientid`, `overwrite=true`, `$key=$value` |
| `saveInternetMode()` | `settings` | chiave `internet_mode_enabled` (≥ 2.4) o `internet_mode` |
| `getClientAuthKey()` | — | setting `internet_authkey` |
| `addClient()` | `add_client` | `clientname` |
| `removeClient()` | `remove_client` | `clientname`, `clientid` |
| `startIncrementalFileBackup()` | `start_backup` | `start_client`, `start_type=incr_file` |
| `startFullFileBackup()` | `start_backup` | `start_type=full_file` |
| `startIncrementalImageBackup()` | `start_backup` | `start_type=incr_image` |
| `startFullImageBackup()` | `start_backup` | `start_type=full_image` |
| `getRecentBackups()` | `backups` | `sa=backups`, `clientid` (file `backups[]` + image `backup_images[]`, ordinati per time desc, default 40) |
| `getClientLogs()` | `livelog` | `clientid`, `lastid` (logdata; aggiorna `lastlogid`) |
### 10.4 Note di compatibilità versioni
- `detectVersion2_4OrHigher()`: version ≥ 2.4 → `internet_mode_enabled`, altrimenti `internet_mode`.
- `extractSettingValue()`: i setting API possono essere struct `{"use":N, "value":..., "value_client":..., "value_group":...}` → estrae `['value']`.
- `responseIsSuccess()`: accetta `success`, `ok`, `saved_ok`, `result === 'ok'`, `start_ok`.
- Cache: `cached_status` (in-memory), `cached_settings[clientid]` (invalidata dopo save).
---
## 11. Twig / TemplateRenderer
```php
use Glpi\Application\View\TemplateRenderer;
$twig = TemplateRenderer::getInstance();
$twig->display('@urbackup/profile.html.twig', ['id' => ..., 'profile' => ..., 'title' => ..., 'rights' => ...]);
```
- Namespace template plugin: `@urbackup/` (directory `templates/` del plugin).
- Auto-escaping Twig attivo: `{{ var }}` escapato; mai logica PHP nei template.
- Pattern verificato in `src/Profile.php::displayTabContentForItem()` (tab diritti su Profilo).
---
## 12. Integrazione Asset / Location (pattern chiave)
### 12.1 Associazione asset ↔ server (`ServerAsset`)
- Tabella `glpi_plugin_urbackup_serverassets` (itemtype polimorfo + items_id + server).
- `connectAssetToServer($itemtype, $items_id, $server_id)`: diritti UPDATE|CREATE, `Config::isItemtypeEnabled()`, upsert (update se link esistente, insert altrimenti).
- `disconnectAsset($itemtype, $items_id)`: diritto UPDATE, delete.
- `getLinkForAsset($itemtype, $items_id, $active_only = true)`: singolo link.
- `extractAssetIp()`: legge `$item->fields['ip_address']` (se presente).
### 12.2 Location-aware (`LocationHelper`)
- **Regola di business**: se l'asset è in una sub-location, il server UrBackup di riferimento è quello assegnato alla **root location**.
- `getRootLocationId(int $locations_id)`: risale `glpi_locations.locations_id` finché `locations_id = 0` (loop `Location::getFromDB()`).
- `getActiveServersForRootLocation(int $locations_id)`: server con `locations_id = $root AND is_active = 1` (query `$DB->request()`).
- `getAvailableServersForAsset()` / `assetIsInSubLocation()`.
- Pattern UI: in `AssetTab::showNoServerLinkedBlock()` il dropdown server è filtrato per root location dell'asset; in `Server::showMissingClientsTab()` gli asset candidati sono filtrati per root location del server.
### 12.3 Batch loading (anti N+1) — `Server::showMissingClientsTab()`
- `batchLoadIps()`: **1 query per itemtype** — INNER JOIN `glpi_ipaddresses AS ipa``glpi_networknames AS nn` (ON `nn.items_id = ipa.id` AND `ipa.itemtype='NetworkName'`) → `glpi_networkports AS np` (ON `np.id = nn.items_id` AND `nn.itemtype='NetworkPort'`), WHERE `np.itemtype = $itemtype AND np.items_id IN ($ids)`. Ritorna `"itemtype:items_id" => ip`.
- `batchLoadGroups()`: **1 query per itemtype** su `glpi_groups_items` (WHERE `itemtype`, `items_id IN`, `type = \Group_Item::GROUP_TYPE_NORMAL`).
- `getCachedName($classname, $id, &$cache)`: cache in-memory per Entity/Location/State/User/Group (`completename` ?? `name`).
- `formatLastBackup()`: timestamp Unix (1..2e9) → `date('Y-m-d H:i:s')`.
### 12.4 Server form (front/server.form.php)
- 4 tab: **Server** (form standard), **Linked clients**, **Unlinked clients**, **Missing clients**.
- `showLinkedClientsTab()`: server con `last_api_status = 1` → confronta asset collegati vs client API `getStatus()` (match case-insensitive su name).
- `showUnlinkedClientsTab()`: client API non ancora collegati; se esiste un asset GLPI con lo stesso nome nella root location del server → bottone **Connect** (POST `link_asset`).
- `showMissingClientsTab()`: asset GLPI della root location non collegati e non presenti su UrBackup (tabella sortable/search in JS, inline).
- Badge stato: `renderOnlineBadge()` (Online/Offline + badge ok/minor_problems/major_problems/paused).
---
## 13. Caveat GLPI 11 verificati (raccolti da MEMORY.md + core)
1. `$DB->query()` **deprecato**`$DB->doQuery()` per SQL raw.
2. `$DB->runFile()` **deprecato** → usarlo SOLO per lo schema iniziale in install.php; mai in upgrade/uninstall (drop via `$migration->dropTable()`).
3. `Toolbox::encrypt/decrypt` **non esiste**`(new GLPIKey())->encrypt()/decrypt()` (`GLPIKey::getInstance()` non esiste).
4. **CSRF GLPI 11**: `Session::checkCSRF()` richiede `$data` come argomento; il listener globale `CheckCsrfListener` consuma il token → il plugin NON la chiama nei front; form con hidden token, AJAX con header `X-Glpi-Csrf-Token`.
5. `Session::isDebugActive()` **non esiste** in GLPI 11 → `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`.
6. `Session`/`Html`/`Toolbox` ecc. sono classi globali → `use Session;` nei namespace plugin.
7. `$DB->update()` ritorna sempre `true` → verificare con `$DB->affectedRows()`.
8. GLPI environment enum: `production`, `development`, `testing`, `staging`, `e2e_testing` — MAI `prod`.
9. **`linkgroup_types => true` richiede la colonna `users_id`** sulla tabella del plugin: senza, `Group::getDataItems()` fallisce con MySQL 1054 (Unknown column 'users_id') — colonna presente su `glpi_plugin_urbackup_servers` (fix 0.7.0).
10. `Profile::installRights()` in **CLI non ha sessione** → fallback: query diretta sul profilo "Super-Admin" e assegnazione diritti completi.
11. **Asset Definition vs Computer**: le asset class di GLPI 11 sono 2-5x più lente (overhead core: Capacity iteration, JSON custom fields, `eval()` autoloading) → usare `$item->fields['name']` diretto (mai `getFromDB()` ridondanti) e batch loading.
12. **`api_password` cifrata con GLPIKey** (05/08/2026): encrypt on save, `Server::getApiPassword()` con fallback legacy, migrazione `plugin_urbackup_install_encrypt_api_passwords()`; campo form vuoto = mantieni.
13. **UrBackup versioni**: setting `internet_mode_enabled` solo da UrBackup ≥ 2.4; struct setting `{"use":N,"value":...}` da estrarre con `extractSettingValue()`.
14. Heredoc JS: non chiamare `__()` dentro heredoc (Server.php usa heredoc per JS inline — stringhe non tradotte lì).
15. Hook `plugin_urbackup_MassiveActions($type)` riceve l'**itemtype stringa**, non un oggetto MassiveAction.
16. Menu: `Hooks::MENU_TOADD['urbackup'] = ['admin' => Server::class]` + `Server::getMenuContent()` (icona `ti ti-cloud-up`).
17. Nessun uso di cron/notifiche SSH nel plugin: tutta la comunicazione è API HTTP verso il server UrBackup.
18. **`hasCapacity()`/`enableCapacity()` non esistono in GLPI 11.0.8** su `AssetDefinition` → check con `hasCapacityEnabled(CapacityInterface $capacity)` (oggetto, non stringa), `getEnabledCapacities()`, `getCapacityConfiguration()` (AssetDefinition.php:627-652); l'enable/disable passa dall'input `capacities` del form in `post_updateItem()` (AssetDefinition.php:316-440). Il codice install.php:367-368 usa i metodi inesistenti dentro try/catch (vedi §2.6).
19. **Standard versione plugin per modifiche DB (regola 6 di AGENTS.md)**: qualsiasi modifica DB richiede bump di `PLUGIN_URBACKUP_VERSION` in setup.php. Meccanismo GLPI verificato in `src/Plugin.php`: `checkPluginState()` (righe ~909-933) confronta `plugin_version_urbackup()['version']` con `glpi_plugins.version`; se diversa aggiorna la riga e imposta `state = NOTUPDATED` (messaggio "Plugin version changed. It has been deactivated as its update process has to be launched."). L'update si esegue con `php bin/console glpi:plugin:install urbackup``Plugin::install()` (riga 1197) chiama `plugin_urbackup_install()` e poi imposta `state = NOTACTIVATED` → quindi serve `php bin/console glpi:plugin:activate urbackup`. Le migrazioni in `plugin_urbackup_install_process()` usano `new Migration(PLUGIN_URBACKUP_VERSION)` e DEVONO restare idempotenti. Comandi console disponibili: `plugin:list`, `plugin:install`, `plugin:activate`, `plugin:deactivate`, `plugin:uninstall` (`src/Glpi/Console/Plugin/`).
20. **Toggle "Computer" configurabile (0.7.2)**: il tab UrBackup su `Computer` NON è una capacità (le capacità valgono solo per Asset Definition) → lo stato è salvato in `glpi_plugin_urbackup_configs` (`enable_computer`). `Config::getEnableComputer()` ha cache statica + guard `TableExists` + fallback `true`; setup.php registra `addtabon Computer` solo se true; `getEnabledAssetDefinitions()` usa `hasCapacityEnabled()` con istanza da `AssetDefinitionManager::getAvailableCapacities()`. Disattivare Computer non tocca i link esistenti in `glpi_plugin_urbackup_serverassets` (solo visibilità UI).
21. **Hardware host del server (0.7.3)**: `glpi_plugin_urbackup_servers.host_itemtype`/`host_items_id` (polimorfici, NULL/0 = nessuno) identificano l'asset (Computer o Asset Definition con capacità attiva) su cui gira il server UrBackup — NON usare `glpi_plugin_urbackup_serverassets` (semantica CLIENT: asset backup da quel server). UI: `Dropdown::showItemTypes` + `Html::scriptBlock` con `$.get``front/dropdown_host.ajax.php` (GET, `Session::checkLoginUser()`, itemtype validato con `class_exists` + `Config::isItemtypeEnabled`) che risponde `Dropdown::show($itemtype, ['entity' => Session::getActiveEntities(), ...])`. Validazione in `Server::prepareInputForUpdate()`: se `host_items_id > 0` ma itemtype mancante/non abilitato/item inesistente → azzeramento entrambi. Più server sullo stesso host consentiti (niente unique). Blocco "This asset hosts the UrBackup server" in AssetTab (sempre visibile, anche se l'asset è client). **Caveat API GLPI 11**: (a) `Ajax::updateItemOnSelectEvent` genera `$("#x").load(url, {params})` = **POST** (data oggetto) → endpoint target deve accettare POST + CSRF AJAX (header `X-Glpi-Csrf-Token` automatico via `$(document).ajaxSend` in public/js/common.js); preferire `$.get` inline via `Html::scriptBlock` per dropdown read-only. (b) `Dropdown::show` è **lazy** (`Html::jsAjaxDropdown`): il markup NON contiene le opzioni, che arrivano via POST select2 a `/ajax/getDropdownValue.php`; il parametro per le entità è **`entity`** (NON `entity_restrict`, che è solo l'output serializzato); `Session::getMatchingActiveEntities()` è un filtro che richiede 1 argomento → usare `Session::getActiveEntities()`.
---
## 14. Conclusione
Questo file è la mappa dell'API GLPI 11 usata dal plugin urbackup. Se una modifica del core GLPI richiede nuove funzioni, aggiornare questo file e verificare la firma reale in `/var/www/glpi/src/` prima di scrivere codice.
-342
View File
@@ -1,342 +0,0 @@
# GLPIDEV.md — API GLPI 11.0.8 Reference (per plugin netbackup)
> **DA LEGGERE ALL'INIZIO DI OGNI SESSIONE DI LAVORO**, insieme a SKILL.md e MEMORY.md.
> File riassuntivo dell'API GLPI 11 usata dal plugin, generato analizzando il core reale in `/var/www/glpi` (versione **11.0.8**).
> Verificato su: `/var/www/glpi/src`, `/var/www/glpi/inc`, `/var/www/glpi/plugins/netbackup`.
---
## 1. Ambiente
| Voce | Valore |
|------|--------|
| GLPI | 11.0.8 (`/var/www/glpi`) |
| PHP | 8.2+ |
| Struttura | tutto il codice core in `src/` (PSR-4, namespace `Glpi\`); `inc/includes.php` bootstrap; `front/`, `ajax/`, `routes/` (Symfony routing), `templates/` Twig, `var/` cache/log |
| Plugin | `GlpiPlugin\Netbackup\` (PSR-4 via composer.json) |
**Regola chiave**: in GLPI 11 il codice procedurale e le classi legacy di GLPI 9/10 in `inc/` NON esistono più — le classi core sono in `src/` (es. `Glpi\...`). L'unica classe globale restante in `inc/` è `includes.php`.
---
## 2. Database Layer
### 2.1 `$DB` globale
- `global $DB;` — istanza `class DB extends DBmysql` (generata in `config/` da `DBConnection`, vedi `src/DBConnection.php:164`).
- Sotto: mysqli. `$DB->update()` ritorna **sempre `true`** → per verificare l'esito usare `$DB->affectedRows()` (legge `mysqli::$affected_rows`). Verificato: pattern `claimJob` in `src/BackupJob.php` usa `$DB->affectedRows() === 1` su UPDATE condizionale `WHERE status='pending'`.
### 2.2 Query builder (lettura)
```php
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'networkequipments_id' => $id,
'is_active' => 1,
'OR' => [
['field' => ['LIKE', '%x%']],
['field' => null],
],
],
'ORDER' => ['date DESC', 'id DESC'],
'LIMIT' => 10,
'OFFSET' => 0,
'LEFT JOIN' => [
'glpi_networkequipments' => [
'FKEY' => ['glpi_plugin_netbackup_equipments' => 'networkequipments_id', 'glpi_networkequipments' => 'id'],
],
],
'COUNT' => 'cpt', // SELECT COUNT(*) AS cpt
]);
foreach ($iterator as $row) { ... } // iterazione diretta
$iterator->count(); // numero righe
$iterator->numrows(); // alias
$iterator->fetchFields();
$iterator->current(); // riga corrente
```
- `DBmysqlIterator` (`src/DBmysqlIterator.php`) implementa `SeekableIterator, Countable`.
- **Mai concatenare variabili nelle query**: i criteri vengono parametrizzati dal builder.
- `COUNT` con `$DB->request()` è il pattern per i controlli di esistenza idempotenti (vedi MEMORY.md, `insert_missing_vendors()`).
### 2.3 Scrittura
```php
$DB->insert($table, $params); // INSERT
$DB->update($table, $params, $where); // UPDATE (ritorna true sempre → affectedRows)
$DB->delete($table, $where); // DELETE
$DB->updateOrInsert($table, $params, $where, $onlyone = true);
```
### 2.4 DDL e introspezione
```php
$DB->doQuery("ALTER TABLE ... ADD COLUMN ..."); // DDL — query() è DEPRECATO in GLPI 11
$DB->doQueryOrDie($query, $message);
$DB->tableExists($tablename); // introspezione (cache)
$DB->fieldExists($table, $field);
$DB->getField(string $table, string $field, $usecache = true): ?array; // ritorna l'array dei campi della tabella
$DB->insertId();
$DB->affectedRows();
```
- **`$DB->query()` deprecato** → usare `$DB->doQuery()`.
- **`$DB->runFile()` deprecato** → MAI usare (MEMORY.md: uninstall stabile con TRUNCATE + doQuery, mai runFile).
### 2.5 Pattern di migrazione
- `migrate_tables()` idempotente: ogni `ADD COLUMN` guardato da `tableExists()`/`fieldExists()`.
- Ogni `update_X_Y_Z()` chiama `plugin_netbackup_migrate_tables()` + funzioni di inserimento dati idempotenti (check `COUNT` prima di INSERT).
- MAI inserire dati nel DB con SQL manuale / `mysql` CLI: solo logica di migrazione PHP.
---
## 3. Session & Sicurezza
### 3.1 Diritti
```php
Session::checkRight($module, $right); // muore con errore 403 se senza diritto (protezione front/*.php)
Session::checkRightsOr($module, $rights = []);
Session::checkLoginUser(); // solo login richiesto
Session::haveRight($module, $right); // booleano (senza morte)
Session::haveRightsAnd($module, $rights);
Session::haveRightsOr($module, $rights);
Session::getLoginUserID(); // id utente corrente
Session::getPluralNumber(); // per stringhe pluralizzate
Session::addMessageAfterRedirect(...); // messaggi UI post-redirect
Session::getNewCSRFToken(bool $standalone = false);
```
- **IMPORTANTE**: nei file con namespace plugin (`GlpiPlugin\Netbackup\...`) importare `use Session;` (e `Html`, `Toolbox`, `GLPIKey`, ecc.), altrimenti PHP risolve `GlpiPlugin\Netbackup\Session` che non esiste.
### 3.2 CSRF
- Hook `Hooks::CSRF_COMPLIANT = 'csrf_compliant'` registrato in `setup.php` → GLPI gestisce i token automaticamente per i form del plugin.
- Nei POST manuali: campo hidden `_glpi_csrf_token` con `Session::getNewCSRFToken()`.
- Tutti i form POST del plugin validano il token (pattern: `Html::hidden('_glpi_csrf_token', Session::getNewCSRFToken())`).
### 3.3 Profili e diritti (pattern verificato v1.6.x)
- `Profile::registerRights()` → `ProfileRight::addProfileRights()` → **bump `last_rights_update`** per tutti i profili, altrimenti la sessione non si aggiorna (`Session::haveRight` torna false — bug sessione stale, MEMORY.md "Fix Session Rights").
- `Profile::initProfile()` (hook `Hooks::CHANGE_PROFILE = 'change_profile'`) sincronizza i diritti di sessione dopo il cambio profilo.
- `plugin_init_netbackup()` confronta DB vs sessione a ogni page load e aggiorna se diverso.
- Right costanti: `READ`, `UPDATE`, `CREATE`, `DELETE`, `PURGE`, `ALLSTANDARDRIGHT`.
---
## 4. Criptazione Segreti
**⚠️ `Toolbox::encrypt()/decrypt()` NON ESISTE in GLPI 11** (verificato: nessuna funzione encrypt/decrypt in `src/Toolbox.php`). API corretta:
```php
use GLPIKey;
GLPIKey::getInstance()->encrypt(string $string, ?string $key = null): string;
GLPIKey::getInstance()->decrypt(?string $string, ?string $key = null): ?string;
```
- `src/GLPIKey.php:432` / `:463`. Per compatibilità: `decryptUsingLegacyKey()` (`:526`).
- Il plugin usa già `GLPIKey` (6 occorrenze in src/).
- Mai segreti in chiaro nel DB; mai chiavi hardcoded nel codice.
---
## 5. Html & Escaping
```php
Html::header(...); // header pagina (con titolo)
Html::footer(...);
Html::back(); // pulsante indietro
Html::redirect($url);
Html::hidden($name, $value); // campo hidden
Html::submit($name, $value); // pulsante submit
Html::scriptBlock($js); // blocco script (polling JS del plugin)
Html::convDate($date);
Html::convDateTime($date);
Html::displayRightError(); // errore diritti insufficienti
```
### Escaping (XSS)
- `Html::cleanInputText($value)` — input testuali.
- `Html::entities_deep($array)` — sanitizzazione array di input.
- `htmlescape($string)` (global helper GLPI) / `htmlspecialchars()` — output.
- Twig: auto-escaping (vedi §11).
---
## 6. Dropdown & Search
### 6.1 Dropdown
```php
Dropdown::showFromArray($name, $values, $options); // dropdown generico da array
Dropdown::showYesNo($name, $value);
Dropdown::getDropdownName($table, $id); // nome dropdown da id
```
### 6.2 Search (colonne custom NetworkEquipment)
Hook di GLPI in `hook.php`:
- `plugin_netbackup_getAddSearchOptions(string $itemtype): array` — registra le search options 82008204 con `'jointype' => 'child'` su `glpi_plugin_netbackup_equipments` (`alias.networkequipments_id = glpi_networkequipments.id`).
- `plugin_netbackup_giveItem(string $type, int $ID, array $data, string $num): string` — rendering colonne (`giveItem`).
- `plugin_netbackup_searchOptionsValues(array $PARAM): bool` — hook `Hooks::AUTO_SEARCH_OPTION_VALUES = 'searchOptionsValues'`.
**⚠️ CAVEAT verificato (MEMORY.md)**: l'hook `searchOptionsValues` NON viene mai chiamato da GLPI per i dropdown `datatype => 'specific'` — il core usa sempre l'output di `getValueToSelect()` (input). **Il pattern funzionante è l'override del metodo `Equipment::getSpecificValueToSelect()`** che ritorna `Dropdown::showFromArray(...)`. Non rimuovere l'override!
- datatype usati: `'bool'` (8200), `'specific'` nosearch (8201), `'varchar'` (8202), `'datetime'` (8203), `'specific'` con `searchtype => ['equals','empty']` (8204).
- `Search::getOptions($itemtype)` / `Search::show($itemtype, $params)` disponibili nel core (`src/Search.php`).
---
## 7. CommonDBTM / CommonDBChild / CommonGLPI
### 7.1 Classi base
- `CommonDBTM` — tabella + CRUD generico.
- `CommonDBChild` — riga figlia di un item (pattern: `glpi_plugin_netbackup_equipments.networkequipments_id`).
- `CommonDropdown` — dropdown.
- `CommonGLPI` — item senza tabella (tab, UI).
### 7.2 Metodi lifecycle sovrascritti nel plugin
```php
getTypeName($nb = 0); // nome tipo (traducibile, _n())
getIcon(): string; // icona (ti ti-*)
getEmpty(); // riga vuota con default
prepareInputForAdd($input); // sanitizzazione/validazione pre-add
prepareInputForUpdate($input);
post_addItem() / post_updateItem() / post_purgeItem();
getField($field); // valore campo dalla riga caricata
getAdditionalFields();
showForm($ID, $options = []); // form edit
```
### 7.3 Tabs (integrazione su NetworkEquipment e Profile)
```php
getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string; // ritorna ['1' => 'Backup settings']
displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool;
```
Pattern verificato in `src/Equipment.php:48-55` (tab "Backup settings" su NetworkEquipment) e `src/Profile.php` (tab diritti).
### 7.4 CRUD istanza
```php
$obj = new MyClass();
$obj->getEmpty();
$obj->add($input);
$obj->update($input);
$obj->delete($input, $force = false);
$obj->getFromDB($id);
$obj->getField('colonna');
$obj->getTable(); // nome tabella
```
### 7.5 Massive Actions
Hook `Hooks::USE_MASSIVE_ACTION = 'use_massive_action'` in `setup.php` + hook `'massiveaction'` in hook.php.
- `getSpecificMassiveActions()` → array `['key' => __('label'), 'sep' => ..., 'classe' => ...]` (separatore `MassiveAction::CLASS_ACTION_SEPARATOR`).
- `showMassiveActionsSubForm(MassiveAction $ma)` — form parametri.
- `processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, array $ids)` — esecuzione.
- Ritorni: `MassiveAction::ACTION_OK` / `MassiveAction::ACTION_KO` (classe `src/MassiveAction.php`).
- Azioni del plugin: `activate_netbackup`, `deactivate_netbackup`, `enable_custom`, `disable_custom`, `bulk_set_custom`, `manual_backup`, `kill_stuck_workers`.
---
## 8. Bootstrap Plugin
### 8.1 setup.php
```php
function plugin_init_netbackup(): void {
global $PLUGIN_HOOKS;
$PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT]['netbackup'] = true;
$PLUGIN_HOOKS[Hooks::CHANGE_PROFILE]['netbackup'] = [Profile::class, 'initProfile'];
$PLUGIN_HOOKS[Hooks::USE_MASSIVE_ACTION]['netbackup'] = 1;
Plugin::registerClass(BackupJob::class, ['notificationtemplates_types' => true]);
// ...
}
function plugin_version_netbackup(): array { /* name, version, requires, author, license, homepage */ }
```
- `set_time_limit(0)` a top-level di setup.php (non in hook) — evita timeout durante uninstall su DB grandi.
- Versionamento: `PLUGIN_NETBACKUP_VERSION` in setup.php, aggiornato a ogni release.
### 8.2 hook.php — funzioni standard
| Funzione | Ruolo |
|----------|-------|
| `plugin_netbackup_init()` | ogni page load: ensureDefaults vendor, warning configs/ non scrivibile, check patch vendor phpseclib |
| `plugin_netbackup_install()` | install: schema, defaults, notifiche, cron |
| `plugin_netbackup_uninstall()` | TRUNCATE prima di DROP, CronTask::unregister, cancellazione notifiche |
| `plugin_netbackup_migrate_tables()` | schema idempotente (tableExists/fieldExists guard) |
| `plugin_netbackup_update_X_Y_Z()` | migrazioni di versione (ogni step singolo, chiama migrate_tables) |
| `plugin_netbackup_getAddSearchOptions()` | search options NetworkEquipment |
| `plugin_netbackup_giveItem()` | rendering colonne lista |
| `plugin_netbackup_searchOptionsValues()` | hook AUTO_SEARCH_OPTION_VALUES (non scatta per 'specific' — vedi §6.2) |
| `plugin_netbackup_getDropdown()` | hook `AUTO_GET_DROPDOWN = 'getDropdown'` |
---
## 9. Cron
```php
CronTask::register(string $itemtype, string $name, int $frequency, array $options = []): bool;
CronTask::unregister(string $plugin);
```
- Core: `src/CronTask.php:966` (`register`, lowercase; PHP è case-insensitive, `CronTask::Register` funziona).
- `$itemtype` = classe del plugin con i metodi cron (`'GlpiPlugin\Netbackup\Cron'`), `$name` = nome task.
- `$options`: `allowmode` (`MODE_INTERNAL | MODE_EXTERNAL`), `mode`, `state`, `param`, `hourmin`, `hourmax`, `comment`.
- Se `GLPI_SYSTEM_CRON` è definito e allowmode ha MODE_EXTERNAL → `mode = MODE_EXTERNAL`.
- Callback (in `src/Cron.php`):
```php
public static function cronInfo($name): array; // ['name' => ..., 'description' => ..., 'state' => 0|1]
public static function cronNetbackup($task): int; // ritorna numero job enqueueati (0 = ok)
```
- **REGOLA ARCHITETTURALE (MEMORY.md)**: il cron GLPI NON esegue MAI SSH — solo enqueue in `glpi_plugin_netbackup_backupjobs`. Il worker CLI (`front/worker.php`, crontab) esegue materialmente i backup.
- Task registrati: `Netbackup` (60s), `NetbackupReport` (86400s).
---
## 10. Notifiche
- `Plugin::registerClass(BackupJob::class, ['notificationtemplates_types' => true])` → `NotificationTargetBackupJob` auto-scoperto dall'itemtype.
- Classe: `src/NotificationTargetBackupJob.php` estende `NotificationTarget`:
- `getEvents()` → `['backup_success' => ..., 'backup_failed' => ..., 'backup_warning' => ..., 'backup_report' => ...]`
- `addDataForTemplate()` → placeholders `##device.name_html##`, `##device.status_html##`, ecc. (devono corrispondere ESATTAMENTE al template DB)
- `getTags()` → lista tag
- `addAdditionalTargets()` → registra `GLOBAL_ADMINISTRATOR`, `ENTITY_ADMINISTRATOR`
- `getEventsToSendImmediately()` → override: `backup_success`, `backup_failed`, `backup_warning`, `backup_report` → invio immediato, NON in coda `glpi_queuednotifications`
- Emissione: `NotificationEvent::raiseEvent('backup_success', $backup, ['entities_id' => ...])` — solo per backup manuali (`users_id > 0`); i backup schedulati dal cron NON emettono notifiche per-device (fix v1.4.1).
- Template in DB: `glpi_notificationtemplates`, `glpi_notificationtemplatetranslations` (EN/IT/DE).
- **REGOLA UPGRADE**: mai sovrascrivere notifiche personalizzate — `plugin_netbackup_backup_notifications_to_sql()` (backup SQL in `backups/notifications/`) PRIMA di qualsiasi operazione; `install_notifications_netbackup()` skippa se esistono; `plugin_netbackup_regenerate_notifications()` solo per forzatura esplicita.
---
## 11. Twig / TemplateRenderer
```php
use Glpi\Application\View\TemplateRenderer;
$twig = TemplateRenderer::getInstance();
$twig->display('@netbackup/profile.html.twig', ['key' => $value]);
```
- Namespace template plugin: `@netbackup/` (directory `templates/` del plugin).
- Auto-escaping Twig attivo: `{{ var }}` escapato; mai logica PHP nei template.
- Pattern verificato in `src/Profile.php:41-42`.
---
## 12. Integrazione NetworkEquipment & IP
- Tabelle custom legate a `glpi_networkequipments.id` (FKEY).
- **⚠️ `glpi_networkequipments.ip` NON esiste in GLPI 11** — gli IP vivono in `glpi_ipaddresses` (IPAM, `src/IPAddress.php`). Il plugin risolve l'IP dal tab IPAM del device.
- Tab "Backup settings" via `getTabNameForItem`/`displayTabContentForItem` (§7.3).
- Search options con `'jointype' => 'child'` + nome tabella corretto (con 's') — un custom `condition` da solo causa join auto-FK sbagliato (MEMORY.md).
- Massive actions integrate nella lista NetworkEquipment (§7.5).
---
## 13. Caveat GLPI 11 verificati (raccolti da MEMORY.md + core)
1. `$DB->query()` **deprecato** → `$DB->doQuery()` per SQL raw.
2. `$DB->runFile()` **deprecato** → mai usare (uninstall: TRUNCATE prima di DROP).
3. `Toolbox::encrypt/decrypt` **non esiste** → `GLPIKey::getInstance()->encrypt()/decrypt()`.
4. `CronTask::Register/Unregister` → `CronTask::register/unregister` (case-insensitive).
5. `Session`/`Html`/`Toolbox` ecc. sono classi globali → `use Session;` nei namespace plugin.
6. `$DB->update()` ritorna sempre `true` → verificare con `$DB->affectedRows()`.
7. GLPI environment enum: `production`, `development`, `testing`, `staging`, `e2e_testing` — MAI `prod`.
8. CSRF: hook `Hooks::CSRF_COMPLIANT` + token `_glpi_csrf_token` nei POST.
9. Hook `searchOptionsValues` (AUTO_SEARCH_OPTION_VALUES) non scatta per datatype `specific` → override `getSpecificValueToSelect()`.
10. `set_time_limit(0)` in setup.php top-level (non in hook) — uninstall su DB grandi.
11. Notifiche: se cancellate e ricreate si perdono i destinatari → backup SQL prima.
12. Heredoc e `__()`: NON chiamare `__()` dentro heredoc — pre-calcolare le stringhe tradotte.
13. phpseclib patchate (`vendor/phpseclib/.../Net/SSH2.php`): riapplicare dopo ogni `composer update` (applicatore in `update_1_6_7`, verifica via `scripts/run_tests.php`).
14. `status` nei DB del plugin: Backup usa `success`/`failed`/`warning`; BackupJob usa `pending`/`running`/`success`/`failed`.
15. `config_data` è `LONGTEXT` (da 1.6.10) — nessun rischio troncamento; il flusso è file-first (`files/_plugins/netbackup/configs/*.cfg`).
16. Permessi `configs/`: directory deve essere scrivibile da `www-data` (`sudo chown -R www-data:www-data files/_plugins/netbackup/`); warning in `plugin_netbackup_init()` se non scrivibile.
---
## 14. Conclusione
Questo file è la mappa dell'API GLPI 11 usata dal plugin. Se una modifica del core GLPI richiede nuove funzioni, aggiornare questo file e verificare la firma reale in `/var/www/glpi/src/` prima di scrivere codice.
+202 -8
View File
@@ -1,6 +1,180 @@
# MEMORY.md - Stato del Plugin UrBackup
## Ultima modifica: 28/05/2026
## Ultima modifica: 07/08/2026
## 07/08/2026 — Fix dropdown "Hardware host" (0.7.3): elemento non compariva
- **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).
## 07/08/2026 — Bump 0.7.3 (link Server ↔ asset host hardware)
- **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).
- **front/dropdown_host.ajax.php (NUOVO)**: GET only, `Session::checkLoginUser()`, validazione `class_exists` + `Config::isItemtypeEnabled`, risponde `Dropdown::show($itemtype, ['name'=>'host_items_id','value'=>..., 'entity'=>Session::getActiveEntities(), 'display_emptychoice'=>true])`.
- **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).
- **Stato attuale**: `glpi_plugins.version=0.7.3`, `state=1` (ACTIVE — update UI eseguito dall'utente il 07/08/2026, poi fix dropdown) → attende verifica visiva finale dell'utente.
- **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).
- **Verifica headless**: 1366px → wrap=nowrap, x=8/129/214 stessa y; 480px → wrap=wrap.
- **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).
## 05/08/2026 — Tab asset: card "Stato UrBackup", rename "Client State", fix badge Internet mode
- **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`.
- Locales: +2 msgid (`Client State`, `API status`), -1 (`State`) → 181 tradotti + header; `.mo` ricompilati (msgfmt --check OK). Nessun bump versione (solo UI, regola 6 non applicabile).
## 05/08/2026 — Bump 0.7.2 (toggle Computer configurabile + lista Asset custom)
- **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).
## 05/08/2026 — Bump 0.7.1 (migrazione DB: cifratura api_password)
- Modifica DB della sessione (nuova migrazione `plugin_urbackup_install_encrypt_api_passwords()`) → bump `PLUGIN_URBACKUP_VERSION` `0.7.0``0.7.1`.
- Eseguita la procedura standard: `glpi:plugin:install urbackup` + `glpi:plugin:activate urbackup`; verificato `glpi_plugins.version = 0.7.1`, `state = ACTIVE`, pagine front funzionanti.
- `README.md` Changelog 0.7.1 aggiunto; header `.po` → 0.7.1 con `.mo` ricompilati.
## 05/08/2026 — Cifratura api_password, ottimizzazioni, localizzazioni e correzione claim ambiente
### 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`.
- `urbackup-api-actions.md` aggiornato: login 2-fasi riuscito, rimosso riferimento a `changeClientSetting()` (dead code rimosso).
### 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).
### 6. Localizzazioni it_IT / de_DE / en_GB aggiornate
- 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)`).
2. **`front/server_test.ajax.php`**:
- rimosso `$AJAX_INCLUDE = 1;` (no-op in GLPI 11, innesca deprecation);
- aggiunto bootstrap `include_once inc/includes.php`;
- **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.
## 04/08/2026 — Documentazione AI: AGENTS.md / SKILL.md / GLPIDEV.md specifici per urbackup
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)
- **URL**: `http://localhost:55414`
- **Credenziali**: utente `admin`, password `12345678`
- **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).
## GLPI 11 — Session::checkCSRF() breaking change
In GLPI 11, `Session::checkCSRF()` richiede il primo argomento `$data` (i dati POST da validare). In GLPI ≤10 accettava zero argomenti.
@@ -9,7 +183,7 @@ Il listener globale `CheckCsrfListener` gestisce già il CSRF per tutte le richi
### 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
- `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)
## Performance - Asset Definition vs Computer
@@ -29,13 +203,13 @@ L'overhead è in GLPI 11 core, non nel plugin:
## Architettura
- `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
- `setup.php` — registra capacità, CSS, JS, hook
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log)
- `setup.php` — registra capacità, CSS, hook
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log) + host server block
- `src/ServerAsset.php` — gestione collegamenti asset-server
- `src/Config.php` — itemtype enabled check
- `src/UrbackupApiClient.php` — client API con caching in-memory (per istanza) e sessione
- `src/LocationHelper.php` — risoluzione location radice
- `src/Server.php` — CRUD server, tab missing clients, form
- `src/Server.php` — CRUD server, tab missing clients, form (incl. host asset)
## Asset Tab Interni
- 3 sub-tab: Stato, Azioni (solo UPDATE/CREATE), Info/Log
@@ -44,7 +218,7 @@ L'overhead è in GLPI 11 core, non nel plugin:
- Dati caricati: status, settings, authkey, backup recenti (10), log (50)
## Cache
- `UrbackupApiClient`: cache in-memory per `getStatus()` e `getClientSettings()`
- `UrbackupApiClient`: cache in-memory per `getStatus()` e `getClientSettings()`
- `AssetTab::loadApiData()`: cache sessione 30s (chiave: server_id + client_name)
- API timeout: 30s, connect timeout: 5s
@@ -79,10 +253,30 @@ La colonna `users_id` non esisteva nella tabella `glpi_plugin_urbackup_servers`.
3. **`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
## Versione
- 0.7.0
- 0.7.3
## 0.7.3 — Link Server ↔ asset host hardware
1. **Colonne `host_itemtype`/`host_items_id` + KEY `host_asset`** su `glpi_plugin_urbackup_servers` (migrazione idempotente + empty.sql) → bump 0.7.2 → 0.7.3 (regola 6).
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).
4. **DB**: migrazione idempotente + INSERT in empty.sql → bump 0.7.2 (regola 6).
## 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.
6. **Docs**: claim "nessun server locale" corretto (server reale verificato attivo + login 2-fasi OK), `GLPIKey::getInstance()``new GLPIKey()` in AGENTS/SKILL/GLPIDEV, `urbackup-api-actions.md` aggiornato.
## 0.7.0 — Pulizia, sicurezza e DB cleanup
1. **CSRF hardening**: `Session::checkCSRF()` aggiunto su `asset.form.php`, `server.form.php`, `server_test.ajax.php`, `config.form.php`
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)
2. **File deprecati rimossi**: 12 file (composer copy, AGENTS_OLD.MD, js/, ajax/, front/test/view, FIX_PERMISSIONS.sh, removed/)
3. **Dead code rimosso**: 3 Controller, 1 Command, 4 template Twig
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
+20
View File
@@ -108,6 +108,26 @@ plugin_urbackup/
## Changelog
### 0.7.3
- Servers can now be linked to their hardware host asset (Computer or custom Asset Definition with the UrBackup capacity): new `host_itemtype`/`host_items_id` columns on `glpi_plugin_urbackup_servers`, editable from the server form (itemtype dropdown + AJAX item dropdown)
- The asset tab now shows a "This asset hosts the UrBackup server" block with links to the hosted servers (always visible, even when the asset is also a linked client)
- Server save validates the host link (unknown/disabled itemtype or missing item resets the link)
- Added it_IT/de_DE/en_GB translations (2 new strings) and recompiled locales
### 0.7.2
- UrBackup on Computer is now configurable: new `enable_computer` setting on the plugin configuration page (previously always enabled, hardcoded)
- Configuration page lists the custom Asset Definitions with the "Urbackup" capacity enabled
- Tab registration, massive actions, links and unlinked/missing client lists now respect the Computer setting
- Asset tab: redesigned the "UrBackup status" section with status cards (server, API status, version, client), renamed the "State" tab to "Client State", fixed the Internet mode badge being rendered as escaped text
- Added it_IT/de_DE/en_GB translations (7 new strings, 1 orphan removed) and recompiled locales
### 0.7.1
- `api_password` now encrypted with `GLPIKey` on save (plaintext values migrated on update by `plugin_urbackup_install_encrypt_api_passwords()`)
- API connection test on server save runs only when connection parameters changed
- OPcache invalidation loop limited to development environments
- Fixed N+1 queries in the unlinked clients tab (batch loading)
- Completed it_IT/de_DE/en_GB translations (115 new strings) and recompiled locales
### 0.7.0
- Dropped legacy `glpi_plugin_urbackup_profiles` and `glpi_plugin_urbackup_assettypes` tables
- Added composite index `(locations_id, is_active)` on servers table
+109
View File
@@ -0,0 +1,109 @@
# SKILL.md - Competenze Richieste e Prompt di Continuazione (plugin urbackup)
# ROLE: Senior GLPI 11+ Enterprise Architect & Security/Network Engineer — UrBackup Web API Specialist
Sei un Architetto Software Senior, Specialista indiscusso nello sviluppo di plugin per **GLPI versione 11+**, con competenze avanzate in **Network Engineering Enterprise**, **Cybersecurity (Zero Trust / OWASP)** e **conoscenza approfondita del software UrBackup e delle sue API Web**.
Il tuo obiettivo è generare codice infallibile, rigoroso, scalabile e sicuro, aderendo al 100% alle linee guida ufficiali degli sviluppatori di GLPI 11+ e ai principi di ingegneria del software enterprise.
## PASSAGGIO ESSENZIALE conoscenza di GLPI11
1. leggi il file GLPIDEV.md; se il file non c'è analizza il contenuto di tutto GLPI installato e crea un file riassuntivo di tutte le funzioni che vengono usate. Questo file deve essere letto all'inizio di ogni sessione di lavoro e quando viene richiesta pianificazione e implementazione di plugin.
## Competenze Chiave dell'Agente
1. **Nessun Codice Legacy:** GLPI 11+ è basato su Symfony e PHP 8.1+. È severamente vietato usare codice procedurale, funzioni deprecate di GLPI 9.x/10.x, o query SQL grezze (`raw SQL`).
2. **GLPI Plugin Architecture**: Padroneggiare l'estensione di `CommonDBTM`/`CommonGLPI` per entità custom (`Server`, `ServerAsset`, `Config`) e il **Capacity system di GLPI 11** (`AbstractCapacity``UrBackupCapacity`) per registrare tab su Asset Definition. Gestione delle `MassiveAction` (connect/disconnect massivi).
3. **UrBackup Web API**: Conoscenza approfondita dell'API REST-ish `/x?a=<action>` di UrBackup: autenticazione con **salt + PBKDF2/MD5**, session id, azioni `status`, `settings` (`sa=clientsettings` / `clientsettings_save`), `backups`, `livelog`, `start_backup` (`start_client`/`start_type`), `add_client`, `remove_client`, `server_identity`. Gestione delle differenze tra versioni server (≥ 2.4 usa `internet_mode_enabled`). Struttura dei setting con struct `{"use":N, "value":..., "value_client":..., "value_group":...}`.
4. **Location-Aware Matching**: Logica di associazione asset↔server basata sulla gerarchia delle location GLPI (root location → server), risoluzione IP asset tramite IPAM (`glpi_ipaddresses`/`glpi_networknames`/`glpi_networkports`) e batch loading per evitare query N+1.
5. **Sicurezza PHP**: Gestione sicura di credenziali, prevenzione XSS nell'output HTML (`htmlspecialchars`), e validazione input. CSRF conforme GLPI 11 (listener globale, token hidden, header `X-Glpi-Csrf-Token`).
6. **Strict Typing:** Ogni file PHP DEVE iniziare con `declare(strict_types=1);`. Usa tipizzazione forte, `readonly`, `enums`, e `match expressions`.
7. **Separazione dei Concerni:** Logica di business nelle classi `src/` (`Server`, `ServerAsset`, `UrbackupApiClient`), presentazione in HTML/Twig. Mai logica HTTP/API nei template o nei front.
8. **Sicurezza by Design:** Ogni input è considerato ostile. Ogni output deve essere escapato. Nessun segreto hardcoded.
9. **Network Resilience:** Qualsiasi comunicazione di rete (API UrBackup, webhook) deve prevedere timeout, retry logic, fallback, e validazione dei certificati TLS.
10. **urbackup** conoscenza approfondita software UrBackup e sue API (architettura server/client, backup file e image, internet mode, client versioni, web interface).
---
## 🏗️ ARCHITETTURA E STACK GLPI 11+
Quando scrivi codice per GLPI 11+, devi utilizzare esclusivamente i seguenti pattern:
### 1. Struttura del Plugin
Rispetta la struttura standardizzata del plugin urbackup:
- `src/`: Codice PHP (Namespace `GlpiPlugin\Urbackup\`, PSR-4).
- `templates/`: File Twig (namespace `@urbackup/`).
- `locales/`: File `.po` / `.mo`.
- `public/`: Asset frontend (CSS/JS) caricati via `Hooks::ADD_CSS` / `ADD_JAVASCRIPT`.
- `front/`: Endpoint PHP con controllo diritti (`Profile::canCurrentUser()`) e CSRF.
- `install/`: Migrazioni DB versionate (classe `Migration`, schema iniziale `mysql/plugin_urbackup-empty.sql`).
- `composer.json`: Autoloading PSR-4, nessuna dipendenza esterna.
### 2. Backend & Integrazione
- **Classi dominio**: `Server` (CRUD, rightname `plugin_urbackup`), `ServerAsset` (collegamenti), `Config` (itemtype enabled), `Profile` (diritti).
- **Capacity system GLPI 11**: `AbstractCapacity` con `getLabel()`, `getIcon()`, `getDescription()`, `onClassBootstrap()` (registra il tab con `CommonGLPI::registerStandardTab()`), `onCapacityDisabled()`, `isUsed()`, `getCapacityUsageDescription()`. Registrazione in `plugin_init_urbackup()` via `AssetDefinitionManager::getInstance()->registerCapacity()`.
- **Event Dispatcher**: Usa il sistema di eventi di GLPI/Symfony per le integrazioni dove necessario (es. `change_profile`).
- **Database:** Usa `$DB->request()` (query builder) e la classe `Migration` per gli schema update. Mai SQL raw concatenato.
### 3. Frontend
- Output sempre escapato: `htmlspecialchars()` (HTML) o auto-escaping Twig (`{{ var|e('html') }}`).
- Usa i componenti Bootstrap 5 di GLPI 11 (badge, tab, table, alert) per la coerenza UI.
- Azioni POST verso `front/*.form.php` con token CSRF hidden; AJAX con header `X-Glpi-Csrf-Token` (`getAjaxCsrfToken()`).
---
## 🛡️ SECURITY & ZERO TRUST (ENTERPRISE MINDSET)
La sicurezza non è un'opzione, è il fondamento. Applica la "Defense in Depth":
1. **Autenticazione & Autorizzazione (RBAC):**
- Verifica SEMPRE i diritti GLPI prima di ogni azione: `Profile::canCurrentUser(READ/UPDATE/CREATE/DELETE/PURGE)` o `Session::haveRight('plugin_urbackup', $right)`.
- rightname: `plugin_urbackup` (READ/UPDATE/CREATE/DELETE/PURGE).
2. **Protezione Input/Output:**
- **CSRF (GLPI 11):** il listener globale `CheckCsrfListener` gestisce i token su ogni POST — NON chiamare `Session::checkCSRF()` nei front (richiede `$data` e il listener consuma il token). Form: `Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()])`. AJAX: header `X-Glpi-Csrf-Token`.
- **XSS:** Valida e sanitizza. Usa `htmlspecialchars()` / `Html::entities_deep()` per gli array.
- **SQLi:** Usa SEMPRE il query builder di GLPI (`$DB->request()`). Mai concatenare variabili nelle query.
3. **Gestione Segreti:**
- **Cifratura (implementata 05/08/2026)**: `api_password` di `glpi_plugin_urbackup_servers` è cifrata con `(new GLPIKey())->encrypt()/decrypt()` (`GLPIKey::getInstance()` NON esiste; `Toolbox::encrypt/decrypt` NON esiste in GLPI 11). `Server::getApiPassword()` decifra on-the-fly con fallback legacy; migrazione `plugin_urbackup_install_encrypt_api_passwords()`; mai loggare o esporre la password decifrata.
4. **Audit & Logging:**
- Logga le azioni critiche (test connessione API, modifiche server) con i meccanismi GLPI; includi `user_id`, `ip_address`, `action`, e `target_item` dove possibile.
---
## 🌐 NETWORK ENGINEERING & INTEGRATIONS (UrBackup Web API)
Il plugin comunica con server UrBackup esterni via **Web API `/x?a=<action>`**:
1. **Client HTTP Sicuro (cURL in `UrbackupApiClient`):**
- `CURLOPT_TIMEOUT` (30s) e `CURLOPT_CONNECTTIMEOUT` (5s) sempre impostati.
- `CURLOPT_SSL_VERIFYPEER`/`CURLOPT_SSL_VERIFYHOST` secondo il flag `ignore_ssl` del server (default: verifica TLS).
- Gestione errori cURL, HTTP status non-2xx, risposte non-JSON (HTML) con messaggi user-friendly.
2. **Autenticazione:**
- Flusso: `login` → se fallisce `salt` (username) → `login` con password hashata (MD5 + PBKDF2 se `pbkdf2_rounds > 0` + `rnd`).
- Session id (`ses`) inoltrato a ogni azione; stato `logged_in` per evitare login ripetuti.
3. **Resilienza:**
- Cache in-memory nel client (`cached_status`, `cached_settings`) e session cache 30s in `AssetTab::loadApiData()`: se un server è down, il plugin non deve degradare le prestazioni di GLPI.
- Tutti i metodi pubblici gestiscono `Throwable` e ritornano `false`/array vuoti invece di propagare errori fatali nella UI.
4. **Differenze versioni:** `getInternetModeSettingKey()``internet_mode_enabled` (≥ 2.4) vs `internet_mode`; `extractSettingValue()` gestisce la struct `{"use":N, "value":...}`.
---
## ⚙️ WORKFLOW DI SVILUPPO (COME DEVI RAGIONARE)
Ogni volta che ti chiedo di sviluppare una feature, segui rigorosamente questo processo:
1. **Analisi & Threat Modeling:** Identifica i requisiti, i flussi di dati e le potenziali vulnerabilità (STRIDE).
2. **Design dell'Architettura:** Definisci le entità DB, le classi `src/`, gli endpoint `front/` e i template necessari.
3. **Implementazione (Codice):**
- Scrivi il codice PHP 8.3+ con tipizzazione stretta e `declare(strict_types=1);`.
- Scrivi le query DB sicure con `$DB->request()`.
- Scrivi l'HTML con output escapato e token CSRF.
4. **Review di Sicurezza e Performance:**
- Controlla se ci sono N+1 query problems (usa batch loading: IP/gruppi in una query per itemtype).
- Verifica che tutti gli input siano validati e che i diritti siano controllati prima di ogni azione.
- Assicurati che le cache siano usate per dati API statici o calcoli pesanti.
5. **Output:** Fornisci il codice strutturato per file, con commenti PHPDoc completi e spiegazioni brevi ma tecniche delle scelte di sicurezza/architettura.
---
## 🚨 FORMATO DI RISPOSTA RICHIESTO
- **Nessun preambolo inutile.** Inizia direttamente con l'analisi tecnica o il codice.
- Usa blocchi di codice markdown specificando il linguaggio e il percorso del file (es. `// src/Server.php`).
- Se una richiesta dell'utente viola le best practice di GLPI 11+ o la sicurezza enterprise, **RIFIUTALA educatamente**, spiega il rischio (es. "Questa richiesta richiede SQL grezzo, che viola la policy di sicurezza. Ecco l'alternativa sicura con `$DB->request()`...") e fornisci la soluzione corretta.
- Includi sempre i comandi di verifica (`php -l`, `php bin/console glpi:plugin:install urbackup`) e il troubleshooting.
**Se hai compreso il tuo ruolo e le regole, rispondi esclusivamente con:**
"🛡️ *GLPI 11+ Enterprise Architect & UrBackup API Specialist initialized. Strict mode ON. Awaiting requirements for secure, scalable, and network-resilient plugin development.*"
-108
View File
@@ -1,108 +0,0 @@
# SKILL.md - Competenze Richieste e Prompt di Continuazione
# ROLE: Senior GLPI 11+ Enterprise Architect & Security/Network Engineer
Sei un Architetto Software Senior, Specialista indiscusso nello sviluppo di plugin per **GLPI versione 11+**, con competenze avanzate in **Network Engineering Enterprise** e **Cybersecurity (Zero Trust / OWASP)**.
Il tuo obiettivo è generare codice infallibile, rigoroso, scalabile e sicuro, aderendo al 100% alle linee guida ufficiali degli sviluppatori di GLPI 11+ e ai principi di ingegneria del software enterprise.
## PASSAGGIO ESSENZIALE conoscenza di GLPI11
1. leggi il file GLPIDEV.md; se il file non c'è analizza il contenuto di tutto GLPI installato e crea un file riassuntivo di tutte le funzioni che vengono usate questo file deve essere letto all'inizio di ogni sessione di lavoro e quando viene richiesta pianificazione e implementazione di plugin.
## Competenze Chiave dell'Agente
1. **Nessun Codice Legacy:** GLPI 11+ è basato su Symfony e PHP 8.1+. È severamente vietato usare codice procedurale, funzioni deprecate di GLPI 9.x/10.x, o query SQL grezze (`raw SQL`).
2. **GLPI Plugin Architecture**: Padroneggiare l'estensione di `CommonDBChild` per legare tabelle custom a `NetworkEquipment`, e la gestione delle `MassiveAction`.
3. **SSH Automation**: Gestire sessioni interattive via `phpseclib`, inviando comandi, aspettando prompt specifici con regex (`$ssh->read('/.*[>#]\s*$/', SSH2::READ_REGEX)`), e gestendo i timeout.
4. **Algoritmi di Diff**: Implementare o integrare librerie di confronto testo (es. `sebastian/diff`) per generare output HTML puliti e accurati, superiori al semplice confronto indice-per-indice.
5. **Sicurezza PHP**: Gestione sicura di credenziali criptate, prevenzione XSS nell'output HTML (`htmlspecialchars`), e validazione input.
#2. **Strict Typing:** Ogni file PHP DEVE iniziare con `declare(strict_types=1);`. Usa tipizzazione forte, `readonly`, `enums`, e `match expressions`.
6. **Separazione dei Concerni:** Logica di business nei Controller/Services (Symfony DI), presentazione rigorosamente in **Twig**. Mai logica PHP nei template.
7. **Sicurezza by Design:** Ogni input è considerato ostile. Ogni output deve essere escapato. Nessun segreto hardcoded.
8. **Network Resilience:** Qualsiasi comunicazione di rete (API esterne, webhook, SNMP, WMI) deve prevedere timeout, retry logic, fallback, e validazione dei certificati TLS.
9. **urbackup** conoscenza approfondita software Urbackup e sue API
---
## 🏗️ ARCHITETTURA E STACK GLPI 11+
Quando scrivi codice per GLPI 11+, devi utilizzare esclusivamente i seguenti pattern:
### 1. Struttura del Plugin
Rispetta la struttura standardizzata di GLPI 11+:
- `src/`: Codice PHP (Namespace `GlpiPlugin\NomePlugin\`).
- `templates/`: File Twig.
- `locales/`: File `.po` / `.mo`.
- `css/` & `js/`: Asset frontend (compilati, no inline JS).
- `migrations/`: Script di migrazione DB versionati.
- `composer.json`: Dipendenze gestite rigorosamente via Composer.
### 2. Backend & Symfony Integration
- **Dependency Injection:** Usa i Service Container di Symfony. Inietta le dipendenze nei costruttori.
- **Routing:** Usa le annotazioni/attributi PHP 8 per le route (`#[Route]`).
- **Event Dispatcher:** Usa il sistema di eventi di GLPI/Symfony per le integrazioni (es. `item.add`, `item.update`).
- **Database:** Usa `DBmysqlIterator` o i Repository Doctrine/ORM se previsti. Usa le classi di Migrazione di GLPI per gli schema update.
### 3. Frontend (Twig)
- Usa `{{ var|e('html') }}` o affidati all'auto-escaping di Twig.
- Usa le macro e i template ereditati da GLPI 11 (`@glpi/...`) per mantenere la coerenza della UI (Design System GLPI).
---
## 🛡️ SECURITY & ZERO TRUST (ENTERPRISE MINDSET)
La sicurezza non è un'opzione, è il fondamento. Applica la "Defense in Depth":
1. **Autenticazione & Autorizzazione (RBAC):**
- Verifica SEMPRE i diritti GLPI prima di ogni azione: `Session::checkRight('plugin_nomeplugin_item', READ/UPDATE/DELETE/PURGE)`.
- Integra i nuovi profili e diritti usando le interfacce GLPI 11+.
2. **Protezione Input/Output:**
- **CSRF:** Usa `Html::hiddenField('_glpi_csrf_token', ...)` o i token Symfony in tutti i form.
- **XSS:** Valida e sanitizza. Usa `Html::cleanInputText()` per i dati testuali, `Html::entities_deep()` per gli array.
- **SQLi:** Usa SEMPRE i prepared statements o l'Iterator di GLPI. Mai concatenare variabili nelle query.
3. **Gestione Segreti:**
- Mai password o API key nel codice. Usa le variabili d'ambiente (`$_ENV`, `getenv()`) o la configurazione crittografata di GLPI.
4. **Audit & Logging:**
- Logga le azioni critiche usando il Logger di Symfony/GLPI. Includi `user_id`, `ip_address`, `action`, e `target_item`.
---
## 🌐 NETWORK ENGINEERING & INTEGRATIONS
Quando il plugin comunica con l'esterno (es. sync con Active Directory, API di monitoring, webhook verso ticketing esterno):
1. **Client HTTP Sicuri:**
- Usa `Guzzle` o `Symfony HttpClient`.
- Imposta SEMPRE `timeout` (es. 5s) e `connect_timeout`.
- Disabilita il fallback a HTTP non cifrato. Forza TLS 1.2/1.3.
- Supporta la validazione di certificati CA custom (per reti enterprise con CA interne).
2. **Webhooks & API Inbound:**
- Se esponi API, usa OAuth2 o Token API di GLPI.
- Se ricevi Webhooks, implementa la verifica della firma (es. HMAC-SHA256) per garantire l'integrità e la provenienza del payload.
3. **Gestione Code (Message Queue):**
- Per task di rete pesanti o lenti, NON bloccare il thread HTTP. Usa **Symfony Messenger** o le code asincrone native di GLPI 11+ per processare in background.
4. **Resilienza:**
- Implementa il pattern *Circuit Breaker* per le API esterne. Se un servizio di rete è down, il plugin non deve degradare le prestazioni di GLPI.
---
## ⚙️ WORKFLOW DI SVILUPPO (COME DEVI RAGIONARE)
Ogni volta che ti chiedo di sviluppare una feature, segui rigorosamente questo processo:
1. **Analisi & Threat Modeling:** Identifica i requisiti, i flussi di dati e le potenziali vulnerabilità (STRIDE).
2. **Design dell'Architettura:** Definisci le entità DB, le route, i servizi e i template Twig necessari.
3. **Implementazione (Codice):**
- Scrivi il codice PHP 8.1+ con tipizzazione stretta.
- Scrivi le query DB sicure.
- Scrivi i template Twig puliti.
4. **Review di Sicurezza e Performance:**
- Controlla se ci sono N+1 query problems.
- Verifica che tutti gli input siano validati (usa `Symfony\Component\Validator`).
- Assicurati che i cache (Symfony Cache) siano usati per dati statici o calcoli pesanti.
5. **Output:** Fornisci il codice strutturato per file, con commenti PHPDoc completi e spiegazioni brevi ma tecniche delle scelte di sicurezza/architettura.
---
## 🚨 FORMATO DI RISPOSTA RICHIESTO
- **Nessun preambolo inutile.** Inizia direttamente con l'analisi tecnica o il codice.
- Usa blocchi di codice markdown specificando il linguaggio e il percorso del file (es. `src/Controller/MyController.php`).
- Se una richiesta dell'utente viola le best practice di GLPI 11+ o la sicurezza enterprise, **RIFIUTALA educatamente**, spiega il rischio (es. "Questa richiesta richiede SQL grezzo, che viola la policy di sicurezza. Ecco l'alternativa sicura con DBmysqlIterator...") e fornisci la soluzione corretta.
- Includi sempre i comandi per la generazione delle migrazioni DB e il clearing della cache di Symfony/GLPI.
**Se hai compreso il tuo ruolo e le regole, rispondi esclusivamente con:**
"🛡️ *GLPI 11+ Enterprise Architect & Security Engineer initialized. Strict mode ON. Awaiting requirements for secure, scalable, and network-resilient plugin development.*"
+1 -19
View File
@@ -84,7 +84,7 @@ if (isset($_POST['connect'])) {
}
if (isset($_POST['disconnect'])) {
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(DELETE)) {
if (!Profile::canCurrentUser(UPDATE)) {
Html::displayRightError();
}
@@ -103,24 +103,6 @@ if (isset($_POST['disconnect'])) {
}
}
if (isset($_POST['start_file_backup'])) {
if (!Profile::canCurrentUser(UPDATE)) {
Html::displayRightError();
}
AssetTab::startBackup($item, 'file');
Html::redirect($item->getFormURL());
}
if (isset($_POST['start_image_backup'])) {
if (!Profile::canCurrentUser(UPDATE)) {
Html::displayRightError();
}
AssetTab::startBackup($item, 'image');
Html::redirect($item->getFormURL());
}
if (isset($_POST['execute'])) {
$action = $_POST['urbackup_action'] ?? '';
+42 -6
View File
@@ -4,17 +4,53 @@ declare(strict_types=1);
use GlpiPlugin\Urbackup\Config;
global $CFG_GLPI;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
}
include_once GLPI_ROOT . '/inc/includes.php';
// Check user has right to manage plugin configuration
if (!Session::haveRight('config', UPDATE)) {
Html::displayRightError();
}
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update'])) {
Config::saveConfiguration($_POST);
Html::redirect($_SERVER['REQUEST_URI']);
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
// CSRF token is validated by the global CheckCsrfListener on every POST.
if (isset($_POST['update'])) {
global $DB;
$enable_computer = (string) ($_POST['enable_computer'] ?? '0') === '1' ? '1' : '0';
$table = Config::getTable();
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => ['name' => 'enable_computer'],
'LIMIT' => 1,
]);
$row = $iterator->current();
$values = [
'value' => $enable_computer,
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
];
if ($row === false) {
$values['name'] = 'enable_computer';
$values['date_creation'] = $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s');
$DB->insert($table, $values);
} else {
$DB->update($table, $values, ['name' => 'enable_computer']);
}
Session::addMessageAfterRedirect(
__('Configuration saved.', 'urbackup'),
false,
INFO
);
Html::redirect(Config::getFormURL());
}
}
// Display GLPI header
@@ -30,4 +66,4 @@ $config = new Config();
$config->showForm(1);
// Display GLPI footer
Html::footer();
Html::footer();
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* AJAX endpoint providing the asset dropdown for the "Hardware host"
* field of the UrBackup server form.
*
* Called through Ajax::updateItemOnSelectEvent when the host itemtype
* selection changes. GET only, no state changes are performed.
*/
use GlpiPlugin\Urbackup\Config;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
}
include_once GLPI_ROOT . '/inc/includes.php';
Html::header_nocache();
Session::checkLoginUser();
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
http_response_code(405);
exit;
}
$itemtype = (string) ($_GET['itemtype'] ?? '');
if ($itemtype === '' || !class_exists($itemtype) || !Config::isItemtypeEnabled($itemtype)) {
exit;
}
$value = (int) ($_GET['value'] ?? 0);
Dropdown::show($itemtype, [
'name' => 'host_items_id',
'value' => $value,
'entity' => Session::getActiveEntities(),
'display_emptychoice' => true,
]);
+1 -1
View File
@@ -44,7 +44,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . "/front/server.php");
}
$ID = $_GET['id'] ?? null;
$ID = (int) ($_GET['id'] ?? 0);
Html::header(
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
+26 -7
View File
@@ -3,29 +3,48 @@
declare(strict_types=1);
/**
* AJAX endpoint for testing UrBackup API
* AJAX endpoint for testing UrBackup API.
*
* This endpoint updates the server monitoring fields (last_api_status,
* last_api_message, last_api_check), so it requires UPDATE right and
* only accepts POST requests (GET requests must not trigger state changes).
*/
$AJAX_INCLUDE = 1;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\UrbackupApiClient;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
}
include_once GLPI_ROOT . '/inc/includes.php';
header("Content-Type: application/json; charset=UTF-8");
Html::header_nocache();
Session::checkLoginUser();
if (!Session::haveRight('plugin_urbackup', READ)) {
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => __('Method not allowed', 'urbackup')]);
exit;
}
if (!Profile::canCurrentUser(UPDATE)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => __('No permission', 'urbackup')]);
exit;
}
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
$server_id = (int) ($_POST['id'] ?? 0);
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => __('Invalid server ID', 'urbackup')]);
exit;
}
$server = new GlpiPlugin\Urbackup\Server();
$server = new Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]);
@@ -33,7 +52,7 @@ if (!$server->getFromDB($server_id)) {
}
try {
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
$client = new UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
@@ -46,4 +65,4 @@ try {
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
}
-3
View File
@@ -8,9 +8,6 @@ use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
require_once __DIR__ . '/install/install.php';
require_once __DIR__ . '/install/uninstall.php';
/**
* Get plugin classes.
*
+122 -58
View File
@@ -8,10 +8,9 @@ declare(strict_types=1);
* -------------------------------------------------------------------------
*/
use Glpi\Asset\AssetDefinitionManager;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
if (!defined('GLPI_ROOT')) {
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
@@ -38,12 +37,15 @@ function plugin_urbackup_install_process(): bool
plugin_urbackup_install_update_servers_table($migration);
plugin_urbackup_install_update_serverassets_table($migration);
plugin_urbackup_install_encrypt_api_passwords();
plugin_urbackup_install_add_enable_computer_config();
// Drop tables that are no longer used (replaced by Profile::registerRights() and Capacity system)
plugin_urbackup_install_drop_legacy_tables($migration);
$migration->executeMigration();
Config::ensureDefaultConfiguration();
Profile::installRights();
return true;
@@ -131,40 +133,6 @@ function plugin_urbackup_install_update_configs_table(Migration $migration): voi
$migration->addKey($table, 'name');
}
/**
* Update assettypes table.
*
* @param Migration $migration Migration instance
*
* @return void
*/
function plugin_urbackup_install_update_assettypes_table(Migration $migration): void
{
global $DB;
$table = 'glpi_plugin_urbackup_assettypes';
if (!$DB->tableExists($table)) {
return;
}
$migration->addField($table, 'itemtype', 'string', [
'value' => '',
'after' => 'id',
]);
$migration->addField($table, 'is_active', 'bool', [
'value' => 0,
'after' => 'itemtype',
]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, 'itemtype');
$migration->addKey($table, 'is_active');
}
/**
* Update servers table.
*
@@ -261,6 +229,15 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
'after' => 'last_api_check',
]);
$migration->addField($table, 'host_itemtype', 'string', [
'after' => 'comment',
]);
$migration->addField($table, 'host_items_id', 'integer', [
'value' => 0,
'after' => 'host_itemtype',
]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
@@ -270,6 +247,7 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
$migration->addKey($table, 'users_id');
$migration->addKey($table, 'is_active');
$migration->addKey($table, ['locations_id', 'is_active'], 'location_active');
$migration->addKey($table, ['host_itemtype', 'host_items_id'], 'host_asset');
}
/**
@@ -311,6 +289,106 @@ function plugin_urbackup_install_update_serverassets_table(Migration $migration)
$migration->addKey($table, ['itemtype', 'items_id'], 'item');
}
/**
* Encrypt api_password values still stored in plaintext.
*
* Runs on every install/update, but skips values that already use the
* GLPIKey encrypted format (idempotent). If the GLPI key is unavailable,
* the plaintext value is kept to avoid data loss.
*
* @return void
*/
function plugin_urbackup_install_encrypt_api_passwords(): void
{
global $DB;
$table = 'glpi_plugin_urbackup_servers';
if (!$DB->tableExists($table)) {
return;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => ['api_password' => ['<>', '']],
]);
foreach ($iterator as $row) {
$password = (string) $row['api_password'];
if ($password === '' || Server::isApiPasswordEncrypted($password)) {
continue;
}
$encrypted = (new GLPIKey())->encrypt($password);
if ($encrypted === '') {
continue;
}
$DB->update(
$table,
['api_password' => $encrypted],
['id' => (int) $row['id']]
);
}
}
/**
* Ensure the `enable_computer` config row exists (default: enabled).
*
* Computer is registered as a plain tab (not a capacity), so its default
* state must be stored in the config table to keep it backward compatible
* with previous versions where Computer was always enabled. Idempotent.
*
* @return void
*/
function plugin_urbackup_install_add_enable_computer_config(): void
{
global $DB;
$table = 'glpi_plugin_urbackup_configs';
if (!$DB->tableExists($table)) {
return;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => ['name' => 'enable_computer'],
]);
if (count($iterator) > 0) {
return;
}
$DB->insert($table, [
'name' => 'enable_computer',
'value' => '1',
'date_creation' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
]);
}
/**
* Drop a plugin table through Migration.
*
* Shared helper used by both install and uninstall processes
* (uninstall.php requires install.php to get this function).
*
* @param Migration $migration Migration instance
* @param string $table Table name
*
* @return void
*/
function plugin_urbackup_migration_drop_table(Migration $migration, string $table): void
{
global $DB;
if (!$DB->tableExists($table)) {
return;
}
$migration->dropTable($table);
}
/**
* Drop legacy tables that are no longer used.
*
@@ -330,11 +408,14 @@ function plugin_urbackup_install_drop_legacy_tables(Migration $migration): void
}
/**
* Convert old glpi_plugin_urbackup_assettypes to GLPI 11 capacity system
* and auto-enable UrBackup capacity on all existing Asset Definitions.
* Convert old glpi_plugin_urbackup_assettypes to GLPI 11 capacity system.
*
* After this migration, admins can disable UrBackup per Asset Definition
* via the native Capacities UI (Config > Asset definitions > Capacities).
* The legacy table is only kept to drop the obsolete `is_default` column.
* Enabling/disabling the UrBackup capacity on Asset Definitions is done by
* the admin via the native Capacities UI (Config > Asset definitions >
* Capacities). GLPI 11.0.x provides no public API to programmatically enable
* a capacity (it is processed from the `capacities` form input in
* AssetDefinition::post_updateItem()).
*
* @param Migration $migration Migration instance
*
@@ -358,23 +439,6 @@ function plugin_urbackup_install_convert_assettypes_to_capacities(Migration $mig
}
}
// Auto-enable UrBackup capacity on ALL Asset Definitions
// so the tab appears out of the box for any asset type.
if (class_exists(AssetDefinitionManager::class)) {
try {
$manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
if (!$definition->hasCapacity(UrBackupCapacity::class)) {
$definition->enableCapacity(UrBackupCapacity::class);
}
}
} catch (\Throwable $e) {
$migration->displayMessage(
__('Error enabling UrBackup capacity on definitions: ', 'urbackup') . $e->getMessage()
);
}
}
// Drop is_default column (no longer needed in capacity system)
if ($has_is_default) {
$migration->dropField($table, 'is_default');
+8 -1
View File
@@ -8,6 +8,10 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_configs` (
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Default configuration: UrBackup tab on Computer is enabled by default.
-- Can be changed from the plugin configuration page.
INSERT INTO `glpi_plugin_urbackup_configs` (`name`, `value`) VALUES ('enable_computer', '1');
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`entities_id` INT UNSIGNED NOT NULL DEFAULT 0,
@@ -27,6 +31,8 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
`last_api_message` TEXT DEFAULT NULL,
`last_api_check` TIMESTAMP NULL DEFAULT NULL,
`comment` TEXT DEFAULT NULL,
`host_itemtype` VARCHAR(255) DEFAULT NULL,
`host_items_id` INT UNSIGNED NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -35,7 +41,8 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
KEY `locations_id` (`locations_id`),
KEY `users_id` (`users_id`),
KEY `is_active` (`is_active`),
KEY `location_active` (`locations_id`, `is_active`)
KEY `location_active` (`locations_id`, `is_active`),
KEY `host_asset` (`host_itemtype`, `host_items_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
+2 -19
View File
@@ -14,6 +14,8 @@ if (!defined('GLPI_ROOT')) {
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
}
require_once __DIR__ . '/install.php';
/**
* Uninstall plugin.
*
@@ -37,22 +39,3 @@ function plugin_urbackup_uninstall_process(): bool
return true;
}
/**
* Drop a plugin table through Migration.
*
* @param Migration $migration Migration instance
* @param string $table Table name
*
* @return void
*/
function plugin_urbackup_migration_drop_table(Migration $migration, $table): void
{
global $DB;
if (!$DB->tableExists($table)) {
return;
}
$migration->dropTable($table);
}
BIN
View File
Binary file not shown.
+534 -153
View File
@@ -1,119 +1,20 @@
msgid ""
msgstr ""
"Project-Id-Version: urbackup 0.4.0\n"
"Project-Id-Version: urbackup 0.7.3\n"
"Language: de_DE\n"
"Content-Type: text/plain; charset=UTF-8\n"
msgid "UrBackup"
msgstr "UrBackup"
msgid "%1$s assets linked to UrBackup servers"
msgstr "%1$s Assets mit UrBackup-Servern verknüpft"
msgid "UrBackup server"
msgstr "UrBackup-Server"
msgid ""
"A server is available, but you do not have permission to link this asset."
msgstr ""
"Ein Server ist verfügbar, aber Sie haben keine Berechtigung, dieses Asset zu "
"verknüpfen."
msgid "UrBackup servers"
msgstr "UrBackup-Server"
msgid "UrBackup configuration"
msgstr "UrBackup-Konfiguration"
msgid "UrBackup rights"
msgstr "UrBackup-Berechtigungen"
msgid "No UrBackup server linked."
msgstr "Kein UrBackup-Server verknüpft."
msgid "UrBackup server selection"
msgstr "UrBackup-Serverauswahl"
msgid "Root location ID"
msgstr "Root-Location-ID"
msgid "Asset location ID"
msgstr "Asset-Location-ID"
msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location."
msgstr "Das Asset befindet sich in einer Unter-Location. Der Server der Root-Location wird verwendet."
msgid "No UrBackup server available for the root location of this asset."
msgstr "Kein UrBackup-Server für die Root-Location dieses Assets verfügbar."
msgid "Available servers for root location"
msgstr "Verfügbare Server für die Root-Location"
msgid "Connect"
msgstr "Verbinden"
msgid "Disconnect"
msgstr "Trennen"
msgid "UrBackup status"
msgstr "UrBackup-Status"
msgid "Linked server"
msgstr "Verknüpfter Server"
msgid "Client name"
msgstr "Clientname"
msgid "Client IP address"
msgstr "Client-IP-Adresse"
msgid "Client version"
msgstr "Client-Version"
msgid "Online"
msgstr "Online"
msgid "Offline"
msgstr "Offline"
msgid "State"
msgstr "Status"
msgid "Actions"
msgstr "Aktionen"
msgid "Info / Log"
msgstr "Info / Protokoll"
msgid "Create client in UrBackup"
msgstr "Client in UrBackup erstellen"
msgid "Backup commands"
msgstr "Backup-Befehle"
msgid "Incremental file backup"
msgstr "Inkrementelles Dateibackup"
msgid "Full file backup"
msgstr "Vollständiges Dateibackup"
msgid "Incremental image backup"
msgstr "Inkrementelles Image-Backup"
msgid "Full image backup"
msgstr "Vollständiges Image-Backup"
msgid "Internet mode"
msgstr "Internetmodus"
msgid "Default directories"
msgstr "Standardverzeichnisse"
msgid "Recent backups"
msgstr "Letzte Backups"
msgid "Client logs"
msgstr "Client-Protokolle"
msgid "Delete client from UrBackup server"
msgstr "Client vom UrBackup-Server löschen"
msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours."
msgstr "Die Löschung des Clients wird auf dem UrBackup-Server in die Warteschlange gestellt und kann bis zu 24 Stunden dauern."
msgid "API connection status"
msgstr "API-Verbindungsstatus"
msgid "API Error"
msgstr "API-Fehler"
msgid "API connection OK"
msgstr "API-Verbindung OK"
@@ -121,41 +22,201 @@ msgstr "API-Verbindung OK"
msgid "API connection failed"
msgstr "API-Verbindung fehlgeschlagen"
msgid "Server unreachable"
msgstr "Server nicht erreichbar"
msgid "API connection not working. Save server to test connection."
msgstr ""
"API-Verbindung funktioniert nicht. Speichern Sie den Server, um die "
"Verbindung zu testen."
msgid "No IP address configured"
msgstr "Keine IP-Adresse konfiguriert"
msgid "API connection status"
msgstr "API-Verbindungsstatus"
msgid "Checking..."
msgstr "Überprüfung..."
msgid "API password"
msgstr "API-Passwort"
msgid "API status"
msgstr "API-Status"
msgid "API username"
msgstr "API-Benutzername"
msgid "Actions"
msgstr "Aktionen"
msgid "Active"
msgstr "Aktiv"
msgid "Add UrBackup server"
msgstr "UrBackup-Server hinzufügen"
msgid ""
"All assets in this location are linked or already on the UrBackup server."
msgstr ""
"Alle Assets dieser Location sind verknüpft oder bereits auf dem UrBackup-"
"Server vorhanden."
msgid "Always enabled"
msgstr "Immer aktiviert"
msgid "Asset"
msgstr "Asset"
msgid "Asset is already linked to a server"
msgstr "Asset ist bereits mit einem Server verknüpft"
msgid ""
"Associate the server with the main/root location. Assets in sub-locations "
"will use this root location server."
msgstr ""
"Verknüpfen Sie den Server mit der Haupt-/Root-Location. Assets in Unter-"
"Locations verwenden den Server der Root-Location."
msgid "Available actions"
msgstr "Verfügbare Aktionen"
msgid "Available servers for root location"
msgstr "Verfügbare Server für die Root-Location"
msgid "Backup ID"
msgstr "Backup-ID"
msgid "Backup commands"
msgstr "Backup-Befehle"
msgid "Characteristics"
msgstr "Eigenschaften"
msgid "Click Save to test connection"
msgstr "Klicken Sie auf Speichern, um die Verbindung zu testen"
msgid "Linked clients"
msgstr "Verknüpfte Clients"
msgid "Client State"
msgstr "Client-Status"
msgid "Unlinked clients"
msgstr "Nicht verknüpfte Clients"
msgid "Client logs"
msgstr "Client-Protokolle"
msgid "No linked assets"
msgstr "Keine verknüpften Assets"
msgid "Client name"
msgstr "Clientname"
msgid "No unlinked clients found on UrBackup server"
msgstr "Keine nicht verknüpften Clients auf dem UrBackup-Server gefunden"
msgid ""
"Client name matches, but GLPI IP \"%s\" differs from UrBackup IP \"%s\"."
msgstr ""
"Der Clientname stimmt überein, aber die GLPI-IP \"%s\" weicht von der "
"UrBackup-IP \"%s\" ab."
msgid "API connection not working. Save server to test connection."
msgstr "API-Verbindung funktioniert nicht. Speichern Sie den Server, um die Verbindung zu testen."
msgid "Client not found on UrBackup server."
msgstr "Client auf dem UrBackup-Server nicht gefunden."
msgid "Status"
msgstr "Status"
msgid "Client state"
msgstr "Client-Status"
msgid "Last backup"
msgstr "Letztes Backup"
msgid "Client version"
msgstr "Client-Version"
msgid "Show"
msgstr "Anzeigen"
msgid "Comments"
msgstr "Kommentare"
msgid "Computer"
msgstr "Computer"
msgid "Configuration saved."
msgstr "Konfiguration gespeichert."
msgid "Connect"
msgstr "Verbinden"
msgid "Connect selected assets to an UrBackup server"
msgstr "Ausgewählte Assets mit einem UrBackup-Server verbinden"
msgid "Connection successful."
msgstr "Verbindung erfolgreich."
msgid "Connection successful. Server identity: %s"
msgstr "Verbindung erfolgreich. Serveridentität: %s"
msgid "Create"
msgstr "Erstellen"
msgid "Create client in UrBackup"
msgstr "Client in UrBackup erstellen"
msgid "Creating UrBackup plugin database schema"
msgstr "Erstelle Datenbankschema des UrBackup-Plugins"
msgid "Creation date"
msgstr "Erstellungsdatum"
msgid "Current activities"
msgstr "Aktuelle Aktivitäten"
msgid "Custom assets with \"Urbackup\" capacity enabled"
msgstr "Benutzerdefinierte Assets mit aktivierter Kapazität \"Urbackup\""
msgid "Database schema file not found: %s"
msgstr "Datei des Datenbankschemas nicht gefunden: %s"
msgid "Date"
msgstr "Datum"
msgid "Default directories"
msgstr "Standardverzeichnisse"
msgid "Delete"
msgstr "Löschen"
msgid "Delete client from UrBackup server"
msgstr "Client vom UrBackup-Server löschen"
msgid "Disconnect"
msgstr "Trennen"
msgid "Disconnect client"
msgstr "Client trennen"
msgid "Disconnect selected assets from UrBackup server"
msgstr "Ausgewählte Assets vom UrBackup-Server trennen"
msgid "Edit UrBackup server"
msgstr "UrBackup-Server bearbeiten"
msgid "Enable Internet mode"
msgstr "Internetmodus aktivieren"
msgid "Enable UrBackup on Computer"
msgstr "UrBackup auf Computern aktivieren"
msgid "Error while creating UrBackup plugin database schema"
msgstr "Fehler beim Erstellen des Datenbankschemas des UrBackup-Plugins"
msgid "Failed"
msgstr "Fehlgeschlagen"
msgid "Failed to disconnect asset from server"
msgstr "Trennung von Asset vom Server fehlgeschlagen"
msgid "Failed to link asset to server"
msgstr "Verknüpfung von Asset mit Server fehlgeschlagen"
msgid "Failed to save default directories"
msgstr "Speichern der Standardverzeichnisse fehlgeschlagen"
msgid "Failed to save internet mode"
msgstr "Speichern des Internetmodus fehlgeschlagen"
msgid "File backup"
msgstr "Dateibackup"
msgid ""
"For Asset Definition types, enable/disable UrBackup via Config > Asset "
"definitions > Capacities."
msgstr ""
"Für Asset-Definition-Typen aktivieren/deaktivieren Sie UrBackup unter "
"Konfiguration > Asset-Definitionen > Kapazitäten."
msgid "Full file backup"
msgstr "Vollständiges Dateibackup"
msgid "Full image backup"
msgstr "Vollständiges Image-Backup"
msgid "HTTP"
msgstr "HTTP"
@@ -163,50 +224,370 @@ msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API-Fehler"
msgid "Unknown"
msgstr "Unbekannt"
msgid "Hardware host"
msgstr "Hardware-Host"
msgid "IP address"
msgstr "IP-Adresse"
msgid "Asset Definition"
msgstr "Asset-Definition"
msgid "Ignore SSL verification"
msgstr "SSL-Prüfung ignorieren"
msgid "Legacy"
msgstr "Legacy"
msgid "Image backup"
msgstr "Image-Backup"
msgid "UrBackup Servers"
msgstr "UrBackup-Server"
msgid "Incremental"
msgstr "Inkrementell"
msgid "Incremental file backup"
msgstr "Inkrementelles Dateibackup"
msgid "Incremental image backup"
msgstr "Inkrementelles Image-Backup"
msgid "Info / Log"
msgstr "Info / Protokoll"
msgid "Internet authentication key"
msgstr "Internet-Authentifizierungsschlüssel"
msgid "Internet mode"
msgstr "Internetmodus"
msgid "Invalid parameters"
msgstr "Ungültige Parameter"
msgid "Invalid server ID"
msgstr "Ungültige Server-ID"
msgid "Inventory number"
msgstr "Inventarnummer"
msgid "Item type not enabled for UrBackup"
msgstr "Elementtyp für UrBackup nicht aktiviert"
msgid "No server selected"
msgstr "Kein Server ausgewählt"
msgid "Last API check"
msgstr "Letzte API-Prüfung"
msgid "Asset is already linked to a server"
msgstr "Asset ist bereits mit einem Server verknüpft"
msgid "Last API status"
msgstr "Letzter API-Status"
msgid "Failed to link asset to server"
msgstr "Verknüpfung von Asset mit Server fehlgeschlagen"
msgid "Last backup"
msgstr "Letztes Backup"
msgid "Failed to disconnect asset from server"
msgstr "Trennung von Asset vom Server fehlgeschlagen"
msgid "Last file backup"
msgstr "Letztes Dateibackup"
msgid "Last file backup result"
msgstr "Ergebnis des letzten Dateibackups"
msgid "Last image backup"
msgstr "Letztes Image-Backup"
msgid "Last image backup result"
msgstr "Ergebnis des letzten Image-Backups"
msgid "Last update"
msgstr "Letzte Aktualisierung"
msgid "Leave empty to keep the current password."
msgstr "Leer lassen, um das aktuelle Passwort beizubehalten."
msgid "Level"
msgstr "Stufe"
msgid "Linked assets"
msgstr "Verknüpfte Assets"
msgid "Linked clients"
msgstr "Verknüpfte Clients"
msgid "Linked server"
msgstr "Verknüpfter Server"
msgid "List"
msgstr "Liste"
msgid "Manage UrBackup backups for these assets"
msgstr "UrBackup-Backups für diese Assets verwalten"
msgid "Message"
msgstr "Nachricht"
msgid "Method not allowed"
msgstr "Methode nicht erlaubt"
msgid "Missing clients"
msgstr "Fehlende Clients"
msgid "Name"
msgstr "Name"
msgid "Network port"
msgstr "Netzwerkport"
msgid "Next"
msgstr "Weiter"
msgid "No"
msgstr "Nein"
msgid "No URL available"
msgstr "Keine URL verfügbar"
msgid "No UrBackup server available for the root location of this asset."
msgstr "Kein UrBackup-Server für die Root-Location dieses Assets verfügbar."
msgid "No UrBackup server linked."
msgstr "Kein UrBackup-Server verknüpft."
msgid "No UrBackup server selected."
msgstr "Kein UrBackup-Server ausgewählt."
msgid "No assets"
msgstr "Keine Assets"
msgid "No client logs available."
msgstr "Keine Client-Protokolle verfügbar."
msgid "No clients found on UrBackup server"
msgstr "Keine Clients auf dem UrBackup-Server gefunden"
msgid "No custom asset with the \"Urbackup\" capacity enabled."
msgstr ""
"Keine benutzerdefinierten Assets mit aktivierter Kapazität \"Urbackup\"."
msgid "No linked assets"
msgstr "Keine verknüpften Assets"
msgid "No location configured for this server."
msgstr "Keine Location für diesen Server konfiguriert."
msgid "No permission"
msgstr "Keine Berechtigung"
msgid "Invalid server ID"
msgstr "Ungültige Server-ID"
msgid "No recent backup information available."
msgstr "Keine Informationen zu letzten Backups verfügbar."
msgid "No server selected"
msgstr "Kein Server ausgewählt"
msgid "No unlinked clients found on UrBackup server"
msgstr "Keine nicht verknüpften Clients auf dem UrBackup-Server gefunden"
msgid "OK"
msgstr "OK"
msgid "Offline"
msgstr "Offline"
msgid "Online"
msgstr "Online"
msgid "Online / Offline"
msgstr "Online / Offline"
msgid "Open UrBackup interface"
msgstr "UrBackup-Oberfläche öffnen"
msgid "PHP cURL extension is required for UrBackup API."
msgstr "Die PHP-cURL-Erweiterung ist für die UrBackup-API erforderlich."
msgid "Previous"
msgstr "Zurück"
msgid "Protocol"
msgstr "Protokoll"
msgid "Purge"
msgstr "Endgültig löschen"
msgid "Read"
msgstr "Lesen"
msgid "Recent backups"
msgstr "Letzte Backups"
msgid "Recursive"
msgstr "Rekursiv"
msgid "Result"
msgstr "Ergebnis"
msgid "Salt response missing salt field."
msgstr "Salt-Antwort ohne Salt-Feld."
msgid "Save"
msgstr "Speichern"
msgid "Search..."
msgstr "Suchen..."
msgid "Server"
msgstr "Server"
msgid "Server not found"
msgstr "Server nicht gefunden"
msgid "Show"
msgstr "Anzeigen"
msgid ""
"Show the UrBackup tab on Computer items. When disabled, Computer is ignored "
"everywhere (tabs, massive actions, links)."
msgstr ""
"Zeigt den UrBackup-Tab auf Computer-Objekten an. Wenn deaktiviert, wird "
"Computer überall ignoriert (Tabs, Massenaktionen, Verknüpfungen)."
msgid "Size"
msgstr "Größe"
msgid "Sorry. You cannot access this file directly."
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
msgid "No assets"
msgstr "Keine Assets"
msgid "Status"
msgstr "Status"
msgid "Success"
msgstr "Erfolg"
msgid ""
"The asset is in a sub-location. The plugin will use the server assigned to "
"the root location."
msgstr ""
"Das Asset befindet sich in einer Unter-Location. Der Server der Root-"
"Location wird verwendet."
msgid ""
"The client deletion will be queued on the UrBackup server and may require up "
"to 24 hours."
msgstr ""
"Die Löschung des Clients wird auf dem UrBackup-Server in die Warteschlange "
"gestellt und kann bis zu 24 Stunden dauern."
msgid "The linked UrBackup server no longer exists."
msgstr "Der verknüpfte UrBackup-Server existiert nicht mehr."
msgid "This asset hosts the UrBackup server"
msgstr "Dieses Asset hostet den UrBackup-Server"
msgid "This plugin requires GLPI < %s."
msgstr "Dieses Plugin erfordert GLPI < %s."
msgid "This plugin requires GLPI >= %s."
msgstr "Dieses Plugin erfordert GLPI >= %s."
msgid "This plugin requires PHP 8.3 or higher."
msgstr "Dieses Plugin erfordert PHP 8.3 oder höher."
msgid "Type"
msgstr "Typ"
msgid "Unable to authenticate against UrBackup server. Password may be wrong."
msgstr ""
"Authentifizierung am UrBackup-Server fehlgeschlagen. Das Passwort könnte "
"falsch sein."
msgid "Unable to get salt from UrBackup server."
msgstr "Salt vom UrBackup-Server konnte nicht abgerufen werden."
msgid "Unable to initialize cURL."
msgstr "cURL konnte nicht initialisiert werden."
msgid "Unknown"
msgstr "Unbekannt"
msgid "Unlinked clients"
msgstr "Nicht verknüpfte Clients"
msgid "Update"
msgstr "Aktualisieren"
msgid "UrBackup"
msgstr "UrBackup"
msgid "UrBackup - connect to server"
msgstr "UrBackup - mit Server verbinden"
msgid "UrBackup - disconnect from server"
msgstr "UrBackup - vom Server trennen"
msgid "UrBackup API request failed: %s"
msgstr "UrBackup-API-Anfrage fehlgeschlagen: %s"
msgid "UrBackup API returned HTTP status %d."
msgstr "Die UrBackup-API gab den HTTP-Status %d zurück."
msgid ""
"UrBackup API returned non-JSON response (HTML). Check server URL and "
"authentication."
msgstr ""
"Die UrBackup-API gab eine Nicht-JSON-Antwort (HTML) zurück. Server-URL und "
"Authentifizierung prüfen."
msgid "UrBackup Servers"
msgstr "UrBackup-Server"
msgid "UrBackup configuration"
msgstr "UrBackup-Konfiguration"
msgid "UrBackup is not enabled for this asset type."
msgstr "UrBackup ist für diesen Asset-Typ nicht aktiviert."
msgid "UrBackup linked asset"
msgstr "Mit UrBackup verknüpftes Asset"
msgid "UrBackup linked assets"
msgstr "Mit UrBackup verknüpfte Assets"
msgid "UrBackup massive action"
msgstr "UrBackup-Massenaktion"
msgid "UrBackup plugin installation"
msgstr "Installation des UrBackup-Plugins"
msgid "UrBackup plugin uninstallation"
msgstr "Deinstallation des UrBackup-Plugins"
msgid "UrBackup server"
msgstr "UrBackup-Server"
msgid "UrBackup server not found."
msgstr "UrBackup-Server nicht gefunden."
msgid "UrBackup server selection"
msgstr "UrBackup-Serverauswahl"
msgid "UrBackup server version"
msgstr "UrBackup-Serverversion"
msgid "UrBackup servers"
msgstr "UrBackup-Server"
msgid "UrBackup status"
msgstr "UrBackup-Status"
msgid "UrBackup web interface"
msgstr "UrBackup-Weboberfläche"
msgid "Username does not exist on UrBackup server."
msgstr "Der Benutzername existiert nicht auf dem UrBackup-Server."
msgid "Version"
msgstr "Version"
msgid "View"
msgstr "Anzeigen"
msgid "Yes"
msgstr "Ja"
msgid "You do not have permission to connect assets to UrBackup servers."
msgstr ""
"Sie haben keine Berechtigung, Assets mit UrBackup-Servern zu verbinden."
msgid "You do not have permission to disconnect assets from UrBackup servers."
msgstr "Sie haben keine Berechtigung, Assets von UrBackup-Servern zu trennen."
msgid "You do not have permission to view UrBackup information."
msgstr "Sie haben keine Berechtigung, UrBackup-Informationen anzuzeigen."
BIN
View File
Binary file not shown.
+525 -153
View File
@@ -1,119 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: urbackup 0.4.0\n"
"Project-Id-Version: urbackup 0.7.3\n"
"Language: en_GB\n"
"Content-Type: text/plain; charset=UTF-8\n"
msgid "UrBackup"
msgstr "UrBackup"
msgid "%1$s assets linked to UrBackup servers"
msgstr "%1$s assets linked to UrBackup servers"
msgid "UrBackup server"
msgstr "UrBackup server"
msgid ""
"A server is available, but you do not have permission to link this asset."
msgstr ""
"A server is available, but you do not have permission to link this asset."
msgid "UrBackup servers"
msgstr "UrBackup servers"
msgid "UrBackup configuration"
msgstr "UrBackup configuration"
msgid "UrBackup rights"
msgstr "UrBackup rights"
msgid "No UrBackup server linked."
msgstr "No UrBackup server linked."
msgid "UrBackup server selection"
msgstr "UrBackup server selection"
msgid "Root location ID"
msgstr "Root location ID"
msgid "Asset location ID"
msgstr "Asset location ID"
msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location."
msgstr "The asset is in a sub-location. The plugin will use the server assigned to the root location."
msgid "No UrBackup server available for the root location of this asset."
msgstr "No UrBackup server available for the root location of this asset."
msgid "Available servers for root location"
msgstr "Available servers for root location"
msgid "Connect"
msgstr "Connect"
msgid "Disconnect"
msgstr "Disconnect"
msgid "UrBackup status"
msgstr "UrBackup status"
msgid "Linked server"
msgstr "Linked server"
msgid "Client name"
msgstr "Client name"
msgid "Client IP address"
msgstr "Client IP address"
msgid "Client version"
msgstr "Client version"
msgid "Online"
msgstr "Online"
msgid "Offline"
msgstr "Offline"
msgid "State"
msgstr "State"
msgid "Actions"
msgstr "Actions"
msgid "Info / Log"
msgstr "Info / Log"
msgid "Create client in UrBackup"
msgstr "Create client in UrBackup"
msgid "Backup commands"
msgstr "Backup commands"
msgid "Incremental file backup"
msgstr "Incremental file backup"
msgid "Full file backup"
msgstr "Full file backup"
msgid "Incremental image backup"
msgstr "Incremental image backup"
msgid "Full image backup"
msgstr "Full image backup"
msgid "Internet mode"
msgstr "Internet mode"
msgid "Default directories"
msgstr "Default directories"
msgid "Recent backups"
msgstr "Recent backups"
msgid "Client logs"
msgstr "Client logs"
msgid "Delete client from UrBackup server"
msgstr "Delete client from UrBackup server"
msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours."
msgstr "The client deletion will be queued on the UrBackup server and may require up to 24 hours."
msgid "API connection status"
msgstr "API connection status"
msgid "API Error"
msgstr "API Error"
msgid "API connection OK"
msgstr "API connection OK"
@@ -121,41 +21,197 @@ msgstr "API connection OK"
msgid "API connection failed"
msgstr "API connection failed"
msgid "Server unreachable"
msgstr "Server unreachable"
msgid "API connection not working. Save server to test connection."
msgstr "API connection not working. Save server to test connection."
msgid "No IP address configured"
msgstr "No IP address configured"
msgid "API connection status"
msgstr "API connection status"
msgid "Checking..."
msgstr "Checking..."
msgid "API password"
msgstr "API password"
msgid "API status"
msgstr "API status"
msgid "API username"
msgstr "API username"
msgid "Actions"
msgstr "Actions"
msgid "Active"
msgstr "Active"
msgid "Add UrBackup server"
msgstr "Add UrBackup server"
msgid ""
"All assets in this location are linked or already on the UrBackup server."
msgstr ""
"All assets in this location are linked or already on the UrBackup server."
msgid "Always enabled"
msgstr "Always enabled"
msgid "Asset"
msgstr "Asset"
msgid "Asset is already linked to a server"
msgstr "Asset is already linked to a server"
msgid ""
"Associate the server with the main/root location. Assets in sub-locations "
"will use this root location server."
msgstr ""
"Associate the server with the main/root location. Assets in sub-locations "
"will use this root location server."
msgid "Available actions"
msgstr "Available actions"
msgid "Available servers for root location"
msgstr "Available servers for root location"
msgid "Backup ID"
msgstr "Backup ID"
msgid "Backup commands"
msgstr "Backup commands"
msgid "Characteristics"
msgstr "Characteristics"
msgid "Click Save to test connection"
msgstr "Click Save to test connection"
msgid "Linked clients"
msgstr "Linked clients"
msgid "Client State"
msgstr "Client State"
msgid "Unlinked clients"
msgstr "Unlinked clients"
msgid "Client logs"
msgstr "Client logs"
msgid "No linked assets"
msgstr "No linked assets"
msgid "Client name"
msgstr "Client name"
msgid "No unlinked clients found on UrBackup server"
msgstr "No unlinked clients found on UrBackup server"
msgid ""
"Client name matches, but GLPI IP \"%s\" differs from UrBackup IP \"%s\"."
msgstr ""
"Client name matches, but GLPI IP \"%s\" differs from UrBackup IP \"%s\"."
msgid "API connection not working. Save server to test connection."
msgstr "API connection not working. Save server to test connection."
msgid "Client not found on UrBackup server."
msgstr "Client not found on UrBackup server."
msgid "Status"
msgstr "Status"
msgid "Client state"
msgstr "Client state"
msgid "Last backup"
msgstr "Last backup"
msgid "Client version"
msgstr "Client version"
msgid "Show"
msgstr "Show"
msgid "Comments"
msgstr "Comments"
msgid "Computer"
msgstr "Computer"
msgid "Configuration saved."
msgstr "Configuration saved."
msgid "Connect"
msgstr "Connect"
msgid "Connect selected assets to an UrBackup server"
msgstr "Connect selected assets to an UrBackup server"
msgid "Connection successful."
msgstr "Connection successful."
msgid "Connection successful. Server identity: %s"
msgstr "Connection successful. Server identity: %s"
msgid "Create"
msgstr "Create"
msgid "Create client in UrBackup"
msgstr "Create client in UrBackup"
msgid "Creating UrBackup plugin database schema"
msgstr "Creating UrBackup plugin database schema"
msgid "Creation date"
msgstr "Creation date"
msgid "Current activities"
msgstr "Current activities"
msgid "Custom assets with \"Urbackup\" capacity enabled"
msgstr "Custom assets with \"Urbackup\" capacity enabled"
msgid "Database schema file not found: %s"
msgstr "Database schema file not found: %s"
msgid "Date"
msgstr "Date"
msgid "Default directories"
msgstr "Default directories"
msgid "Delete"
msgstr "Delete"
msgid "Delete client from UrBackup server"
msgstr "Delete client from UrBackup server"
msgid "Disconnect"
msgstr "Disconnect"
msgid "Disconnect client"
msgstr "Disconnect client"
msgid "Disconnect selected assets from UrBackup server"
msgstr "Disconnect selected assets from UrBackup server"
msgid "Edit UrBackup server"
msgstr "Edit UrBackup server"
msgid "Enable Internet mode"
msgstr "Enable Internet mode"
msgid "Enable UrBackup on Computer"
msgstr "Enable UrBackup on Computer"
msgid "Error while creating UrBackup plugin database schema"
msgstr "Error while creating UrBackup plugin database schema"
msgid "Failed"
msgstr "Failed"
msgid "Failed to disconnect asset from server"
msgstr "Failed to disconnect asset from server"
msgid "Failed to link asset to server"
msgstr "Failed to link asset to server"
msgid "Failed to save default directories"
msgstr "Failed to save default directories"
msgid "Failed to save internet mode"
msgstr "Failed to save internet mode"
msgid "File backup"
msgstr "File backup"
msgid ""
"For Asset Definition types, enable/disable UrBackup via Config > Asset "
"definitions > Capacities."
msgstr ""
"For Asset Definition types, enable/disable UrBackup via Config > Asset "
"definitions > Capacities."
msgid "Full file backup"
msgstr "Full file backup"
msgid "Full image backup"
msgstr "Full image backup"
msgid "HTTP"
msgstr "HTTP"
@@ -163,50 +219,366 @@ msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API Error"
msgid "Unknown"
msgstr "Unknown"
msgid "Hardware host"
msgstr "Hardware host"
msgid "IP address"
msgstr "IP address"
msgid "Asset Definition"
msgstr "Asset Definition"
msgid "Ignore SSL verification"
msgstr "Ignore SSL verification"
msgid "Legacy"
msgstr "Legacy"
msgid "Image backup"
msgstr "Image backup"
msgid "UrBackup Servers"
msgstr "UrBackup Servers"
msgid "Incremental"
msgstr "Incremental"
msgid "Incremental file backup"
msgstr "Incremental file backup"
msgid "Incremental image backup"
msgstr "Incremental image backup"
msgid "Info / Log"
msgstr "Info / Log"
msgid "Internet authentication key"
msgstr "Internet authentication key"
msgid "Internet mode"
msgstr "Internet mode"
msgid "Invalid parameters"
msgstr "Invalid parameters"
msgid "Invalid server ID"
msgstr "Invalid server ID"
msgid "Inventory number"
msgstr "Inventory number"
msgid "Item type not enabled for UrBackup"
msgstr "Item type not enabled for UrBackup"
msgid "No server selected"
msgstr "No server selected"
msgid "Last API check"
msgstr "Last API check"
msgid "Asset is already linked to a server"
msgstr "Asset is already linked to a server"
msgid "Last API status"
msgstr "Last API status"
msgid "Failed to link asset to server"
msgstr "Failed to link asset to server"
msgid "Last backup"
msgstr "Last backup"
msgid "Failed to disconnect asset from server"
msgstr "Failed to disconnect asset from server"
msgid "Last file backup"
msgstr "Last file backup"
msgid "Last file backup result"
msgstr "Last file backup result"
msgid "Last image backup"
msgstr "Last image backup"
msgid "Last image backup result"
msgstr "Last image backup result"
msgid "Last update"
msgstr "Last update"
msgid "Leave empty to keep the current password."
msgstr "Leave empty to keep the current password."
msgid "Level"
msgstr "Level"
msgid "Linked assets"
msgstr "Linked assets"
msgid "Linked clients"
msgstr "Linked clients"
msgid "Linked server"
msgstr "Linked server"
msgid "List"
msgstr "List"
msgid "Manage UrBackup backups for these assets"
msgstr "Manage UrBackup backups for these assets"
msgid "Message"
msgstr "Message"
msgid "Method not allowed"
msgstr "Method not allowed"
msgid "Missing clients"
msgstr "Missing clients"
msgid "Name"
msgstr "Name"
msgid "Network port"
msgstr "Network port"
msgid "Next"
msgstr "Next"
msgid "No"
msgstr "No"
msgid "No URL available"
msgstr "No URL available"
msgid "No UrBackup server available for the root location of this asset."
msgstr "No UrBackup server available for the root location of this asset."
msgid "No UrBackup server linked."
msgstr "No UrBackup server linked."
msgid "No UrBackup server selected."
msgstr "No UrBackup server selected."
msgid "No assets"
msgstr "No assets"
msgid "No client logs available."
msgstr "No client logs available."
msgid "No clients found on UrBackup server"
msgstr "No clients found on UrBackup server"
msgid "No custom asset with the \"Urbackup\" capacity enabled."
msgstr "No custom asset with the \"Urbackup\" capacity enabled."
msgid "No linked assets"
msgstr "No linked assets"
msgid "No location configured for this server."
msgstr "No location configured for this server."
msgid "No permission"
msgstr "No permission"
msgid "Invalid server ID"
msgstr "Invalid server ID"
msgid "No recent backup information available."
msgstr "No recent backup information available."
msgid "No server selected"
msgstr "No server selected"
msgid "No unlinked clients found on UrBackup server"
msgstr "No unlinked clients found on UrBackup server"
msgid "OK"
msgstr "OK"
msgid "Offline"
msgstr "Offline"
msgid "Online"
msgstr "Online"
msgid "Online / Offline"
msgstr "Online / Offline"
msgid "Open UrBackup interface"
msgstr "Open UrBackup interface"
msgid "PHP cURL extension is required for UrBackup API."
msgstr "PHP cURL extension is required for UrBackup API."
msgid "Previous"
msgstr "Previous"
msgid "Protocol"
msgstr "Protocol"
msgid "Purge"
msgstr "Purge"
msgid "Read"
msgstr "Read"
msgid "Recent backups"
msgstr "Recent backups"
msgid "Recursive"
msgstr "Recursive"
msgid "Result"
msgstr "Result"
msgid "Salt response missing salt field."
msgstr "Salt response missing salt field."
msgid "Save"
msgstr "Save"
msgid "Search..."
msgstr "Search..."
msgid "Server"
msgstr "Server"
msgid "Server not found"
msgstr "Server not found"
msgid "Show"
msgstr "Show"
msgid ""
"Show the UrBackup tab on Computer items. When disabled, Computer is ignored "
"everywhere (tabs, massive actions, links)."
msgstr ""
"Show the UrBackup tab on Computer items. When disabled, Computer is ignored "
"everywhere (tabs, massive actions, links)."
msgid "Size"
msgstr "Size"
msgid "Sorry. You cannot access this file directly."
msgstr "Sorry. You cannot access this file directly."
msgid "No assets"
msgstr "No assets"
msgid "Status"
msgstr "Status"
msgid "Success"
msgstr "Success"
msgid ""
"The asset is in a sub-location. The plugin will use the server assigned to "
"the root location."
msgstr ""
"The asset is in a sub-location. The plugin will use the server assigned to "
"the root location."
msgid ""
"The client deletion will be queued on the UrBackup server and may require up "
"to 24 hours."
msgstr ""
"The client deletion will be queued on the UrBackup server and may require up "
"to 24 hours."
msgid "The linked UrBackup server no longer exists."
msgstr "The linked UrBackup server no longer exists."
msgid "This asset hosts the UrBackup server"
msgstr "This asset hosts the UrBackup server"
msgid "This plugin requires GLPI < %s."
msgstr "This plugin requires GLPI < %s."
msgid "This plugin requires GLPI >= %s."
msgstr "This plugin requires GLPI >= %s."
msgid "This plugin requires PHP 8.3 or higher."
msgstr "This plugin requires PHP 8.3 or higher."
msgid "Type"
msgstr "Type"
msgid "Unable to authenticate against UrBackup server. Password may be wrong."
msgstr "Unable to authenticate against UrBackup server. Password may be wrong."
msgid "Unable to get salt from UrBackup server."
msgstr "Unable to get salt from UrBackup server."
msgid "Unable to initialize cURL."
msgstr "Unable to initialize cURL."
msgid "Unknown"
msgstr "Unknown"
msgid "Unlinked clients"
msgstr "Unlinked clients"
msgid "Update"
msgstr "Update"
msgid "UrBackup"
msgstr "UrBackup"
msgid "UrBackup - connect to server"
msgstr "UrBackup - connect to server"
msgid "UrBackup - disconnect from server"
msgstr "UrBackup - disconnect from server"
msgid "UrBackup API request failed: %s"
msgstr "UrBackup API request failed: %s"
msgid "UrBackup API returned HTTP status %d."
msgstr "UrBackup API returned HTTP status %d."
msgid ""
"UrBackup API returned non-JSON response (HTML). Check server URL and "
"authentication."
msgstr ""
"UrBackup API returned non-JSON response (HTML). Check server URL and "
"authentication."
msgid "UrBackup Servers"
msgstr "UrBackup Servers"
msgid "UrBackup configuration"
msgstr "UrBackup configuration"
msgid "UrBackup is not enabled for this asset type."
msgstr "UrBackup is not enabled for this asset type."
msgid "UrBackup linked asset"
msgstr "UrBackup linked asset"
msgid "UrBackup linked assets"
msgstr "UrBackup linked assets"
msgid "UrBackup massive action"
msgstr "UrBackup massive action"
msgid "UrBackup plugin installation"
msgstr "UrBackup plugin installation"
msgid "UrBackup plugin uninstallation"
msgstr "UrBackup plugin uninstallation"
msgid "UrBackup server"
msgstr "UrBackup server"
msgid "UrBackup server not found."
msgstr "UrBackup server not found."
msgid "UrBackup server selection"
msgstr "UrBackup server selection"
msgid "UrBackup server version"
msgstr "UrBackup server version"
msgid "UrBackup servers"
msgstr "UrBackup servers"
msgid "UrBackup status"
msgstr "UrBackup status"
msgid "UrBackup web interface"
msgstr "UrBackup web interface"
msgid "Username does not exist on UrBackup server."
msgstr "Username does not exist on UrBackup server."
msgid "Version"
msgstr "Version"
msgid "View"
msgstr "View"
msgid "Yes"
msgstr "Yes"
msgid "You do not have permission to connect assets to UrBackup servers."
msgstr "You do not have permission to connect assets to UrBackup servers."
msgid "You do not have permission to disconnect assets from UrBackup servers."
msgstr "You do not have permission to disconnect assets from UrBackup servers."
msgid "You do not have permission to view UrBackup information."
msgstr "You do not have permission to view UrBackup information."
BIN
View File
Binary file not shown.
+534 -153
View File
@@ -1,119 +1,20 @@
msgid ""
msgstr ""
"Project-Id-Version: urbackup 0.4.0\n"
"Project-Id-Version: urbackup 0.7.3\n"
"Language: it_IT\n"
"Content-Type: text/plain; charset=UTF-8\n"
msgid "UrBackup"
msgstr "UrBackup"
msgid "%1$s assets linked to UrBackup servers"
msgstr "%1$s asset collegati a server UrBackup"
msgid "UrBackup server"
msgstr "Server UrBackup"
msgid ""
"A server is available, but you do not have permission to link this asset."
msgstr ""
"È disponibile un server, ma non si dispone del permesso per collegare questo "
"asset."
msgid "UrBackup servers"
msgstr "Server UrBackup"
msgid "UrBackup configuration"
msgstr "Configurazione UrBackup"
msgid "UrBackup rights"
msgstr "Diritti UrBackup"
msgid "No UrBackup server linked."
msgstr "Nessun server UrBackup collegato."
msgid "UrBackup server selection"
msgstr "Selezione server UrBackup"
msgid "Root location ID"
msgstr "ID location principale"
msgid "Asset location ID"
msgstr "ID location asset"
msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location."
msgstr "L'asset è in una sotto-location. Il plugin userà il server della location principale."
msgid "No UrBackup server available for the root location of this asset."
msgstr "Nessun server UrBackup disponibile per la location principale di questo asset."
msgid "Available servers for root location"
msgstr "Server disponibili per la location principale"
msgid "Connect"
msgstr "Collega"
msgid "Disconnect"
msgstr "Disconnetti"
msgid "UrBackup status"
msgstr "Stato UrBackup"
msgid "Linked server"
msgstr "Server collegato"
msgid "Client name"
msgstr "Nome client"
msgid "Client IP address"
msgstr "Indirizzo IP client"
msgid "Client version"
msgstr "Versione client"
msgid "Online"
msgstr "Online"
msgid "Offline"
msgstr "Offline"
msgid "State"
msgstr "Stato"
msgid "Actions"
msgstr "Azioni"
msgid "Info / Log"
msgstr "Info / Log"
msgid "Create client in UrBackup"
msgstr "Crea client in UrBackup"
msgid "Backup commands"
msgstr "Comandi backup"
msgid "Incremental file backup"
msgstr "Backup file incrementale"
msgid "Full file backup"
msgstr "Backup file completo"
msgid "Incremental image backup"
msgstr "Backup immagine incrementale"
msgid "Full image backup"
msgstr "Backup immagine completo"
msgid "Internet mode"
msgstr "Modalità Internet"
msgid "Default directories"
msgstr "Directory predefinite"
msgid "Recent backups"
msgstr "Backup recenti"
msgid "Client logs"
msgstr "Log client"
msgid "Delete client from UrBackup server"
msgstr "Elimina client dal server UrBackup"
msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours."
msgstr "L'eliminazione del client verrà messa in coda sul server UrBackup e potrebbe richiedere fino a 24 ore."
msgid "API connection status"
msgstr "Stato connessione API"
msgid "API Error"
msgstr "Errore API"
msgid "API connection OK"
msgstr "Connessione API OK"
@@ -121,41 +22,201 @@ msgstr "Connessione API OK"
msgid "API connection failed"
msgstr "Connessione API fallita"
msgid "Server unreachable"
msgstr "Server irraggiungibile"
msgid "API connection not working. Save server to test connection."
msgstr ""
"Connessione API non funzionante. Salva il server per testare la connessione."
msgid "No IP address configured"
msgstr "Nessun indirizzo IP configurato"
msgid "API connection status"
msgstr "Stato connessione API"
msgid "Checking..."
msgstr "Verifica in corso..."
msgid "API password"
msgstr "Password API"
msgid "API status"
msgstr "Stato API"
msgid "API username"
msgstr "Nome utente API"
msgid "Actions"
msgstr "Azioni"
msgid "Active"
msgstr "Attivo"
msgid "Add UrBackup server"
msgstr "Aggiungi server UrBackup"
msgid ""
"All assets in this location are linked or already on the UrBackup server."
msgstr ""
"Tutti gli asset di questa location sono collegati o già presenti sul server "
"UrBackup."
msgid "Always enabled"
msgstr "Sempre abilitato"
msgid "Asset"
msgstr "Asset"
msgid "Asset is already linked to a server"
msgstr "L'asset è già collegato a un server"
msgid ""
"Associate the server with the main/root location. Assets in sub-locations "
"will use this root location server."
msgstr ""
"Associa il server alla location principale/root. Gli asset nelle sotto-"
"location useranno il server della location principale."
msgid "Available actions"
msgstr "Azioni disponibili"
msgid "Available servers for root location"
msgstr "Server disponibili per la location principale"
msgid "Backup ID"
msgstr "ID backup"
msgid "Backup commands"
msgstr "Comandi backup"
msgid "Characteristics"
msgstr "Caratteristiche"
msgid "Click Save to test connection"
msgstr "Clicca \"Salva\" per testare la connessione"
msgid "Linked clients"
msgstr "Clienti collegati"
msgid "Client State"
msgstr "Stato Client"
msgid "Unlinked clients"
msgstr "Clienti non collegati"
msgid "Client logs"
msgstr "Log client"
msgid "No linked assets"
msgstr "Nessun asset collegato"
msgid "Client name"
msgstr "Nome client"
msgid "No unlinked clients found on UrBackup server"
msgstr "Nessun cliente non collegato trovato sul server UrBackup"
msgid ""
"Client name matches, but GLPI IP \"%s\" differs from UrBackup IP \"%s\"."
msgstr ""
"Il nome client corrisponde, ma l'IP GLPI \"%s\" differisce dall'IP UrBackup "
"\"%s\"."
msgid "API connection not working. Save server to test connection."
msgstr "Connessione API non funzionante. Salva il server per testare la connessione."
msgid "Client not found on UrBackup server."
msgstr "Client non trovato sul server UrBackup."
msgid "Status"
msgstr "Stato"
msgid "Client state"
msgstr "Stato client"
msgid "Last backup"
msgstr "Ultimo backup"
msgid "Client version"
msgstr "Versione client"
msgid "Show"
msgstr "Mostra"
msgid "Comments"
msgstr "Commenti"
msgid "Computer"
msgstr "Computer"
msgid "Configuration saved."
msgstr "Configurazione salvata."
msgid "Connect"
msgstr "Collega"
msgid "Connect selected assets to an UrBackup server"
msgstr "Collega gli asset selezionati a un server UrBackup"
msgid "Connection successful."
msgstr "Connessione riuscita."
msgid "Connection successful. Server identity: %s"
msgstr "Connessione riuscita. Identità del server: %s"
msgid "Create"
msgstr "Crea"
msgid "Create client in UrBackup"
msgstr "Crea client in UrBackup"
msgid "Creating UrBackup plugin database schema"
msgstr "Creazione dello schema del database del plugin UrBackup"
msgid "Creation date"
msgstr "Data di creazione"
msgid "Current activities"
msgstr "Attività correnti"
msgid "Custom assets with \"Urbackup\" capacity enabled"
msgstr "Asset custom con Capacità \"Urbackup\" attivata"
msgid "Database schema file not found: %s"
msgstr "File dello schema del database non trovato: %s"
msgid "Date"
msgstr "Data"
msgid "Default directories"
msgstr "Directory predefinite"
msgid "Delete"
msgstr "Elimina"
msgid "Delete client from UrBackup server"
msgstr "Elimina client dal server UrBackup"
msgid "Disconnect"
msgstr "Disconnetti"
msgid "Disconnect client"
msgstr "Disconnetti client"
msgid "Disconnect selected assets from UrBackup server"
msgstr "Disconnetti gli asset selezionati dal server UrBackup"
msgid "Edit UrBackup server"
msgstr "Modifica server UrBackup"
msgid "Enable Internet mode"
msgstr "Abilita modalità Internet"
msgid "Enable UrBackup on Computer"
msgstr "Abilita UrBackup sui Computer"
msgid "Error while creating UrBackup plugin database schema"
msgstr ""
"Errore durante la creazione dello schema del database del plugin UrBackup"
msgid "Failed"
msgstr "Fallito"
msgid "Failed to disconnect asset from server"
msgstr "Disconnessione asset dal server fallita"
msgid "Failed to link asset to server"
msgstr "Collegamento asset al server fallito"
msgid "Failed to save default directories"
msgstr "Salvataggio delle directory predefinite fallito"
msgid "Failed to save internet mode"
msgstr "Salvataggio della modalità Internet fallito"
msgid "File backup"
msgstr "Backup file"
msgid ""
"For Asset Definition types, enable/disable UrBackup via Config > Asset "
"definitions > Capacities."
msgstr ""
"Per i tipi Asset Definition, abilita/disabilita UrBackup tramite "
"Configurazione > Definizioni asset > Capacità."
msgid "Full file backup"
msgstr "Backup file completo"
msgid "Full image backup"
msgstr "Backup immagine completo"
msgid "HTTP"
msgstr "HTTP"
@@ -163,50 +224,370 @@ msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "Errore API"
msgid "Unknown"
msgstr "Sconosciuto"
msgid "Hardware host"
msgstr "Host hardware"
msgid "IP address"
msgstr "Indirizzo IP"
msgid "Asset Definition"
msgstr "Definizione asset"
msgid "Ignore SSL verification"
msgstr "Ignora verifica SSL"
msgid "Legacy"
msgstr "Legacy"
msgid "Image backup"
msgstr "Backup immagine"
msgid "UrBackup Servers"
msgstr "Server UrBackup"
msgid "Incremental"
msgstr "Incrementale"
msgid "Incremental file backup"
msgstr "Backup file incrementale"
msgid "Incremental image backup"
msgstr "Backup immagine incrementale"
msgid "Info / Log"
msgstr "Info / Log"
msgid "Internet authentication key"
msgstr "Chiave di autenticazione Internet"
msgid "Internet mode"
msgstr "Modalità Internet"
msgid "Invalid parameters"
msgstr "Parametri non validi"
msgid "Invalid server ID"
msgstr "ID server non valido"
msgid "Inventory number"
msgstr "Numero inventario"
msgid "Item type not enabled for UrBackup"
msgstr "Tipo oggetto non abilitato per UrBackup"
msgid "No server selected"
msgstr "Nessun server selezionato"
msgid "Last API check"
msgstr "Ultima verifica API"
msgid "Asset is already linked to a server"
msgstr "L'asset è già collegato a un server"
msgid "Last API status"
msgstr "Ultimo stato API"
msgid "Failed to link asset to server"
msgstr "Collegamento asset al server fallito"
msgid "Last backup"
msgstr "Ultimo backup"
msgid "Failed to disconnect asset from server"
msgstr "Disconnessione asset dal server fallita"
msgid "Last file backup"
msgstr "Ultimo backup file"
msgid "Last file backup result"
msgstr "Risultato ultimo backup file"
msgid "Last image backup"
msgstr "Ultimo backup immagine"
msgid "Last image backup result"
msgstr "Risultato ultimo backup immagine"
msgid "Last update"
msgstr "Ultimo aggiornamento"
msgid "Leave empty to keep the current password."
msgstr "Lasciare vuoto per mantenere la password corrente."
msgid "Level"
msgstr "Livello"
msgid "Linked assets"
msgstr "Asset collegati"
msgid "Linked clients"
msgstr "Clienti collegati"
msgid "Linked server"
msgstr "Server collegato"
msgid "List"
msgstr "Elenco"
msgid "Manage UrBackup backups for these assets"
msgstr "Gestisci i backup UrBackup per questi asset"
msgid "Message"
msgstr "Messaggio"
msgid "Method not allowed"
msgstr "Metodo non consentito"
msgid "Missing clients"
msgstr "Client mancanti"
msgid "Name"
msgstr "Nome"
msgid "Network port"
msgstr "Porta di rete"
msgid "Next"
msgstr "Successivo"
msgid "No"
msgstr "No"
msgid "No URL available"
msgstr "Nessun URL disponibile"
msgid "No UrBackup server available for the root location of this asset."
msgstr ""
"Nessun server UrBackup disponibile per la location principale di questo "
"asset."
msgid "No UrBackup server linked."
msgstr "Nessun server UrBackup collegato."
msgid "No UrBackup server selected."
msgstr "Nessun server UrBackup selezionato."
msgid "No assets"
msgstr "Nessun Asset"
msgid "No client logs available."
msgstr "Nessun log client disponibile."
msgid "No clients found on UrBackup server"
msgstr "Nessun client trovato sul server UrBackup"
msgid "No custom asset with the \"Urbackup\" capacity enabled."
msgstr "Nessun asset custom con la capacità \"Urbackup\" attivata."
msgid "No linked assets"
msgstr "Nessun asset collegato"
msgid "No location configured for this server."
msgstr "Nessuna location configurata per questo server."
msgid "No permission"
msgstr "Nessun permesso"
msgid "Invalid server ID"
msgstr "ID server non valido"
msgid "No recent backup information available."
msgstr "Nessuna informazione sui backup recenti disponibile."
msgid "No server selected"
msgstr "Nessun server selezionato"
msgid "No unlinked clients found on UrBackup server"
msgstr "Nessun cliente non collegato trovato sul server UrBackup"
msgid "OK"
msgstr "OK"
msgid "Offline"
msgstr "Offline"
msgid "Online"
msgstr "Online"
msgid "Online / Offline"
msgstr "Online / Offline"
msgid "Open UrBackup interface"
msgstr "Apri interfaccia UrBackup"
msgid "PHP cURL extension is required for UrBackup API."
msgstr "L'estensione PHP cURL è richiesta per l'API UrBackup."
msgid "Previous"
msgstr "Precedente"
msgid "Protocol"
msgstr "Protocollo"
msgid "Purge"
msgstr "Elimina definitivamente"
msgid "Read"
msgstr "Lettura"
msgid "Recent backups"
msgstr "Backup recenti"
msgid "Recursive"
msgstr "Ricorsivo"
msgid "Result"
msgstr "Risultato"
msgid "Salt response missing salt field."
msgstr "Risposta salt senza campo salt."
msgid "Save"
msgstr "Salva"
msgid "Search..."
msgstr "Cerca..."
msgid "Server"
msgstr "Server"
msgid "Server not found"
msgstr "Server non trovato"
msgid "Show"
msgstr "Mostra"
msgid ""
"Show the UrBackup tab on Computer items. When disabled, Computer is ignored "
"everywhere (tabs, massive actions, links)."
msgstr ""
"Mostra la scheda UrBackup sugli oggetti Computer. Se disabilitato, Computer "
"viene ignorato ovunque (schede, azioni massive, collegamenti)."
msgid "Size"
msgstr "Dimensione"
msgid "Sorry. You cannot access this file directly."
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
msgid "No assets"
msgstr "Nessun Asset"
msgid "Status"
msgstr "Stato"
msgid "Success"
msgstr "Successo"
msgid ""
"The asset is in a sub-location. The plugin will use the server assigned to "
"the root location."
msgstr ""
"L'asset è in una sotto-location. Il plugin userà il server della location "
"principale."
msgid ""
"The client deletion will be queued on the UrBackup server and may require up "
"to 24 hours."
msgstr ""
"L'eliminazione del client verrà messa in coda sul server UrBackup e potrebbe "
"richiedere fino a 24 ore."
msgid "The linked UrBackup server no longer exists."
msgstr "Il server UrBackup collegato non esiste più."
msgid "This asset hosts the UrBackup server"
msgstr "Questo asset ospita il server UrBackup"
msgid "This plugin requires GLPI < %s."
msgstr "Questo plugin richiede GLPI < %s."
msgid "This plugin requires GLPI >= %s."
msgstr "Questo plugin richiede GLPI >= %s."
msgid "This plugin requires PHP 8.3 or higher."
msgstr "Questo plugin richiede PHP 8.3 o superiore."
msgid "Type"
msgstr "Tipo"
msgid "Unable to authenticate against UrBackup server. Password may be wrong."
msgstr ""
"Impossibile autenticarsi sul server UrBackup. La password potrebbe essere "
"errata."
msgid "Unable to get salt from UrBackup server."
msgstr "Impossibile ottenere il salt dal server UrBackup."
msgid "Unable to initialize cURL."
msgstr "Impossibile inizializzare cURL."
msgid "Unknown"
msgstr "Sconosciuto"
msgid "Unlinked clients"
msgstr "Clienti non collegati"
msgid "Update"
msgstr "Aggiorna"
msgid "UrBackup"
msgstr "UrBackup"
msgid "UrBackup - connect to server"
msgstr "UrBackup - collega al server"
msgid "UrBackup - disconnect from server"
msgstr "UrBackup - scollega dal server"
msgid "UrBackup API request failed: %s"
msgstr "Richiesta API UrBackup fallita: %s"
msgid "UrBackup API returned HTTP status %d."
msgstr "L'API UrBackup ha restituito lo stato HTTP %d."
msgid ""
"UrBackup API returned non-JSON response (HTML). Check server URL and "
"authentication."
msgstr ""
"L'API UrBackup ha restituito una risposta non-JSON (HTML). Controllare URL "
"del server e autenticazione."
msgid "UrBackup Servers"
msgstr "Server UrBackup"
msgid "UrBackup configuration"
msgstr "Configurazione UrBackup"
msgid "UrBackup is not enabled for this asset type."
msgstr "UrBackup non è abilitato per questo tipo di asset."
msgid "UrBackup linked asset"
msgstr "Asset collegato a UrBackup"
msgid "UrBackup linked assets"
msgstr "Asset collegati a UrBackup"
msgid "UrBackup massive action"
msgstr "Azione massiva UrBackup"
msgid "UrBackup plugin installation"
msgstr "Installazione del plugin UrBackup"
msgid "UrBackup plugin uninstallation"
msgstr "Disinstallazione del plugin UrBackup"
msgid "UrBackup server"
msgstr "Server UrBackup"
msgid "UrBackup server not found."
msgstr "Server UrBackup non trovato."
msgid "UrBackup server selection"
msgstr "Selezione server UrBackup"
msgid "UrBackup server version"
msgstr "Versione del server UrBackup"
msgid "UrBackup servers"
msgstr "Server UrBackup"
msgid "UrBackup status"
msgstr "Stato UrBackup"
msgid "UrBackup web interface"
msgstr "Interfaccia web UrBackup"
msgid "Username does not exist on UrBackup server."
msgstr "Il nome utente non esiste sul server UrBackup."
msgid "Version"
msgstr "Versione"
msgid "View"
msgstr "Visualizza"
msgid "Yes"
msgstr "Sì"
msgid "You do not have permission to connect assets to UrBackup servers."
msgstr "Non si dispone del permesso per collegare asset ai server UrBackup."
msgid "You do not have permission to disconnect assets from UrBackup servers."
msgstr "Non si dispone del permesso per scollegare asset dai server UrBackup."
msgid "You do not have permission to view UrBackup information."
msgstr "Non si dispone del permesso per visualizzare le informazioni UrBackup."
BIN
View File
Binary file not shown.
+32
View File
@@ -18,6 +18,38 @@
margin-right: 4px;
}
.plugin-urbackup-inner-tabs .nav-tabs {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
overflow-x: auto;
width: 100%;
}
.plugin-urbackup-inner-tabs .nav-item {
display: inline-flex !important;
flex: 0 0 auto !important;
width: auto !important;
max-width: none !important;
}
.plugin-urbackup-inner-tabs .nav-link {
flex: 0 0 auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
flex-basis: auto !important;
width: auto !important;
max-width: none !important;
white-space: nowrap;
}
@media (max-width: 575.98px) {
.plugin-urbackup-inner-tabs .nav-tabs {
flex-wrap: wrap !important;
overflow-x: visible;
}
}
#urbackupTabs .nav-link#state-tab {
color: #fff;
background-color: #4a4a4a;
-89
View File
@@ -1,89 +0,0 @@
/**
* UrBackup API Test JavaScript
*/
(function () {
'use strict';
function testApi(serverId, resultBox) {
if (!serverId || !resultBox) return;
var xhr = new XMLHttpRequest();
var pluginUrl = (window.CFG_GLPI && CFG_GLPI.root_doc ? CFG_GLPI.root_doc : '') + '/plugins/urbackup';
xhr.open('POST', pluginUrl + '/front/server_test.ajax.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Glpi-Csrf-Token', getAjaxCsrfToken());
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000;
xhr.onload = function () {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.success) {
resultBox.innerHTML = '<span class="text-success fw-bold"><i class="ti ti-check"></i> API connection OK</span>';
} else {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> API connection failed</span><br><small class="text-muted">' + (data.message || '') + '</small>';
}
} catch (e) {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> Error</span>';
}
} else {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> HTTP ' + xhr.status + '</span>';
}
};
xhr.ontimeout = function () {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> API connection failed</span><br><small class="text-muted">Connection timeout</small>';
};
xhr.onerror = function () {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> API connection failed</span><br><small class="text-muted">Network error</small>';
};
var csrfToken = getAjaxCsrfToken();
var params = 'id=' + encodeURIComponent(serverId);
if (csrfToken) {
params += '&_glpi_csrf_token=' + encodeURIComponent(csrfToken);
}
xhr.send(params);
}
function initApiStatusCheck() {
var statusBox = document.getElementById('plugin-urbackup-api-status');
if (!statusBox) return;
if (statusBox._initialized) return;
statusBox._initialized = true;
var serverId = statusBox.getAttribute('data-server-id');
if (serverId) {
testApi(serverId, statusBox);
}
}
function initApiTestButtons() {
var buttons = document.querySelectorAll('.plugin-urbackup-test-api');
buttons.forEach(function (button) {
if (button._initialized) return;
button._initialized = true;
button.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var serverId = button.getAttribute('data-server-id');
var resultBox = document.getElementById('plugin-urbackup-api-test-result');
testApi(serverId, resultBox);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApiStatusCheck);
document.addEventListener('DOMContentLoaded', initApiTestButtons);
} else {
setTimeout(initApiStatusCheck, 100);
setTimeout(initApiTestButtons, 100);
}
})();
+13 -11
View File
@@ -2,8 +2,12 @@
declare(strict_types=1);
// Force OPcache to reload plugin files when accessed via web
if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) {
// Force OPcache to reload plugin files when accessed via web.
// Only active on development environments: invalidating every plugin file
// on each web request would be too expensive in production.
$is_development_env = defined('GLPI_ENVIRONMENT_TYPE') && GLPI_ENVIRONMENT_TYPE === 'development';
if (PHP_SAPI !== 'cli' && $is_development_env && function_exists('opcache_invalidate')) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
);
@@ -24,7 +28,7 @@ use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
define('PLUGIN_URBACKUP_VERSION', '0.7.0');
define('PLUGIN_URBACKUP_VERSION', '0.7.3');
define('PLUGIN_URBACKUP_MIN_GLPI', '11.0.6');
define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99');
@@ -61,10 +65,12 @@ function plugin_init_urbackup(): void
Plugin::registerClass(ServerAsset::class);
Plugin::registerClass(PluginUrbackupMassiveAction::class);
// Register tab on Computer (legacy, always enabled)
Plugin::registerClass(AssetTab::class, [
'addtabon' => ['Computer'],
]);
// Register tab on Computer (configurable from the plugin configuration page)
if (Config::getEnableComputer()) {
Plugin::registerClass(AssetTab::class, [
'addtabon' => ['Computer'],
]);
}
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
@@ -87,10 +93,6 @@ function plugin_init_urbackup(): void
$PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = [
'public/css/urbackup.css',
];
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [
'public/js/urbackup.js',
];
}
/**
+131 -33
View File
@@ -102,6 +102,8 @@ class AssetTab extends CommonDBTM
echo "<div class='plugin-urbackup-asset-tab'>";
self::showHostServerBlock($item);
if ($link === null) {
self::showNoServerLinkedBlock($item);
} else {
@@ -113,6 +115,40 @@ class AssetTab extends CommonDBTM
return true;
}
/**
* Show the block displaying the UrBackup servers hosted on this asset.
*
* Always visible when the asset hosts one or more UrBackup servers,
* even when the asset is already linked as a client.
*
* @param CommonDBTM $item Asset item
*
* @return void
*/
private static function showHostServerBlock(CommonDBTM $item): void
{
$hostingServers = Server::getServersHostingAsset($item::class, (int) ($item->fields['id'] ?? 0));
if (count($hostingServers) === 0) {
return;
}
echo "<div class='card mb-3'>";
echo "<div class='card-header'><i class='ti ti-server me-1'></i> " .
htmlspecialchars(__('This asset hosts the UrBackup server', 'urbackup')) . "</div>";
echo "<div class='card-body py-3'>";
echo "<ul class='mb-0'>";
foreach ($hostingServers as $serverRow) {
$serverId = (int) ($serverRow['id'] ?? 0);
$serverName = (string) ($serverRow['name'] ?? '');
echo "<li><a href='" . htmlspecialchars(Server::getFormURLWithID($serverId)) . "'>" .
htmlspecialchars($serverName) . "</a></li>";
}
echo "</ul>";
echo "</div>";
echo "</div>";
}
/**
* Show block when no server is linked.
*
@@ -218,24 +254,88 @@ class AssetTab extends CommonDBTM
$api_data = self::loadApiData($item, $server, $link);
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup status', 'urbackup')) . "</th></tr>";
$ip = (string) ($server->fields['ip_address'] ?? '');
$port = (int) ($server->fields['port'] ?? 0);
$protocol = (string) ($server->fields['protocol'] ?? 'http');
$version = (string) ($server->fields['server_version'] ?? '');
$api_ok = (int) ($server->fields['last_api_status'] ?? 0) === 1;
$api_message = (string) ($server->fields['last_api_message'] ?? '');
$last_check = (string) ($server->fields['last_api_check'] ?? '');
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Linked server', 'urbackup')) . "</td>";
echo "<td>" . $server->getLink() . "</td>";
echo "<td>" . htmlspecialchars(__('IP address', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) $server->fields['ip_address']) . "</td>";
echo "</tr>";
echo "<div class='card mb-3'>";
echo "<div class='card-header'><i class='ti ti-cloud-up me-1'></i> "
. htmlspecialchars(__('UrBackup status', 'urbackup')) . "</div>";
echo "<div class='card-body py-3'>";
echo "<div class='row row-cols-1 row-cols-sm-2 row-cols-lg-4 g-2'>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</td>";
echo "</tr>";
// Linked server
echo "<div class='col'>";
echo "<div class='card h-100'>";
echo "<div class='card-body p-3'>";
echo "<div class='text-muted small text-uppercase fw-semibold'>"
. htmlspecialchars(__('Linked server', 'urbackup')) . "</div>";
echo "<div class='fs-5 fw-bold mt-1'>" . $server->getLink() . "</div>";
echo "<div class='text-muted small mt-1'><i class='ti ti-network'></i> ";
echo htmlspecialchars($protocol) . "://<code>" . htmlspecialchars($ip) . "</code>:" . $port;
echo "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "</table>";
// API status
echo "<div class='col'>";
echo "<div class='card h-100'>";
echo "<div class='card-body p-3'>";
echo "<div class='text-muted small text-uppercase fw-semibold'>"
. htmlspecialchars(__('API status', 'urbackup')) . "</div>";
echo "<div class='mt-1'>";
if ($api_ok) {
echo "<span class='badge bg-success'><i class='ti ti-check'></i> "
. htmlspecialchars(__('API connection OK', 'urbackup')) . "</span>";
} else {
echo "<span class='badge bg-danger'><i class='ti ti-x'></i> "
. htmlspecialchars(__('API connection failed', 'urbackup')) . "</span>";
}
echo "</div>";
if ($api_message !== '') {
echo "<div class='text-muted small mt-1'>" . htmlspecialchars($api_message) . "</div>";
}
if ($last_check !== '') {
echo "<div class='text-muted small mt-1'><i class='ti ti-clock'></i> "
. htmlspecialchars(Html::convDateTime($last_check)) . "</div>";
}
echo "</div>";
echo "</div>";
echo "</div>";
// Server version
echo "<div class='col'>";
echo "<div class='card h-100'>";
echo "<div class='card-body p-3'>";
echo "<div class='text-muted small text-uppercase fw-semibold'>"
. htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</div>";
echo "<div class='fs-5 fw-bold mt-1'>"
. ($version !== '' ? htmlspecialchars($version) : '<span class="text-muted">-</span>')
. "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
// Client
echo "<div class='col'>";
echo "<div class='card h-100'>";
echo "<div class='card-body p-3'>";
echo "<div class='text-muted small text-uppercase fw-semibold'>"
. htmlspecialchars(__('Client name', 'urbackup')) . "</div>";
echo "<div class='fs-5 fw-bold mt-1'>"
. htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
echo "</div>";
if ($api_data['error'] !== '') {
echo "<div class='alert alert-warning'>";
@@ -249,7 +349,7 @@ class AssetTab extends CommonDBTM
echo "</div>";
}
self::showInternalTabs($item, $server, $link, $api_data);
self::showInternalTabs($item, $server, $api_data);
}
/**
@@ -327,7 +427,6 @@ class AssetTab extends CommonDBTM
*
* @param CommonDBTM $item Asset
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
@@ -335,7 +434,6 @@ class AssetTab extends CommonDBTM
private static function showInternalTabs(
CommonDBTM $item,
Server $server,
array $link,
array $api_data
): void {
echo "<div class='plugin-urbackup-inner-tabs'>";
@@ -345,7 +443,7 @@ class AssetTab extends CommonDBTM
echo '<ul class="nav nav-tabs" id="urbackupTabs" role="tablist">';
echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab" aria-selected="true">';
echo htmlspecialchars(__('State', 'urbackup'));
echo htmlspecialchars(__('Client State', 'urbackup'));
echo '</a></li>';
if ($canWrite) {
echo '<li class="nav-item" role="presentation">';
@@ -362,12 +460,12 @@ class AssetTab extends CommonDBTM
echo '<div class="tab-content">';
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
self::showStateSection($server, $link, $api_data);
self::showStateSection($api_data);
echo '</div>';
if ($canWrite) {
echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
self::showActionsSection($item, $server, $link, $api_data);
self::showActionsSection($item, $api_data);
echo '</div>';
}
@@ -382,13 +480,11 @@ class AssetTab extends CommonDBTM
/**
* Show state section.
*
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showStateSection(Server $server, array $link, array $api_data): void
private static function showStateSection(array $api_data): void
{
$status = $api_data['client_status'];
$settings = $api_data['client_settings'];
@@ -408,9 +504,9 @@ class AssetTab extends CommonDBTM
}
$internetMode = self::extractSettingValue($settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null, 0);
$internetModeDisplay = ((int) $internetMode === 1)
? '<span class="badge bg-success">' . __('Yes', 'urbackup') . '</span>'
: '<span class="badge bg-secondary">' . __('No', 'urbackup') . '</span>';
$internetModeDisplay = ((int) $internetMode === 1)
? '<span class="badge bg-success">' . htmlspecialchars(__('Yes', 'urbackup')) . '</span>'
: '<span class="badge bg-secondary">' . htmlspecialchars(__('No', 'urbackup')) . '</span>';
$rows = [
__('Client version', 'urbackup') => $status['client_version_string'] ?? $status['client_version'] ?? $status['version'] ?? '-',
@@ -420,17 +516,23 @@ class AssetTab extends CommonDBTM
__('Last file backup result', 'urbackup') => self::formatBoolStatus($status['file_ok'] ?? null),
__('Last image backup result', 'urbackup') => self::formatBoolStatus($status['image_ok'] ?? null),
__('Current activities', 'urbackup') => $status['status'] ?? $status['activity'] ?? '-',
__('Internet mode', 'urbackup') => $internetModeDisplay,
];
foreach ($rows as $label => $value) {
$displayValue = is_array($value) ? json_encode($value) : (string) $value;
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars((string) $label) . "</td>";
echo "<td>" . $displayValue . "</td>";
echo "<td>" . htmlspecialchars((string) $displayValue) . "</td>";
echo "</tr>";
}
// Internet mode is rendered as a badge (intentional HTML), so it must
// not go through the generic escaped loop above.
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Internet mode', 'urbackup')) . "</td>";
echo "<td>" . $internetModeDisplay . "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Internet authentication key', 'urbackup')) . "</td>";
echo "<td>";
@@ -455,16 +557,12 @@ class AssetTab extends CommonDBTM
* Show actions section.
*
* @param CommonDBTM $item Asset
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showActionsSection(
CommonDBTM $item,
Server $server,
array $link,
array $api_data
): void {
echo "<table class='tab_cadre_fixe'>";
+169 -33
View File
@@ -11,9 +11,10 @@ declare(strict_types=1);
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Dropdown;
use Glpi\Asset\Asset;
use Glpi\Asset\AssetDefinitionManager;
use DBmysql;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use Html;
use Session;
@@ -21,6 +22,16 @@ class Config extends CommonDBTM
{
public static $rightname = 'config';
/**
* Config key storing whether the UrBackup tab is enabled on Computer.
*/
private const CONFIG_ENABLE_COMPUTER = 'enable_computer';
/**
* In-memory cache of the `enable_computer` config value.
*/
private static ?bool $enable_computer_cache = null;
/**
* Get type name.
*
@@ -45,17 +56,6 @@ class Config extends CommonDBTM
return 'glpi_plugin_urbackup_configs';
}
/**
* Ensure default configuration.
*
* @return void
*/
public static function ensureDefaultConfiguration(): void
{
// Computer is always enabled by default.
// Asset Definition types are managed via the native Capacities UI.
}
/**
* Get assettypes table name.
*
@@ -66,6 +66,47 @@ class Config extends CommonDBTM
return 'glpi_plugin_urbackup_assettypes';
}
/**
* Whether the UrBackup tab is enabled on Computer items.
*
* Stored in glpi_plugin_urbackup_configs under the `enable_computer` key.
* Enabled by default; falls back to true when the table does not exist yet
* (e.g. during plugin install) or when the row is missing (backward
* compatibility with versions where Computer was always enabled).
*
* @return bool
*/
public static function getEnableComputer(): bool
{
if (self::$enable_computer_cache !== null) {
return self::$enable_computer_cache;
}
global $DB;
$table = static::getTable();
if (!$DB->tableExists($table)) {
self::$enable_computer_cache = true;
return self::$enable_computer_cache;
}
try {
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => ['name' => self::CONFIG_ENABLE_COMPUTER],
'LIMIT' => 1,
]);
$row = $iterator->current();
self::$enable_computer_cache = $row === false || (string) ($row['value'] ?? '') === '1';
} catch (\Throwable) {
self::$enable_computer_cache = true;
}
return self::$enable_computer_cache;
}
/**
* Check whether itemtype is enabled for UrBackup.
*
@@ -80,7 +121,7 @@ class Config extends CommonDBTM
}
if ($itemtype === 'Computer') {
return true;
return self::getEnableComputer();
}
// All Asset subclasses are enabled by default.
@@ -101,7 +142,11 @@ class Config extends CommonDBTM
*/
public static function getEnabledItemtypes(): array
{
$itemtypes = ['Computer'];
$itemtypes = [];
if (self::getEnableComputer()) {
$itemtypes[] = 'Computer';
}
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
@@ -116,6 +161,42 @@ class Config extends CommonDBTM
return $itemtypes;
}
/**
* Get the Asset Definitions that have the UrBackup capacity enabled.
*
* @return array<int, \Glpi\Asset\AssetDefinition>
*/
public static function getEnabledAssetDefinitions(): array
{
if (!class_exists(AssetDefinitionManager::class)) {
return [];
}
$manager = AssetDefinitionManager::getInstance();
$capacities = $manager->getAvailableCapacities();
$urbackup_capacity = $capacities[UrBackupCapacity::class] ?? null;
$definitions = [];
foreach ($manager->getDefinitions() as $definition) {
if ($urbackup_capacity !== null) {
if ($definition->hasCapacityEnabled($urbackup_capacity)) {
$definitions[] = $definition;
}
} else {
// Fallback: decode the raw capacities JSON.
$decoded = json_decode((string) ($definition->fields['capacities'] ?? '[]'), true);
if (
is_array($decoded)
&& in_array(UrBackupCapacity::class, array_column($decoded, 'name'), true)
) {
$definitions[] = $definition;
}
}
}
return $definitions;
}
/**
* Show config form.
*
@@ -128,38 +209,93 @@ class Config extends CommonDBTM
{
Session::checkRight('config', UPDATE);
$enable_computer = self::getEnableComputer();
echo "<div class='center'>";
echo "<form method='post' action='" . static::getFormURL() . "'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
echo "<tr class='tab_bg_1'>";
echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>";
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</span></td>";
echo "<td>";
echo "<strong>" . htmlspecialchars(__('Enable UrBackup on Computer', 'urbackup')) . "</strong><br>";
echo "<span class='text-muted'>" . htmlspecialchars(
__(
'Show the UrBackup tab on Computer items. When disabled, Computer is ignored everywhere (tabs, massive actions, links).',
'urbackup'
)
) . "</span>";
echo "</td>";
echo "<td>";
Dropdown::showYesNo(self::CONFIG_ENABLE_COMPUTER, (int) $enable_computer);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_2'>";
echo "<td class='center' colspan='2'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::submit(__('Save'), ['name' => 'update', 'class' => 'btn btn-primary']);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</form>";
echo "<br>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='3'>";
echo htmlspecialchars(__('Custom assets with "Urbackup" capacity enabled', 'urbackup'));
echo "</th></tr>";
$definitions = self::getEnabledAssetDefinitions();
if (count($definitions) === 0) {
echo "<tr class='tab_bg_1'>";
echo "<td class='center' colspan='3'>";
echo "<span class='alert alert-info'>";
echo htmlspecialchars(
__('No custom asset with the "Urbackup" capacity enabled.', 'urbackup')
);
echo "</span>";
echo "</td>";
echo "</tr>";
} else {
echo "<tr class='tab_bg_1'>";
echo "<th>" . htmlspecialchars(__('Name')) . "</th>";
echo "<th>" . htmlspecialchars(__('System name')) . "</th>";
echo "<th>" . htmlspecialchars(__('Active')) . "</th>";
echo "</tr>";
foreach ($definitions as $definition) {
$is_active = (bool) ($definition->fields['is_active'] ?? 0);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars((string) ($definition->fields['label'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($definition->fields['system_name'] ?? '')) . "</td>";
echo "<td>";
if ($is_active) {
echo "<span class='badge bg-success'>" . htmlspecialchars(__('Yes')) . "</span>";
} else {
echo "<span class='badge bg-secondary'>" . htmlspecialchars(__('No')) . "</span>";
}
echo "</td>";
echo "</tr>";
}
}
echo "</table>";
echo "<br>";
echo "<div class='alert alert-info'>";
echo htmlspecialchars(
__('For Asset Definition types, enable/disable UrBackup via Config > Asset definitions > Capacities.', 'urbackup')
__(
'For Asset Definition types, enable/disable UrBackup via Config > Asset definitions > Capacities.',
'urbackup'
)
);
echo "</div>";
echo "</div>";
return true;
}
/**
* Save configuration.
*
* In the new capacity system, asset types are managed via the native Capacities UI.
* This method is kept for backward compatibility but does nothing.
*
* @param array<string, mixed> $input Input data
*
* @return void
*/
public static function saveConfiguration(array $input): void
{
Session::checkRight('config', UPDATE);
}
}
}
+2 -5
View File
@@ -12,7 +12,6 @@ namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Html;
use Session;
class MassiveAction extends CommonDBTM
{
@@ -216,8 +215,7 @@ class MassiveAction extends CommonDBTM
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
);
}
}
}
/**
* Process disconnect-from-server massive action.
@@ -261,6 +259,5 @@ class MassiveAction extends CommonDBTM
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
);
}
}
}
}
+239 -132
View File
@@ -11,9 +11,9 @@ declare(strict_types=1);
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use CommonGLPI;
use Dropdown;
use Entity;
use GLPIKey;
use Group;
use Html;
use Location;
@@ -465,9 +465,10 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>";
if ($canEdit) {
echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>";
echo "<input type='password' name='api_password' value='' placeholder='******' autocomplete='new-password'>";
echo "<br><small class='text-muted'>" .
htmlspecialchars(__('Leave empty to keep the current password.', 'urbackup')) .
"</small>";
} else {
echo '******';
}
@@ -483,6 +484,53 @@ class Server extends CommonDBTM
echo "</td>";
echo "</tr>";
$hostItemtype = (string) ($this->fields['host_itemtype'] ?? '');
$hostItemsId = (int) ($this->fields['host_items_id'] ?? 0);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Hardware host', 'urbackup')) . "</td>";
echo "<td colspan='3'>";
$rand = Dropdown::showItemTypes(
'host_itemtype',
Config::getEnabledItemtypes(),
[
'value' => $hostItemtype,
]
);
echo Html::scriptBlock(
"$(document).on('change', '#dropdown_host_itemtype$rand', function () {"
. "var itemtype = this.value;"
. "if (!itemtype) { $('#urbackup_host_items$rand').html(''); return; }"
. "$.get('" . PLUGIN_URBACKUP_WEB_DIR . "/front/dropdown_host.ajax.php',"
. " { itemtype: itemtype, value: 0 },"
. " function (html) { $('#urbackup_host_items$rand').html(html); }"
. ");"
. "});"
);
echo "<div id='urbackup_host_items$rand' class='mt-2'>";
if ($hostItemtype !== '' && $hostItemsId > 0 && class_exists($hostItemtype)) {
Dropdown::show($hostItemtype, [
'name' => 'host_items_id',
'value' => $hostItemsId,
'entity' => Session::getActiveEntities(),
'display_emptychoice' => true,
]);
}
echo "</div>";
$hostAsset = $this->getHostAsset();
if ($hostAsset !== null) {
echo "<div class='mt-2'><a href='" . htmlspecialchars($hostAsset::getFormURLWithID((int) $hostAsset->fields['id'])) . "'>" .
htmlspecialchars((string) $hostAsset->getName()) .
"</a></div>";
}
echo "</td>";
echo "</tr>";
if ($ID > 0) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup web interface', 'urbackup')) . "</td>";
@@ -536,6 +584,102 @@ class Server extends CommonDBTM
return sprintf('%s://%s:%d', $protocol, $ip, $port);
}
/**
* Get the asset hosting this UrBackup server.
*
* @return CommonDBTM|null The hosting asset, or null when not set/unresolvable
*/
public function getHostAsset(): ?CommonDBTM
{
$hostItemtype = (string) ($this->fields['host_itemtype'] ?? '');
$hostItemsId = (int) ($this->fields['host_items_id'] ?? 0);
if ($hostItemtype === '' || $hostItemsId <= 0 || !class_exists($hostItemtype)) {
return null;
}
$hostItem = getItemForItemtype($hostItemtype);
if (!$hostItem instanceof CommonDBTM || !$hostItem->getFromDB($hostItemsId)) {
return null;
}
return $hostItem;
}
/**
* Get the UrBackup servers hosted on a given asset.
*
* @param string $itemtype Asset itemtype
* @param int $items_id Asset ID
*
* @return array<int, array<string, mixed>> List of servers hosting the asset
*/
public static function getServersHostingAsset(string $itemtype, int $items_id): array
{
global $DB;
if ($itemtype === '' || $items_id <= 0) {
return [];
}
if (!$DB->fieldExists(self::getTable(), 'host_itemtype')) {
return [];
}
$servers = [];
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'host_itemtype' => $itemtype,
'host_items_id' => $items_id,
],
'ORDER' => 'name',
]);
foreach ($iterator as $row) {
$servers[] = $row;
}
return $servers;
}
/**
* Get the decrypted API password.
*
* Values encrypted with GLPIKey are decrypted on the fly. Plaintext
* values stored by plugin versions older than 0.7.1 are returned as-is.
*
* @return string
*/
public function getApiPassword(): string
{
$value = (string) ($this->fields['api_password'] ?? '');
if ($value === '' || !self::isApiPasswordEncrypted($value)) {
return $value;
}
return (string) (new GLPIKey())->decrypt($value);
}
/**
* Check whether a stored API password uses the GLPIKey encrypted format.
*
* @param string $value Stored value
*
* @return bool
*/
public static function isApiPasswordEncrypted(string $value): bool
{
$decoded = base64_decode($value, true);
if ($decoded === false) {
return false;
}
return strlen($decoded) >= SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES;
}
/**
* Get active servers assigned to a root location.
*
@@ -585,8 +729,10 @@ class Server extends CommonDBTM
if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) {
$input['port'] = 55414;
} else {
$input['port'] = max(1, min(65535, (int) $input['port']));
}
if (!isset($input['protocol']) || $input['protocol'] === '') {
if (!isset($input['protocol']) || !in_array($input['protocol'], ['http', 'https'], true)) {
$input['protocol'] = 'http';
}
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
@@ -608,16 +754,20 @@ class Server extends CommonDBTM
return $input;
}
if (isset($input['port']) && ($input['port'] === '' || $input['port'] === null)) {
$input['port'] = 55414;
if (isset($input['port'])) {
$input['port'] = ($input['port'] === '' || $input['port'] === null)
? 55414
: max(1, min(65535, (int) $input['port']));
}
if (isset($input['protocol']) && $input['protocol'] === '') {
if (isset($input['protocol']) && !in_array($input['protocol'], ['http', 'https'], true)) {
$input['protocol'] = 'http';
}
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
$input['ignore_ssl'] = 0;
}
$hasNewPassword = isset($input['api_password']) && $input['api_password'] !== '';
if (!empty($input['id']) && (int) $input['id'] > 0) {
$server = new self();
if ($server->getFromDB((int) $input['id'])) {
@@ -626,19 +776,33 @@ class Server extends CommonDBTM
$port = $input['port'] ?? $serverFields['port'] ?? 55414;
$protocol = $input['protocol'] ?? $serverFields['protocol'] ?? 'http';
$apiUsername = $input['api_username'] ?? $serverFields['api_username'] ?? '';
$apiPassword = $input['api_password'] ?? $serverFields['api_password'] ?? '';
$apiPassword = $hasNewPassword
? (string) $input['api_password']
: $server->getApiPassword();
$ignoreSsl = $input['ignore_ssl'] ?? $serverFields['ignore_ssl'] ?? 0;
if ($ip !== '') {
// Only test the API connection when connection parameters actually changed,
// otherwise every save would trigger a request (up to 30s when unreachable).
$connectionChanged = $hasNewPassword;
foreach (['ip_address', 'port', 'protocol', 'api_username', 'ignore_ssl'] as $field) {
$newValue = (string) ($input[$field] ?? $serverFields[$field] ?? '');
$oldValue = (string) ($serverFields[$field] ?? '');
if ($newValue !== $oldValue) {
$connectionChanged = true;
break;
}
}
if ($connectionChanged && $ip !== '') {
$tmpServer = new self();
$tmpServer->fields = [
'id' => (int) $input['id'],
'ip_address' => $ip,
'port' => $port,
'protocol' => $protocol,
'api_username' => $apiUsername,
'api_password' => $apiPassword,
'ignore_ssl' => $ignoreSsl,
'id' => (int) $input['id'],
'ip_address' => $ip,
'port' => $port,
'protocol' => $protocol,
'api_username' => $apiUsername,
'api_password' => $apiPassword,
'ignore_ssl' => $ignoreSsl,
];
try {
@@ -656,97 +820,38 @@ class Server extends CommonDBTM
}
}
if (isset($input['api_password'])) {
if ($input['api_password'] === '') {
unset($input['api_password']);
} else {
$input['api_password'] = (new GLPIKey())->encrypt((string) $input['api_password']);
}
}
if (array_key_exists('host_itemtype', $input) || array_key_exists('host_items_id', $input)) {
$hostItemtype = (string) ($input['host_itemtype'] ?? '');
$hostItemsId = (int) ($input['host_items_id'] ?? 0);
if (
$hostItemtype === ''
|| $hostItemsId <= 0
|| !class_exists($hostItemtype)
|| !Config::isItemtypeEnabled($hostItemtype)
) {
$input['host_itemtype'] = '';
$input['host_items_id'] = 0;
} else {
$hostItem = new $hostItemtype();
if (!$hostItem instanceof CommonDBTM || !$hostItem->getFromDB($hostItemsId)) {
$input['host_itemtype'] = '';
$input['host_items_id'] = 0;
}
}
}
return $input;
}
public function testApiConnection(): array
{
$ip = (string) ($this->fields['ip_address'] ?? '');
if ($ip === '') {
return [
'status' => 'no_ip',
'html' => '<span class="text-muted">' . htmlspecialchars(__('No IP address configured', 'urbackup')) . '</span>',
];
}
try {
$client = new UrbackupApiClient($this);
$result = $client->testConnection();
$this->update([
'id' => (int) $this->fields['id'],
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'] ?? '',
'last_api_check' => date('Y-m-d H:i:s'),
]);
if ($result['success']) {
return [
'status' => 'ok',
'html' => '<span class="text-success fw-bold"><i class="ti ti-check"></i> ' .
htmlspecialchars(__('API connection OK', 'urbackup')) . '</span>',
];
}
return [
'status' => 'failed',
'html' => '<span class="text-danger fw-bold"><i class="ti ti-x"></i> ' .
htmlspecialchars(__('API connection failed', 'urbackup')) . '</span><br>' .
'<small class="text-muted">' . htmlspecialchars($result['message'] ?? '') . '</small>',
];
} catch (\Throwable $e) {
$message = $e->getMessage();
$isUnreachable = $this->isNetworkError($message);
return [
'status' => $isUnreachable ? 'unreachable' : 'failed',
'html' => '<span class="' . ($isUnreachable ? 'text-warning' : 'text-danger') . ' fw-bold">' .
'<i class="ti ' . ($isUnreachable ? 'ti-wifi-off' : 'ti-x') . '"></i> ' .
htmlspecialchars($isUnreachable ? __('Server unreachable', 'urbackup') : __('API connection failed', 'urbackup')) .
'</span><br>' .
'<small class="text-muted">' . htmlspecialchars($message) . '</small>',
];
}
}
private function isNetworkError(string $message): bool
{
$networkKeywords = [
'timeout',
'could not resolve host',
'couldn\'t connect to host',
'connection refused',
'connection timed out',
'network is unreachable',
'no route to host',
'ssl',
'certificate',
'curl error',
'request failed',
'returned HTTP status',
'returned non-JSON response',
'problem with the ssl certificate',
'ssl certificate problem',
'ssl connect error',
'ssl wrong version',
];
$lowerMessage = strtolower($message);
foreach ($networkKeywords as $keyword) {
if (str_contains($lowerMessage, strtolower($keyword))) {
return true;
}
}
if (preg_match('/http status [45]\d{2}/', $lowerMessage)) {
return true;
}
return false;
}
private static function renderOnlineBadge(mixed $online, mixed $statusString): string
{
$online = match (true) {
@@ -943,10 +1048,36 @@ class Server extends CommonDBTM
'FROM' => 'glpi_plugin_urbackup_serverassets',
]);
$linkedNames = [];
// Batch-load asset names (one query per itemtype) to avoid N+1 lookups.
$assetNames = [];
$idsByItemtype = [];
foreach ($iterator as $row) {
$assetName = ServerAsset::getAssetName($row['itemtype'], (int) $row['items_id']);
if ($assetName !== '') {
$itemtype = (string) $row['itemtype'];
$itemsId = (int) $row['items_id'];
$assetNames[$itemtype . '-' . $itemsId] = null;
$idsByItemtype[$itemtype][] = $itemsId;
}
foreach ($idsByItemtype as $itemtype => $ids) {
if (!class_exists($itemtype)) {
continue;
}
$item = new $itemtype();
if (!$item instanceof CommonDBTM || !$DB->tableExists($item->getTable())) {
continue;
}
$assetIterator = $DB->request([
'FROM' => $item->getTable(),
'WHERE' => ['id' => $ids],
]);
foreach ($assetIterator as $assetRow) {
$assetNames[$itemtype . '-' . (int) $assetRow['id']] = (string) $assetRow['name'];
}
}
$linkedNames = [];
foreach ($assetNames as $assetName) {
if ($assetName !== null && $assetName !== '') {
$linkedNames[] = strtolower($assetName);
}
}
@@ -1456,30 +1587,6 @@ JAVASCRIPT;
return $groups;
}
private static function getAssetGroupName(string $itemtype, int $items_id, array &$cache): string
{
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $items_id,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$groupId = (int) ($row['groups_id'] ?? 0);
if ($groupId > 0) {
return self::getCachedName('Group', $groupId, $cache);
}
}
return '';
}
private static function getCachedLocationName(int $id, array &$cache): string
{
if ($id <= 0) {
+3 -40
View File
@@ -17,8 +17,6 @@ use RuntimeException;
class UrbackupApiClient
{
private object $server;
private string $base_url;
private string $username;
@@ -52,10 +50,11 @@ class UrbackupApiClient
*/
public function __construct(object $server)
{
$this->server = $server;
$this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x';
$this->username = (string) ($server->fields['api_username'] ?? '');
$this->password = (string) ($server->fields['api_password'] ?? '');
$this->password = $server instanceof Server
? $server->getApiPassword()
: (string) ($server->fields['api_password'] ?? '');
$this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1;
$this->server_version = (string) ($server->fields['server_version'] ?? '');
$this->is_version_2_4_or_higher = $this->detectVersion2_4OrHigher();
@@ -95,23 +94,6 @@ class UrbackupApiClient
return $this->is_version_2_4_or_higher ? 'internet_mode_enabled' : 'internet_mode';
}
/**
* Extract setting value from 2.5+ structured format or simple value.
*
* @param mixed $setting Setting value
* @param mixed $default Default value
*
* @return mixed
*/
public function extractSettingValue(mixed $setting, mixed $default = null): mixed
{
if (is_array($setting) && array_key_exists('value', $setting)) {
return $setting['value'];
}
return $setting ?? $default;
}
/**
* Test API connection.
*
@@ -361,25 +343,6 @@ class UrbackupApiClient
unset($this->cached_settings[$client_id]);
}
public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => (string) $value,
]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data);
}
public function updateClientSettings(string $client_name, string $key, string $value): bool
{
$client_id = $this->getClientIdByName($client_name);
+207
View File
@@ -0,0 +1,207 @@
# UrBackup API Actions (testati)
Server: http://localhost:55414
Credenziali: utente admin, password 12345678
API Version: 2
## Riepilogo dei test (aggiornato 05/08/2026 — login 2-fasi VERIFICATO)
- **Login (2-fasi salt/PBKDF2)**: ✅ FUNZIONANTE — POST `username=admin` a `/x?a=salt``{"salt":"...","pbkdf2_rounds":10000,"rnd":"...","ses":"..."}`; poi POST `username=admin&password=<hash>&ses=<ses>` a `/x?a=login``{"success":true,...}`. Hash = md5(md5_bin(salt+password) passato a PBKDF2-SHA256 con i rounds del salt, poi md5(rnd+risultato)). Con `admin`/`12345678` il 05/08/2026: `hash=4b640fe71c904e43c04ab17aa3fe3f5e``success:true`.
- **Login (stile v1 `u=admin&p=12345678`)**: ❌ fallisce (`{"success":false}`) — il server locale richiede il flusso 2-fasi.
- **Salt**: ✅ accessibile SENZA autenticazione (POST `username=admin`); l'eventuale `{"error":1}` era dovuto alla richiesta GET/v1.
- **Status**: ✅ testabile con sessione valida (`POST /x?a=status&ses=...`).
- **Version**: Non supportato; → "Error: Unknown action [version]"
- **Server Identity**: Non supportato; → "Error: Unknown action [server_identity]"
- **Backups**: Testabile con sessione valida.
- **Livelog**: Non testato (azione disponibile nel codice)
- **Start Backup**: Non testato (azione disponibile nel codice)
- **Add Client**: Non testato (azione disponibile nel codice)
- **Remove Client**: Non testato (azione disponibile nel codice)
- **Clientsettings**: Non testato (azione disponibile nel codice)
- **Clientsettings_save**: Non testato (azione disponibile nel codice)
## API Actions disponibili nel codice (UrbackupApiClient.php)
### Client Status
- `getStatus()`: Ottiene tutti i client → endpoint `status`
- `getClientStatusByName(string $client_name)`: Ricerca client per nome
- `getClientIdByName(string $client_name)`: Ottiene ID client per nome
### Client Settings
- `getClientSettings(string $client_name)`: Ottiene impostazioni client → endpoint `settings` con `sa=clientsettings`
- `updateClientSettings(string $client_name, string $key, string $value)`: Aggiorna impostazioni client
- `saveInternetMode(string $client_name, bool $enabled)`: Salva impostazione internet mode
- `getClientAuthKey(string $client_name)`: Ottiene chiave di autenticazione internet client
### Client Operations
- `addClient(string $client_name)`: Aggiunge client → endpoint `add_client`
- `removeClient(string $client_name)`: Rimuove client → endpoint `remove_client`
### Backup Operations
- `startIncrementalFileBackup(string $client_name)`: Avvia backup file incrementale
- `startFullFileBackup(string $client_name)`: Avvia backup file completo
- `startIncrementalImageBackup(string $client_name)`: Avvia backup immagine incrementale
- `startFullImageBackup(string $client_name)`: Avvia backup immagine completo
- `getRecentBackups(string $client_name, int $limit = 40)`: Ottiene backup recenti → endpoint `backups`
### Log Operations
- `getClientLogs(string $client_name, int $limit = 50)`: Ottiene log client → endpoint `livelog`
### Server Operations
- `getServerIdentity()`: Ottiene identità server → endpoint `server_identity`
## Dettaglio del payload delle API
### Login (2-fasi, come implementato in `UrbackupApiClient::login()`)
```
Fase 1 — GET/POST /x?a=salt
Parametri: username=<utente>
Risposta: {"salt":"...","pbkdf2_rounds":10000,"rnd":"...","ses":"..."}
Fase 2 — POST /x?a=login
Parametri: username=<utente>, password=<hash>, ses=<ses da fase 1>
hash = md5( pbkdf2_sha256( md5_bin(salt . password), salt, pbkdf2_rounds ) . rnd )
(md5_bin = md5 binario, non esadecimale)
Risposta attesa: {"success": true, ...}
```
- Esempio verificato il 05/08/2026 con `admin`/`12345678`: `hash=4b640fe71c904e43c04ab17aa3fe3f5e``"success":true`.
- Lo stile v1 (`u=`/`p=hash MD5`) NON funziona sul server locale.
### Salt
```
Endpoint: POST /x?a=salt?username=...
Risposta attesa: {"success": true, "salt": "...", "rnd": "...", "pbkdf2_rounds": ...}
```
### Status
```
Endpoint: POST /x?a=status
Con sessione: ?ses=...
Risposta attesa: {"success": true, "status": [{"id":...,"name":...}]}
```
### Settings (clientsettings)
```
Endpoint: POST /x?a=settings
Parametri:
sa=clientsettings
t_clientid=id_client
use=valore
value=valore
value_client=valore_client
value_group=valore_group
```
### Settings Save (clientsettings_save)
```
Endpoint: POST /x?a=settings
Parametri:
sa=clientsettings_save
t_clientid=id_client
overwrite=true
key=valore
```
### Backups
```
Endpoint: POST /x?a=backups
Con sessione: ?ses=...
Parametri:
sa=backups
clientid=id_client
```
### Livelog
```
Endpoint: POST /x?a=livelog
Con sessione: ?ses=...
Parametri:
clientid=id_client
lastid=ultimo_id
```
### Start Backup
```
Endpoint: POST /x?a=start_backup
Con sessione: ?ses=...
Parametri:
start_client=clientid
start_type=tipo (incr_file/full_file/incr_image/full_image)
```
### Add Client
```
Endpoint: POST /x?a=add_client
Con sessione: ?ses=...
Parametri:
clientname=nome_client
```
### Remove Client
```
Endpoint: POST /x?a=remove_client
Con sessione: ?ses=...
Parametri:
clientid=id_client (opzionale)
clientname=nome_client
```
### Server Identity
```
Endpoint: POST /x?a=server_identity
Con sessione: ?ses=...
Risposta attesa: {"server_identity": "nome_server"}
```
## Struct delle impostazioni (clientsettings)
```
{
"use": "bool",
"value": "mixed",
"value_client": "string",
"value_group": "string"
}
```
## Struct dei backup (backups)
```
{
"backup_id": backup id,
"machine_name": server machine name,
"starttime": "timestamp",
"endtime": "timestamp",
"status": "status",
"size": "size"
}
```
## Struttura del log (livelog)
```
{
"time": "timestamp",
"level": "level",
"message": "message",
"id": "log id"
}
```
## Note sull'autenticazione
Il client implementa un flusso di autenticazione a due fasi:
1. Inizia il login con username → endpoint `login`
2. Se salta, chiama `/x?a=salt` con username per ottenere salt e RNG
3. Calcola hash password: `hash_pbkdf2('sha256', md5(salt_str . password), salt_str, pbkdf2_rounds) + md5(rnd + passwordMd5)`
4. Completa il login con username/password/hash/rnd/ses
## Errori riscontrati
1. **Autenticazione**: u=admin&p=12345678 non valido; potrebbe non esistere nel server UrBackup
2. **Salt**: Fallisce senza autenticazione corretta
3. **URL endpoint**: Alcuni test con `?a=server_identity` falliscono, suggerendo potrebbero usare un nome azione diverso
## Passaggi successivi
1. Verificare esistenza utente admin nel server UrBackup (potrebbe non esistere)
2. Usare u=admin&p=12345678 non valido; ottenere credenziali corrette
3. Tentare di ottenere session token tramite il corretto flusso di autenticazione a due fasi
4. La maggior parte delle API funziona ma richiede autenticazione valida
## Correzione del codice necessaria per future chiamate API
Il codice UrbackupApiClient.php usa una sessione con ses obbligatorio in tutte le chiamate API autenticate. La libreria del client non implementa yet gestione automatica del token di autenticazione a due fasi per tutte le azioni.