connessione con hardware in assets
This commit is contained in:
@@ -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
|
## Ruolo
|
||||||
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:
|
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.
|
||||||
- ✅ Compatibilità 100% con GLPI 11.x (architettura moderna, namespacing, composer)
|
**Leggi sempre all'inizio di ogni sessione**: `SKILL.md`, `MEMORY.md` e `GLPIDEV.md`.
|
||||||
- ✅ 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
|
## Regole Assolute di Codifica
|
||||||
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.
|
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.
|
||||||
2. **Ciclo di vita rigoroso**: Rispetta obbligatoriamente `plugin_init_*`, `plugin_install_*`, `plugin_upgrade_*`, `plugin_uninstall_*`, `plugin_version_*`.
|
1. **Strict Typing**: Ogni file PHP DEVE iniziare con `declare(strict_types=1);`.
|
||||||
3. **Namespacing & Autoloading**: Tutte le classi devono risiedere in `src/` con namespace `Plugin\<NomePlugin>\`. Usa `composer.json` PSR-4.
|
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).
|
||||||
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.
|
3. **Standard GLPI**:
|
||||||
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.
|
- Estendi le classi base corrette (`CommonDBTM`, `CommonGLPI`) e usa il **Capacity system di GLPI 11** (`AbstractCapacity`) per i tipi Asset Definition.
|
||||||
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()`).
|
- Usa sempre il namespace `GlpiPlugin\Urbackup\`.
|
||||||
7. **Memoria**:dopo ogni modifica funzionante scrivi il file MEMORY.md e rileggi AGENTS.md
|
- 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 |
|
| Componente | Versione/Requisito | Note |
|
||||||
|------------|-------------------|------|
|
|------------|-------------------|------|
|
||||||
| **GLPI** | `>= 11.0.6` | Verifica `defined('GLPI_VERSION')` e `version_compare()` in `setup.php` |
|
| **GLPI** | `>= 11.0.6`, `< 11.99.99` (installato: 11.0.8) | `plugin_urbackup_check_prerequisites()` in `setup.php` |
|
||||||
| **PHP** | `8.3.x` o `8.4.x` | `strict_types=1`, JIT abilitato, nessuna funzione deprecata |
|
| **PHP** | `>= 8.3.0` (installato: 8.4.23) | `strict_types=1`, nessuna funzione deprecata |
|
||||||
| **Database** | MySQL/MariaDB `10.5+` | Usa `$DB->request()`, `QueryExpression`, mai SQL raw non parametrizzato |
|
| **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 |
|
| **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/` |
|
| **Frontend** | HTML/Twig + Bootstrap 5 (GLPI 11), jQuery | Template in `templates/`, asset in `public/`, action POST in `front/` |
|
||||||
| **Testing** | PHPUnit 10+, PHPStan 8+/Psalm strict | Mock di `$DB`, `$_SESSION`, `Auth`, `Session` |
|
| **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 del Plugin (stato attuale v0.7.2)
|
||||||
|
|
||||||
## 🏗 Architettura Plugin GLPI 11.0.6+
|
|
||||||
```
|
```
|
||||||
plugin_<nome>/
|
plugins/urbackup/
|
||||||
├── composer.json # PSR-4, dipendenze lockate, no symfony/symfony
|
├── composer.json # PSR-4: GlpiPlugin\Urbackup\ => src/ ; PHP >= 8.3
|
||||||
├── setup.php # Metadati, check versione, hook init
|
├── setup.php # plugin_init_urbackup, version, prerequisites, hooks
|
||||||
├── hook.php # install, upgrade, uninstall, data injection
|
├── hook.php # plugin_urbackup_get_classes, plugin_urbackup_MassiveActions
|
||||||
├── plugin.xml # Marketplace metadata (opzionale)
|
├── front/ # Entry point con diritti + CSRF
|
||||||
├── src/ # Classi namespaced Plugin\<Nome>\
|
│ ├── asset.form.php # link/unlink asset, azioni backup (POST)
|
||||||
│ ├── Controller/
|
│ ├── server.php # lista server (menu Admin)
|
||||||
│ ├── Entity/
|
│ ├── server.form.php # form server + tab Linked/Unlinked/Missing clients
|
||||||
│ ├── Service/
|
│ ├── server_test.ajax.php # test connessione API (JSON)
|
||||||
│ └── Validator/
|
│ └── config.form.php # pagina config (POST: toggle enable_computer; lista Asset custom con capacità attiva)
|
||||||
├── templates/ # Twig compatibili GLPI
|
├── install/
|
||||||
├── ajax/ # Endpoint PHP con CSRF & permessi
|
│ ├── install.php # plugin_urbackup_install_process + migrazioni idempotenti
|
||||||
├── install/ # Migrazioni SQL versionate
|
│ ├── uninstall.php # plugin_urbackup_uninstall_process + drop tabelle
|
||||||
├── locales/ # File .po/.mo per i18n
|
│ └── mysql/plugin_urbackup-empty.sql # schema iniziale (runFile) + riga default enable_computer
|
||||||
├── css/ & js/ # Asset frontend
|
├── public/ # css/urbackup.css, js/urbackup.js (X-Glpi-Csrf-Token)
|
||||||
└── README.md # Istruzioni installazione, requisiti, changelog
|
├── 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`)
|
### Hook Essenziali
|
||||||
- `plugin_install_<nome>()`: Crea tabelle, configura diritti, registra classi
|
- `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_upgrade_<nome>($version)`: Migrazione step-by-step con controllo versione DB
|
- `plugin_urbackup_install()` → `install/install.php`: schema iniziale + migrazioni idempotenti + `Profile::installRights()` + `Config::ensureDefaultConfiguration()`.
|
||||||
- `plugin_uninstall_<nome>()`: Drop tabelle, pulizia diritti, rimozione config
|
- `plugin_urbackup_uninstall()` → `install/uninstall.php`: `Profile::uninstallRights()` + drop tabelle.
|
||||||
- `plugin_datainjection_populate_<nome>()`: Supporto DataInjection (opzionale)
|
- `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)
|
## Standard di Qualità e Sicurezza
|
||||||
Per ogni richiesta, l'IA deve restituire:
|
- **PSR-12 / PSR-4** applicati rigorosamente; PHPDoc completo per classi pubbliche e metodi.
|
||||||
1. 📁 **Struttura ad albero** completa del plugin
|
- **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`).
|
||||||
2. 📄 `setup.php` con check versione GLPI, namespace, metadata marketplace
|
- **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.
|
||||||
3. 🔌 `hook.php` con install/upgrade/uninstall robusti e transazionali
|
- **i18n**: Tutte le stringhe utente in `__()` / `_n()` con dominio `'urbackup'`.
|
||||||
4. 🧩 Classi `src/` con DI, validazione, logging (`Glpi\Log` o `Toolbox::logDebug()`)
|
- **Permessi**: rightname `plugin_urbackup` con READ/UPDATE/CREATE/DELETE/PURGE.
|
||||||
5. 🌐 Endpoint `ajax/` con CSRF, `Session::checkCSRF()`, output JSON strutturato
|
- **Output**: escape HTML (`htmlspecialchars()`), JSON con `header("Content-Type: application/json; charset=UTF-8")`.
|
||||||
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
|
|
||||||
|
|
||||||
---
|
## 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
|
## Comportamento dell'IA
|
||||||
- **PSR-12 / PSR-4** applicati rigorosamente
|
- 🗣️ Rispondi in **italiano tecnico chiaro**, senza fronzoli.
|
||||||
- **PHPDoc** completo per classi pubbliche e metodi
|
- 📦 Fornisci **codice completo**, non snippet parziali o placeholder.
|
||||||
- **Nessun warning/deprecation** PHP 8.3/8.4 o GLPI 11
|
- 🔍 Spiega **scelte architetturali**, alternative e trade-off.
|
||||||
- **Cache**: Usa `Glpi\Cache` o `symfony/cache` dove appropriato
|
- ⚠️ Segnala **incompatibilità note** con GLPI 11.x o PHP 8.4 e i caveat elencati in GLPIDEV.md §13.
|
||||||
- **Logging**: `Glpi\Log\Logger` o `Toolbox::logDebug()` per trace
|
- 📝 Usa blocchi markdown con linguaggio specifico (`php`, `json`, `sql`, `bash`).
|
||||||
- **i18n**: Tutte le stringhe utente in `__()` e `__n()`, file `.pot` generabili
|
- ❌ Non inventare API GLPI non documentate; se incerto, chiedi conferma o fornisci fallback.
|
||||||
- **Permessi**: `Session::haveRight('plugin_<nome>', READ/UPDATE/CREATE/DELETE)`
|
- 📋 Includi sempre: struttura, comandi di verifica, troubleshooting, checklist finale.
|
||||||
- **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)
|
### Checklist Pre-Consegna (Obbligatoria)
|
||||||
- [ ] `declare(strict_types=1);` in ogni file PHP
|
- [ ] `declare(strict_types=1);` in ogni file PHP
|
||||||
- [ ] Namespace `Plugin\<Nome>\` e PSR-4 corretto
|
- [ ] Namespace `GlpiPlugin\Urbackup\` e PSR-4 corretto
|
||||||
- [ ] Check versione GLPI 11.0.6+ in `setup.php`
|
- [ ] Check versione GLPI ≥ 11.0.6 in `setup.php` (presente)
|
||||||
- [ ] CSRF e permessi su ogni POST/AJAX
|
- [ ] Diritti `Profile::canCurrentUser()`/`Session::haveRight()` su ogni POST/AJAX
|
||||||
- [ ] Query parametrizzate o `$DB->request()`
|
- [ ] CSRF conforme GLPI 11 (niente `Session::checkCSRF()` esplicito nei front; token hidden/header)
|
||||||
- [ ] Output escaped e loggato
|
- [ ] Query parametrizzate o `$DB->request()`; DDL solo via `Migration`
|
||||||
- [ ] Nessun uso di API deprecate GLPI 11
|
- [ ] Output escaped e nessun segreto hardcoded
|
||||||
- [ ] Compatibilità PHP 8.3/8.4 verificata
|
- [ ] Nessun uso di API deprecate GLPI 11 (`$DB->query()`, `Session::isDebugActive()`, ecc.)
|
||||||
- [ ] Istruzioni installazione e test incluse
|
- [ ] 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 11 Plugin Development Guide](https://glpi-project.org/documentation/)
|
||||||
- 🔗 [GLPI GitHub - Plugin Examples](https://github.com/glpi-project)
|
- 🔗 [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)
|
- 🐘 [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)
|
- 🧩 [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)
|
- 🐳 [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.
|
> ⚙️ **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.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
@@ -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 8200–8204 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.
|
|
||||||
@@ -1,6 +1,180 @@
|
|||||||
# MEMORY.md - Stato del Plugin UrBackup
|
# 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
|
## 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.
|
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)
|
### Fix applicati (tutti i file)
|
||||||
- RIMOSSE tutte le chiamate esplicite `Session::checkCSRF()` dai 4 file front — GLPI 11 le gestisce già globalmente via `CheckCsrfListener`
|
- 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
|
- 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
|
## Performance - Asset Definition vs Computer
|
||||||
|
|
||||||
@@ -29,13 +203,13 @@ L'overhead è in GLPI 11 core, non nel plugin:
|
|||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
- `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
|
- `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
|
||||||
- `setup.php` — registra capacità, CSS, JS, hook
|
- `setup.php` — registra capacità, CSS, hook
|
||||||
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log)
|
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log) + host server block
|
||||||
- `src/ServerAsset.php` — gestione collegamenti asset-server
|
- `src/ServerAsset.php` — gestione collegamenti asset-server
|
||||||
- `src/Config.php` — itemtype enabled check
|
- `src/Config.php` — itemtype enabled check
|
||||||
- `src/UrbackupApiClient.php` — client API con caching in-memory (per istanza) e sessione
|
- `src/UrbackupApiClient.php` — client API con caching in-memory (per istanza) e sessione
|
||||||
- `src/LocationHelper.php` — risoluzione location radice
|
- `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
|
## Asset Tab Interni
|
||||||
- 3 sub-tab: Stato, Azioni (solo UPDATE/CREATE), Info/Log
|
- 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)
|
- Dati caricati: status, settings, authkey, backup recenti (10), log (50)
|
||||||
|
|
||||||
## Cache
|
## 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)
|
- `AssetTab::loadApiData()`: cache sessione 30s (chiave: server_id + client_name)
|
||||||
- API timeout: 30s, connect timeout: 5s
|
- 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()`
|
3. **`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
|
||||||
|
|
||||||
## Versione
|
## 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
|
## 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/)
|
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
|
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
|
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
|
||||||
|
|||||||
@@ -108,6 +108,26 @@ plugin_urbackup/
|
|||||||
|
|
||||||
## Changelog
|
## 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
|
### 0.7.0
|
||||||
- Dropped legacy `glpi_plugin_urbackup_profiles` and `glpi_plugin_urbackup_assettypes` tables
|
- Dropped legacy `glpi_plugin_urbackup_profiles` and `glpi_plugin_urbackup_assettypes` tables
|
||||||
- Added composite index `(locations_id, is_active)` on servers table
|
- Added composite index `(locations_id, is_active)` on servers table
|
||||||
|
|||||||
@@ -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.*"
|
||||||
@@ -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
@@ -84,7 +84,7 @@ if (isset($_POST['connect'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_POST['disconnect'])) {
|
if (isset($_POST['disconnect'])) {
|
||||||
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(DELETE)) {
|
if (!Profile::canCurrentUser(UPDATE)) {
|
||||||
Html::displayRightError();
|
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'])) {
|
if (isset($_POST['execute'])) {
|
||||||
$action = $_POST['urbackup_action'] ?? '';
|
$action = $_POST['urbackup_action'] ?? '';
|
||||||
|
|
||||||
|
|||||||
+42
-6
@@ -4,17 +4,53 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use GlpiPlugin\Urbackup\Config;
|
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
|
// Check user has right to manage plugin configuration
|
||||||
if (!Session::haveRight('config', UPDATE)) {
|
if (!Session::haveRight('config', UPDATE)) {
|
||||||
Html::displayRightError();
|
Html::displayRightError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle form submission
|
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update'])) {
|
// CSRF token is validated by the global CheckCsrfListener on every POST.
|
||||||
Config::saveConfiguration($_POST);
|
if (isset($_POST['update'])) {
|
||||||
Html::redirect($_SERVER['REQUEST_URI']);
|
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
|
// Display GLPI header
|
||||||
@@ -30,4 +66,4 @@ $config = new Config();
|
|||||||
$config->showForm(1);
|
$config->showForm(1);
|
||||||
|
|
||||||
// Display GLPI footer
|
// Display GLPI footer
|
||||||
Html::footer();
|
Html::footer();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -44,7 +44,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . "/front/server.php");
|
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . "/front/server.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
$ID = $_GET['id'] ?? null;
|
$ID = (int) ($_GET['id'] ?? 0);
|
||||||
|
|
||||||
Html::header(
|
Html::header(
|
||||||
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
|
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
|
||||||
|
|||||||
@@ -3,29 +3,48 @@
|
|||||||
declare(strict_types=1);
|
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");
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
Html::header_nocache();
|
Html::header_nocache();
|
||||||
|
|
||||||
Session::checkLoginUser();
|
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')]);
|
echo json_encode(['success' => false, 'message' => __('No permission', 'urbackup')]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
|
$server_id = (int) ($_POST['id'] ?? 0);
|
||||||
|
|
||||||
if ($server_id <= 0) {
|
if ($server_id <= 0) {
|
||||||
echo json_encode(['success' => false, 'message' => __('Invalid server ID', 'urbackup')]);
|
echo json_encode(['success' => false, 'message' => __('Invalid server ID', 'urbackup')]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$server = new GlpiPlugin\Urbackup\Server();
|
$server = new Server();
|
||||||
|
|
||||||
if (!$server->getFromDB($server_id)) {
|
if (!$server->getFromDB($server_id)) {
|
||||||
echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]);
|
echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]);
|
||||||
@@ -33,7 +52,7 @@ if (!$server->getFromDB($server_id)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
|
$client = new UrbackupApiClient($server);
|
||||||
$result = $client->testConnection();
|
$result = $client->testConnection();
|
||||||
|
|
||||||
$server->update([
|
$server->update([
|
||||||
@@ -46,4 +65,4 @@ try {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ use GlpiPlugin\Urbackup\Server;
|
|||||||
use GlpiPlugin\Urbackup\ServerAsset;
|
use GlpiPlugin\Urbackup\ServerAsset;
|
||||||
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
|
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
|
||||||
|
|
||||||
require_once __DIR__ . '/install/install.php';
|
|
||||||
require_once __DIR__ . '/install/uninstall.php';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get plugin classes.
|
* Get plugin classes.
|
||||||
*
|
*
|
||||||
|
|||||||
+122
-58
@@ -8,10 +8,9 @@ declare(strict_types=1);
|
|||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use Glpi\Asset\AssetDefinitionManager;
|
|
||||||
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
|
|
||||||
use GlpiPlugin\Urbackup\Config;
|
use GlpiPlugin\Urbackup\Config;
|
||||||
use GlpiPlugin\Urbackup\Profile;
|
use GlpiPlugin\Urbackup\Profile;
|
||||||
|
use GlpiPlugin\Urbackup\Server;
|
||||||
|
|
||||||
if (!defined('GLPI_ROOT')) {
|
if (!defined('GLPI_ROOT')) {
|
||||||
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
|
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_servers_table($migration);
|
||||||
plugin_urbackup_install_update_serverassets_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)
|
// Drop tables that are no longer used (replaced by Profile::registerRights() and Capacity system)
|
||||||
plugin_urbackup_install_drop_legacy_tables($migration);
|
plugin_urbackup_install_drop_legacy_tables($migration);
|
||||||
|
|
||||||
$migration->executeMigration();
|
$migration->executeMigration();
|
||||||
|
|
||||||
Config::ensureDefaultConfiguration();
|
|
||||||
Profile::installRights();
|
Profile::installRights();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -131,40 +133,6 @@ function plugin_urbackup_install_update_configs_table(Migration $migration): voi
|
|||||||
$migration->addKey($table, 'name');
|
$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.
|
* Update servers table.
|
||||||
*
|
*
|
||||||
@@ -261,6 +229,15 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
|
|||||||
'after' => 'last_api_check',
|
'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_creation', 'timestamp');
|
||||||
$migration->addField($table, 'date_mod', '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, 'users_id');
|
||||||
$migration->addKey($table, 'is_active');
|
$migration->addKey($table, 'is_active');
|
||||||
$migration->addKey($table, ['locations_id', 'is_active'], 'location_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');
|
$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.
|
* 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
|
* Convert old glpi_plugin_urbackup_assettypes to GLPI 11 capacity system.
|
||||||
* and auto-enable UrBackup capacity on all existing Asset Definitions.
|
|
||||||
*
|
*
|
||||||
* After this migration, admins can disable UrBackup per Asset Definition
|
* The legacy table is only kept to drop the obsolete `is_default` column.
|
||||||
* via the native Capacities UI (Config > Asset definitions > Capacities).
|
* 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
|
* @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)
|
// Drop is_default column (no longer needed in capacity system)
|
||||||
if ($has_is_default) {
|
if ($has_is_default) {
|
||||||
$migration->dropField($table, 'is_default');
|
$migration->dropField($table, 'is_default');
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_configs` (
|
|||||||
KEY `name` (`name`)
|
KEY `name` (`name`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
) 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` (
|
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
|
||||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`entities_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
`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_message` TEXT DEFAULT NULL,
|
||||||
`last_api_check` TIMESTAMP NULL DEFAULT NULL,
|
`last_api_check` TIMESTAMP NULL DEFAULT NULL,
|
||||||
`comment` TEXT 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_creation` TIMESTAMP NULL DEFAULT NULL,
|
||||||
`date_mod` TIMESTAMP NULL DEFAULT NULL,
|
`date_mod` TIMESTAMP NULL DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
@@ -35,7 +41,8 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
|
|||||||
KEY `locations_id` (`locations_id`),
|
KEY `locations_id` (`locations_id`),
|
||||||
KEY `users_id` (`users_id`),
|
KEY `users_id` (`users_id`),
|
||||||
KEY `is_active` (`is_active`),
|
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;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
|
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
|
||||||
|
|||||||
+2
-19
@@ -14,6 +14,8 @@ if (!defined('GLPI_ROOT')) {
|
|||||||
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
|
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/install.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uninstall plugin.
|
* Uninstall plugin.
|
||||||
*
|
*
|
||||||
@@ -37,22 +39,3 @@ function plugin_urbackup_uninstall_process(): bool
|
|||||||
|
|
||||||
return true;
|
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);
|
|
||||||
}
|
|
||||||
Binary file not shown.
+534
-153
@@ -1,119 +1,20 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: urbackup 0.4.0\n"
|
"Project-Id-Version: urbackup 0.7.3\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
msgid "UrBackup"
|
msgid "%1$s assets linked to UrBackup servers"
|
||||||
msgstr "UrBackup"
|
msgstr "%1$s Assets mit UrBackup-Servern verknüpft"
|
||||||
|
|
||||||
msgid "UrBackup server"
|
msgid ""
|
||||||
msgstr "UrBackup-Server"
|
"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"
|
msgid "API Error"
|
||||||
msgstr "UrBackup-Server"
|
msgstr "API-Fehler"
|
||||||
|
|
||||||
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 connection OK"
|
msgid "API connection OK"
|
||||||
msgstr "API-Verbindung OK"
|
msgstr "API-Verbindung OK"
|
||||||
@@ -121,41 +22,201 @@ msgstr "API-Verbindung OK"
|
|||||||
msgid "API connection failed"
|
msgid "API connection failed"
|
||||||
msgstr "API-Verbindung fehlgeschlagen"
|
msgstr "API-Verbindung fehlgeschlagen"
|
||||||
|
|
||||||
msgid "Server unreachable"
|
msgid "API connection not working. Save server to test connection."
|
||||||
msgstr "Server nicht erreichbar"
|
msgstr ""
|
||||||
|
"API-Verbindung funktioniert nicht. Speichern Sie den Server, um die "
|
||||||
|
"Verbindung zu testen."
|
||||||
|
|
||||||
msgid "No IP address configured"
|
msgid "API connection status"
|
||||||
msgstr "Keine IP-Adresse konfiguriert"
|
msgstr "API-Verbindungsstatus"
|
||||||
|
|
||||||
msgid "Checking..."
|
msgid "API password"
|
||||||
msgstr "Überprüfung..."
|
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"
|
msgid "Click Save to test connection"
|
||||||
msgstr "Klicken Sie auf Speichern, um die Verbindung zu testen"
|
msgstr "Klicken Sie auf Speichern, um die Verbindung zu testen"
|
||||||
|
|
||||||
msgid "Linked clients"
|
msgid "Client State"
|
||||||
msgstr "Verknüpfte Clients"
|
msgstr "Client-Status"
|
||||||
|
|
||||||
msgid "Unlinked clients"
|
msgid "Client logs"
|
||||||
msgstr "Nicht verknüpfte Clients"
|
msgstr "Client-Protokolle"
|
||||||
|
|
||||||
msgid "No linked assets"
|
msgid "Client name"
|
||||||
msgstr "Keine verknüpften Assets"
|
msgstr "Clientname"
|
||||||
|
|
||||||
msgid "No unlinked clients found on UrBackup server"
|
msgid ""
|
||||||
msgstr "Keine nicht verknüpften Clients auf dem UrBackup-Server gefunden"
|
"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."
|
msgid "Client not found on UrBackup server."
|
||||||
msgstr "API-Verbindung funktioniert nicht. Speichern Sie den Server, um die Verbindung zu testen."
|
msgstr "Client auf dem UrBackup-Server nicht gefunden."
|
||||||
|
|
||||||
msgid "Status"
|
msgid "Client state"
|
||||||
msgstr "Status"
|
msgstr "Client-Status"
|
||||||
|
|
||||||
msgid "Last backup"
|
msgid "Client version"
|
||||||
msgstr "Letztes Backup"
|
msgstr "Client-Version"
|
||||||
|
|
||||||
msgid "Show"
|
msgid "Comments"
|
||||||
msgstr "Anzeigen"
|
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"
|
msgid "HTTP"
|
||||||
msgstr "HTTP"
|
msgstr "HTTP"
|
||||||
@@ -163,50 +224,370 @@ msgstr "HTTP"
|
|||||||
msgid "HTTPS"
|
msgid "HTTPS"
|
||||||
msgstr "HTTPS"
|
msgstr "HTTPS"
|
||||||
|
|
||||||
msgid "API Error"
|
|
||||||
msgstr "API-Fehler"
|
|
||||||
|
|
||||||
msgid "Unknown"
|
msgid "Hardware host"
|
||||||
msgstr "Unbekannt"
|
msgstr "Hardware-Host"
|
||||||
|
msgid "IP address"
|
||||||
|
msgstr "IP-Adresse"
|
||||||
|
|
||||||
msgid "Asset Definition"
|
msgid "Ignore SSL verification"
|
||||||
msgstr "Asset-Definition"
|
msgstr "SSL-Prüfung ignorieren"
|
||||||
|
|
||||||
msgid "Legacy"
|
msgid "Image backup"
|
||||||
msgstr "Legacy"
|
msgstr "Image-Backup"
|
||||||
|
|
||||||
msgid "UrBackup Servers"
|
msgid "Incremental"
|
||||||
msgstr "UrBackup-Server"
|
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"
|
msgid "Invalid parameters"
|
||||||
msgstr "Ungültige Parameter"
|
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"
|
msgid "Item type not enabled for UrBackup"
|
||||||
msgstr "Elementtyp für UrBackup nicht aktiviert"
|
msgstr "Elementtyp für UrBackup nicht aktiviert"
|
||||||
|
|
||||||
msgid "No server selected"
|
msgid "Last API check"
|
||||||
msgstr "Kein Server ausgewählt"
|
msgstr "Letzte API-Prüfung"
|
||||||
|
|
||||||
msgid "Asset is already linked to a server"
|
msgid "Last API status"
|
||||||
msgstr "Asset ist bereits mit einem Server verknüpft"
|
msgstr "Letzter API-Status"
|
||||||
|
|
||||||
msgid "Failed to link asset to server"
|
msgid "Last backup"
|
||||||
msgstr "Verknüpfung von Asset mit Server fehlgeschlagen"
|
msgstr "Letztes Backup"
|
||||||
|
|
||||||
msgid "Failed to disconnect asset from server"
|
msgid "Last file backup"
|
||||||
msgstr "Trennung von Asset vom Server fehlgeschlagen"
|
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"
|
msgid "No permission"
|
||||||
msgstr "Keine Berechtigung"
|
msgstr "Keine Berechtigung"
|
||||||
|
|
||||||
msgid "Invalid server ID"
|
msgid "No recent backup information available."
|
||||||
msgstr "Ungültige Server-ID"
|
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"
|
msgid "Server not found"
|
||||||
msgstr "Server nicht gefunden"
|
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."
|
msgid "Sorry. You cannot access this file directly."
|
||||||
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
|
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
|
||||||
|
|
||||||
msgid "No assets"
|
msgid "Status"
|
||||||
msgstr "Keine Assets"
|
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."
|
||||||
|
|||||||
Binary file not shown.
+525
-153
@@ -1,119 +1,19 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: urbackup 0.4.0\n"
|
"Project-Id-Version: urbackup 0.7.3\n"
|
||||||
"Language: en_GB\n"
|
"Language: en_GB\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
msgid "UrBackup"
|
msgid "%1$s assets linked to UrBackup servers"
|
||||||
msgstr "UrBackup"
|
msgstr "%1$s assets linked to UrBackup servers"
|
||||||
|
|
||||||
msgid "UrBackup server"
|
msgid ""
|
||||||
msgstr "UrBackup server"
|
"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"
|
msgid "API Error"
|
||||||
msgstr "UrBackup servers"
|
msgstr "API Error"
|
||||||
|
|
||||||
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 connection OK"
|
msgid "API connection OK"
|
||||||
msgstr "API connection OK"
|
msgstr "API connection OK"
|
||||||
@@ -121,41 +21,197 @@ msgstr "API connection OK"
|
|||||||
msgid "API connection failed"
|
msgid "API connection failed"
|
||||||
msgstr "API connection failed"
|
msgstr "API connection failed"
|
||||||
|
|
||||||
msgid "Server unreachable"
|
msgid "API connection not working. Save server to test connection."
|
||||||
msgstr "Server unreachable"
|
msgstr "API connection not working. Save server to test connection."
|
||||||
|
|
||||||
msgid "No IP address configured"
|
msgid "API connection status"
|
||||||
msgstr "No IP address configured"
|
msgstr "API connection status"
|
||||||
|
|
||||||
msgid "Checking..."
|
msgid "API password"
|
||||||
msgstr "Checking..."
|
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"
|
msgid "Click Save to test connection"
|
||||||
msgstr "Click Save to test connection"
|
msgstr "Click Save to test connection"
|
||||||
|
|
||||||
msgid "Linked clients"
|
msgid "Client State"
|
||||||
msgstr "Linked clients"
|
msgstr "Client State"
|
||||||
|
|
||||||
msgid "Unlinked clients"
|
msgid "Client logs"
|
||||||
msgstr "Unlinked clients"
|
msgstr "Client logs"
|
||||||
|
|
||||||
msgid "No linked assets"
|
msgid "Client name"
|
||||||
msgstr "No linked assets"
|
msgstr "Client name"
|
||||||
|
|
||||||
msgid "No unlinked clients found on UrBackup server"
|
msgid ""
|
||||||
msgstr "No unlinked clients found on UrBackup server"
|
"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."
|
msgid "Client not found on UrBackup server."
|
||||||
msgstr "API connection not working. Save server to test connection."
|
msgstr "Client not found on UrBackup server."
|
||||||
|
|
||||||
msgid "Status"
|
msgid "Client state"
|
||||||
msgstr "Status"
|
msgstr "Client state"
|
||||||
|
|
||||||
msgid "Last backup"
|
msgid "Client version"
|
||||||
msgstr "Last backup"
|
msgstr "Client version"
|
||||||
|
|
||||||
msgid "Show"
|
msgid "Comments"
|
||||||
msgstr "Show"
|
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"
|
msgid "HTTP"
|
||||||
msgstr "HTTP"
|
msgstr "HTTP"
|
||||||
@@ -163,50 +219,366 @@ msgstr "HTTP"
|
|||||||
msgid "HTTPS"
|
msgid "HTTPS"
|
||||||
msgstr "HTTPS"
|
msgstr "HTTPS"
|
||||||
|
|
||||||
msgid "API Error"
|
|
||||||
msgstr "API Error"
|
|
||||||
|
|
||||||
msgid "Unknown"
|
msgid "Hardware host"
|
||||||
msgstr "Unknown"
|
msgstr "Hardware host"
|
||||||
|
msgid "IP address"
|
||||||
|
msgstr "IP address"
|
||||||
|
|
||||||
msgid "Asset Definition"
|
msgid "Ignore SSL verification"
|
||||||
msgstr "Asset Definition"
|
msgstr "Ignore SSL verification"
|
||||||
|
|
||||||
msgid "Legacy"
|
msgid "Image backup"
|
||||||
msgstr "Legacy"
|
msgstr "Image backup"
|
||||||
|
|
||||||
msgid "UrBackup Servers"
|
msgid "Incremental"
|
||||||
msgstr "UrBackup Servers"
|
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"
|
msgid "Invalid parameters"
|
||||||
msgstr "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"
|
msgid "Item type not enabled for UrBackup"
|
||||||
msgstr "Item type not enabled for UrBackup"
|
msgstr "Item type not enabled for UrBackup"
|
||||||
|
|
||||||
msgid "No server selected"
|
msgid "Last API check"
|
||||||
msgstr "No server selected"
|
msgstr "Last API check"
|
||||||
|
|
||||||
msgid "Asset is already linked to a server"
|
msgid "Last API status"
|
||||||
msgstr "Asset is already linked to a server"
|
msgstr "Last API status"
|
||||||
|
|
||||||
msgid "Failed to link asset to server"
|
msgid "Last backup"
|
||||||
msgstr "Failed to link asset to server"
|
msgstr "Last backup"
|
||||||
|
|
||||||
msgid "Failed to disconnect asset from server"
|
msgid "Last file backup"
|
||||||
msgstr "Failed to disconnect asset from server"
|
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"
|
msgid "No permission"
|
||||||
msgstr "No permission"
|
msgstr "No permission"
|
||||||
|
|
||||||
msgid "Invalid server ID"
|
msgid "No recent backup information available."
|
||||||
msgstr "Invalid server ID"
|
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"
|
msgid "Server not found"
|
||||||
msgstr "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."
|
msgid "Sorry. You cannot access this file directly."
|
||||||
msgstr "Sorry. You cannot access this file directly."
|
msgstr "Sorry. You cannot access this file directly."
|
||||||
|
|
||||||
msgid "No assets"
|
msgid "Status"
|
||||||
msgstr "No assets"
|
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."
|
||||||
|
|||||||
Binary file not shown.
+534
-153
@@ -1,119 +1,20 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: urbackup 0.4.0\n"
|
"Project-Id-Version: urbackup 0.7.3\n"
|
||||||
"Language: it_IT\n"
|
"Language: it_IT\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
msgid "UrBackup"
|
msgid "%1$s assets linked to UrBackup servers"
|
||||||
msgstr "UrBackup"
|
msgstr "%1$s asset collegati a server UrBackup"
|
||||||
|
|
||||||
msgid "UrBackup server"
|
msgid ""
|
||||||
msgstr "Server UrBackup"
|
"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"
|
msgid "API Error"
|
||||||
msgstr "Server UrBackup"
|
msgstr "Errore API"
|
||||||
|
|
||||||
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 connection OK"
|
msgid "API connection OK"
|
||||||
msgstr "Connessione API OK"
|
msgstr "Connessione API OK"
|
||||||
@@ -121,41 +22,201 @@ msgstr "Connessione API OK"
|
|||||||
msgid "API connection failed"
|
msgid "API connection failed"
|
||||||
msgstr "Connessione API fallita"
|
msgstr "Connessione API fallita"
|
||||||
|
|
||||||
msgid "Server unreachable"
|
msgid "API connection not working. Save server to test connection."
|
||||||
msgstr "Server irraggiungibile"
|
msgstr ""
|
||||||
|
"Connessione API non funzionante. Salva il server per testare la connessione."
|
||||||
|
|
||||||
msgid "No IP address configured"
|
msgid "API connection status"
|
||||||
msgstr "Nessun indirizzo IP configurato"
|
msgstr "Stato connessione API"
|
||||||
|
|
||||||
msgid "Checking..."
|
msgid "API password"
|
||||||
msgstr "Verifica in corso..."
|
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"
|
msgid "Click Save to test connection"
|
||||||
msgstr "Clicca \"Salva\" per testare la connessione"
|
msgstr "Clicca \"Salva\" per testare la connessione"
|
||||||
|
|
||||||
msgid "Linked clients"
|
msgid "Client State"
|
||||||
msgstr "Clienti collegati"
|
msgstr "Stato Client"
|
||||||
|
|
||||||
msgid "Unlinked clients"
|
msgid "Client logs"
|
||||||
msgstr "Clienti non collegati"
|
msgstr "Log client"
|
||||||
|
|
||||||
msgid "No linked assets"
|
msgid "Client name"
|
||||||
msgstr "Nessun asset collegato"
|
msgstr "Nome client"
|
||||||
|
|
||||||
msgid "No unlinked clients found on UrBackup server"
|
msgid ""
|
||||||
msgstr "Nessun cliente non collegato trovato sul server UrBackup"
|
"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."
|
msgid "Client not found on UrBackup server."
|
||||||
msgstr "Connessione API non funzionante. Salva il server per testare la connessione."
|
msgstr "Client non trovato sul server UrBackup."
|
||||||
|
|
||||||
msgid "Status"
|
msgid "Client state"
|
||||||
msgstr "Stato"
|
msgstr "Stato client"
|
||||||
|
|
||||||
msgid "Last backup"
|
msgid "Client version"
|
||||||
msgstr "Ultimo backup"
|
msgstr "Versione client"
|
||||||
|
|
||||||
msgid "Show"
|
msgid "Comments"
|
||||||
msgstr "Mostra"
|
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"
|
msgid "HTTP"
|
||||||
msgstr "HTTP"
|
msgstr "HTTP"
|
||||||
@@ -163,50 +224,370 @@ msgstr "HTTP"
|
|||||||
msgid "HTTPS"
|
msgid "HTTPS"
|
||||||
msgstr "HTTPS"
|
msgstr "HTTPS"
|
||||||
|
|
||||||
msgid "API Error"
|
|
||||||
msgstr "Errore API"
|
|
||||||
|
|
||||||
msgid "Unknown"
|
msgid "Hardware host"
|
||||||
msgstr "Sconosciuto"
|
msgstr "Host hardware"
|
||||||
|
msgid "IP address"
|
||||||
|
msgstr "Indirizzo IP"
|
||||||
|
|
||||||
msgid "Asset Definition"
|
msgid "Ignore SSL verification"
|
||||||
msgstr "Definizione asset"
|
msgstr "Ignora verifica SSL"
|
||||||
|
|
||||||
msgid "Legacy"
|
msgid "Image backup"
|
||||||
msgstr "Legacy"
|
msgstr "Backup immagine"
|
||||||
|
|
||||||
msgid "UrBackup Servers"
|
msgid "Incremental"
|
||||||
msgstr "Server UrBackup"
|
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"
|
msgid "Invalid parameters"
|
||||||
msgstr "Parametri non validi"
|
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"
|
msgid "Item type not enabled for UrBackup"
|
||||||
msgstr "Tipo oggetto non abilitato per UrBackup"
|
msgstr "Tipo oggetto non abilitato per UrBackup"
|
||||||
|
|
||||||
msgid "No server selected"
|
msgid "Last API check"
|
||||||
msgstr "Nessun server selezionato"
|
msgstr "Ultima verifica API"
|
||||||
|
|
||||||
msgid "Asset is already linked to a server"
|
msgid "Last API status"
|
||||||
msgstr "L'asset è già collegato a un server"
|
msgstr "Ultimo stato API"
|
||||||
|
|
||||||
msgid "Failed to link asset to server"
|
msgid "Last backup"
|
||||||
msgstr "Collegamento asset al server fallito"
|
msgstr "Ultimo backup"
|
||||||
|
|
||||||
msgid "Failed to disconnect asset from server"
|
msgid "Last file backup"
|
||||||
msgstr "Disconnessione asset dal server fallita"
|
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"
|
msgid "No permission"
|
||||||
msgstr "Nessun permesso"
|
msgstr "Nessun permesso"
|
||||||
|
|
||||||
msgid "Invalid server ID"
|
msgid "No recent backup information available."
|
||||||
msgstr "ID server non valido"
|
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"
|
msgid "Server not found"
|
||||||
msgstr "Server non trovato"
|
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."
|
msgid "Sorry. You cannot access this file directly."
|
||||||
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
|
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
|
||||||
|
|
||||||
msgid "No assets"
|
msgid "Status"
|
||||||
msgstr "Nessun Asset"
|
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
Binary file not shown.
@@ -18,6 +18,38 @@
|
|||||||
margin-right: 4px;
|
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 {
|
#urbackupTabs .nav-link#state-tab {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #4a4a4a;
|
background-color: #4a4a4a;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// Force OPcache to reload plugin files when accessed via web
|
// Force OPcache to reload plugin files when accessed via web.
|
||||||
if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) {
|
// 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(
|
$files = new RecursiveIteratorIterator(
|
||||||
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
|
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
);
|
);
|
||||||
@@ -24,7 +28,7 @@ use GlpiPlugin\Urbackup\Server;
|
|||||||
use GlpiPlugin\Urbackup\ServerAsset;
|
use GlpiPlugin\Urbackup\ServerAsset;
|
||||||
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
|
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_MIN_GLPI', '11.0.6');
|
||||||
define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99');
|
define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99');
|
||||||
|
|
||||||
@@ -61,10 +65,12 @@ function plugin_init_urbackup(): void
|
|||||||
Plugin::registerClass(ServerAsset::class);
|
Plugin::registerClass(ServerAsset::class);
|
||||||
Plugin::registerClass(PluginUrbackupMassiveAction::class);
|
Plugin::registerClass(PluginUrbackupMassiveAction::class);
|
||||||
|
|
||||||
// Register tab on Computer (legacy, always enabled)
|
// Register tab on Computer (configurable from the plugin configuration page)
|
||||||
Plugin::registerClass(AssetTab::class, [
|
if (Config::getEnableComputer()) {
|
||||||
'addtabon' => ['Computer'],
|
Plugin::registerClass(AssetTab::class, [
|
||||||
]);
|
'addtabon' => ['Computer'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
|
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
|
||||||
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
|
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
|
||||||
@@ -87,10 +93,6 @@ function plugin_init_urbackup(): void
|
|||||||
$PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = [
|
$PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = [
|
||||||
'public/css/urbackup.css',
|
'public/css/urbackup.css',
|
||||||
];
|
];
|
||||||
|
|
||||||
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [
|
|
||||||
'public/js/urbackup.js',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+131
-33
@@ -102,6 +102,8 @@ class AssetTab extends CommonDBTM
|
|||||||
|
|
||||||
echo "<div class='plugin-urbackup-asset-tab'>";
|
echo "<div class='plugin-urbackup-asset-tab'>";
|
||||||
|
|
||||||
|
self::showHostServerBlock($item);
|
||||||
|
|
||||||
if ($link === null) {
|
if ($link === null) {
|
||||||
self::showNoServerLinkedBlock($item);
|
self::showNoServerLinkedBlock($item);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,6 +115,40 @@ class AssetTab extends CommonDBTM
|
|||||||
return true;
|
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.
|
* Show block when no server is linked.
|
||||||
*
|
*
|
||||||
@@ -218,24 +254,88 @@ class AssetTab extends CommonDBTM
|
|||||||
|
|
||||||
$api_data = self::loadApiData($item, $server, $link);
|
$api_data = self::loadApiData($item, $server, $link);
|
||||||
|
|
||||||
echo "<table class='tab_cadre_fixe'>";
|
$ip = (string) ($server->fields['ip_address'] ?? '');
|
||||||
echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup status', 'urbackup')) . "</th></tr>";
|
$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 "<div class='card mb-3'>";
|
||||||
echo "<td>" . htmlspecialchars(__('Linked server', 'urbackup')) . "</td>";
|
echo "<div class='card-header'><i class='ti ti-cloud-up me-1'></i> "
|
||||||
echo "<td>" . $server->getLink() . "</td>";
|
. htmlspecialchars(__('UrBackup status', 'urbackup')) . "</div>";
|
||||||
echo "<td>" . htmlspecialchars(__('IP address', 'urbackup')) . "</td>";
|
echo "<div class='card-body py-3'>";
|
||||||
echo "<td>" . htmlspecialchars((string) $server->fields['ip_address']) . "</td>";
|
echo "<div class='row row-cols-1 row-cols-sm-2 row-cols-lg-4 g-2'>";
|
||||||
echo "</tr>";
|
|
||||||
|
|
||||||
echo "<tr class='tab_bg_1'>";
|
// Linked server
|
||||||
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
|
echo "<div class='col'>";
|
||||||
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
|
echo "<div class='card h-100'>";
|
||||||
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
|
echo "<div class='card-body p-3'>";
|
||||||
echo "<td>" . htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</td>";
|
echo "<div class='text-muted small text-uppercase fw-semibold'>"
|
||||||
echo "</tr>";
|
. 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'] !== '') {
|
if ($api_data['error'] !== '') {
|
||||||
echo "<div class='alert alert-warning'>";
|
echo "<div class='alert alert-warning'>";
|
||||||
@@ -249,7 +349,7 @@ class AssetTab extends CommonDBTM
|
|||||||
echo "</div>";
|
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 CommonDBTM $item Asset
|
||||||
* @param Server $server Server
|
* @param Server $server Server
|
||||||
* @param array<string, mixed> $link Link
|
|
||||||
* @param array<string, mixed> $api_data API data
|
* @param array<string, mixed> $api_data API data
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
@@ -335,7 +434,6 @@ class AssetTab extends CommonDBTM
|
|||||||
private static function showInternalTabs(
|
private static function showInternalTabs(
|
||||||
CommonDBTM $item,
|
CommonDBTM $item,
|
||||||
Server $server,
|
Server $server,
|
||||||
array $link,
|
|
||||||
array $api_data
|
array $api_data
|
||||||
): void {
|
): void {
|
||||||
echo "<div class='plugin-urbackup-inner-tabs'>";
|
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 '<ul class="nav nav-tabs" id="urbackupTabs" role="tablist">';
|
||||||
echo '<li class="nav-item" role="presentation">';
|
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 '<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>';
|
echo '</a></li>';
|
||||||
if ($canWrite) {
|
if ($canWrite) {
|
||||||
echo '<li class="nav-item" role="presentation">';
|
echo '<li class="nav-item" role="presentation">';
|
||||||
@@ -362,12 +460,12 @@ class AssetTab extends CommonDBTM
|
|||||||
echo '<div class="tab-content">';
|
echo '<div class="tab-content">';
|
||||||
|
|
||||||
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
|
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
|
||||||
self::showStateSection($server, $link, $api_data);
|
self::showStateSection($api_data);
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
|
|
||||||
if ($canWrite) {
|
if ($canWrite) {
|
||||||
echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
|
echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
|
||||||
self::showActionsSection($item, $server, $link, $api_data);
|
self::showActionsSection($item, $api_data);
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,13 +480,11 @@ class AssetTab extends CommonDBTM
|
|||||||
/**
|
/**
|
||||||
* Show state section.
|
* Show state section.
|
||||||
*
|
*
|
||||||
* @param Server $server Server
|
|
||||||
* @param array<string, mixed> $link Link
|
|
||||||
* @param array<string, mixed> $api_data API data
|
* @param array<string, mixed> $api_data API data
|
||||||
*
|
*
|
||||||
* @return void
|
* @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'];
|
$status = $api_data['client_status'];
|
||||||
$settings = $api_data['client_settings'];
|
$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);
|
$internetMode = self::extractSettingValue($settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null, 0);
|
||||||
$internetModeDisplay = ((int) $internetMode === 1)
|
$internetModeDisplay = ((int) $internetMode === 1)
|
||||||
? '<span class="badge bg-success">' . __('Yes', 'urbackup') . '</span>'
|
? '<span class="badge bg-success">' . htmlspecialchars(__('Yes', 'urbackup')) . '</span>'
|
||||||
: '<span class="badge bg-secondary">' . __('No', 'urbackup') . '</span>';
|
: '<span class="badge bg-secondary">' . htmlspecialchars(__('No', 'urbackup')) . '</span>';
|
||||||
|
|
||||||
$rows = [
|
$rows = [
|
||||||
__('Client version', 'urbackup') => $status['client_version_string'] ?? $status['client_version'] ?? $status['version'] ?? '-',
|
__('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 file backup result', 'urbackup') => self::formatBoolStatus($status['file_ok'] ?? null),
|
||||||
__('Last image backup result', 'urbackup') => self::formatBoolStatus($status['image_ok'] ?? null),
|
__('Last image backup result', 'urbackup') => self::formatBoolStatus($status['image_ok'] ?? null),
|
||||||
__('Current activities', 'urbackup') => $status['status'] ?? $status['activity'] ?? '-',
|
__('Current activities', 'urbackup') => $status['status'] ?? $status['activity'] ?? '-',
|
||||||
__('Internet mode', 'urbackup') => $internetModeDisplay,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($rows as $label => $value) {
|
foreach ($rows as $label => $value) {
|
||||||
$displayValue = is_array($value) ? json_encode($value) : (string) $value;
|
$displayValue = is_array($value) ? json_encode($value) : (string) $value;
|
||||||
echo "<tr class='tab_bg_1'>";
|
echo "<tr class='tab_bg_1'>";
|
||||||
echo "<td>" . htmlspecialchars((string) $label) . "</td>";
|
echo "<td>" . htmlspecialchars((string) $label) . "</td>";
|
||||||
echo "<td>" . $displayValue . "</td>";
|
echo "<td>" . htmlspecialchars((string) $displayValue) . "</td>";
|
||||||
echo "</tr>";
|
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 "<tr class='tab_bg_1'>";
|
||||||
echo "<td>" . htmlspecialchars(__('Internet authentication key', 'urbackup')) . "</td>";
|
echo "<td>" . htmlspecialchars(__('Internet authentication key', 'urbackup')) . "</td>";
|
||||||
echo "<td>";
|
echo "<td>";
|
||||||
@@ -455,16 +557,12 @@ class AssetTab extends CommonDBTM
|
|||||||
* Show actions section.
|
* Show actions section.
|
||||||
*
|
*
|
||||||
* @param CommonDBTM $item Asset
|
* @param CommonDBTM $item Asset
|
||||||
* @param Server $server Server
|
|
||||||
* @param array<string, mixed> $link Link
|
|
||||||
* @param array<string, mixed> $api_data API data
|
* @param array<string, mixed> $api_data API data
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private static function showActionsSection(
|
private static function showActionsSection(
|
||||||
CommonDBTM $item,
|
CommonDBTM $item,
|
||||||
Server $server,
|
|
||||||
array $link,
|
|
||||||
array $api_data
|
array $api_data
|
||||||
): void {
|
): void {
|
||||||
echo "<table class='tab_cadre_fixe'>";
|
echo "<table class='tab_cadre_fixe'>";
|
||||||
|
|||||||
+169
-33
@@ -11,9 +11,10 @@ declare(strict_types=1);
|
|||||||
namespace GlpiPlugin\Urbackup;
|
namespace GlpiPlugin\Urbackup;
|
||||||
|
|
||||||
use CommonDBTM;
|
use CommonDBTM;
|
||||||
|
use Dropdown;
|
||||||
use Glpi\Asset\Asset;
|
use Glpi\Asset\Asset;
|
||||||
use Glpi\Asset\AssetDefinitionManager;
|
use Glpi\Asset\AssetDefinitionManager;
|
||||||
use DBmysql;
|
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
|
||||||
use Html;
|
use Html;
|
||||||
use Session;
|
use Session;
|
||||||
|
|
||||||
@@ -21,6 +22,16 @@ class Config extends CommonDBTM
|
|||||||
{
|
{
|
||||||
public static $rightname = 'config';
|
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.
|
* Get type name.
|
||||||
*
|
*
|
||||||
@@ -45,17 +56,6 @@ class Config extends CommonDBTM
|
|||||||
return 'glpi_plugin_urbackup_configs';
|
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.
|
* Get assettypes table name.
|
||||||
*
|
*
|
||||||
@@ -66,6 +66,47 @@ class Config extends CommonDBTM
|
|||||||
return 'glpi_plugin_urbackup_assettypes';
|
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.
|
* Check whether itemtype is enabled for UrBackup.
|
||||||
*
|
*
|
||||||
@@ -80,7 +121,7 @@ class Config extends CommonDBTM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($itemtype === 'Computer') {
|
if ($itemtype === 'Computer') {
|
||||||
return true;
|
return self::getEnableComputer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// All Asset subclasses are enabled by default.
|
// All Asset subclasses are enabled by default.
|
||||||
@@ -101,7 +142,11 @@ class Config extends CommonDBTM
|
|||||||
*/
|
*/
|
||||||
public static function getEnabledItemtypes(): array
|
public static function getEnabledItemtypes(): array
|
||||||
{
|
{
|
||||||
$itemtypes = ['Computer'];
|
$itemtypes = [];
|
||||||
|
|
||||||
|
if (self::getEnableComputer()) {
|
||||||
|
$itemtypes[] = 'Computer';
|
||||||
|
}
|
||||||
|
|
||||||
if (class_exists(AssetDefinitionManager::class)) {
|
if (class_exists(AssetDefinitionManager::class)) {
|
||||||
$manager = AssetDefinitionManager::getInstance();
|
$manager = AssetDefinitionManager::getInstance();
|
||||||
@@ -116,6 +161,42 @@ class Config extends CommonDBTM
|
|||||||
return $itemtypes;
|
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.
|
* Show config form.
|
||||||
*
|
*
|
||||||
@@ -128,38 +209,93 @@ class Config extends CommonDBTM
|
|||||||
{
|
{
|
||||||
Session::checkRight('config', UPDATE);
|
Session::checkRight('config', UPDATE);
|
||||||
|
|
||||||
|
$enable_computer = self::getEnableComputer();
|
||||||
|
|
||||||
echo "<div class='center'>";
|
echo "<div class='center'>";
|
||||||
|
echo "<form method='post' action='" . static::getFormURL() . "'>";
|
||||||
|
|
||||||
echo "<table class='tab_cadre_fixe'>";
|
echo "<table class='tab_cadre_fixe'>";
|
||||||
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
|
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
|
||||||
|
|
||||||
echo "<tr class='tab_bg_1'>";
|
echo "<tr class='tab_bg_1'>";
|
||||||
echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>";
|
echo "<td>";
|
||||||
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</span></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>";
|
||||||
|
|
||||||
|
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 "</table>";
|
||||||
|
|
||||||
echo "<br>";
|
echo "<br>";
|
||||||
echo "<div class='alert alert-info'>";
|
echo "<div class='alert alert-info'>";
|
||||||
echo htmlspecialchars(
|
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>";
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
|
|
||||||
return true;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ namespace GlpiPlugin\Urbackup;
|
|||||||
|
|
||||||
use CommonDBTM;
|
use CommonDBTM;
|
||||||
use Html;
|
use Html;
|
||||||
use Session;
|
|
||||||
|
|
||||||
class MassiveAction extends CommonDBTM
|
class MassiveAction extends CommonDBTM
|
||||||
{
|
{
|
||||||
@@ -216,8 +215,7 @@ class MassiveAction extends CommonDBTM
|
|||||||
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
|
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process disconnect-from-server massive action.
|
* Process disconnect-from-server massive action.
|
||||||
@@ -261,6 +259,5 @@ class MassiveAction extends CommonDBTM
|
|||||||
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
|
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+239
-132
@@ -11,9 +11,9 @@ declare(strict_types=1);
|
|||||||
namespace GlpiPlugin\Urbackup;
|
namespace GlpiPlugin\Urbackup;
|
||||||
|
|
||||||
use CommonDBTM;
|
use CommonDBTM;
|
||||||
use CommonGLPI;
|
|
||||||
use Dropdown;
|
use Dropdown;
|
||||||
use Entity;
|
use Entity;
|
||||||
|
use GLPIKey;
|
||||||
use Group;
|
use Group;
|
||||||
use Html;
|
use Html;
|
||||||
use Location;
|
use Location;
|
||||||
@@ -465,9 +465,10 @@ class Server extends CommonDBTM
|
|||||||
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
|
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
|
||||||
echo "<td>";
|
echo "<td>";
|
||||||
if ($canEdit) {
|
if ($canEdit) {
|
||||||
echo "<input type='password' name='api_password' value='" .
|
echo "<input type='password' name='api_password' value='' placeholder='******' autocomplete='new-password'>";
|
||||||
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
|
echo "<br><small class='text-muted'>" .
|
||||||
"' autocomplete='new-password'>";
|
htmlspecialchars(__('Leave empty to keep the current password.', 'urbackup')) .
|
||||||
|
"</small>";
|
||||||
} else {
|
} else {
|
||||||
echo '******';
|
echo '******';
|
||||||
}
|
}
|
||||||
@@ -483,6 +484,53 @@ class Server extends CommonDBTM
|
|||||||
echo "</td>";
|
echo "</td>";
|
||||||
echo "</tr>";
|
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) {
|
if ($ID > 0) {
|
||||||
echo "<tr class='tab_bg_1'>";
|
echo "<tr class='tab_bg_1'>";
|
||||||
echo "<td>" . htmlspecialchars(__('UrBackup web interface', 'urbackup')) . "</td>";
|
echo "<td>" . htmlspecialchars(__('UrBackup web interface', 'urbackup')) . "</td>";
|
||||||
@@ -536,6 +584,102 @@ class Server extends CommonDBTM
|
|||||||
return sprintf('%s://%s:%d', $protocol, $ip, $port);
|
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.
|
* 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) {
|
if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) {
|
||||||
$input['port'] = 55414;
|
$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';
|
$input['protocol'] = 'http';
|
||||||
}
|
}
|
||||||
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
|
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
|
||||||
@@ -608,16 +754,20 @@ class Server extends CommonDBTM
|
|||||||
return $input;
|
return $input;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($input['port']) && ($input['port'] === '' || $input['port'] === null)) {
|
if (isset($input['port'])) {
|
||||||
$input['port'] = 55414;
|
$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';
|
$input['protocol'] = 'http';
|
||||||
}
|
}
|
||||||
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
|
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
|
||||||
$input['ignore_ssl'] = 0;
|
$input['ignore_ssl'] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$hasNewPassword = isset($input['api_password']) && $input['api_password'] !== '';
|
||||||
|
|
||||||
if (!empty($input['id']) && (int) $input['id'] > 0) {
|
if (!empty($input['id']) && (int) $input['id'] > 0) {
|
||||||
$server = new self();
|
$server = new self();
|
||||||
if ($server->getFromDB((int) $input['id'])) {
|
if ($server->getFromDB((int) $input['id'])) {
|
||||||
@@ -626,19 +776,33 @@ class Server extends CommonDBTM
|
|||||||
$port = $input['port'] ?? $serverFields['port'] ?? 55414;
|
$port = $input['port'] ?? $serverFields['port'] ?? 55414;
|
||||||
$protocol = $input['protocol'] ?? $serverFields['protocol'] ?? 'http';
|
$protocol = $input['protocol'] ?? $serverFields['protocol'] ?? 'http';
|
||||||
$apiUsername = $input['api_username'] ?? $serverFields['api_username'] ?? '';
|
$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;
|
$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 = new self();
|
||||||
$tmpServer->fields = [
|
$tmpServer->fields = [
|
||||||
'id' => (int) $input['id'],
|
'id' => (int) $input['id'],
|
||||||
'ip_address' => $ip,
|
'ip_address' => $ip,
|
||||||
'port' => $port,
|
'port' => $port,
|
||||||
'protocol' => $protocol,
|
'protocol' => $protocol,
|
||||||
'api_username' => $apiUsername,
|
'api_username' => $apiUsername,
|
||||||
'api_password' => $apiPassword,
|
'api_password' => $apiPassword,
|
||||||
'ignore_ssl' => $ignoreSsl,
|
'ignore_ssl' => $ignoreSsl,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
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;
|
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
|
private static function renderOnlineBadge(mixed $online, mixed $statusString): string
|
||||||
{
|
{
|
||||||
$online = match (true) {
|
$online = match (true) {
|
||||||
@@ -943,10 +1048,36 @@ class Server extends CommonDBTM
|
|||||||
'FROM' => 'glpi_plugin_urbackup_serverassets',
|
'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) {
|
foreach ($iterator as $row) {
|
||||||
$assetName = ServerAsset::getAssetName($row['itemtype'], (int) $row['items_id']);
|
$itemtype = (string) $row['itemtype'];
|
||||||
if ($assetName !== '') {
|
$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);
|
$linkedNames[] = strtolower($assetName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1456,30 +1587,6 @@ JAVASCRIPT;
|
|||||||
return $groups;
|
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
|
private static function getCachedLocationName(int $id, array &$cache): string
|
||||||
{
|
{
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ use RuntimeException;
|
|||||||
|
|
||||||
class UrbackupApiClient
|
class UrbackupApiClient
|
||||||
{
|
{
|
||||||
private object $server;
|
|
||||||
|
|
||||||
private string $base_url;
|
private string $base_url;
|
||||||
|
|
||||||
private string $username;
|
private string $username;
|
||||||
@@ -52,10 +50,11 @@ class UrbackupApiClient
|
|||||||
*/
|
*/
|
||||||
public function __construct(object $server)
|
public function __construct(object $server)
|
||||||
{
|
{
|
||||||
$this->server = $server;
|
|
||||||
$this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x';
|
$this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x';
|
||||||
$this->username = (string) ($server->fields['api_username'] ?? '');
|
$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->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1;
|
||||||
$this->server_version = (string) ($server->fields['server_version'] ?? '');
|
$this->server_version = (string) ($server->fields['server_version'] ?? '');
|
||||||
$this->is_version_2_4_or_higher = $this->detectVersion2_4OrHigher();
|
$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';
|
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.
|
* Test API connection.
|
||||||
*
|
*
|
||||||
@@ -361,25 +343,6 @@ class UrbackupApiClient
|
|||||||
unset($this->cached_settings[$client_id]);
|
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
|
public function updateClientSettings(string $client_name, string $key, string $value): bool
|
||||||
{
|
{
|
||||||
$client_id = $this->getClientIdByName($client_name);
|
$client_id = $this->getClientIdByName($client_name);
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user