26 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
mariano f7c1ab8aa4 diritti sistemati 2026-05-21 12:29:33 +02:00
mariano f0a5be45ee traduzione e composer.json 2026-05-21 11:08:15 +02:00
mariano f0eaa76f17 change Author 2026-05-21 10:57:59 +02:00
mariano 223180d1a9 pluizia codice 2026-05-21 10:54:04 +02:00
mariano 5fac26040a change icon to ti-cloud-up 2026-05-21 10:25:21 +02:00
mariano 9ee28330ac modifica icone 2 2026-05-21 09:49:06 +02:00
mariano 8a52489b41 modifica icone 2026-05-21 09:37:58 +02:00
mariano 9979d62a2a sistemazione finale - server .php 2026-05-21 09:26:03 +02:00
mariano 01d678e855 group in list 2026-05-21 07:35:32 +02:00
mariano 0e80c1430f problema gruppo pc 2026-05-20 14:21:18 +02:00
mariano d69c465c92 modifica lista missing computer 2026-05-20 14:17:45 +02:00
mariano aba2ecf4aa modifica visualizzazione pagina server 2026-05-20 14:08:23 +02:00
mariano df28fc0018 sistemazione erore pag server.form.php 2026-05-20 13:48:33 +02:00
mariano 7a578db677 modifica pulsanti comandi 2026-05-20 13:44:44 +02:00
mariano 771409e396 tab in pagina computer.form 2026-05-20 13:39:31 +02:00
mariano 6441a8636d elimina pippo.txt 2026-05-20 13:16:07 +02:00
mariano 6a49489b73 merge all 2026-05-20 13:11:03 +02:00
mariano f4bd50c1b1 pulsanti e sistemazione create client 2026-05-20 11:16:14 +02:00
47 changed files with 1486 additions and 1985 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 -248
View File
@@ -1,269 +1,94 @@
# UrBackup Plugin - Memoria di Sviluppo
# MEMORY.md - Stato del Plugin UrBackup
## 📋 Informazioni Plugin
## Ultima modifica: 28/05/2026
- **Nome**: UrBackup
- **Versione**: 0.4.2
- **Compatibilità**: GLPI 11.0.6+, PHP 8.3+
- **Namespace**: `GlpiPlugin\Urbackup`
## GLPI 11 — Session::checkCSRF() breaking change
In GLPI 11, `Session::checkCSRF()` richiede il primo argomento `$data` (i dati POST da validare). In GLPI ≤10 accettava zero argomenti.
Il listener globale `CheckCsrfListener` gestisce già il CSRF per tutte le richieste POST, ma il plugin chiamava `Session::checkCSRF()` senza argomenti causando errore fatale.
## ✅ 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
- ✅ 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`
## Performance - Asset Definition vs Computer
### Struttura File
- `setup.php`: Hook di inizializzazione, menu, tab, versione, prerequisiti
- `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`)
### Problema
Gli Asset Definition custom (GLPI 11) sono 2-5x più lenti dei Computer nativi nel caricamento del tab UrBackup.
### Classi Principali
| Classe | Descrizione |
|--------|-------------|
| `Config` | Configurazione plugin, tipi asset abilitati |
| `Profile` | Diritti utente, permessi |
| `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 |
### Causa
L'overhead è in GLPI 11 core, non nel plugin:
1. **`Asset::__construct()`** — itera 30+ Capacity classi per ogni istanza
2. **`Asset::post_getFromDB()`** — decodifica JSON custom fields e li processa
3. **`eval()` autoloading** — le classi concrete sono definite via `eval()` a runtime
### Pagine Frontend
- `front/config.form.php`: Pagina configurazione plugin
- `front/server.php`: Lista server UrBackup
- `front/server.form.php`: Form aggiunta/modifica server
- `front/asset.form.php`: Form azioni su asset (connect/disconnect/start backup)
- `ajax/server_test.php`: Endpoint AJAX per test connessione API
### Ottimizzazioni applicate
1. **`AssetTab.php::loadApiData()`** — letto `$item->fields['name']` direttamente invece di chiamare `ServerAsset::getAssetName()` che faceva una seconda `getFromDB()` ridondante
2. **`AssetTab.php::startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`, `showServerLinkedBlock()`** — stesso pattern, `$item->fields['name']` al posto di `getAssetName()`
3. **`Server.php::showMissingClientsTab()`** — batch loading IP e gruppi: `batchLoadIps()` (1 query per itemtype vs 1 per riga) + `batchLoadGroups()` (1 query per itemtype vs 1 per riga)
### Interfacce GLPI
- ✅ Menu: Amministrazione → Server UrBackup
- ✅ Tab: UrBackup su Computer e asset abilitati
- ✅ Configurazione: Abilitazione tipi asset (legacy + Asset Definition GLPI 11)
## Architettura
- `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()`
- `setup.php` — registra capacità, CSS, JS, hook
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log)
- `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
2. **Duplicate profilerights**: Aggiunto check in `Profile::registerRights()` per evitare duplicati
3. **CSRF**: Include `inc/includes.php` nei file front per gestione CSRF
4. **Firme metodi**: Corretto `getTabNameForItem(CommonGLPI, int)` e `displayTabContentForItem(CommonGLPI, int, int)`
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.
## Cache
- `UrbackupApiClient`: cache in-memory per `getStatus()` e `getClientSettings()`
- `AssetTab::loadApiData()`: cache sessione 30s (chiave: server_id + client_name)
- API timeout: 30s, connect timeout: 5s
### 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:
- Rimosso il pulsante "Test API Connection" manuale
- Rimosse le righe "Last API status", "Last API message", "Last API check"
- Aggiunto controllo automatico della connessione API eseguito ad ogni visualizzazione del form
### Cause (2 bug distinti)
1. **`src/Server.php:436`** — `$canUpdate` controllava solo `Session::haveRight(self::$rightname, UPDATE)`. In fase di creazione di un nuovo server, l'utente ha diritto CREATE ma non necessariamente UPDATE, quindi il campo non veniva mostrato.
2. **`src/Profile.php:73-77`** — `installRights()` usava `$_SESSION['glpiactiveprofile']['id']` per assegnare i diritti completi al profilo corrente. In installazione via CLI (`php bin/console glpi:plugin:install`), non c'è sessione, quindi `$profiles_id = 0` e nessun profilo riceveva i diritti completi.
**Stati visualizzati**:
- 🟢 **Verde** "Connessione API OK" - Connessione riuscita
- 🔴 **Rosso** "Connessione API fallita" - Errore di autenticazione o configurazione
- 🟡 **Giallo** "Server irraggiungibile" - Problemi di rete (timeout, DNS, HTTP 4xx/5xx, SSL)
### Fix applicati
1. **`Server.php::showFormFields()`** — Sostituito `$canUpdate` con `$canEdit`: per nuovi server (`$ID <= 0`) usa `CREATE`, per server esistenti usa `UPDATE`.
2. **`Profile.php::installRights()`** — In assenza di sessione attiva (CLI), cerca il profilo "Super-Admin" tramite query diretta e gli assegna tutti i diritti.
**File modificati**:
- `src/Server.php`: Rimossa sezione pulsante e aggiunto metodo `testApiConnection()`
- `locales/it_IT.po`, `locales/en_GB.po`, `locales/de_DE.po`: Aggiunte traduzioni
## Bugfix: Unknown column 'users_id' — Group::getDataItems() SQL error
### API UrBackup - Endpoints Verificati
| Endpoint | Metodo | Note |
|----------|--------|------|
| `/x?a=login` | POST | Login anonimo o con sessione |
| `/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
### Problema
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
WHERE ((... ) OR (`users_id` IN (SELECT `users_id` FROM `glpi_groups_users` WHERE `groups_id` IN ('7483'))))
```
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/
- Plugin Example: https://github.com/pluginsGLPI/example
- GLPI Inventory: https://github.com/glpi-project/glpi-inventory-plugin
### Fix
1. **`install/mysql/plugin_urbackup-empty.sql`** — Aggiunta colonna `users_id INT UNSIGNED NOT NULL DEFAULT 0` dopo `locations_id` + indice
2. **`install/install.php`** — Aggiunto `$migration->addField('users_id', ...)` e `$migration->addKey('users_id')` nella migrazione del server
3. **`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
---
## Versione
- 0.7.0
## 🔌 API UrBackup - Riferimenti Ufficiali
### Documentazione UrBackup Web API
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`).
### Risorse API Utilizzate
1. **Python Wrapper** (uroni/urbackup-server-python-web-api-wrapper)
- URL: https://github.com/uroni/urbackup-server-python-web-api-wrapper
- Esempio di connessione:
```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
## 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`
2. **File deprecati rimossi**: 12 file (composer copy, AGENTS_OLD.MD, js/, ajax/, front/test/view, FIX_PERMISSIONS.sh, removed/)
3. **Dead code rimosso**: 3 Controller, 1 Command, 4 template Twig
4. **DB migration**: DROP `glpi_plugin_urbackup_profiles` e `glpi_plugin_urbackup_assettypes`; add index `location_active` su servers; add `date_creation`/`date_mod` su serverassets
5. **SQL schema pulito**: Rimosse tabelle legacy dall'empty.sql
6. **PHP lint warnings**: Rimosse `use Html;` e `use Search;` superflue
7. **README.md** creato
8. **PURGE right**: Aggiunto PURGE ai diritti di installazione (`Profile.php:76,90`); applicato a tutti i profili Super-Admin esistenti nel DB
9. **CSRF GLPI 11 fix**: `Session::checkCSRF()` richiede `$_POST` come argomento in GLPI 11; aggiunto `X-Glpi-Csrf-Token` header al JS AJAX
10. **Session::isDebugActive()**: Metodo inesistente in GLPI 11; sostituito in `AssetTab.php:150` con `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`
+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
centralizzata dei server UrBackup e dei client direttamente dallinterfaccia GLPI.
[![GLPI](https://img.shields.io/badge/GLPI-11.0.6%2B-blue?logo=glpi)](https://glpi-project.org)
[![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:
- 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).
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.
---
## 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 |
|-----------|----------|
| GLPI | 11.x |
| PHP | ≥ 8.3 |
| Database | MySQL / MariaDB (GLPI standard) |
## Requirements
---
| 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
- Percorso: **Configurazione → Plugin → UrBackup**
- Asset supportati:
- Computer (sempre attivo)
- Altri asset GLPI configurabili
- Attivazione automatica tab e massive action sugli asset abilitati
1. **Download** the plugin and extract it to `GLPI_ROOT/plugins/urbackup/`
2. **Install** via GLPI web interface or CLI:
---
```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()]);
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "finstral/glpi-urbackup-plugin",
"name": "glpi/urbackup-plugin",
"description": "UrBackup integration plugin for GLPI 11",
"type": "glpi-plugin",
"license": "GPL-2.0-or-later",
-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()]);
}
+30 -15
View File
@@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
use Glpi\Exception\Http\BadRequestHttpException;
use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
@@ -25,11 +28,11 @@ $itemtype = (string) ($_POST['itemtype'] ?? $_GET['itemtype'] ?? '');
$items_id = (int) ($_POST['items_id'] ?? $_GET['items_id'] ?? 0);
if ($itemtype === '' || $items_id <= 0) {
Html::displayValidationError(__('Invalid parameters'));
throw new BadRequestHttpException(__('Invalid parameters', 'urbackup'));
}
if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) {
Html::displayValidationError(__('Item type not enabled for UrBackup'));
throw new BadRequestHttpException(__('Item type not enabled for UrBackup', 'urbackup'));
}
$item = getItemForItemtype($itemtype);
@@ -46,13 +49,23 @@ if (isset($_POST['connect'])) {
$server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) {
Html::displayValidationError(__('No server selected'));
Session::addMessageAfterRedirect(
__('No server selected', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
$link = ServerAsset::getLinkForAsset($itemtype, $items_id);
if ($link !== null) {
Html::displayValidationError(__('Asset is already linked to a server'));
Session::addMessageAfterRedirect(
__('Asset is already linked to a server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
$result = ServerAsset::createForAsset($itemtype, $items_id, $server_id);
@@ -61,7 +74,12 @@ if (isset($_POST['connect'])) {
$item->getFromDB($items_id);
Html::redirect($item->getLinkURL());
} else {
Html::displayValidationError(__('Failed to link asset to server'));
Session::addMessageAfterRedirect(
__('Failed to link asset to server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
}
@@ -70,21 +88,18 @@ if (isset($_POST['disconnect'])) {
Html::displayRightError();
}
global $DB;
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, false);
$result = ServerAsset::disconnectAsset($itemtype, $items_id);
if ($link === null) {
Html::displayValidationError(__('Asset is not linked to any server'));
}
$result = $DB->delete('glpi_plugin_urbackup_serverassets', ['id' => (int) $link['id']]);
if ($result) {
$item->getFromDB($items_id);
Html::redirect($item->getLinkURL());
} else {
Html::displayValidationError(__('Failed to disconnect asset from server'));
Session::addMessageAfterRedirect(
__('Failed to disconnect asset from server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Config;
use Html;
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);
+114 -23
View File
@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use Html;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
@@ -11,7 +12,7 @@ if (!defined('GLPI_ROOT')) {
include_once GLPI_ROOT . "/inc/includes.php";
if (!Profile::canCurrentUser(UPDATE)) {
if (!Profile::canCurrentUser(READ)) {
Html::displayRightError();
}
@@ -48,30 +49,120 @@ $ID = $_GET['id'] ?? null;
Html::header(
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
'',
'Assets',
'admin',
'GlpiPlugin\Urbackup\Server'
);
$server->showForm($ID);
if ($ID > 0) {
global $DB;
if ($ID > 0 && $server->getFromDB($ID)) {
echo '<div class="card mt-4">';
echo '<div class="card-header">';
echo '<h5 class="mb-0">' . htmlspecialchars(__('Linked clients', 'urbackup')) . '</h5>';
echo '</div>';
echo '<div class="card-body">';
Server::showLinkedClientsTab($server);
echo '</div>';
echo '</div>';
echo '<div class="card mt-4">';
echo '<div class="card-header">';
echo '<h5 class="mb-0">' . htmlspecialchars(__('Unlinked clients', 'urbackup')) . '</h5>';
echo '</div>';
echo '<div class="card-body">';
Server::showUnlinkedClientsTab($server);
echo '</div>';
echo '</div>';
$server->getFromDB($ID);
$serverIterator = $DB->request([
'FROM' => Server::getTable(),
'ORDER' => 'name',
]);
$serverIds = [];
foreach ($serverIterator as $row) {
$serverIds[(int) $row['id']] = (string) $row['name'];
}
$serverIdKeys = array_keys($serverIds);
$currentIndex = array_search((int) $ID, $serverIdKeys, true);
$totalServers = count($serverIds);
$prevId = ($currentIndex !== false && $currentIndex > 0) ? $serverIdKeys[$currentIndex - 1] : null;
$nextId = ($currentIndex !== false && $currentIndex < $totalServers - 1) ? $serverIdKeys[$currentIndex + 1] : null;
$baseUrl = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
?>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<a href="<?php echo htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php'); ?>" class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('List')); ?>">
<i class="ti ti-list-search fs-2"></i>
</a>
<?php if ($prevId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $prevId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Previous')); ?>">
<i class="fs-2 ti ti-chevron-left"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary me-2 bs-invisible">
<i class="fs-2 ti ti-chevron-left"></i>
</span>
<?php endif; ?>
<span class="fw-bold mx-2"><?php echo htmlspecialchars(($currentIndex !== false ? $currentIndex + 1 : 1) . ' / ' . $totalServers); ?></span>
<?php if ($nextId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $nextId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary ms-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Next')); ?>">
<i class="fs-2 ti ti-chevron-right"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary ms-2 bs-invisible">
<i class="fs-2 ti ti-chevron-right"></i>
</span>
<?php endif; ?>
</div>
</div>
<div class="row">
<div class="col-2">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-server">
<?php echo htmlspecialchars(__('Server')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-linked">
<?php echo htmlspecialchars(__('Linked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-unlinked">
<?php echo htmlspecialchars(__('Unlinked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-missing">
<?php echo htmlspecialchars(__('Missing clients', 'urbackup')); ?>
</a>
</li>
</ul>
</div>
<div class="col-10">
<div class="tab-content">
<div class="tab-pane active" id="tab-server">
<?php $server->showForm($ID); ?>
</div>
<div class="tab-pane" id="tab-linked">
<?php Server::showLinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-unlinked">
<?php Server::showUnlinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-missing">
<?php Server::showMissingClientsTab($server); ?>
</div>
</div>
</div>
</div>
<?php
} else {
$server->showForm($ID);
}
Html::footer();
?>
<script>
$(function () {
var hash = window.location.hash;
if (hash) {
$('[data-bs-toggle="tab"][href="' + hash + '"]').tab('show');
}
$('[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
window.location.hash = $(this).attr('href');
});
});
</script>
<?php
Html::footer();
+6 -20
View File
@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use Html;
use Search;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
@@ -15,27 +15,13 @@ if (!Profile::canCurrentUser(READ)) {
Html::displayRightError();
}
$can_read = Profile::canCurrentUser(READ);
$can_create = Profile::canCurrentUser(CREATE);
Html::header(
'UrBackup Servers',
__('UrBackup Servers', 'urbackup'),
'',
'Assets',
''
'admin',
'GlpiPlugin\Urbackup\Server'
);
if ($can_create) {
echo "<div class='center'>";
echo Html::link(
__('Add UrBackup server', 'urbackup'),
'/plugins/urbackup/front/server.form.php',
['class' => 'btn btn-primary']
);
echo "</div>";
echo "<br>";
}
Search::show('GlpiPlugin\Urbackup\Server', [
'is_deleted' => 0,
'massiveaction' => true,
@@ -45,4 +31,4 @@ Search::show('GlpiPlugin\Urbackup\Server', [
],
]);
Html::footer();
Html::footer();
-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();
+6 -3
View File
@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* AJAX endpoint for testing UrBackup API
*/
@@ -11,21 +14,21 @@ Html::header_nocache();
Session::checkLoginUser();
if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => 'No permission']);
echo json_encode(['success' => false, 'message' => __('No permission', 'urbackup')]);
exit;
}
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
echo json_encode(['success' => false, 'message' => __('Invalid server ID', 'urbackup')]);
exit;
}
$server = new GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]);
exit;
}
-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();
+83 -39
View File
@@ -1,26 +1,20 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*
* Install/update process for GLPI 11.
*
* GLPI 11 Migration class does not expose createTable()/addTable().
* Compatible GLPI plugin pattern:
*
* - initial schema creation from SQL file using $DB->runFile()
* - schema evolution using Migration addField(), addKey(), executeMigration()
*
* -------------------------------------------------------------------------
*/
use Glpi\Asset\AssetDefinitionManager;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
if (!defined('GLPI_ROOT')) {
die('Sorry. You cannot access this file directly.');
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
}
/**
@@ -37,10 +31,15 @@ function plugin_urbackup_install_process(): bool
plugin_urbackup_install_create_initial_schema($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_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();
@@ -63,10 +62,8 @@ function plugin_urbackup_install_create_initial_schema(Migration $migration): vo
$required_tables = [
'glpi_plugin_urbackup_configs',
'glpi_plugin_urbackup_assettypes',
'glpi_plugin_urbackup_servers',
'glpi_plugin_urbackup_serverassets',
'glpi_plugin_urbackup_profiles',
];
$missing_table_found = false;
@@ -161,11 +158,6 @@ function plugin_urbackup_install_update_assettypes_table(Migration $migration):
'after' => 'itemtype',
]);
$migration->addField($table, 'is_default', 'bool', [
'value' => 0,
'after' => 'is_active',
]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
@@ -210,6 +202,11 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
'after' => 'name',
]);
$migration->addField($table, 'users_id', 'integer', [
'value' => 0,
'after' => 'locations_id',
]);
$migration->addField($table, 'ip_address', 'string', [
'value' => '',
'after' => 'locations_id',
@@ -270,7 +267,9 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
$migration->addKey($table, 'name');
$migration->addKey($table, 'entities_id');
$migration->addKey($table, 'locations_id');
$migration->addKey($table, 'users_id');
$migration->addKey($table, 'is_active');
$migration->addKey($table, ['locations_id', 'is_active'], 'location_active');
}
/**
@@ -305,44 +304,89 @@ function plugin_urbackup_install_update_serverassets_table(Migration $migration)
'after' => 'itemtype',
]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, 'plugin_urbackup_servers_id');
$migration->addKey($table, ['itemtype', 'items_id'], 'item');
}
/**
* Update profiles table.
* Drop legacy tables that are no longer used.
*
* @param Migration $migration Migration instance
*
* @return void
*/
function plugin_urbackup_install_update_profiles_table(Migration $migration): void
function plugin_urbackup_install_drop_legacy_tables(Migration $migration): void
{
global $DB;
// glpi_plugin_urbackup_profiles was replaced by Profile::registerRights() in 0.5.0
$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)) {
return;
}
$migration->addField($table, 'profiles_id', 'integer', [
'value' => 0,
'after' => 'id',
]);
// Check if is_default column exists
$has_is_default = false;
foreach ($DB->listFields($table) as $col) {
if ($col['Field'] === 'is_default') {
$has_is_default = true;
break;
}
}
$migration->addField($table, 'rightname', 'string', [
'value' => '',
'after' => 'profiles_id',
]);
// Auto-enable UrBackup capacity on ALL Asset Definitions
// so the tab appears out of the box for any asset type.
if (class_exists(AssetDefinitionManager::class)) {
try {
$manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
if (!$definition->hasCapacity(UrBackupCapacity::class)) {
$definition->enableCapacity(UrBackupCapacity::class);
}
}
} catch (\Throwable $e) {
$migration->displayMessage(
__('Error enabling UrBackup capacity on definitions: ', 'urbackup') . $e->getMessage()
);
}
}
$migration->addField($table, 'rights', 'integer', [
'value' => 0,
'after' => 'rightname',
]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, ['profiles_id', 'rightname'], 'profile_right');
// Drop is_default column (no longer needed in capacity system)
if ($has_is_default) {
$migration->dropField($table, 'is_default');
}
}
+7 -23
View File
@@ -8,24 +8,13 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_configs` (
KEY `name` (`name`)
) 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` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`entities_id` INT UNSIGNED NOT NULL DEFAULT 0,
`is_recursive` TINYINT NOT NULL DEFAULT 0,
`name` VARCHAR(255) NOT NULL DEFAULT '',
`locations_id` INT UNSIGNED NOT NULL DEFAULT 0,
`users_id` INT UNSIGNED NOT NULL DEFAULT 0,
`ip_address` VARCHAR(255) NOT NULL DEFAULT '',
`port` INT UNSIGNED NOT NULL DEFAULT 55414,
`protocol` VARCHAR(10) NOT NULL DEFAULT 'http',
@@ -44,7 +33,9 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
KEY `name` (`name`),
KEY `entities_id` (`entities_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;
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,
`itemtype` VARCHAR(255) NOT NULL DEFAULT '',
`items_id` INT UNSIGNED NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`),
KEY `item` (`itemtype`, `items_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_profiles` (
`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;
-- glpi_plugin_urbackup_profiles was dropped in 0.7.0 (replaced by Profile::registerRights())
+3 -4
View File
@@ -1,18 +1,17 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*
* Uninstall process for GLPI 11.
* -------------------------------------------------------------------------
*/
use GlpiPlugin\Urbackup\Profile;
if (!defined('GLPI_ROOT')) {
die('Sorry. You cannot access this file directly.');
die(__('Sorry. You cannot access this file directly.', 'urbackup'));
}
/**
-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);
}
})();
BIN
View File
Binary file not shown.
+55 -1
View File
@@ -155,4 +155,58 @@ msgid "Last backup"
msgstr "Letztes Backup"
msgid "Show"
msgstr "Anzeigen"
msgstr "Anzeigen"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API-Fehler"
msgid "Unknown"
msgstr "Unbekannt"
msgid "Asset Definition"
msgstr "Asset-Definition"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "UrBackup-Server"
msgid "Invalid parameters"
msgstr "Ungültige Parameter"
msgid "Item type not enabled for UrBackup"
msgstr "Elementtyp für UrBackup nicht aktiviert"
msgid "No server selected"
msgstr "Kein Server ausgewählt"
msgid "Asset is already linked to a server"
msgstr "Asset ist bereits mit einem Server verknüpft"
msgid "Failed to link asset to server"
msgstr "Verknüpfung von Asset mit Server fehlgeschlagen"
msgid "Failed to disconnect asset from server"
msgstr "Trennung von Asset vom Server fehlgeschlagen"
msgid "No permission"
msgstr "Keine Berechtigung"
msgid "Invalid server ID"
msgstr "Ungültige Server-ID"
msgid "Server not found"
msgstr "Server nicht gefunden"
msgid "Sorry. You cannot access this file directly."
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
msgid "No assets"
msgstr "Keine Assets"
BIN
View File
Binary file not shown.
+55 -1
View File
@@ -155,4 +155,58 @@ msgid "Last backup"
msgstr "Last backup"
msgid "Show"
msgstr "Show"
msgstr "Show"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API Error"
msgid "Unknown"
msgstr "Unknown"
msgid "Asset Definition"
msgstr "Asset Definition"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "UrBackup Servers"
msgid "Invalid parameters"
msgstr "Invalid parameters"
msgid "Item type not enabled for UrBackup"
msgstr "Item type not enabled for UrBackup"
msgid "No server selected"
msgstr "No server selected"
msgid "Asset is already linked to a server"
msgstr "Asset is already linked to a server"
msgid "Failed to link asset to server"
msgstr "Failed to link asset to server"
msgid "Failed to disconnect asset from server"
msgstr "Failed to disconnect asset from server"
msgid "No permission"
msgstr "No permission"
msgid "Invalid server ID"
msgstr "Invalid server ID"
msgid "Server not found"
msgstr "Server not found"
msgid "Sorry. You cannot access this file directly."
msgstr "Sorry. You cannot access this file directly."
msgid "No assets"
msgstr "No assets"
BIN
View File
Binary file not shown.
+55 -1
View File
@@ -155,4 +155,58 @@ msgid "Last backup"
msgstr "Ultimo backup"
msgid "Show"
msgstr "Mostra"
msgstr "Mostra"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "Errore API"
msgid "Unknown"
msgstr "Sconosciuto"
msgid "Asset Definition"
msgstr "Definizione asset"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "Server UrBackup"
msgid "Invalid parameters"
msgstr "Parametri non validi"
msgid "Item type not enabled for UrBackup"
msgstr "Tipo oggetto non abilitato per UrBackup"
msgid "No server selected"
msgstr "Nessun server selezionato"
msgid "Asset is already linked to a server"
msgstr "L'asset è già collegato a un server"
msgid "Failed to link asset to server"
msgstr "Collegamento asset al server fallito"
msgid "Failed to disconnect asset from server"
msgstr "Disconnessione asset dal server fallita"
msgid "No permission"
msgstr "Nessun permesso"
msgid "Invalid server ID"
msgstr "ID server non valido"
msgid "Server not found"
msgstr "Server non trovato"
msgid "Sorry. You cannot access this file directly."
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
msgid "No assets"
msgstr "Nessun Asset"
View File
+43
View File
@@ -10,3 +10,46 @@
color: #b00;
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();
xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Glpi-Csrf-Token', getAjaxCsrfToken());
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000;
-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."
+28 -22
View File
@@ -1,19 +1,30 @@
<?php
// Force OPcache to reload this file when accessed via web
declare(strict_types=1);
// Force OPcache to reload plugin files when accessed via web
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 GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
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_MAX_GLPI', '11.99.99');
@@ -50,12 +61,19 @@ function plugin_init_urbackup(): void
Plugin::registerClass(ServerAsset::class);
Plugin::registerClass(PluginUrbackupMassiveAction::class);
// Register tab on Computer and other assets
$enabled_types = Config::getEnabledItemtypes();
if (!empty($enabled_types)) {
Plugin::registerClass(AssetTab::class, [
'addtabon' => $enabled_types,
]);
// Register tab on Computer (legacy, always enabled)
Plugin::registerClass(AssetTab::class, [
'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';
@@ -73,18 +91,6 @@ function plugin_init_urbackup(): void
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [
'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();
}
/**
@@ -97,7 +103,7 @@ function plugin_version_urbackup(): array
return [
'name' => __('UrBackup', 'urbackup'),
'version' => PLUGIN_URBACKUP_VERSION,
'author' => 'Finstral',
'author' => 'Mariano Benzi',
'license' => 'GPL-2.0-or-later',
'homepage' => '',
'requirements' => [
+65 -23
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
@@ -55,15 +57,19 @@ class AssetTab extends CommonDBTM
*/
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 '';
}
if (!Profile::canCurrentUser(READ)) {
if (!$hasRight) {
return '';
}
return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-upload');
return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-up');
}
/**
@@ -226,7 +232,7 @@ class AssetTab extends CommonDBTM
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(ServerAsset::getAssetName($item::class, (int) $item->fields['id'])) . "</td>";
echo "<td>" . htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</td>";
echo "</tr>";
echo "</table>";
@@ -257,9 +263,14 @@ class AssetTab extends CommonDBTM
*/
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);
$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 = [
'client_found' => false,
'client_status' => [],
@@ -303,6 +314,11 @@ class AssetTab extends CommonDBTM
$data['error'] = $e->getMessage();
}
$_SESSION[$cache_key] = [
'time' => time(),
'data' => $data,
];
return $data;
}
@@ -324,15 +340,42 @@ class AssetTab extends CommonDBTM
): void {
echo "<div class='plugin-urbackup-inner-tabs'>";
echo "<h3>" . htmlspecialchars(__('State', 'urbackup')) . "</h3>";
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
echo '<ul class="nav nav-tabs" id="urbackupTabs" role="tablist">';
echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab" aria-selected="true">';
echo htmlspecialchars(__('State', 'urbackup'));
echo '</a></li>';
if ($canWrite) {
echo '<li class="nav-item" role="presentation">';
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 '</a></li>';
}
echo '<li class="nav-item" role="presentation">';
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 '</a></li>';
echo '</ul>';
echo '<div class="tab-content">';
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
self::showStateSection($server, $link, $api_data);
echo '</div>';
echo "<h3>" . htmlspecialchars(__('Actions', 'urbackup')) . "</h3>";
self::showActionsSection($item, $server, $link, $api_data);
if ($canWrite) {
echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
self::showActionsSection($item, $server, $link, $api_data);
echo '</div>';
}
echo "<h3>" . htmlspecialchars(__('Info / Log', 'urbackup')) . "</h3>";
echo '<div class="tab-pane fade" id="logs" role="tabpanel">';
self::showInfoLogSection($api_data);
echo '</div>';
echo '</div>';
echo "</div>";
}
@@ -427,17 +470,6 @@ class AssetTab extends CommonDBTM
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('Available actions', 'urbackup')) . "</th></tr>";
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo htmlspecialchars(__('You do not have permission for UrBackup actions.', 'urbackup'));
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
if (!$api_data['client_found'] && Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Create client in UrBackup', 'urbackup')) . "</td>";
@@ -465,10 +497,20 @@ class AssetTab extends CommonDBTM
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>";
echo "<td>";
echo "<div style='display:flex;gap:24px'>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('File backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('Image backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "</div>";
echo "</td>";
echo "</tr>";
@@ -803,7 +845,7 @@ class AssetTab extends CommonDBTM
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
$client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') {
return false;
}
@@ -841,7 +883,7 @@ class AssetTab extends CommonDBTM
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
$client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') {
return false;
}
@@ -871,7 +913,7 @@ class AssetTab extends CommonDBTM
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
$client_name = (string) ($item->fields['name'] ?? '');
if ($client_name === '') {
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;
}
}
}
+33 -282
View File
@@ -11,8 +11,7 @@ declare(strict_types=1);
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Computer;
use Glpi\Asset\AssetDefinition;
use Glpi\Asset\Asset;
use Glpi\Asset\AssetDefinitionManager;
use DBmysql;
use Html;
@@ -53,67 +52,8 @@ class Config extends CommonDBTM
*/
public static function ensureDefaultConfiguration(): void
{
self::ensureAssetType('Computer', true, true);
}
/**
* 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'),
]
);
// Computer is always enabled by default.
// Asset Definition types are managed via the native Capacities UI.
}
/**
@@ -135,8 +75,6 @@ class Config extends CommonDBTM
*/
public static function isItemtypeEnabled(string $itemtype): bool
{
global $DB;
if ($itemtype === '') {
return false;
}
@@ -145,22 +83,15 @@ class Config extends CommonDBTM
return true;
}
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return false;
// All Asset subclasses are enabled by default.
// The UrBackupCapacity is still registered for cleanup (onCapacityDisabled)
// and usage tracking in the Capacities UI, but visibility is always on
// so the tab appears out of the box on any Asset Definition.
if (is_a($itemtype, Asset::class, true)) {
return true;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
'is_active' => 1,
],
'LIMIT' => 1,
]);
return count($iterator) > 0;
return false;
}
/**
@@ -170,54 +101,19 @@ class Config extends CommonDBTM
*/
public static function getEnabledItemtypes(): array
{
global $DB;
$itemtypes = ['Computer'];
$types = ['Computer'];
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return $types;
}
$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;
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
$classname = $definition->getAssetClassName();
if (!in_array($classname, $itemtypes, true)) {
$itemtypes[] = $classname;
}
}
}
return $types;
}
/**
* 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,
]);
}
return $itemtypes;
}
/**
@@ -232,71 +128,22 @@ class Config extends CommonDBTM
{
Session::checkRight('config', UPDATE);
echo "<form method='post' action='" . htmlspecialchars($options['target'] ?? '') . "'>";
echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='4'>" . 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><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
echo "<tr class='tab_bg_1'>";
echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>";
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</span></td>";
echo "</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 "<td>" . htmlspecialchars($label) . "</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'>Asset Definition</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'>Legacy</span></td>";
}
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 "</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;
}
@@ -304,6 +151,9 @@ class Config extends CommonDBTM
/**
* Save configuration.
*
* In the new capacity system, asset types are managed via the native Capacities UI.
* This method is kept for backward compatibility but does nothing.
*
* @param array<string, mixed> $input Input data
*
* @return void
@@ -311,104 +161,5 @@ class Config extends CommonDBTM
public static function saveConfiguration(array $input): void
{
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);
}
}
}
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
+18 -3
View File
@@ -14,7 +14,7 @@ class Profile extends \Profile
public static function getIcon()
{
return 'ti ti-server';
return 'ti ti-cloud-up';
}
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
@@ -73,7 +73,22 @@ class Profile extends \Profile
$profiles_id = (int) ($_SESSION['glpiactiveprofile']['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;
@@ -83,7 +98,7 @@ class Profile extends \Profile
]);
foreach ($all_profiles as $profile) {
if ($profile['id'] !== $profiles_id) {
if ((int) $profile['id'] !== $profiles_id) {
$existing = $DB->request([
'FROM' => 'glpi_profilerights',
'WHERE' => [
+582 -52
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
@@ -12,9 +14,12 @@ use CommonDBTM;
use CommonGLPI;
use Dropdown;
use Entity;
use Group;
use Html;
use Location;
use Session;
use State;
use User;
class Server extends CommonDBTM
{
@@ -151,7 +156,7 @@ class Server extends CommonDBTM
if (self::canView()) {
$menu['title'] = self::getMenuName();
$menu['page'] = self::getSearchURL(false);
$menu['icon'] = 'ti ti-server';
$menu['icon'] = 'ti ti-cloud-up';
$menu['links']['search'] = self::getSearchURL(false);
@@ -254,6 +259,14 @@ class Server extends CommonDBTM
$tab[] = [
'id' => 8,
'table' => User::getTable(),
'field' => 'name',
'name' => User::getTypeName(1),
'datatype' => 'dropdown',
];
$tab[] = [
'id' => 9,
'table' => self::getTable(),
'field' => 'is_active',
'name' => __('Active'),
@@ -261,7 +274,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 9,
'id' => 10,
'table' => self::getTable(),
'field' => 'last_api_status',
'name' => __('Last API status', 'urbackup'),
@@ -269,7 +282,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 10,
'id' => 11,
'table' => self::getTable(),
'field' => 'last_api_check',
'name' => __('Last API check', 'urbackup'),
@@ -277,7 +290,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 11,
'id' => 12,
'table' => self::getTable(),
'field' => 'date_creation',
'name' => __('Creation date'),
@@ -285,22 +298,24 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 12,
'id' => 13,
'table' => self::getTable(),
'field' => 'date_mod',
'name' => __('Last update'),
'datatype' => 'datetime',
];
$tab[] = [
'id' => 13,
'table' => self::getTable(),
'field' => 'id',
'name' => __('View', 'urbackup'),
'massiveaction' => false,
'datatype' => 'raw',
'searchtype' => 'view',
];
if (Session::haveRight(self::$rightname, UPDATE)) {
$tab[] = [
'id' => 14,
'table' => self::getTable(),
'field' => 'id',
'name' => __('View', 'urbackup'),
'massiveaction' => false,
'datatype' => 'raw',
'searchtype' => 'view',
];
}
return $tab;
}
@@ -324,7 +339,14 @@ class Server extends CommonDBTM
$this->initForm($ID, $options);
$this->showFormHeader($options);
$this->showFormFields($ID > 0 ? (int) $ID : null);
$this->showFormButtons($options);
return true;
}
public function showFormFields(?int $ID): void
{
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Name')) . "</td>";
echo "<td>";
@@ -336,7 +358,10 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('Active')) . "</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 "</tr>";
@@ -374,8 +399,8 @@ class Server extends CommonDBTM
Dropdown::showFromArray(
'protocol',
[
'http' => 'HTTP',
'https' => 'HTTPS',
'http' => __('HTTP', 'urbackup'),
'https' => __('HTTPS', 'urbackup'),
],
[
'value' => $this->fields['protocol'] ?? 'http',
@@ -419,21 +444,33 @@ class Server extends CommonDBTM
echo "</td>";
echo "</tr>";
$canEdit = $ID > 0
? Session::haveRight(self::$rightname, UPDATE)
: Session::haveRight(self::$rightname, CREATE);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>";
echo "<td>";
echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '',
'size' => 40,
'autocomplete' => 'off',
]);
if ($canEdit) {
echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '',
'size' => 40,
'autocomplete' => 'off',
]);
} else {
echo htmlspecialchars($this->fields['api_username'] ?? '');
}
echo "</td>";
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>";
echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>";
if ($canEdit) {
echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>";
} else {
echo '******';
}
echo "</td>";
echo "</tr>";
@@ -464,7 +501,7 @@ class Server extends CommonDBTM
echo "</td>";
echo "</tr>";
$apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
$apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API connection status', 'urbackup')) . "<br><small class='text-muted'>" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . "</small></td>";
echo "<td colspan='3'>";
@@ -479,10 +516,6 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "</td>";
echo "</tr>";
}
$this->showFormButtons($options);
return true;
}
/**
@@ -542,6 +575,30 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
public function prepareInputForAdd(mixed $input): mixed
{
if (!is_array($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) {
$input['port'] = 55414;
}
if (!isset($input['protocol']) || $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
$input['is_active'] = 1;
}
if (!isset($input['is_recursive']) || $input['is_recursive'] === '' || $input['is_recursive'] === null) {
$input['is_recursive'] = 0;
}
if (!isset($input['ignore_ssl']) || $input['ignore_ssl'] === '' || $input['ignore_ssl'] === null) {
$input['ignore_ssl'] = 0;
}
return $this->prepareInputForUpdate($input);
}
@@ -551,6 +608,16 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
return $input;
}
if (isset($input['port']) && ($input['port'] === '' || $input['port'] === null)) {
$input['port'] = 55414;
}
if (isset($input['protocol']) && $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
$input['ignore_ssl'] = 0;
}
if (!empty($input['id']) && (int) $input['id'] > 0) {
$server = new self();
if ($server->getFromDB((int) $input['id'])) {
@@ -780,7 +847,7 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
}
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo 'API Error: ' . htmlspecialchars($e->getMessage());
echo htmlspecialchars(__('API Error', 'urbackup')) . ': ' . htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
@@ -789,7 +856,6 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo '<thead>';
echo '<tr>';
echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
@@ -840,8 +906,14 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
$itemUrl = $glpiItem ? $glpiItem->getLinkURL() : '';
echo '<tr>';
echo '<td>' . htmlspecialchars($itemTypeLabel . ' #' . $link['items_id']) . '</td>';
echo '<td>' . ($itemUrl ? '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>' : htmlspecialchars($clientName)) . '</td>';
echo '<td>';
if ($itemUrl) {
echo '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>';
} else {
echo htmlspecialchars($clientName);
}
echo ' <small class="text-muted">(' . htmlspecialchars($itemTypeLabel) . ')</small>';
echo '</td>';
echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>';
@@ -949,6 +1021,8 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
}
}
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
echo '<table class="table table-striped table-hover">';
echo '<thead>';
echo '<tr>';
@@ -957,13 +1031,15 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
if ($canWrite) {
echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
}
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ($unlinkedClients as $uc) {
$clientName = (string) ($uc['name'] ?? 'Unknown');
$clientName = (string) ($uc['name'] ?? __('Unknown', 'urbackup'));
$clientNameLower = strtolower($clientName);
$online = $uc['online'] ?? null;
@@ -982,27 +1058,481 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo '<td>' . $statusHtml . '</td>';
echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>';
echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</td>';
echo '<td>';
if (isset($linkableAssets[$clientNameLower])) {
$match = $linkableAssets[$clientNameLower];
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $match['itemtype']]);
echo Html::hidden('items_id', ['value' => $match['items_id']]);
echo Html::hidden('id', ['value' => (int) $server->fields['id']]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">';
echo htmlspecialchars(__('Connect'));
echo '</button>';
Html::closeForm();
} else {
echo '<span class="text-muted">—</span>';
if ($canWrite) {
echo '<td>';
if (isset($linkableAssets[$clientNameLower])) {
$match = $linkableAssets[$clientNameLower];
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $match['itemtype']]);
echo Html::hidden('items_id', ['value' => $match['items_id']]);
echo Html::hidden('id', ['value' => (int) $server->fields['id']]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">';
echo htmlspecialchars(__('Connect'));
echo '</button>';
Html::closeForm();
} else {
echo '<span class="text-muted">—</span>';
}
echo '</td>';
}
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
}
public static function showMissingClientsTab(Server $server): void
{
global $DB;
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
$apiStatus = (int) ($server->fields['last_api_status'] ?? 0);
if ($apiStatus !== 1) {
echo '<div class="alert alert-warning">';
echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup'));
echo '</div>';
return;
}
$serverLocationId = (int) ($server->fields['locations_id'] ?? 0);
if ($serverLocationId <= 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No location configured for this server.', 'urbackup'));
echo '</div>';
return;
}
$rootLocationId = LocationHelper::getRootLocationId($serverLocationId);
$linkedIterator = $DB->request([
'FROM' => ServerAsset::getTable(),
]);
$linkedAssetKeys = [];
foreach ($linkedIterator as $row) {
$linkedAssetKeys[$row['itemtype'] . ':' . $row['items_id']] = true;
}
try {
$client = new UrbackupApiClient($server);
$urbackupClients = $client->getStatus();
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
$urbackupClientNames = [];
foreach ($urbackupClients as $uc) {
$name = strtolower((string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? ''));
if ($name !== '') {
$urbackupClientNames[$name] = true;
}
}
$cacheEntity = [];
$cacheLocation = [];
$cacheState = [];
$cacheUser = [];
$cacheGroup = [];
$itemtypes = Config::getEnabledItemtypes();
$missingAssets = [];
$candidatesByType = [];
$totalAssets = 0;
foreach ($itemtypes as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
$assetItem = new $itemtype();
if (!$assetItem instanceof CommonDBTM) {
continue;
}
$table = $assetItem->getTable();
if (!$DB->tableExists($table)) {
continue;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_deleted' => 0,
],
]);
foreach ($iterator as $assetRow) {
$totalAssets++;
$name = (string) ($assetRow['name'] ?? '');
if ($name === '') {
continue;
}
$nameLower = strtolower($name);
$assetLocationId = (int) ($assetRow['locations_id'] ?? 0);
$assetRootLocationId = LocationHelper::getRootLocationId($assetLocationId);
if ($assetRootLocationId !== $rootLocationId) {
continue;
}
$key = $itemtype . ':' . $assetRow['id'];
$assetId = (int) $assetRow['id'];
if (isset($linkedAssetKeys[$key])) {
continue;
}
if (isset($urbackupClientNames[$nameLower])) {
continue;
}
$entityName = self::getCachedName('Entity', (int) ($assetRow['entities_id'] ?? 0), $cacheEntity);
$locationName = self::getCachedLocationName($assetLocationId, $cacheLocation);
$stateName = self::getCachedName('State', (int) ($assetRow['states_id'] ?? 0), $cacheState);
$userName = self::getCachedName('User', (int) ($assetRow['users_id'] ?? 0), $cacheUser);
$missingAssets[] = [
'itemtype' => $itemtype,
'items_id' => $assetId,
'name' => $name,
'entity' => $entityName,
'location' => $locationName,
'otherserial' => (string) ($assetRow['otherserial'] ?? ''),
'state' => $stateName,
'user' => $userName,
];
$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 ($totalAssets === 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No assets', 'urbackup'));
echo '</div>';
} else {
echo '<div class="alert alert-success">';
echo htmlspecialchars(__('All assets in this location are linked or already on the UrBackup server.', 'urbackup'));
echo '</div>';
}
return;
}
echo '<div class="d-flex justify-content-end mb-2">';
echo '<input type="text" id="missing-search" class="form-control form-control-sm" placeholder="' . htmlspecialchars(__('Search...', 'urbackup')) . '" style="width:250px">';
echo '</div>';
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<table id="missing-table" class="table table-striped table-hover">';
echo '<thead><tr>';
echo '<th class="sortable" data-col="0">' . htmlspecialchars(__('Name')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="1">' . htmlspecialchars(Entity::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="2">' . htmlspecialchars(Location::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="3">' . htmlspecialchars(__('Inventory number')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="4">' . htmlspecialchars(__('IP address', 'urbackup')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="5">' . htmlspecialchars(State::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="6">' . htmlspecialchars(User::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="7">' . htmlspecialchars(Group::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
if ($canWrite) {
echo '<th data-col="8">' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
}
echo '</tr></thead>';
echo '<tbody>';
foreach ($missingAssets as $asset) {
$item = new $asset['itemtype']();
$itemLink = '';
if ($item instanceof CommonDBTM && $item->getFromDB($asset['items_id'])) {
$itemLink = $item->getLinkURL();
}
echo '<tr>';
echo '<td>';
if ($itemLink !== '') {
echo '<a href="' . htmlspecialchars($itemLink) . '">' . htmlspecialchars($asset['name']) . '</a>';
} else {
echo htmlspecialchars($asset['name']);
}
echo '</td>';
echo '<td>' . htmlspecialchars($asset['entity']) . '</td>';
echo '<td>' . htmlspecialchars($asset['location']) . '</td>';
echo '<td>' . htmlspecialchars($asset['otherserial']) . '</td>';
echo '<td>' . htmlspecialchars($asset['ip']) . '</td>';
echo '<td>' . htmlspecialchars($asset['state']) . '</td>';
echo '<td>' . htmlspecialchars($asset['user']) . '</td>';
echo '<td>' . htmlspecialchars($asset['group']) . '</td>';
if ($canWrite) {
echo '<td>';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $asset['itemtype']]);
echo Html::hidden('items_id', ['value' => $asset['items_id']]);
echo Html::hidden('id', ['value' => (int) $server->fields['id']]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">';
echo htmlspecialchars(__('Connect'));
echo '</button>';
Html::closeForm();
echo '</td>';
}
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo <<<'JAVASCRIPT'
<script>
$(function () {
var sortDir = {};
$('#missing-table th.sortable').on('click', function () {
var col = parseInt($(this).data('col'));
var $table = $('#missing-table');
var $tbody = $table.find('tbody');
var rows = $tbody.find('tr').get();
var dir = sortDir[col] === 'asc' ? 'desc' : 'asc';
sortDir[col] = dir;
rows.sort(function (a, b) {
var aVal = $(a).children('td').eq(col).text().trim().toLowerCase();
var bVal = $(b).children('td').eq(col).text().trim().toLowerCase();
if (aVal < bVal) return dir === 'asc' ? -1 : 1;
if (aVal > bVal) return dir === 'asc' ? 1 : -1;
return 0;
});
$.each(rows, function (i, row) { $tbody.append(row); });
$table.find('th .sort-arrow').text('');
$(this).find('.sort-arrow').text(dir === 'asc' ? '\u25B2' : '\u25BC');
});
$('#missing-search').on('keyup', function () {
var val = $(this).val().toLowerCase();
$('#missing-table tbody tr').each(function () {
var match = false;
$(this).children('td').each(function () {
if ($(this).text().toLowerCase().indexOf(val) > -1) {
match = true;
return false;
}
});
$(this).toggle(match);
});
});
});
</script>
JAVASCRIPT;
}
private static function getCachedName(string $classname, int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$fqcn = '\\' . ltrim($classname, '\\');
$obj = new $fqcn();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$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
{
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $items_id,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$groupId = (int) ($row['groups_id'] ?? 0);
if ($groupId > 0) {
return self::getCachedName('Group', $groupId, $cache);
}
}
return '';
}
private static function getCachedLocationName(int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$obj = new Location();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$cache[$id] = '';
}
}
return $cache[$id];
}
public static function getAssetIp(string $itemtype, int $items_id): string
{
global $DB;
$iterator = $DB->request([
'SELECT' => ['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' => $items_id,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$ip = (string) ($row['name'] ?? '');
if ($ip !== '') {
return $ip;
}
}
return '';
}
}
+6 -14
View File
@@ -79,7 +79,6 @@ class ServerAsset extends CommonDBTM
}
$table = self::getTable();
$date = $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s');
$existing = self::getLinkForAsset($itemtype, $items_id, false);
@@ -88,10 +87,6 @@ class ServerAsset extends CommonDBTM
$table,
[
'plugin_urbackup_servers_id' => $server_id,
'client_name' => (string) ($item->fields['name'] ?? ''),
'client_ip' => self::extractAssetIp($item),
'is_active' => 1,
'date_mod' => $date,
],
[
'id' => (int) $existing['id'],
@@ -243,43 +238,40 @@ class ServerAsset extends CommonDBTM
echo "<div class='center'>";
echo "<table class='tab_cadre_fixehov'>";
echo "<tr><th colspan='5'>" . htmlspecialchars(__('Linked assets', 'urbackup')) . "</th></tr>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('Linked assets', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Asset', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type')) . "</th>";
echo "<th>" . htmlspecialchars(__('IP address', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last file backup', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last image backup', 'urbackup')) . "</th>";
echo "</tr>";
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'plugin_urbackup_servers_id' => (int) $item->fields['id'],
'is_active' => 1,
],
'ORDER' => 'client_name',
]);
foreach ($iterator as $row) {
$asset_label = (string) $row['client_name'];
$itemtype = (string) $row['itemtype'];
$items_id = (int) $row['items_id'];
$asset_label = (string) $items_id;
$asset_ip = '';
if (class_exists($itemtype)) {
$asset = new $itemtype();
if ($asset instanceof CommonDBTM && $asset->getFromDB($items_id)) {
$asset_label = $asset->getLink();
$asset_ip = Server::getAssetIp($itemtype, $items_id);
}
}
echo "<tr class='tab_bg_1'>";
echo "<td>" . $asset_label . "</td>";
echo "<td>" . htmlspecialchars($itemtype) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['client_ip']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_file_backup']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_image_backup']) . "</td>";
echo "<td>" . htmlspecialchars($asset_ip ?: '-') . "</td>";
echo "</tr>";
}
+36 -7
View File
@@ -27,7 +27,9 @@ class UrbackupApiClient
private bool $ignore_ssl;
private int $timeout = 10;
private int $timeout = 30;
private int $connect_timeout = 5;
private string $session = '';
@@ -35,6 +37,10 @@ class UrbackupApiClient
private int $lastlogid = 0;
private ?array $cached_status = null;
private array $cached_settings = [];
private string $server_version = '';
private bool $is_version_2_4_or_higher = false;
@@ -245,21 +251,29 @@ class UrbackupApiClient
*/
public function getStatus(): array
{
if ($this->cached_status !== null) {
return $this->cached_status;
}
$data = $this->apiAction('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'])) {
return array_values($data['clients']);
$this->cached_status = array_values($data['clients']);
return $this->cached_status;
}
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 [];
}
if (isset($this->cached_settings[$client_id])) {
return $this->cached_settings[$client_id];
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings',
't_clientid' => $client_id,
]);
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
*/
private function clearSettingsCache(int $client_id): void
{
unset($this->cached_settings[$client_id]);
}
public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{
$client_id = $this->getClientIdByName($client_name);
@@ -351,6 +376,7 @@ class UrbackupApiClient
$key => (string) $value,
]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data);
}
@@ -369,6 +395,7 @@ class UrbackupApiClient
$key => $value,
]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data);
}
@@ -391,6 +418,7 @@ class UrbackupApiClient
$data = $this->apiAction('settings', $params);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data);
}
@@ -667,6 +695,7 @@ class UrbackupApiClient
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => $this->connect_timeout,
CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl,
CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2,
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 %}