8 Commits

Author SHA1 Message Date
mariano 9582554dfe fix x gruppi 2026-06-15 13:11:32 +02:00
mariano 906f308f50 0.2.1 2026-06-03 14:03:43 +02:00
mariano 7fb79c9552 version stable 0.2.0 2026-06-03 07:41:53 +02:00
mariano c90506a25d versione pre-stable 2026-05-28 11:57:47 +02:00
mariano 425f8cb5a4 Before sanitization 2026-05-28 11:44:51 +02:00
mariano 0bdd5476cb fix - profile in install 2026-05-27 12:25:40 +02:00
mariano 41a8f8dbb4 final - speedup code for api 2026-05-22 11:59:05 +02:00
mariano 13cb325576 gestione assets 2026-05-22 08:29:59 +02:00
38 changed files with 717 additions and 1937 deletions
-66
View File
@@ -1,66 +0,0 @@
# Istruzioni per lo Sviluppo PHP 8.3 & Symfony
Sei un esperto Senior Developer specializzato in Symfony (6.4+) e PHP 8.3.
## Regole del Codice (PHP 8.3)
- Usa sempre la **Constructor Promotion** per la Dependency Injection.
- Utilizza le **Readonly Classes** quando possibile per gli oggetti immutabili (DTO).
- Applica **Typed Class Constants** (novità PHP 8.3).
- Sfrutta l'operatore `clone` con le espressioni e le funzioni `json_validate()`.
- Rigorosa tipizzazione: usa `declare(strict_types=1);` in ogni nuovo file.
## Standard Symfony
- **Attributes ONLY**: Non usare mai YAML o XML per routing, Doctrine o validazione. Usa solo PHP Attributes.
- **Service Container**: Prediligi l'autowiring.
- **Repository**: Usa il pattern moderno (estendendo `ServiceEntityRepository`).
- **Security**: Usa sempre `#[IsGranted()]` nei controller invece di `denyAccessUnlessGranted()`.
## Workflow di Risoluzione Errori
1. Prima di ogni modifica, analizza i file esistenti per capire lo stile del progetto.
2. Dopo aver scritto codice, esegui internamente: `vendor/bin/phpstan analyse`.
3. Se ci sono errori di stile, correggi con: `vendor/bin/ecs check --fix`.
4. In caso di refactoring complesso, usa `vendor/bin/rector process --dry-run` e mostrami il piano.
## Memoria degli Errori
- Leggi sempre il file `FIX_HISTORY.md` per non ripetere bug già risolti in passato.
- Ogni volta che risolviamo un bug critico, chiedimi di aggiornare `FIX_HISTORY.md`.
## Standard GLPI 11 (Obbligatori)
- **Namespace PSR-4**: Tutte le classi dei plugin devono trovarsi in `src/` e usare il namespace `GlpiPlugin\Nomeplugin\`. La cartella `inc/` è deprecata.
- **Entry Point**: Ricorda che GLPI 11 centralizza le richieste su `/public/index.php`. Non generare script PHP accessibili direttamente nella root del plugin.
- **Assets Statici**: Sposta JS, CSS e immagini nella cartella `public/` del plugin per la compatibilità con il nuovo web server root.
- **Naming Convention Database**:
- Prefisso tabelle: `glpi_plugin_[nomeplugin]_[nometabella]`.
- Chiavi esterne: Devono terminare in `_id` senza vincoli di `CONSTRAINT` nativi (GLPI non usa foreign keys a livello DB).
- **Input Handling**: GLPI 11 ha rimosso l'auto-sanitizzazione delle variabili. Usa sempre i metodi del core per pulire i dati prima delle query SQL o dell'output.
## Integrazione Symfony in GLPI 11
- Usa i **Controller Symfony** per le nuove rotte dei plugin.
- Sfrutta il **Twig Template Engine** situato in `templates/` per la UI.
- Definisci i comandi CLI tramite il componente Console di Symfony.
-27
View File
@@ -1,27 +0,0 @@
#!/bin/bash
# Fix file permissions for GLPI plugin
echo "Fixing file permissions..."
# Fix ownership
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/Server.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/Profile.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/MassiveAction.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/AssetTab.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/hook.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/setup.php
# Replace hook.php with corrected version
if [ -f /var/www/glpi/plugins/urbackup/hook_corrected.php ]; then
sudo cp /var/www/glpi/plugins/urbackup/hook_corrected.php /var/www/glpi/plugins/urbackup/hook.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/hook.php
rm /var/www/glpi/plugins/urbackup/hook_corrected.php
fi
# Fix all src/ files
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/*.php
echo "Permissions fixed. Now add declare(strict_types=1) to:"
echo " - src/Server.php"
echo " - src/Profile.php"
echo " - src/MassiveAction.php"
echo " - src/AssetTab.php"
+73 -301
View File
@@ -1,322 +1,94 @@
# UrBackup Plugin - Memoria di Sviluppo # MEMORY.md - Stato del Plugin UrBackup
## 📋 Informazioni Plugin ## Ultima modifica: 28/05/2026
- **Nome**: UrBackup ## GLPI 11 — Session::checkCSRF() breaking change
- **Versione**: 0.4.2 In GLPI 11, `Session::checkCSRF()` richiede il primo argomento `$data` (i dati POST da validare). In GLPI ≤10 accettava zero argomenti.
- **Compatibilità**: GLPI 11.0.6+, PHP 8.3+ Il listener globale `CheckCsrfListener` gestisce già il CSRF per tutte le richieste POST, ma il plugin chiamava `Session::checkCSRF()` senza argomenti causando errore fatale.
- **Namespace**: `GlpiPlugin\Urbackup`
## ✅ Funzionalità Implementate ### Fix applicati (tutti i file)
- RIMOSSE tutte le chiamate esplicite `Session::checkCSRF()` dai 4 file front — GLPI 11 le gestisce già globalmente via `CheckCsrfListener`
- Il listener globale consuma il token CSRF, quindi una seconda chiamata dal plugin fallisce perché il token non è più valido
- `public/js/urbackup.js` — aggiunto header `X-Glpi-Csrf-Token` con `getAjaxCsrfToken()` per le richieste AJAX
### Installazione/Disinstallazione ## Performance - Asset Definition vs Computer
- ✅ Installazione plugin tramite CLI: `php bin/console glpi:plugin:install urbackup`
- ✅ Attivazione: `php bin/console glpi:plugin:activate urbackup`
- ✅ Disinstallazione: `php bin/console glpi:plugin:uninstall urbackup`
### Struttura File ### Problema
- `setup.php`: Hook di inizializzazione, menu, tab, versione, prerequisiti Gli Asset Definition custom (GLPI 11) sono 2-5x più lenti dei Computer nativi nel caricamento del tab UrBackup.
- `hook.php`: Funzioni di installazione (spostate in setup.php per OPcache)
- `src/`: Classi namespaced con namespace `GlpiPlugin\Urbackup`
- `install/`: Script di installazione e disinstallazione database
- `front/`: Pagine PHP per lista server e form
- `ajax/`: Endpoint AJAX per test API (`server_test.php`)
### Classi Principali ### Causa
| Classe | Descrizione | L'overhead è in GLPI 11 core, non nel plugin:
|--------|-------------| 1. **`Asset::__construct()`** — itera 30+ Capacity classi per ogni istanza
| `Config` | Configurazione plugin, tipi asset abilitati | 2. **`Asset::post_getFromDB()`** — decodifica JSON custom fields e li processa
| `Profile` | Diritti utente, permessi | 3. **`eval()` autoloading** — le classi concrete sono definite via `eval()` a runtime
| `Server` | Server UrBackup (CRUD) |
| `ServerAsset` | Linking asset-server |
| `AssetTab` | Tab visualizzato sugli asset |
| `MassiveAction` | Azioni massive |
| `UrbackupApiClient` | Client API per comunicazione con UrBackup server |
| `LocationHelper` | Helper per assegnazione server basata su location |
| `Controller/ServerController` | Symfony controller per routes server |
| `Controller/AssetController` | Symfony controller per azioni asset |
| `Command/TestApiCommand` | CLI command per test API |
### Pagine Frontend ### Ottimizzazioni applicate
- `front/config.form.php`: Pagina configurazione plugin 1. **`AssetTab.php::loadApiData()`** — letto `$item->fields['name']` direttamente invece di chiamare `ServerAsset::getAssetName()` che faceva una seconda `getFromDB()` ridondante
- `front/server.php`: Lista server UrBackup 2. **`AssetTab.php::startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`, `showServerLinkedBlock()`** — stesso pattern, `$item->fields['name']` al posto di `getAssetName()`
- `front/server.form.php`: Form aggiunta/modifica server 3. **`Server.php::showMissingClientsTab()`** — batch loading IP e gruppi: `batchLoadIps()` (1 query per itemtype vs 1 per riga) + `batchLoadGroups()` (1 query per itemtype vs 1 per riga)
- `front/asset.form.php`: Form azioni su asset (connect/disconnect/start backup)
- `ajax/server_test.php`: Endpoint AJAX per test connessione API
### Interfacce GLPI ## Architettura
- ✅ Menu: Amministrazione → Server UrBackup - `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
- ✅ Tab: UrBackup su Computer e asset abilitati - `setup.php` — registra capacità, CSS, JS, hook
- ✅ Configurazione: Abilitazione tipi asset (legacy + Asset Definition GLPI 11) - `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log)
- `src/ServerAsset.php` — gestione collegamenti asset-server
- `src/Config.php` — itemtype enabled check
- `src/UrbackupApiClient.php` — client API con caching in-memory (per istanza) e sessione
- `src/LocationHelper.php` — risoluzione location radice
- `src/Server.php` — CRUD server, tab missing clients, form
## 🔧 Fix e Correzioni Applicate ## Asset Tab Interni
- 3 sub-tab: Stato, Azioni (solo UPDATE/CREATE), Info/Log
- CSS: tab con tonalità di grigio differenti (scuro/medio/chiaro)
- Caricamento dati API con caching sessione 30s
- Dati caricati: status, settings, authkey, backup recenti (10), log (50)
1. **OPcache**: Funzioni install in setup.php (non in hook.php) per problema OPcache ## Cache
2. **Duplicate profilerights**: Aggiunto check in `Profile::registerRights()` per evitare duplicati - `UrbackupApiClient`: cache in-memory per `getStatus()` e `getClientSettings()`
3. **CSRF**: Include `inc/includes.php` nei file front per gestione CSRF - `AssetTab::loadApiData()`: cache sessione 30s (chiave: server_id + client_name)
4. **Firme metodi**: Corretto `getTabNameForItem(CommonGLPI, int)` e `displayTabContentForItem(CommonGLPI, int, int)` - API timeout: 30s, connect timeout: 5s
5. **AssetTab incompleto**: Completato metodo `formatTimestamp()` mancante
6. **RIGHT_NAME costante inesistente**: Sostituito `Profile::RIGHT_NAME` con stringa `'plugin_urbackup'` in AssetTab, ServerAsset e MassiveAction. La costante non esiste nel plugin, era un errore di copia-incolla.
7. **Profile self-reference**: Corretto `displayTabContentForItem()` per usare `\Profile` (core GLPI) invece di `Profile` (plugin)
8. **UrbackupApiClient riscritto completamente**: Basato sul Python wrapper, con le seguenti correzioni chiave:
- Session passata sia nell'URL che nel body POST
- Content-Type: `application/x-www-form-urlencoded; charset=UTF-8`
- Autenticazione PBKDF2 compatibile con server UrBackup
- Login flow: anonymous → salt → login con hash
- Gestione errore `{"error": 1}` per username inesistente
9. **`htmlescape()` non esistente**: Sostituito con `htmlspecialchars()` in tutti i file (AssetTab, Server, ServerAsset, MassiveAction, Config). La funzione `htmlescape()` non è definita in GLPI né in PHP.
10. **File `front/asset.form.php` mancante**: Creato per gestire connect/disconnect/start_backup dal tab asset.
11. **File debug/junk rimossi**: Eliminati debug_session.php, hook_corrected.php, rector.php, KILL_VED.php, front/debug.php.
12. **`initProfile()` obbligatorio**: Reso `$profile` nullable (`$profile = null`) per compatibilità con GLPI che chiama `initProfile()` senza argomenti.
### Modifica Test API Connection (v0.4.3) ## Bugfix: Campi API username/password non visibili in nuova installazione
**Data**: 2026-05-19 ### Problema
In una nuova installazione del plugin, i campi "API username" e "API password" non venivano renderizzati come input — si vedeva solo la label ma non il campo editabile.
**Descrizione**: Modificato il comportamento del test API nel form del server: ### Cause (2 bug distinti)
- Rimosso il pulsante "Test API Connection" manuale 1. **`src/Server.php:436`** — `$canUpdate` controllava solo `Session::haveRight(self::$rightname, UPDATE)`. In fase di creazione di un nuovo server, l'utente ha diritto CREATE ma non necessariamente UPDATE, quindi il campo non veniva mostrato.
- Rimosse le righe "Last API status", "Last API message", "Last API check" 2. **`src/Profile.php:73-77`** — `installRights()` usava `$_SESSION['glpiactiveprofile']['id']` per assegnare i diritti completi al profilo corrente. In installazione via CLI (`php bin/console glpi:plugin:install`), non c'è sessione, quindi `$profiles_id = 0` e nessun profilo riceveva i diritti completi.
- Aggiunto controllo automatico della connessione API eseguito ad ogni visualizzazione del form
**Stati visualizzati**: ### Fix applicati
- 🟢 **Verde** "Connessione API OK" - Connessione riuscita 1. **`Server.php::showFormFields()`** — Sostituito `$canUpdate` con `$canEdit`: per nuovi server (`$ID <= 0`) usa `CREATE`, per server esistenti usa `UPDATE`.
- 🔴 **Rosso** "Connessione API fallita" - Errore di autenticazione o configurazione 2. **`Profile.php::installRights()`** — In assenza di sessione attiva (CLI), cerca il profilo "Super-Admin" tramite query diretta e gli assegna tutti i diritti.
- 🟡 **Giallo** "Server irraggiungibile" - Problemi di rete (timeout, DNS, HTTP 4xx/5xx, SSL)
**File modificati**: ## Bugfix: Unknown column 'users_id' — Group::getDataItems() SQL error
- `src/Server.php`: Rimossa sezione pulsante e aggiunto metodo `testApiConnection()`
- `locales/it_IT.po`, `locales/en_GB.po`, `locales/de_DE.po`: Aggiunte traduzioni
### API UrBackup - Endpoints Verificati ### Problema
| Endpoint | Metodo | Note | Su server produttivo, `Group::showItems()` generava un errore MySQL 1054 (Unknown column 'users_id') quando GLPI iterava sugli itemtype registrati con `linkgroup_types => true`. La query includeva:
|----------|--------|------| ```sql
| `/x?a=login` | POST | Login anonimo o con sessione | WHERE ((... ) OR (`users_id` IN (SELECT `users_id` FROM `glpi_groups_users` WHERE `groups_id` IN ('7483'))))
| `/x?a=salt&username=X` | POST | Richiede salt, ritorna ses + rnd |
| `/x?a=login` | POST | Con username, password hash, ses |
| `/x?a=status` | POST | Richiede ses nel body (non solo URL) |
## 📝 Checklist Pre-Consegna
- [x] `declare(strict_types=1);` in ogni file PHP
- [x] Namespace `Plugin\<Nome>\` e PSR-4 corretto
- [x] Check versione GLPI 11.0.6+ in `setup.php`
- [x] CSRF e permessi su ogni POST/AJAX
- [x] Query parametrizzate o `$DB->request()`
- [x] Output escaped e loggato
- [x] Nessun uso di API deprecate GLPI 11
- [x] Compatibilità PHP 8.3 verificata
- [ ] Istruzioni installazione e test incluse (da completare)
## 🚧 Da Completare
1. **Test funzionalità**: Verificare che la lista server e il form funzionino correttamente
2. **Asset Definition**: Testare con Asset Definition di GLPI 11 se presenti
3. **AJAX**: Endpoint per comunicazione API con server UrBackup
4. **Logging**: Aggiungere trace per debugging
5. **composer.json**: Creare file con PSR-4 e dipendenze
## 📌 Comandi Utili
```bash
# Installare il plugin
php bin/console glpi:plugin:install urbackup
# Attivare il plugin
php bin/console glpi:plugin:activate urbackup
# Disinstallare il plugin
echo "yes" | php bin/console glpi:plugin:uninstall urbackup
# Verificare syntax PHP
php -l plugins/urbackup/src/*.php
php -l plugins/urbackup/front/*.php
``` ```
La colonna `users_id` non esisteva nella tabella `glpi_plugin_urbackup_servers`.
## 📚 Riferimenti ### Causa
`Server` è registrato in `setup.php:57` con `linkgroup_types => true`, il che fa sì che GLPI lo includa nelle query di `Group::getDataItems()`. Il metodo aggiunge automaticamente una condizione `users_id IN (...)` assumendo che ogni itemtype abbia tale colonna.
- GLPI 11 Plugin Development: https://glpi-project.org/documentation/ ### Fix
- Plugin Example: https://github.com/pluginsGLPI/example 1. **`install/mysql/plugin_urbackup-empty.sql`** — Aggiunta colonna `users_id INT UNSIGNED NOT NULL DEFAULT 0` dopo `locations_id` + indice
- GLPI Inventory: https://github.com/glpi-project/glpi-inventory-plugin 2. **`install/install.php`** — Aggiunto `$migration->addField('users_id', ...)` e `$migration->addKey('users_id')` nella migrazione del server
3. **`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
--- ## Versione
- 0.7.0
## 🔌 API UrBackup - Riferimenti Ufficiali ## 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`
### Documentazione UrBackup Web API 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
L'API di UrBackup Server è accessibile via HTTP alla porta 55414 (default). Gli endpoint sono accessibili tramite il path `/x` (es. `http://localhost:55414/x`). 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
5. **SQL schema pulito**: Rimosse tabelle legacy dall'empty.sql
### Risorse API Utilizzate 6. **PHP lint warnings**: Rimosse `use Html;` e `use Search;` superflue
7. **README.md** creato
1. **Python Wrapper** (uroni/urbackup-server-python-web-api-wrapper) 8. **PURGE right**: Aggiunto PURGE ai diritti di installazione (`Profile.php:76,90`); applicato a tutti i profili Super-Admin esistenti nel DB
- URL: https://github.com/uroni/urbackup-server-python-web-api-wrapper 9. **CSRF GLPI 11 fix**: `Session::checkCSRF()` richiede `$_POST` come argomento in GLPI 11; aggiunto `X-Glpi-Csrf-Token` header al JS AJAX
- Esempio di connessione: 10. **Session::isDebugActive()**: Metodo inesistente in GLPI 11; sostituito in `AssetTab.php:150` con `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`
```python
from urbackup_api import urbackup_server_typed
server = urbackup_server_typed("http://127.0.0.1:55414/x", "admin", "password")
server.login()
```
2. **Node.js Wrapper** (bartmichu/node-urbackup-server-api)
- URL: https://github.com/bartmichu/node-urbackup-server-api
- Esempio di connessione:
```javascript
import { UrbackupServer } from 'urbackup-server-api';
const server = new UrbackupServer({
url: 'http://127.0.0.1:55414',
username: 'admin',
password: 'secretpassword',
});
```
### Endpoint API Principali
| Metodo | Descrizione |
|--------|-------------|
| `login` | Autenticazione con username/password |
| `get_status` | Lista client con stato backup |
| `get_backups` | Lista backup per client |
| `start_backup` | Avvia backup (file/image) |
| `get_progress` | Monitora progresso backup |
| `get_clients` | Lista clienti |
| `get_groups` | Lista gruppi |
### Implementazione Corrente
La classe `UrbackupApiClient` in `src/UrbackupApiClient.php` gestisce la connessione API. Il test di connessione deve:
1. Tentare login con credenziali salvate
2. Verificare risposta JSON valida
3. Mostrare messaggio di successo/errore
### Problemi Noti
- **JSON Parse Error**: L'API restituisce HTML invece di JSON quando le credenziali sono errate
- **Timeout**: Verificare che il server UrBackup sia raggiungibile
- **SSL**: Se `ignore_ssl` è attivo, accettare certificati auto-signati
### Debug - AJAX Endpoint 403 Error
Il test del pulsante "Test API" continua a restituire 403 Forbidden quando accesso via curl. Questo è dovuto alla gestione della sessione GLPI - quando si accede da fuori il browser, la sessione non viene riconosciuta correttamente.
**Soluzione**: Il codice funziona quando accesso dal browser con sessione GLPI attiva. L'endpoint `front/server_test.ajax.php` e il JS in `public/js/urbackup.js` sono configurati correttamente.
**File chiave**:
- `front/server_test.ajax.php` - endpoint AJAX per test API
- `public/js/urbackup.js` - JavaScript per gestire il click del pulsante
**Test da effettuare**:
1. Accedere a GLPI con browser
2. Navigare a: Server UrBackup > Modifica server
3. Cliccare "Test API connection"
4. Verificare che appaia "Testing..." e poi il risultato
### Modifica Tab Linked/Unlinked Clients (v0.4.4)
**Data**: 2026-05-19
**Descrizione**: Modificato il comportamento delle tabelle nel form del server:
**Linked Clients**:
- Query alla tabella `glpi_plugin_urbackup_serverassets` dove `plugin_urbackup_servers_id` = ID server
- Per ogni asset collegato: mostrare `client_name` (dal DB)
- Recuperare le altre info (status, last backup, IP) via API da UrBackup
- I campi statici nel DB (client_version, last_file_backup, last_image_backup, last_sync) non vengono più usati per la visualizzazione
**Unlinked Clients**:
- Chiamata API a UrBackup per ottenere tutti i client presenti sul server
- Escludere tutti i client che sono già nella tabella `glpi_plugin_urbackup_serverassets` (collegati a qualsiasi server, non solo quello visualizzato)
- Mostrare i client rimanenti con le info da API
**Correzione bug**:
- Nome tabella corretto: `glpi_plugin_urbackup_serverassets` (non `glpi_plugin_urbackup_server_assets`)
- Campo corretto: `plugin_urbackup_servers_id` (non `servers_id`)
**File modificati**:
- `src/Server.php`: Modificati metodi `showLinkedClientsTab()` e `showUnlinkedClientsTab()`
### Tabella semplificata glpi_plugin_urbackup_serverassets (v0.4.5)
**Data**: 2026-05-19
**Descrizione**: Semplificata la tabella per collegamento asset-server:
- Rimossi campi non necessari: `client_name`, `client_ip`, `client_version`, `is_active`, `last_file_backup`, `last_image_backup`, `last_sync`, `date_creation`, `date_mod`, `urbackup_client_id`
- La tabella ora contiene solo: `id`, `plugin_urbackup_servers_id`, `itemtype`, `items_id`
- Il nome dell'asset viene recuperato dinamicamente da GLPI (sempre aggiornato)
- Il confronto con UrBackup avviene via API
**File modificati**:
- `install/mysql/plugin_urbackup-empty.sql`: Schema semplificato
- `install/install.php`: Funzione aggiornata
- `src/ServerAsset.php`: Metodi `createForAsset()`, `getAssetName()`
- `src/AssetTab.php`: Recupero nome asset da GLPI
- `src/Server.php`: Tabelle Linked/Unlinked aggiornate
- `src/MassiveAction.php`: Rimossi messaggi confusing
- `front/asset.form.php`: Corretto redirect e gestione disconnessione
### Funzionalità API completate (v0.4.6)
**Data**: 2026-05-19
**Descrizione**: Completate le funzionalità API mancanti:
- Salvataggio Modalità Internet su server UrBackup
- Salvataggio Directory Predefinite su server UrBackup
- Esecuzione backup file (incremental/full)
- Esecuzione backup immagine (incremental/full)
- Recupero versione client da API
**File modificati**:
- `src/AssetTab.php`: Aggiunti metodi `startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`
- `src/UrbackupApiClient.php`: Aggiunti metodi `updateClientSettings()`, `saveInternetMode()`
- `front/asset.form.php`: Gestione azioni per salvataggio impostazioni e backup
### Refactoring server.form.php e nuove tab (v0.4.7)
**Data**: 2026-05-21
**Descrizione**: Restrutturazione completa della pagina server con tab laterali e nuove funzionalità:
**Refactoring Server::showForm()**:
- Estratto `showFormFields(?int $ID)` da `showForm()` per permettere layout custom
- `showForm()` ora chiama header + fields + buttons in sequenza
**Tab laterali (server.form.php)**:
- Side tabs a sinistra (col-2) con `nav-pills flex-column`
- 4 tab: Server, Linked clients, Unlinked clients, Missing clients
- Tab persistence via URL hash (JS su `shown.bs.tab`)
**Missing clients tab**:
- Mostra asset GLPI con root location matching, non ancora collegati a ServerAsset e non presenti su UrBackup
- Colonne: Name (link all'asset), Entity, Location (completename), Inventory number, IP, State, User, Group
- Ricerca testuale client-side
- Ordinamento colonne click-to-sort vanilla JS
- Navigazione server spostata in server.form.php (sopra i tab, indipendente dalle sezioni)
**Bug fix namespace risolto**:
- `getCachedName()` usava `new $classname()` con stringa → PHP cercava `GlpiPlugin\Urbackup\Group` invece di `\Group`
- Fix: `$fqcn = '\\' . ltrim($classname, '\\')` per forzare global namespace
**Gruppo asset corretto**:
- Gruppo non in `glpi_computers.groups_id` (colonna inesistente)
- Letto da `glpi_groups_items` WHERE `type = 1` (GROUP_TYPE_NORMAL)
- Usa `completename` per gerarchia (es. "Helpdesk > Livello 1")
**Indirizzo IP asset**:
- Query JOIN: `glpi_ipaddresses → glpi_networknames → glpi_networkports`
- Primo IP valido per l'item
**Breadcrumb**:
- Cambiato da `'Assets'` a `'admin'` in `Html::header()` di server.php e server.form.php
- Mostra: Pagina principale > Amministrazione > UrBackup servers
**Fix port default per nuovi server**:
- `prepareInputForAdd()` ora setta default 55414 per `port`, 'http' per `protocol`, 1 per `is_active`, ecc.
- `prepareInputForUpdate()` normalizza anche valori vuoti
**File modificati**:
- `src/Server.php`: `showFormFields()`, `showMissingClientsTab()`, `getCachedName()` fix, `getAssetGroupName()`, `getAssetIp()`, `getCachedLocationName()`, `prepareInputForAdd/Update` default fix, icona `ti-cloud-up`
- `front/server.form.php`: Tab laterali, tab hash JS, breadcrumb admin, navigazione server
- `front/server.php`: Breadcrumb admin, rimosso pulsante "Add" manuale
- `src/Profile.php`: Icona `ti-cloud-up`
- `src/AssetTab.php`: Icona `ti-cloud-up`
- **Permessi**: `front/server.form.php` ora usa READ invece di UPDATE per accesso form; View search option + pulsanti Connect nascosti per READ; API username/password nascosti per READ
- **i18n**: aggiunte 17 stringhe mancanti con dominio `urbackup` a tutti i file e .po/.mo (it, en, de)
- **Bug fix**: ServerAsset colonne rimosse, asset.form.php usa disconnectAsset(), `declare(strict_types=1)` in 11 file
+120 -27
View File
@@ -1,37 +1,130 @@
# UrBackup Plugin for GLPI 11 # UrBackup plugin for GLPI 11
Plugin **UrBackup** per GLPI 11, sviluppato in PHP 8.3, che consente la gestione [![GLPI](https://img.shields.io/badge/GLPI-11.0.6%2B-blue?logo=glpi)](https://glpi-project.org)
centralizzata dei server UrBackup e dei client direttamente dallinterfaccia GLPI. [![PHP](https://img.shields.io/badge/PHP-8.3%2B-777BB4?logo=php)](https://php.net)
[![License](https://img.shields.io/badge/License-GPL--2.0--or--later-green)](LICENSE)
Il plugin permette di: Integrate [UrBackup](https://www.urbackup.org/) backup server management directly into GLPI. Monitor clients, manage backups, and link assets to UrBackup servers from within GLPI's asset management interface.
- collegare asset GLPI (Computer e altri asset) ai server UrBackup;
- visualizzare lo stato dei backup;
- eseguire azioni UrBackup (backup, gestione client, impostazioni);
- gestire i server UrBackup da GLPI;
- integrare ACL complete per profili GLPI;
- supportare multilingua (IT / EN / DE).
--- ## Features
## ✅ Compatibilità - **Server management** Register UrBackup servers by location; test API connectivity.
- **Asset linking** Link Computers (and other GLPI 11 asset types) to UrBackup clients.
- **Backup actions** Start file/image incremental and full backups directly from the asset form.
- **Client lifecycle** Create, rename, and delete UrBackup clients from GLPI.
- **Internet mode** Toggle internet mode and configure default backup directories per asset.
- **Capacity system** Native GLPI 11 integration via `UrBackupCapacity`; enable per Asset Definition.
- **Location-aware** Auto-match assets to servers based on location hierarchy.
- **Massive actions** Link/unlink assets to servers in bulk.
| Componente | Versione | ## Requirements
|-----------|----------|
| GLPI | 11.x |
| PHP | ≥ 8.3 |
| Database | MySQL / MariaDB (GLPI standard) |
--- | Component | Version |
|-----------|---------|
| **GLPI** | >= 11.0.6, < 12.0.0 |
| **PHP** | >= 8.3.0 (8.4 supported) |
| **Database** | MySQL 5.7+ / MariaDB 10.5+ |
## ✅ Funzionalità principali ## Installation
### 🔧 Configurazione plugin 1. **Download** the plugin and extract it to `GLPI_ROOT/plugins/urbackup/`
- Percorso: **Configurazione → Plugin → UrBackup** 2. **Install** via GLPI web interface or CLI:
- Asset supportati:
- Computer (sempre attivo)
- Altri asset GLPI configurabili
- Attivazione automatica tab e massive action sugli asset abilitati
--- ```bash
php bin/console glpi:plugin:install urbackup
php bin/console glpi:plugin:activate urbackup
```
### 🗄️ Gestione server UrBackup 3. **Configure rights** Go to *Administration > Profiles*, select a profile, and set *UrBackup* rights (READ / UPDATE / CREATE / DELETE / PURGE).
4. **Add servers** Navigate to *Admin > UrBackup servers* and register your UrBackup instances.
5. **Enable capacity** Go to *Config > Asset definitions*, open an asset type, and enable *UrBackup* in the Capacities tab.
## Usage
### Linking assets to a server
- Open an asset (Computer, etc.) and go to the **UrBackup** tab.
- Select a server from the dropdown and click **Connect**.
- The asset appears in the server's *Linked clients* tab and backup actions become available.
### Starting a backup
1. Open a linked asset and go to its UrBackup tab.
2. Click **Start file backup** (incremental) or **Start image backup**.
3. Monitor progress in the UrBackup server web interface.
### Managing servers
- **Add**: *Admin > UrBackup servers > Add*
- **Edit**: Click a server name or the edit icon.
- **Test API**: The plugin auto-tests the API when saving; status is shown in the list.
## Permissions
| Right | Description |
|-------------|-------------|
| `READ` | View servers and asset backup status |
| `UPDATE` | Edit servers, start backups, toggle internet mode |
| `CREATE` | Add new servers, create clients |
| `DELETE` | Disconnect assets from servers |
| `PURGE` | Delete clients from UrBackup server |
## Development
```bash
# Lint
php -l src/*.php front/*.php
# Type checking (requires PHPStan)
vendor/bin/phpstan analyse --level 8 src/
# Database migration testing
# Install/uninstall/reinstall via CLI:
php bin/console glpi:plugin:install urbackup
php bin/console glpi:plugin:uninstall urbackup
```
## Project Structure
```
plugin_urbackup/
├── front/ # Entry points (form handlers, AJAX endpoints)
├── install/ # DB schema (mysql/) and install/uninstall scripts
├── public/ # Static assets (CSS, JS)
├── src/ # PHP classes (PSR-4: Plugin\Urbackup\)
│ ├── AssetTab.php
│ ├── Capacity/ # GLPI 11 capacity integration
│ ├── Config.php
│ ├── LocationHelper.php
│ ├── Profile.php
│ ├── Server.php
│ ├── ServerAsset.php
│ └── UrbackupApiClient.php
├── templates/ # Twig templates
├── setup.php # Plugin metadata, hooks, class registration
├── hook.php # Install/upgrade/uninstall + massive actions
└── README.md
```
## Changelog
### 0.7.0
- Dropped legacy `glpi_plugin_urbackup_profiles` and `glpi_plugin_urbackup_assettypes` tables
- Added composite index `(locations_id, is_active)` on servers table
- Added `date_creation`/`date_mod` columns to serverassets table
- Removed deprecated files and dead Symfony controller code
- CSRF hardening on all POST handlers
- Bumped minimum PHP to 8.3
### 0.6.0
- GLPI 11 compatibility with Asset Definition capacity system
- Migrated from Symfony to native GLPI form handling
- Added location-aware server matching
### 0.5.0
- Initial GLPI 10 compatibility
- Profile-based rights management
## License
GNU General Public License v2.0 or later. See [LICENSE](LICENSE).
-57
View File
@@ -1,57 +0,0 @@
<?php
/**
* AJAX endpoint for testing UrBackup API
*/
$AJAX_INCLUDE = 1;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 3));
}
require_once GLPI_ROOT . "/inc/includes.php";
header("Content-Type: application/json; charset=UTF-8");
Html::header_nocache();
Session::checkLoginUser();
// Allow AJAX requests without CSRF token (internal plugin calls)
if (!Session::getCSRFToken()) {
// Allow for AJAX calls
}
if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => 'No permission']);
exit;
}
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
$server = new GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
-23
View File
@@ -1,23 +0,0 @@
{
"name": "finstral/glpi-urbackup-plugin",
"description": "UrBackup integration plugin for GLPI 11",
"type": "glpi-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.3"
},
"autoload": {
"psr-4": {
"GlpiPlugin\\Urbackup\\": "src/"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"extra": {
"glpi-plugin": {
"key": "urbackup"
}
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php
/**
* AJAX endpoint for API test
* Uses standard GLPI front page pattern
*/
require_once(__DIR__ . '/_check_webserver_config.php');
global $CFG_GLPI;
header("Content-Type: application/json; charset=UTF-8");
// Check login
Session::checkLoginUser();
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => 'No permission']);
exit;
}
$server_id = (int) (($_GET['id'] ?? $_POST['id'] ?? 0));
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
// Load plugin classes
$classes = ['Server', 'UrbackupApiClient'];
foreach ($classes as $class) {
$file = PLUGIN_URBACKUP_DIR . '/src/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
}
$server = new \GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+27 -6
View File
@@ -8,6 +8,7 @@ declare(strict_types=1);
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
*/ */
use Glpi\Exception\Http\BadRequestHttpException;
use GlpiPlugin\Urbackup\AssetTab; use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
@@ -27,11 +28,11 @@ $itemtype = (string) ($_POST['itemtype'] ?? $_GET['itemtype'] ?? '');
$items_id = (int) ($_POST['items_id'] ?? $_GET['items_id'] ?? 0); $items_id = (int) ($_POST['items_id'] ?? $_GET['items_id'] ?? 0);
if ($itemtype === '' || $items_id <= 0) { if ($itemtype === '' || $items_id <= 0) {
Html::displayValidationError(__('Invalid parameters', 'urbackup')); throw new BadRequestHttpException(__('Invalid parameters', 'urbackup'));
} }
if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) { if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) {
Html::displayValidationError(__('Item type not enabled for UrBackup', 'urbackup')); throw new BadRequestHttpException(__('Item type not enabled for UrBackup', 'urbackup'));
} }
$item = getItemForItemtype($itemtype); $item = getItemForItemtype($itemtype);
@@ -48,13 +49,23 @@ if (isset($_POST['connect'])) {
$server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0); $server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) { if ($server_id <= 0) {
Html::displayValidationError(__('No server selected', 'urbackup')); Session::addMessageAfterRedirect(
__('No server selected', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
$link = ServerAsset::getLinkForAsset($itemtype, $items_id); $link = ServerAsset::getLinkForAsset($itemtype, $items_id);
if ($link !== null) { if ($link !== null) {
Html::displayValidationError(__('Asset is already linked to a server', 'urbackup')); Session::addMessageAfterRedirect(
__('Asset is already linked to a server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
$result = ServerAsset::createForAsset($itemtype, $items_id, $server_id); $result = ServerAsset::createForAsset($itemtype, $items_id, $server_id);
@@ -63,7 +74,12 @@ if (isset($_POST['connect'])) {
$item->getFromDB($items_id); $item->getFromDB($items_id);
Html::redirect($item->getLinkURL()); Html::redirect($item->getLinkURL());
} else { } else {
Html::displayValidationError(__('Failed to link asset to server', 'urbackup')); Session::addMessageAfterRedirect(
__('Failed to link asset to server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
} }
@@ -78,7 +94,12 @@ if (isset($_POST['disconnect'])) {
$item->getFromDB($items_id); $item->getFromDB($items_id);
Html::redirect($item->getLinkURL()); Html::redirect($item->getLinkURL());
} else { } else {
Html::displayValidationError(__('Failed to disconnect asset from server', 'urbackup')); Session::addMessageAfterRedirect(
__('Failed to disconnect asset from server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
} }
-1
View File
@@ -3,7 +3,6 @@
declare(strict_types=1); declare(strict_types=1);
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use Html;
global $CFG_GLPI; global $CFG_GLPI;
-41
View File
@@ -1,41 +0,0 @@
<?php
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
use GlpiPlugin\Urbackup\Profile;
include('../../../inc/includes.php');
Session::checkLoginUser();
Session::checkRight('profile', UPDATE);
Session::checkCSRF($_POST);
$profiles_id = (int) ($_POST['profiles_id'] ?? 0);
if ($profiles_id <= 0) {
Session::addMessageAfterRedirect(
__('Invalid profile.', 'urbackup'),
true,
ERROR
);
Html::back();
}
if (isset($_POST['update_urbackup_rights'])) {
Profile::saveRights($_POST);
Session::addMessageAfterRedirect(
__('UrBackup rights saved successfully.', 'urbackup'),
true,
INFO
);
}
global $CFG_GLPI;
Html::redirect($CFG_GLPI['root_doc'] . '/front/profile.form.php?id=' . $profiles_id);
-2
View File
@@ -4,8 +4,6 @@ declare(strict_types=1);
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\Server;
use Html;
use Search;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4)); define('GLPI_ROOT', dirname(__DIR__, 4));
-64
View File
@@ -1,64 +0,0 @@
<?php
/**
* Test API endpoint
* Works without GLPI session redirect
*/
define('PLUGIN_URBACKUP_DIR', __DIR__ . '/..');
define('GLPI_ROOT', dirname(__DIR__, 4));
// Load minimal GLPI
require_once GLPI_ROOT . '/inc/includes.php';
// Check session exists
if (!isset($_SESSION['glpiID']) || (int) $_SESSION['glpiID'] <= 0) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Forbidden - No right plugin_urbackup READ']);
exit;
}
$server_id = (int) (($_GET['id'] ?? $_POST['id'] ?? 0));
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
// Load plugin classes
$classes = ['Server', 'UrbackupApiClient'];
foreach ($classes as $class) {
$file = PLUGIN_URBACKUP_DIR . '/src/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
}
$server = new \GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
-42
View File
@@ -1,42 +0,0 @@
<?php
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use Html;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
}
include_once GLPI_ROOT . "/inc/includes.php";
if (!Profile::canCurrentUser(READ)) {
Html::displayRightError();
}
$ID = $_GET['id'] ?? 0;
if ($ID <= 0) {
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php');
}
$server = new Server();
if (!$server->getFromDB($ID)) {
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php');
}
Html::header(
$server->fields['name'] . ' - UrBackup',
'',
'Assets',
'GlpiPlugin\Urbackup\Server'
);
$server->display([
'id' => $ID,
'show_nav' => true,
'show_tabs' => true,
]);
Html::footer();
-43
View File
@@ -1,43 +0,0 @@
<?php
/**
* Test page - will work when accessed from within GLPI session
*/
$AJAX_INCLUDE = 1;
define('GLPI_ROOT', dirname(__DIR__, 4));
require_once GLPI_ROOT . '/inc/includes.php';
header('Content-Type: text/html; charset=UTF-8');
Html::header_nocache();
Session::checkLoginUser();
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
echo "<p style='color:red'>No READ permission on plugin_urbackup</p>";
Html::footer();
exit;
}
echo "<p style='color:green'>READ permission OK</p>";
// Get server ID
$server_id = (int) ($_GET['id'] ?? 0);
if ($server_id > 0) {
$server = new GlpiPlugin\Urbackup\Server();
if ($server->getFromDB($server_id)) {
echo "<p>Server: " . $server->fields['name'] . "</p>";
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
echo "<pre>" . print_r($result, true) . "</pre>";
} else {
echo "<p>Server not found</p>";
}
} else {
echo "<p>No server ID provided</p>";
}
Html::footer();
+80 -28
View File
@@ -8,6 +8,8 @@ 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;
@@ -29,10 +31,15 @@ function plugin_urbackup_install_process(): bool
plugin_urbackup_install_create_initial_schema($migration); plugin_urbackup_install_create_initial_schema($migration);
plugin_urbackup_install_update_configs_table($migration); plugin_urbackup_install_update_configs_table($migration);
plugin_urbackup_install_update_assettypes_table($migration);
// Convert old assettype configurations to GLPI 11 capacity system
plugin_urbackup_install_convert_assettypes_to_capacities($migration);
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_update_profiles_table($migration);
// Drop tables that are no longer used (replaced by Profile::registerRights() and Capacity system)
plugin_urbackup_install_drop_legacy_tables($migration);
$migration->executeMigration(); $migration->executeMigration();
@@ -55,10 +62,8 @@ function plugin_urbackup_install_create_initial_schema(Migration $migration): vo
$required_tables = [ $required_tables = [
'glpi_plugin_urbackup_configs', 'glpi_plugin_urbackup_configs',
'glpi_plugin_urbackup_assettypes',
'glpi_plugin_urbackup_servers', 'glpi_plugin_urbackup_servers',
'glpi_plugin_urbackup_serverassets', 'glpi_plugin_urbackup_serverassets',
'glpi_plugin_urbackup_profiles',
]; ];
$missing_table_found = false; $missing_table_found = false;
@@ -153,11 +158,6 @@ function plugin_urbackup_install_update_assettypes_table(Migration $migration):
'after' => 'itemtype', 'after' => 'itemtype',
]); ]);
$migration->addField($table, 'is_default', 'bool', [
'value' => 0,
'after' => 'is_active',
]);
$migration->addField($table, 'date_creation', 'timestamp'); $migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp'); $migration->addField($table, 'date_mod', 'timestamp');
@@ -202,6 +202,11 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
'after' => 'name', 'after' => 'name',
]); ]);
$migration->addField($table, 'users_id', 'integer', [
'value' => 0,
'after' => 'locations_id',
]);
$migration->addField($table, 'ip_address', 'string', [ $migration->addField($table, 'ip_address', 'string', [
'value' => '', 'value' => '',
'after' => 'locations_id', 'after' => 'locations_id',
@@ -262,7 +267,9 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
$migration->addKey($table, 'name'); $migration->addKey($table, 'name');
$migration->addKey($table, 'entities_id'); $migration->addKey($table, 'entities_id');
$migration->addKey($table, 'locations_id'); $migration->addKey($table, 'locations_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');
} }
/** /**
@@ -297,44 +304,89 @@ function plugin_urbackup_install_update_serverassets_table(Migration $migration)
'after' => 'itemtype', 'after' => 'itemtype',
]); ]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, 'plugin_urbackup_servers_id'); $migration->addKey($table, 'plugin_urbackup_servers_id');
$migration->addKey($table, ['itemtype', 'items_id'], 'item'); $migration->addKey($table, ['itemtype', 'items_id'], 'item');
} }
/** /**
* Update profiles table. * Drop legacy tables that are no longer used.
* *
* @param Migration $migration Migration instance * @param Migration $migration Migration instance
* *
* @return void * @return void
*/ */
function plugin_urbackup_install_update_profiles_table(Migration $migration): void function plugin_urbackup_install_drop_legacy_tables(Migration $migration): void
{ {
global $DB; global $DB;
// glpi_plugin_urbackup_profiles was replaced by Profile::registerRights() in 0.5.0
$table = 'glpi_plugin_urbackup_profiles'; $table = 'glpi_plugin_urbackup_profiles';
if ($DB->tableExists($table)) {
$migration->displayMessage(__('Dropping legacy glpi_plugin_urbackup_profiles table', 'urbackup'));
$DB->queryOrDie("DROP TABLE IF EXISTS `$table`");
}
// glpi_plugin_urbackup_assettypes was replaced by GLPI 11 Capacity system in 0.6.0
$table2 = 'glpi_plugin_urbackup_assettypes';
if ($DB->tableExists($table2)) {
$migration->displayMessage(__('Dropping legacy glpi_plugin_urbackup_assettypes table', 'urbackup'));
$DB->queryOrDie("DROP TABLE IF EXISTS `$table2`");
}
$migration->displayMessage(__('Legacy tables dropped successfully', 'urbackup'));
}
/**
* 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
* via the native Capacities UI (Config > Asset definitions > Capacities).
*
* @param Migration $migration Migration instance
*
* @return void
*/
function plugin_urbackup_install_convert_assettypes_to_capacities(Migration $migration): void
{
global $DB;
$table = Config::getAssetTypesTable();
if (!$DB->tableExists($table)) { if (!$DB->tableExists($table)) {
return; return;
} }
$migration->addField($table, 'profiles_id', 'integer', [ // Check if is_default column exists
'value' => 0, $has_is_default = false;
'after' => 'id', foreach ($DB->listFields($table) as $col) {
]); if ($col['Field'] === 'is_default') {
$has_is_default = true;
break;
}
}
$migration->addField($table, 'rightname', 'string', [ // Auto-enable UrBackup capacity on ALL Asset Definitions
'value' => '', // so the tab appears out of the box for any asset type.
'after' => 'profiles_id', 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()
);
}
}
$migration->addField($table, 'rights', 'integer', [ // Drop is_default column (no longer needed in capacity system)
'value' => 0, if ($has_is_default) {
'after' => 'rightname', $migration->dropField($table, 'is_default');
]); }
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, ['profiles_id', 'rightname'], 'profile_right');
} }
+7 -23
View File
@@ -8,24 +8,13 @@ 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;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_assettypes` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`itemtype` VARCHAR(255) NOT NULL DEFAULT '',
`is_active` TINYINT NOT NULL DEFAULT 0,
`is_default` TINYINT NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `itemtype` (`itemtype`),
KEY `is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
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,
`is_recursive` TINYINT NOT NULL DEFAULT 0, `is_recursive` TINYINT NOT NULL DEFAULT 0,
`name` VARCHAR(255) NOT NULL DEFAULT '', `name` VARCHAR(255) NOT NULL DEFAULT '',
`locations_id` INT UNSIGNED NOT NULL DEFAULT 0, `locations_id` INT UNSIGNED NOT NULL DEFAULT 0,
`users_id` INT UNSIGNED NOT NULL DEFAULT 0,
`ip_address` VARCHAR(255) NOT NULL DEFAULT '', `ip_address` VARCHAR(255) NOT NULL DEFAULT '',
`port` INT UNSIGNED NOT NULL DEFAULT 55414, `port` INT UNSIGNED NOT NULL DEFAULT 55414,
`protocol` VARCHAR(10) NOT NULL DEFAULT 'http', `protocol` VARCHAR(10) NOT NULL DEFAULT 'http',
@@ -44,7 +33,9 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
KEY `name` (`name`), KEY `name` (`name`),
KEY `entities_id` (`entities_id`), KEY `entities_id` (`entities_id`),
KEY `locations_id` (`locations_id`), KEY `locations_id` (`locations_id`),
KEY `is_active` (`is_active`) KEY `users_id` (`users_id`),
KEY `is_active` (`is_active`),
KEY `location_active` (`locations_id`, `is_active`)
) 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` (
@@ -52,18 +43,11 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
`plugin_urbackup_servers_id` INT UNSIGNED NOT NULL DEFAULT 0, `plugin_urbackup_servers_id` INT UNSIGNED NOT NULL DEFAULT 0,
`itemtype` VARCHAR(255) NOT NULL DEFAULT '', `itemtype` VARCHAR(255) NOT NULL DEFAULT '',
`items_id` INT UNSIGNED NOT NULL DEFAULT 0, `items_id` INT UNSIGNED NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`), KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`),
KEY `item` (`itemtype`, `items_id`) KEY `item` (`itemtype`, `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_profiles` ( -- glpi_plugin_urbackup_profiles was dropped in 0.7.0 (replaced by Profile::registerRights())
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`profiles_id` INT UNSIGNED NOT NULL DEFAULT 0,
`rightname` VARCHAR(255) NOT NULL DEFAULT '',
`rights` INT NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `profile_right` (`profiles_id`, `rightname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-87
View File
@@ -1,87 +0,0 @@
/**
* UrBackup API Test JavaScript
*/
(function () {
'use strict';
function testApi(serverId, resultBox) {
if (!serverId || !resultBox) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/plugins/urbackup/ajax/server_test.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
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 {
var message = data.message || 'Connection failed';
var isNetwork = /timeout|could not resolve|couldn't connect|connection refused|connection timed out|network is unreachable|no route to host|returned HTTP status/i.test(message);
var statusClass = isNetwork ? 'text-warning' : 'text-danger';
var icon = isNetwork ? 'ti-wifi-off' : 'ti-x';
var label = isNetwork ? 'Server unreachable' : 'API connection failed';
resultBox.innerHTML = '<span class="' + statusClass + ' fw-bold"><i class="ti ' + icon + '"></i> ' + label + '</span><br><small class="text-muted">' + 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-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Connection timeout</small>';
};
xhr.onerror = function () {
resultBox.innerHTML = '<span class="text-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Network error</small>';
};
xhr.send('id=' + encodeURIComponent(serverId));
}
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);
}
})();
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server nicht gefunden"
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"
msgstr "Keine Assets"
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server not found"
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"
msgstr "No assets"
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server non trovato"
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"
msgstr "Nessun Asset"
+43
View File
@@ -10,3 +10,46 @@
color: #b00; color: #b00;
font-weight: bold; font-weight: bold;
} }
#urbackupTabs .nav-link {
font-weight: 600;
border-width: 2px;
padding: 8px 18px;
margin-right: 4px;
}
#urbackupTabs .nav-link#state-tab {
color: #fff;
background-color: #4a4a4a;
border-color: #4a4a4a;
}
#urbackupTabs .nav-link#state-tab.active {
color: #fff;
background-color: #2d2d2d;
border-color: #2d2d2d;
}
#urbackupTabs .nav-link#actions-tab {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
#urbackupTabs .nav-link#actions-tab.active {
color: #fff;
background-color: #495057;
border-color: #495057;
}
#urbackupTabs .nav-link#logs-tab {
color: #fff;
background-color: #adb5bd;
border-color: #adb5bd;
}
#urbackupTabs .nav-link#logs-tab.active {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
+1
View File
@@ -10,6 +10,7 @@
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.php', true); xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Glpi-Csrf-Token', getAjaxCsrfToken());
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000; xhr.timeout = 8000;
-11
View File
@@ -1,11 +0,0 @@
#!/bin/bash
# Remove deprecated front/ and ajax/ files for GLPI 11 compliance
echo "Removing deprecated front/ and ajax/ files..."
rm -f /var/www/glpi/plugins/urbackup/front/config.form.php
rm -f /var/www/glpi/plugins/urbackup/front/server.php
rm -f /var/www/glpi/plugins/urbackup/front/server.form.php
rm -f /var/www/glpi/plugins/urbackup/front/asset.form.php
rm -f /var/www/glpi/plugins/urbackup/ajax/server_test.php
rm -f /var/www/glpi/plugins/urbackup/ajax/server_clients.php
rm -f /var/www/glpi/plugins/urbackup/ajax/asset_action.php
echo "Files removed. Note: The plugin now uses Symfony Controllers."
+23 -19
View File
@@ -2,20 +2,29 @@
declare(strict_types=1); declare(strict_types=1);
// Force OPcache to reload this file when accessed via web // Force OPcache to reload plugin files when accessed via web
if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) { if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) {
opcache_invalidate(__FILE__, true); $files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if ($file->getExtension() === 'php') {
@opcache_invalidate($file->getRealPath(), true);
}
}
} }
use Glpi\Asset\AssetDefinitionManager;
use Glpi\Plugin\Hooks; use Glpi\Plugin\Hooks;
use GlpiPlugin\Urbackup\AssetTab; use GlpiPlugin\Urbackup\AssetTab;
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; 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.4.2'); define('PLUGIN_URBACKUP_VERSION', '0.7.0');
define('PLUGIN_URBACKUP_MIN_GLPI', '11.0.0'); define('PLUGIN_URBACKUP_MIN_GLPI', '11.0.0');
define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99'); define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99');
@@ -52,12 +61,19 @@ 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 and other assets // Register tab on Computer (legacy, always enabled)
$enabled_types = Config::getEnabledItemtypes();
if (!empty($enabled_types)) {
Plugin::registerClass(AssetTab::class, [ Plugin::registerClass(AssetTab::class, [
'addtabon' => $enabled_types, 'addtabon' => ['Computer'],
]); ]);
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
// which is called by `bootDefinitions()` during kernel PostBoot event.
// This ensures tabs are registered on all definitions that have the
// UrBackup capacity enabled, even if they are created after plugin init.
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
$manager->registerCapacity(new UrBackupCapacity());
} }
$PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php'; $PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php';
@@ -75,18 +91,6 @@ function plugin_init_urbackup(): void
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [ $PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [
'public/js/urbackup.js', 'public/js/urbackup.js',
]; ];
$PLUGIN_HOOKS[Hooks::POST_INIT]['urbackup'] = 'plugin_urbackup_postinit';
}
/**
* Post init hook.
*
* @return void
*/
function plugin_urbackup_postinit(): void
{
Config::registerAssetTabs();
} }
/** /**
+28 -14
View File
@@ -57,11 +57,15 @@ class AssetTab extends CommonDBTM
*/ */
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string
{ {
if (!Config::isItemtypeEnabled($item::class)) { $itemtype = $item::class;
$enabled = Config::isItemtypeEnabled($itemtype);
$hasRight = Profile::canCurrentUser(READ);
if (!$enabled) {
return ''; return '';
} }
if (!Profile::canCurrentUser(READ)) { if (!$hasRight) {
return ''; return '';
} }
@@ -228,7 +232,7 @@ class AssetTab extends CommonDBTM
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>"; echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(ServerAsset::getAssetName($item::class, (int) $item->fields['id'])) . "</td>"; echo "<td>" . htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</td>";
echo "</tr>"; echo "</tr>";
echo "</table>"; echo "</table>";
@@ -259,9 +263,14 @@ class AssetTab extends CommonDBTM
*/ */
private static function loadApiData(CommonDBTM $item, Server $server, array $link): array private static function loadApiData(CommonDBTM $item, Server $server, array $link): array
{ {
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); $client_name = (string) ($item->fields['name'] ?? '');
$asset_ip = ServerAsset::extractAssetIp($item); $asset_ip = ServerAsset::extractAssetIp($item);
$cache_key = 'urbackup_data_' . $server->fields['id'] . '_' . $client_name;
if (isset($_SESSION[$cache_key]) && $_SESSION[$cache_key]['time'] > time() - 30) {
return $_SESSION[$cache_key]['data'];
}
$data = [ $data = [
'client_found' => false, 'client_found' => false,
'client_status' => [], 'client_status' => [],
@@ -305,6 +314,11 @@ class AssetTab extends CommonDBTM
$data['error'] = $e->getMessage(); $data['error'] = $e->getMessage();
} }
$_SESSION[$cache_key] = [
'time' => time(),
'data' => $data,
];
return $data; return $data;
} }
@@ -328,19 +342,19 @@ class AssetTab extends CommonDBTM
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE); $canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
echo '<ul class="nav nav-tabs" id="urbackupTabs">'; echo '<ul class="nav nav-tabs" id="urbackupTabs" role="tablist">';
echo '<li class="nav-item">'; echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab">'; 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(__('State', 'urbackup'));
echo '</a></li>'; echo '</a></li>';
if ($canWrite) { if ($canWrite) {
echo '<li class="nav-item">'; echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link" id="actions-tab" data-bs-toggle="tab" href="#actions" role="tab">'; echo '<a class="nav-link" id="actions-tab" data-bs-toggle="tab" href="#actions" role="tab" aria-selected="false" tabindex="-1">';
echo htmlspecialchars(__('Actions', 'urbackup')); echo htmlspecialchars(__('Actions', 'urbackup'));
echo '</a></li>'; echo '</a></li>';
} }
echo '<li class="nav-item">'; echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link" id="logs-tab" data-bs-toggle="tab" href="#logs" role="tab">'; echo '<a class="nav-link" id="logs-tab" data-bs-toggle="tab" href="#logs" role="tab" aria-selected="false" tabindex="-1">';
echo htmlspecialchars(__('Info / Log', 'urbackup')); echo htmlspecialchars(__('Info / Log', 'urbackup'));
echo '</a></li>'; echo '</a></li>';
echo '</ul>'; echo '</ul>';
@@ -831,7 +845,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); $client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
@@ -869,7 +883,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); $client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
@@ -899,7 +913,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); $client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace GlpiPlugin\Urbackup\Capacity;
use CommonGLPI;
use Glpi\Asset\Capacity\AbstractCapacity;
use Glpi\Asset\CapacityConfig;
use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
final class UrBackupCapacity extends AbstractCapacity
{
public function getLabel(): string
{
return __('UrBackup', 'urbackup');
}
public function getIcon(): string
{
return 'ti ti-cloud-up';
}
public function getDescription(): string
{
return __('Manage UrBackup backups for these assets', 'urbackup');
}
public function onClassBootstrap(string $classname, CapacityConfig $config): void
{
CommonGLPI::registerStandardTab($classname, AssetTab::class);
}
public function onCapacityDisabled(string $classname, CapacityConfig $config): void
{
(new ServerAsset())->deleteByCriteria(
['itemtype' => $classname],
force: true,
history: false
);
$this->deleteRelationLogs($classname, Server::class);
}
public function isUsed(string $classname): bool
{
return parent::isUsed($classname)
&& $this->countAssetsLinkedToPeerItem(
$classname,
ServerAsset::class
) > 0;
}
public function getCapacityUsageDescription(string $classname): string
{
return sprintf(
__('%1$s assets linked to UrBackup servers', 'urbackup'),
$this->countAssetsLinkedToPeerItem($classname, ServerAsset::class)
);
}
}
-56
View File
@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace GlpiPlugin\Urbackup\Command;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TestApiCommand extends Command
{
protected function configure(): void
{
$this
->setName('urbackup:test-api')
->setDescription('Test UrBackup server API connection')
->addArgument('server_id', InputArgument::REQUIRED, 'Server ID');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$server_id = (int) $input->getArgument('server_id');
$server = new Server();
if (!$server->getFromDB($server_id)) {
$output->writeln("<error>Server not found: $server_id</error>");
return Command::FAILURE;
}
$output->writeln("Testing API for: " . $server->fields['name']);
$output->writeln("URL: " . $server->getWebInterfaceUrl());
$output->writeln("Username: " . $server->fields['api_username']);
$client = new UrbackupApiClient($server);
try {
$result = $client->testConnection();
if ($result['success']) {
$output->writeln("<info>SUCCESS: " . $result['message'] . "</info>");
$output->writeln("Identity: " . $result['identity']);
} else {
$output->writeln("<error>FAILED: " . $result['message'] . "</error>");
}
return $result['success'] ? Command::SUCCESS : Command::FAILURE;
} catch (\Throwable $e) {
$output->writeln("<error>Exception: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+31 -280
View File
@@ -11,8 +11,7 @@ declare(strict_types=1);
namespace GlpiPlugin\Urbackup; namespace GlpiPlugin\Urbackup;
use CommonDBTM; use CommonDBTM;
use Computer; use Glpi\Asset\Asset;
use Glpi\Asset\AssetDefinition;
use Glpi\Asset\AssetDefinitionManager; use Glpi\Asset\AssetDefinitionManager;
use DBmysql; use DBmysql;
use Html; use Html;
@@ -53,67 +52,8 @@ class Config extends CommonDBTM
*/ */
public static function ensureDefaultConfiguration(): void public static function ensureDefaultConfiguration(): void
{ {
self::ensureAssetType('Computer', true, true); // Computer is always enabled by default.
} // Asset Definition types are managed via the native Capacities UI.
/**
* Ensure an asset type is registered.
*
* @param string $itemtype Itemtype
* @param bool $is_active Active
* @param bool $is_default Default
*
* @return void
*/
public static function ensureAssetType(
string $itemtype,
bool $is_active = true,
bool $is_default = false
): void {
global $DB;
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return;
}
$existing = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
],
'LIMIT' => 1,
]);
if (count($existing) > 0) {
$row = $existing->current();
$DB->update(
$table,
[
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
],
[
'id' => (int) $row['id'],
]
);
return;
}
$DB->insert(
$table,
[
'itemtype' => $itemtype,
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_creation' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
]
);
} }
/** /**
@@ -135,8 +75,6 @@ class Config extends CommonDBTM
*/ */
public static function isItemtypeEnabled(string $itemtype): bool public static function isItemtypeEnabled(string $itemtype): bool
{ {
global $DB;
if ($itemtype === '') { if ($itemtype === '') {
return false; return false;
} }
@@ -145,22 +83,15 @@ class Config extends CommonDBTM
return true; return true;
} }
$table = self::getAssetTypesTable(); // All Asset subclasses are enabled by default.
// The UrBackupCapacity is still registered for cleanup (onCapacityDisabled)
if (!$DB->tableExists($table)) { // and usage tracking in the Capacities UI, but visibility is always on
return false; // so the tab appears out of the box on any Asset Definition.
if (is_a($itemtype, Asset::class, true)) {
return true;
} }
$iterator = $DB->request([ return false;
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
'is_active' => 1,
],
'LIMIT' => 1,
]);
return count($iterator) > 0;
} }
/** /**
@@ -170,54 +101,19 @@ class Config extends CommonDBTM
*/ */
public static function getEnabledItemtypes(): array public static function getEnabledItemtypes(): array
{ {
global $DB; $itemtypes = ['Computer'];
$types = ['Computer']; if (class_exists(AssetDefinitionManager::class)) {
$table = self::getAssetTypesTable(); $manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
if (!$DB->tableExists($table)) { $classname = $definition->getAssetClassName();
return $types; if (!in_array($classname, $itemtypes, true)) {
$itemtypes[] = $classname;
} }
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_active' => 1,
],
'ORDER' => 'itemtype',
]);
foreach ($iterator as $row) {
$itemtype = (string) $row['itemtype'];
if ($itemtype !== '' && !in_array($itemtype, $types, true)) {
$types[] = $itemtype;
} }
} }
return $types; return $itemtypes;
}
/**
* Register tabs on enabled asset types.
*
* @return void
*/
public static function registerAssetTabs(): void
{
foreach (self::getEnabledItemtypes() as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue;
}
\Plugin::registerClass(AssetTab::class, [
'addtabon' => $itemtype,
]);
}
} }
/** /**
@@ -232,71 +128,22 @@ class Config extends CommonDBTM
{ {
Session::checkRight('config', UPDATE); Session::checkRight('config', UPDATE);
echo "<form method='post' action='" . htmlspecialchars($options['target'] ?? '') . "'>";
echo "<div class='center'>"; echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>"; echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>"; echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Asset type', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Enabled', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Default', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type', 'urbackup')) . "</th>";
echo "</tr>";
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
$enabled = self::isItemtypeEnabled($itemtype);
$is_default = ($itemtype === 'Computer');
$is_asset_definition = self::isAssetDefinition($itemtype);
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars($label) . "</td>"; echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>";
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</span></td>";
if ($itemtype === 'Computer') {
echo "<td>" . htmlspecialchars(__('Always', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(__('Yes', 'urbackup')) . "</td>";
} elseif ($is_asset_definition) {
// GLPI 11 Asset Definition
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_active]',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_default]',
'checked' => $is_default,
]);
echo "</td>";
echo "<td><span class='badge bg-info'>" . htmlspecialchars(__('Asset Definition', 'urbackup')) . "</span></td>";
} else {
// Legacy type
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . ']',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>" . ($is_default ? htmlspecialchars(__('Yes', 'urbackup')) : '') . "</td>";
echo "<td><span class='badge bg-secondary'>" . htmlspecialchars(__('Legacy', 'urbackup')) . "</span></td>";
}
echo "</tr>"; echo "</tr>";
}
echo "<tr>";
echo "<td colspan='4' class='center'>";
echo Html::submit(__('Save', 'urbackup'), [
'name' => 'update',
'class' => 'btn btn-primary',
]);
echo "</td>";
echo "</tr>";
echo "</table>"; echo "</table>";
echo "</div>";
Html::closeForm(); echo "<br>";
echo "<div class='alert alert-info'>";
echo htmlspecialchars(
__('For Asset Definition types, enable/disable UrBackup via Config > Asset definitions > Capacities.', 'urbackup')
);
echo "</div>";
echo "</div>";
return true; return true;
} }
@@ -304,6 +151,9 @@ class Config extends CommonDBTM
/** /**
* Save configuration. * 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 * @param array<string, mixed> $input Input data
* *
* @return void * @return void
@@ -311,104 +161,5 @@ class Config extends CommonDBTM
public static function saveConfiguration(array $input): void public static function saveConfiguration(array $input): void
{ {
Session::checkRight('config', UPDATE); Session::checkRight('config', UPDATE);
$selected = $input['assettypes'] ?? [];
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
if ($itemtype === 'Computer') {
self::ensureAssetType('Computer', true, true);
continue;
}
// Check if it's an Asset Definition (new format with is_active/is_default)
if (self::isAssetDefinition($itemtype)) {
$is_active = isset($selected[$itemtype]['is_active']) && (bool) $selected[$itemtype]['is_active'];
$is_default = isset($selected[$itemtype]['is_default']) && (bool) $selected[$itemtype]['is_default'];
self::ensureAssetType($itemtype, $is_active, $is_default);
} else {
// Legacy format (simple checkbox)
$is_enabled = isset($selected[$itemtype]) && (bool) $selected[$itemtype];
self::ensureAssetType($itemtype, $is_enabled, false);
}
}
}
/**
* Get configurable asset types (legacy + GLPI 11 Asset Definition).
*
* @return array<string, string>
*/
public static function getConfigurableAssetTypes(): array
{
$types = [
'Computer' => Computer::getTypeName(Session::getPluralNumber()),
];
// Legacy types
$known_types = [
'Printer',
'Peripheral',
'NetworkEquipment',
'Phone',
'Monitor',
];
foreach ($known_types as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue;
}
$types[$itemtype] = $itemtype::getTypeName(Session::getPluralNumber());
}
// GLPI 11 Asset Definition
if (class_exists(AssetDefinitionManager::class)) {
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
// Access fields directly like CommonDBTM
$system_name = $definition->fields['system_name'] ?? '';
$name = $definition->fields['name'] ?? '';
if (!empty($system_name)) {
$types[$system_name] = !empty($name) ? $name : $system_name;
}
}
} catch (\Throwable $e) {
// If AssetDefinitionManager fails, skip
}
}
return $types;
}
/**
* Check if itemtype is a GLPI 11 Asset Definition.
*
* @param string $itemtype Itemtype or system name
*
* @return bool
*/
public static function isAssetDefinition(string $itemtype): bool
{
if (!class_exists(AssetDefinitionManager::class)) {
return false;
}
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
if (($definition->fields['system_name'] ?? '') === $itemtype) {
return true;
}
}
} catch (\Throwable $e) {
// If fails, return false
}
return false;
} }
} }
-191
View File
@@ -1,191 +0,0 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use CommonDBTM;
use Html;
use Session;
class AssetController
{
#[Route('/plugin/urbackup/asset/action', name: 'urbackup_asset_action', methods: ['POST'])]
public function assetAction(Request $request): void
{
Session::checkLoginUser();
Session::checkCSRF($_POST);
$itemtype = (string) $request->request->get('itemtype', '');
$items_id = (int) $request->request->get('items_id', 0);
if ($itemtype === '' || $items_id <= 0 || !class_exists($itemtype)) {
Session::addMessageAfterRedirect(
__('Invalid asset reference.', 'urbackup'),
true,
ERROR
);
Html::back();
}
if (!Config::isItemtypeEnabled($itemtype)) {
Session::addMessageAfterRedirect(
__('UrBackup is not enabled for this asset type.', 'urbackup'),
true,
ERROR
);
Html::back();
}
$item = new $itemtype();
if (!$item instanceof CommonDBTM || !$item->getFromDB($items_id)) {
Session::addMessageAfterRedirect(
__('Asset not found.', 'urbackup'),
true,
ERROR
);
Html::back();
}
$action = (string) $request->request->get('urbackup_action', '');
switch ($action) {
case 'connect':
$server_id = (int) $request->request->get('plugin_urbackup_servers_id', 0);
if (ServerAsset::connectAssetToServer($itemtype, $items_id, $server_id)) {
Session::addMessageAfterRedirect(
__('Asset connected to UrBackup server.', 'urbackup'),
true,
INFO
);
}
break;
case 'disconnect':
if (ServerAsset::disconnectAsset($itemtype, $items_id)) {
Session::addMessageAfterRedirect(
__('Asset disconnected from UrBackup server.', 'urbackup'),
true,
INFO
);
}
break;
case 'set_internet_mode':
if (Profile::canCurrentUser(UPDATE)) {
$enabled = (int) $request->request->get('internet_mode', 0) === 1;
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$api->saveInternetMode($client_name, $enabled);
}
}
}
break;
case 'create_client':
if (Profile::canCurrentUser(CREATE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$api->addClient($client_name);
}
}
}
break;
case 'incremental_file_backup':
case 'full_file_backup':
case 'incremental_image_backup':
case 'full_image_backup':
if (Profile::canCurrentUser(UPDATE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
switch ($action) {
case 'incremental_file_backup':
$api->startIncrementalFileBackup($client_name);
break;
case 'full_file_backup':
$api->startFullFileBackup($client_name);
break;
case 'incremental_image_backup':
$api->startIncrementalImageBackup($client_name);
break;
case 'full_image_backup':
$api->startFullImageBackup($client_name);
break;
}
}
}
}
break;
case 'delete_client':
if (Profile::canCurrentUser(PURGE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$api->removeClient($client_name);
}
}
}
break;
}
Html::back();
}
#[Route('/plugin/urbackup/api/clients', name: 'urbackup_api_clients', methods: ['GET'])]
public function getClients(Request $request): JsonResponse
{
Session::checkLoginUser();
$server_id = (int) $request->query->get('server_id', 0);
if ($server_id <= 0) {
return new JsonResponse([]);
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
return new JsonResponse([]);
}
$api = new UrbackupApiClient($server);
$clients = $api->getStatus();
return new JsonResponse($clients);
}
}
-45
View File
@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Html;
use Session;
class ConfigController extends AbstractController
{
#[Route('/plugin/urbackup/config', name: 'urbackup_config', methods: ['GET', 'POST'])]
public function configure(Request $request): Response
{
Session::checkRight('config', UPDATE);
if ($request->isMethod('POST') && $request->request->has('update')) {
Config::saveConfiguration($request->request->all());
Html::back();
}
$configurableTypes = Config::getConfigurableAssetTypes();
$enabledTypes = [];
foreach ($configurableTypes as $itemtype => $label) {
$enabledTypes[$itemtype] = Config::isItemtypeEnabled($itemtype);
}
return $this->render('config/config.html.twig', [
'configurable_types' => $configurableTypes,
'enabled_types' => $enabledTypes,
]);
}
}
-109
View File
@@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Html;
use Session;
use Search;
class ServerController extends AbstractController
{
#[Route('/plugin/urbackup/servers', name: 'urbackup_server_list', methods: ['GET'])]
public function listServers(): void
{
if (!Server::canView()) {
Html::displayRightError();
}
Html::header(
Server::getTypeName(Session::getPluralNumber()),
$_SERVER['PHP_SELF'],
'admin',
Server::class
);
Search::show(Server::class);
Html::footer();
}
#[Route('/plugin/urbackup/server/{id}', name: 'urbackup_server_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function showServer(int $id): void
{
$server = new Server();
if ($id > 0) {
$server->check($id, READ);
} else {
$server->check(-1, CREATE);
$server->getEmpty();
}
$server->display(['id' => $id]);
}
#[Route('/plugin/urbackup/server/test/{id}', name: 'urbackup_server_test', methods: ['POST', 'GET'])]
public function testConnection(int $id = 0): JsonResponse
{
if (!Profile::canCurrentUser(READ)) {
return new JsonResponse([
'success' => false,
'message' => 'No permission',
], 403);
}
if ($id <= 0) {
$id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
}
if ($id <= 0) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid server ID',
], 400);
}
$server = new Server();
if (!$server->getFromDB($id)) {
return new JsonResponse([
'success' => false,
'message' => 'Server not found',
], 404);
}
try {
$client = new UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
return new JsonResponse($result);
} catch (Throwable $e) {
return new JsonResponse([
'success' => false,
'message' => $e->getMessage(),
], 500);
}
}
}
+17 -2
View File
@@ -73,7 +73,22 @@ class Profile extends \Profile
$profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0); $profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0);
if ($profiles_id > 0) { if ($profiles_id > 0) {
self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE); self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE | PURGE);
} else {
// CLI installation: no session available.
// Assign full rights to the Super-Admin profile.
global $DB;
$sa_iterator = $DB->request([
'FROM' => 'glpi_profiles',
'WHERE' => [
'name' => 'Super-Admin',
],
'LIMIT' => 1,
]);
foreach ($sa_iterator as $sa_profile) {
$profiles_id = (int) $sa_profile['id'];
self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE | PURGE);
}
} }
global $DB; global $DB;
@@ -83,7 +98,7 @@ class Profile extends \Profile
]); ]);
foreach ($all_profiles as $profile) { foreach ($all_profiles as $profile) {
if ($profile['id'] !== $profiles_id) { if ((int) $profile['id'] !== $profiles_id) {
$existing = $DB->request([ $existing = $DB->request([
'FROM' => 'glpi_profilerights', 'FROM' => 'glpi_profilerights',
'WHERE' => [ 'WHERE' => [
+145 -14
View File
@@ -259,6 +259,14 @@ class Server extends CommonDBTM
$tab[] = [ $tab[] = [
'id' => 8, 'id' => 8,
'table' => User::getTable(),
'field' => 'name',
'name' => User::getTypeName(1),
'datatype' => 'dropdown',
];
$tab[] = [
'id' => 9,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'is_active', 'field' => 'is_active',
'name' => __('Active'), 'name' => __('Active'),
@@ -266,7 +274,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 9, 'id' => 10,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'last_api_status', 'field' => 'last_api_status',
'name' => __('Last API status', 'urbackup'), 'name' => __('Last API status', 'urbackup'),
@@ -274,7 +282,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 10, 'id' => 11,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'last_api_check', 'field' => 'last_api_check',
'name' => __('Last API check', 'urbackup'), 'name' => __('Last API check', 'urbackup'),
@@ -282,7 +290,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 11, 'id' => 12,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'date_creation', 'field' => 'date_creation',
'name' => __('Creation date'), 'name' => __('Creation date'),
@@ -290,7 +298,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 12, 'id' => 13,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'date_mod', 'field' => 'date_mod',
'name' => __('Last update'), 'name' => __('Last update'),
@@ -299,7 +307,7 @@ class Server extends CommonDBTM
if (Session::haveRight(self::$rightname, UPDATE)) { if (Session::haveRight(self::$rightname, UPDATE)) {
$tab[] = [ $tab[] = [
'id' => 13, 'id' => 14,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'id', 'field' => 'id',
'name' => __('View', 'urbackup'), 'name' => __('View', 'urbackup'),
@@ -350,7 +358,10 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('Active')) . "</td>"; echo "<td>" . htmlspecialchars(__('Active')) . "</td>";
echo "<td>"; echo "<td>";
Dropdown::showYesNo('is_active', (int) ($this->fields['is_active'] ?? 1)); Dropdown::showYesNo(
'is_active',
($ID !== null && $ID > 0) ? (int) ($this->fields['is_active'] ?? 1) : 1
);
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
@@ -433,12 +444,14 @@ class Server extends CommonDBTM
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
$canUpdate = Session::haveRight(self::$rightname, UPDATE); $canEdit = $ID > 0
? Session::haveRight(self::$rightname, UPDATE)
: Session::haveRight(self::$rightname, CREATE);
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
if ($canUpdate) { if ($canEdit) {
echo Html::input('api_username', [ echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '', 'value' => $this->fields['api_username'] ?? '',
'size' => 40, 'size' => 40,
@@ -451,7 +464,7 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
if ($canUpdate) { if ($canEdit) {
echo "<input type='password' name='api_password' value='" . echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) . htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>"; "' autocomplete='new-password'>";
@@ -566,6 +579,10 @@ class Server extends CommonDBTM
return $input; return $input;
} }
if (!isset($input['users_id']) || $input['users_id'] <= 0) {
$input['users_id'] = (int) ($_SESSION['glpiID'] ?? 0);
}
if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) { if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) {
$input['port'] = 55414; $input['port'] = 55414;
} }
@@ -1125,6 +1142,8 @@ class Server extends CommonDBTM
$itemtypes = Config::getEnabledItemtypes(); $itemtypes = Config::getEnabledItemtypes();
$missingAssets = []; $missingAssets = [];
$candidatesByType = [];
$totalAssets = 0;
foreach ($itemtypes as $itemtype) { foreach ($itemtypes as $itemtype) {
if (!class_exists($itemtype)) { if (!class_exists($itemtype)) {
@@ -1147,6 +1166,7 @@ class Server extends CommonDBTM
]); ]);
foreach ($iterator as $assetRow) { foreach ($iterator as $assetRow) {
$totalAssets++;
$name = (string) ($assetRow['name'] ?? ''); $name = (string) ($assetRow['name'] ?? '');
if ($name === '') { if ($name === '') {
continue; continue;
@@ -1174,9 +1194,6 @@ class Server extends CommonDBTM
$locationName = self::getCachedLocationName($assetLocationId, $cacheLocation); $locationName = self::getCachedLocationName($assetLocationId, $cacheLocation);
$stateName = self::getCachedName('State', (int) ($assetRow['states_id'] ?? 0), $cacheState); $stateName = self::getCachedName('State', (int) ($assetRow['states_id'] ?? 0), $cacheState);
$userName = self::getCachedName('User', (int) ($assetRow['users_id'] ?? 0), $cacheUser); $userName = self::getCachedName('User', (int) ($assetRow['users_id'] ?? 0), $cacheUser);
$groupName = self::getAssetGroupName($itemtype, $assetId, $cacheGroup);
$ip = self::getAssetIp($itemtype, $assetId);
$missingAssets[] = [ $missingAssets[] = [
'itemtype' => $itemtype, 'itemtype' => $itemtype,
@@ -1185,18 +1202,36 @@ class Server extends CommonDBTM
'entity' => $entityName, 'entity' => $entityName,
'location' => $locationName, 'location' => $locationName,
'otherserial' => (string) ($assetRow['otherserial'] ?? ''), 'otherserial' => (string) ($assetRow['otherserial'] ?? ''),
'ip' => $ip,
'state' => $stateName, 'state' => $stateName,
'user' => $userName, 'user' => $userName,
'group' => $groupName,
]; ];
$candidatesByType[$itemtype][] = $assetId;
} }
} }
$batchIps = self::batchLoadIps($candidatesByType);
$batchGroups = self::batchLoadGroups($candidatesByType);
foreach ($missingAssets as &$asset) {
$key = $asset['itemtype'] . ':' . $asset['items_id'];
$asset['ip'] = $batchIps[$key] ?? '';
$groupId = $batchGroups[$key] ?? 0;
$asset['group'] = $groupId > 0
? self::getCachedName('Group', $groupId, $cacheGroup)
: '';
}
unset($asset);
if (count($missingAssets) === 0) { if (count($missingAssets) === 0) {
if ($totalAssets === 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No assets', 'urbackup'));
echo '</div>';
} else {
echo '<div class="alert alert-success">'; echo '<div class="alert alert-success">';
echo htmlspecialchars(__('All assets in this location are linked or already on the UrBackup server.', 'urbackup')); echo htmlspecialchars(__('All assets in this location are linked or already on the UrBackup server.', 'urbackup'));
echo '</div>'; echo '</div>';
}
return; return;
} }
@@ -1325,6 +1360,102 @@ JAVASCRIPT;
return $cache[$id]; return $cache[$id];
} }
/**
* Batch-load IPs for multiple assets across itemtypes.
*
* @param array<string, list<int>> $candidatesByType itemtype => [items_id, ...]
*
* @return array<string, string> key "itemtype:items_id" => IP
*/
private static function batchLoadIps(array $candidatesByType): array
{
global $DB;
$ips = [];
foreach ($candidatesByType as $itemtype => $ids) {
if (count($ids) === 0) {
continue;
}
$iterator = $DB->request([
'SELECT' => ['np.items_id', 'ipa.name'],
'FROM' => 'glpi_ipaddresses AS ipa',
'INNER JOIN' => [
'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,
'np.items_id' => $ids,
],
]);
foreach ($iterator as $row) {
$ip = (string) ($row['name'] ?? '');
if ($ip === '') {
continue;
}
$key = $itemtype . ':' . $row['items_id'];
if (!isset($ips[$key])) {
$ips[$key] = $ip;
}
}
}
return $ips;
}
/**
* Batch-load group IDs for multiple assets across itemtypes.
*
* @param array<string, list<int>> $candidatesByType itemtype => [items_id, ...]
*
* @return array<string, int> key "itemtype:items_id" => groups_id
*/
private static function batchLoadGroups(array $candidatesByType): array
{
global $DB;
$groups = [];
foreach ($candidatesByType as $itemtype => $ids) {
if (count($ids) === 0) {
continue;
}
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $ids,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
]);
foreach ($iterator as $row) {
$key = $itemtype . ':' . $row['items_id'];
if (!isset($groups[$key])) {
$groups[$key] = (int) ($row['groups_id'] ?? 0);
}
}
}
return $groups;
}
private static function getAssetGroupName(string $itemtype, int $items_id, array &$cache): string private static function getAssetGroupName(string $itemtype, int $items_id, array &$cache): string
{ {
global $DB; global $DB;
+36 -7
View File
@@ -27,7 +27,9 @@ class UrbackupApiClient
private bool $ignore_ssl; private bool $ignore_ssl;
private int $timeout = 10; private int $timeout = 30;
private int $connect_timeout = 5;
private string $session = ''; private string $session = '';
@@ -35,6 +37,10 @@ class UrbackupApiClient
private int $lastlogid = 0; private int $lastlogid = 0;
private ?array $cached_status = null;
private array $cached_settings = [];
private string $server_version = ''; private string $server_version = '';
private bool $is_version_2_4_or_higher = false; private bool $is_version_2_4_or_higher = false;
@@ -245,21 +251,29 @@ class UrbackupApiClient
*/ */
public function getStatus(): array public function getStatus(): array
{ {
if ($this->cached_status !== null) {
return $this->cached_status;
}
$data = $this->apiAction('status'); $data = $this->apiAction('status');
if (isset($data['status']) && is_array($data['status'])) { if (isset($data['status']) && is_array($data['status'])) {
return array_values($data['status']); $this->cached_status = array_values($data['status']);
return $this->cached_status;
} }
if (isset($data['clients']) && is_array($data['clients'])) { if (isset($data['clients']) && is_array($data['clients'])) {
return array_values($data['clients']); $this->cached_status = array_values($data['clients']);
return $this->cached_status;
} }
if (array_is_list($data)) { if (array_is_list($data)) {
return $data; $this->cached_status = $data;
return $this->cached_status;
} }
return []; $this->cached_status = [];
return $this->cached_status;
} }
/** /**
@@ -315,16 +329,22 @@ class UrbackupApiClient
return []; return [];
} }
if (isset($this->cached_settings[$client_id])) {
return $this->cached_settings[$client_id];
}
$data = $this->apiAction('settings', [ $data = $this->apiAction('settings', [
'sa' => 'clientsettings', 'sa' => 'clientsettings',
't_clientid' => $client_id, 't_clientid' => $client_id,
]); ]);
if (isset($data['settings']) && is_array($data['settings'])) { if (isset($data['settings']) && is_array($data['settings'])) {
return $data['settings']; $this->cached_settings[$client_id] = $data['settings'];
return $this->cached_settings[$client_id];
} }
return $data; $this->cached_settings[$client_id] = $data;
return $this->cached_settings[$client_id];
} }
/** /**
@@ -336,6 +356,11 @@ class UrbackupApiClient
* *
* @return bool * @return bool
*/ */
private function clearSettingsCache(int $client_id): void
{
unset($this->cached_settings[$client_id]);
}
public function changeClientSetting(string $client_name, string $key, mixed $value): bool public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{ {
$client_id = $this->getClientIdByName($client_name); $client_id = $this->getClientIdByName($client_name);
@@ -351,6 +376,7 @@ class UrbackupApiClient
$key => (string) $value, $key => (string) $value,
]); ]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -369,6 +395,7 @@ class UrbackupApiClient
$key => $value, $key => $value,
]); ]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -391,6 +418,7 @@ class UrbackupApiClient
$data = $this->apiAction('settings', $params); $data = $this->apiAction('settings', $params);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -667,6 +695,7 @@ class UrbackupApiClient
$options = [ $options = [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout, CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => $this->connect_timeout,
CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl, CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl,
CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2, CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2,
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
-69
View File
@@ -1,69 +0,0 @@
<div class="plugin-urbackup-asset-tab">
{% if not linked %}
<div class="alert alert-info">
{{ __('No UrBackup server linked.', 'urbackup') }}
</div>
<table class="tab_cadre_fixe">
<tr>
<th colspan="2">{{ __('UrBackup server selection', 'urbackup') }}</th>
</tr>
{% if is_sub_location %}
<tr class="tab_bg_1">
<td colspan="2"><em>
{{ __('The asset is in a sub-location. The plugin will use the server assigned to the root location.', 'urbackup') }}
</em></td>
</tr>
{% endif %}
{% if servers|length == 0 %}
<tr class="tab_bg_1">
<td colspan="2">
<div class="alert alert-warning">
{{ __('No UrBackup server available for the root location of this asset.', 'urbackup') }}
</div>
</td>
</tr>
{% elseif can_connect %}
<tr class="tab_bg_1">
<td>{{ __('Available servers for root location', 'urbackup') }}</td>
<td>
<form method="post" action="{{ path('urbackup_asset_action') }}">
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="itemtype" value="{{ itemtype }}">
<input type="hidden" name="items_id" value="{{ items_id }}">
<select name="plugin_urbackup_servers_id">
{% for id, name in servers %}
<option value="{{ id }}">{{ name }}</option>
{% endfor %}
</select>
<button type="submit" name="connect" class="btn btn-primary">{{ __('Connect', 'urbackup') }}</button>
</form>
</td>
</tr>
{% else %}
<tr class="tab_bg_1">
<td colspan="2">
{{ __('A server is available, but you do not have permission to link this asset.', 'urbackup') }}
</td>
</tr>
{% endif %}
</table>
{% else %}
<table class="tab_cadre_fixe">
<tr>
<th colspan="4">{{ __('UrBackup status', 'urbackup') }}</th>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Linked server', 'urbackup') }}</td>
<td>{{ server_link }}</td>
<td>{{ __('IP address', 'urbackup') }}</td>
<td>{{ server_ip }}</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('UrBackup server version', 'urbackup') }}</td>
<td>{{ server_version }}</td>
<td>{{ __('Client name', 'urbackup') }}</td>
<td>{{ client_name }}</td>
</tr>
</table>
{% endif %}
</div>
-39
View File
@@ -1,39 +0,0 @@
<form method="post" action="{{ path('urbackup_config') }}">
<div class="center">
<table class="tab_cadre_fixe">
<tr>
<th colspan="3">{{ __('UrBackup configuration', 'urbackup') }}</th>
</tr>
<tr>
<th>{{ __('Asset type', 'urbackup') }}</th>
<th>{{ __('Enabled', 'urbackup') }}</th>
<th>{{ __('Default', 'urbackup') }}</th>
</tr>
{% for itemtype, label in configurable_types %}
<tr class="tab_bg_1">
<td>{{ label }}</td>
{% if itemtype == 'Computer' %}
<td>{{ __('Always enabled', 'urbackup') }}</td>
{% else %}
<td>
<input type="checkbox" name="assettypes[{{ itemtype }}]" value="1"{% if enabled_types[itemtype] %} checked{% endif %}>
</td>
{% endif %}
<td>
{% if itemtype == 'Computer' %}
{{ __('Yes', 'urbackup') }}
{% endif %}
</td>
</tr>
{% endfor %}
<tr>
<td colspan="3" class="center">
<button type="submit" name="update" class="btn btn-primary">
{{ __('Save', 'urbackup') }}
</button>
</td>
</tr>
</table>
</div>
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">
</form>
-118
View File
@@ -1,118 +0,0 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="center">
<form method="post" action="{{ path('urbackup_server_save') }}">
<table class="tab_cadre_fixe">
<tr>
<th colspan="4">{{ __('UrBackup server', 'urbackup') }}</th>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Name') }}</td>
<td>
<input type="text" name="name" value="{{ server.name ?? '' }}" size="40">
</td>
<td>{{ __('Active') }}</td>
<td>
<select name="is_active">
<option value="1"{% if server.is_active ?? true %} selected{% endif %}>{{ __('Yes') }}</option>
<option value="0"{% if not (server.is_active ?? true) %} selected{% endif %}>{{ __('No') }}</option>
</select>
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Entity') }}</td>
<td>
{{ render_entity_dropdown('entities_id', server.entities_id ?? 0) }}
</td>
<td>{{ __('Recursive') }}</td>
<td>
<select name="is_recursive">
<option value="1"{% if server.is_recursive ?? false %} selected{% endif %}>{{ __('Yes') }}</option>
<option value="0"{% if not (server.is_recursive ?? false) %} selected{% endif %}>{{ __('No') }}</option>
</select>
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Location') }}</td>
<td>
{{ render_location_dropdown('locations_id', server.locations_id ?? 0) }}
<br><small>{{ __('Associate the server with the main/root location. Assets in sub-locations will use this root location server.', 'urbackup') }}</small>
</td>
<td>{{ __('Protocol', 'urbackup') }}</td>
<td>
<select name="protocol">
<option value="http"{% if server.protocol == 'http' %} selected{% endif %}>HTTP</option>
<option value="https"{% if server.protocol == 'https' %} selected{% endif %}>HTTPS</option>
</select>
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('IP address', 'urbackup') }}</td>
<td>
<input type="text" name="ip_address" value="{{ server.ip_address ?? '' }}" size="40">
</td>
<td>{{ __('Network port', 'urbackup') }}</td>
<td>
<input type="number" name="port" value="{{ server.port ?? 55414 }}" min="1" max="65535">
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('UrBackup server version', 'urbackup') }}</td>
<td>
<input type="text" name="server_version" value="{{ server.server_version ?? '' }}" size="30">
</td>
<td>{{ __('Ignore SSL verification', 'urbackup') }}</td>
<td>
<select name="ignore_ssl">
<option value="1"{% if server.ignore_ssl ?? false %} selected{% endif %}>{{ __('Yes') }}</option>
<option value="0"{% if not (server.ignore_ssl ?? false) %} selected{% endif %}>{{ __('No') }}</option>
</select>
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('API username', 'urbackup') }}</td>
<td>
<input type="text" name="api_username" value="{{ server.api_username ?? '' }}" size="40" autocomplete="off">
</td>
<td>{{ __('API password', 'urbackup') }}</td>
<td>
<input type="password" name="api_password" value="{{ server.api_password ?? '' }}" autocomplete="new-password">
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Comments') }}</td>
<td colspan="3">
<textarea name="comment" rows="5" cols="100">{{ server.comment ?? '' }}</textarea>
</td>
</tr>
{% if server.id > 0 %}
<tr class="tab_bg_1">
<td>{{ __('UrBackup web interface', 'urbackup') }}</td>
<td colspan="3">
{% if server.url %}
<a href="{{ server.url }}" target="_blank" rel="noopener" class="btn btn-secondary">
{{ __('Open UrBackup interface', 'urbackup') }}
</a>
{% else %}
{{ __('No URL available', 'urbackup') }}
{% endif %}
</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('API test', 'urbackup') }}</td>
<td colspan="3">
<button type="button" class="btn btn-primary plugin-urbackup-test-api" data-server-id="{{ server.id }}">
{{ __('Test API connection', 'urbackup') }}
</button>
<span id="plugin-urbackup-api-test-result"></span>
</td>
</tr>
{% endif %}
</table>
<input type="hidden" name="id" value="{{ server.id ?? 0 }}">
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}">
<button type="submit" name="save" class="btn btn-primary">{{ __('Save') }}</button>
</form>
</div>
{% endblock %}
-51
View File
@@ -1,51 +0,0 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="center">
<table class="tab_cadre_fixe">
<tr>
<th colspan="6">{{ __('UrBackup servers', 'urbackup') }}</th>
</tr>
<tr>
<th>{{ __('Name') }}</th>
<th>{{ __('IP address', 'urbackup') }}</th>
<th>{{ __('Port') }}</th>
<th>{{ __('Version') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Actions') }}</th>
</tr>
{% for server in servers %}
<tr class="tab_bg_1">
<td>{{ server.name }}</td>
<td>{{ server.ip_address }}</td>
<td>{{ server.port }}</td>
<td>{{ server.server_version }}</td>
<td>
{% if server.last_api_status %}
<span class="badge bg-success">{{ __('OK', 'urbackup') }}</span>
{% else %}
<span class="badge bg-danger">{{ __('Failed', 'urbackup') }}</span>
{% endif %}
</td>
<td>
<a href="{{ path('urbackup_server_show', {'id': server.id}) }}" class="btn btn-sm btn-primary">
{{ __('View') }}
</a>
<button class="btn btn-sm btn-secondary plugin-urbackup-test-api" data-server-id="{{ server.id }}">
{{ __('Test API', 'urbackup') }}
</button>
</td>
</tr>
{% else %}
<tr class="tab_bg_1">
<td colspan="6" class="center">{{ __('No records found', 'urbackup') }}</td>
</tr>
{% endfor %}
</table>
{% if can_create %}
<a href="{{ path('urbackup_server_show', {'id': 0}) }}" class="btn btn-success">
{{ __('Add') }}
</a>
{% endif %}
</div>
{% endblock %}