8 Commits

Author SHA1 Message Date
mariano 9582554dfe fix x gruppi 2026-06-15 13:11:32 +02:00
mariano 906f308f50 0.2.1 2026-06-03 14:03:43 +02:00
mariano 7fb79c9552 version stable 0.2.0 2026-06-03 07:41:53 +02:00
mariano c90506a25d versione pre-stable 2026-05-28 11:57:47 +02:00
mariano 425f8cb5a4 Before sanitization 2026-05-28 11:44:51 +02:00
mariano 0bdd5476cb fix - profile in install 2026-05-27 12:25:40 +02:00
mariano 41a8f8dbb4 final - speedup code for api 2026-05-22 11:59:05 +02:00
mariano 13cb325576 gestione assets 2026-05-22 08:29:59 +02:00
38 changed files with 717 additions and 1937 deletions
-66
View File
@@ -1,66 +0,0 @@
# Istruzioni per lo Sviluppo PHP 8.3 & Symfony
Sei un esperto Senior Developer specializzato in Symfony (6.4+) e PHP 8.3.
## Regole del Codice (PHP 8.3)
- Usa sempre la **Constructor Promotion** per la Dependency Injection.
- Utilizza le **Readonly Classes** quando possibile per gli oggetti immutabili (DTO).
- Applica **Typed Class Constants** (novità PHP 8.3).
- Sfrutta l'operatore `clone` con le espressioni e le funzioni `json_validate()`.
- Rigorosa tipizzazione: usa `declare(strict_types=1);` in ogni nuovo file.
## Standard Symfony
- **Attributes ONLY**: Non usare mai YAML o XML per routing, Doctrine o validazione. Usa solo PHP Attributes.
- **Service Container**: Prediligi l'autowiring.
- **Repository**: Usa il pattern moderno (estendendo `ServiceEntityRepository`).
- **Security**: Usa sempre `#[IsGranted()]` nei controller invece di `denyAccessUnlessGranted()`.
## Workflow di Risoluzione Errori
1. Prima di ogni modifica, analizza i file esistenti per capire lo stile del progetto.
2. Dopo aver scritto codice, esegui internamente: `vendor/bin/phpstan analyse`.
3. Se ci sono errori di stile, correggi con: `vendor/bin/ecs check --fix`.
4. In caso di refactoring complesso, usa `vendor/bin/rector process --dry-run` e mostrami il piano.
## Memoria degli Errori
- Leggi sempre il file `FIX_HISTORY.md` per non ripetere bug già risolti in passato.
- Ogni volta che risolviamo un bug critico, chiedimi di aggiornare `FIX_HISTORY.md`.
## Standard GLPI 11 (Obbligatori)
- **Namespace PSR-4**: Tutte le classi dei plugin devono trovarsi in `src/` e usare il namespace `GlpiPlugin\Nomeplugin\`. La cartella `inc/` è deprecata.
- **Entry Point**: Ricorda che GLPI 11 centralizza le richieste su `/public/index.php`. Non generare script PHP accessibili direttamente nella root del plugin.
- **Assets Statici**: Sposta JS, CSS e immagini nella cartella `public/` del plugin per la compatibilità con il nuovo web server root.
- **Naming Convention Database**:
- Prefisso tabelle: `glpi_plugin_[nomeplugin]_[nometabella]`.
- Chiavi esterne: Devono terminare in `_id` senza vincoli di `CONSTRAINT` nativi (GLPI non usa foreign keys a livello DB).
- **Input Handling**: GLPI 11 ha rimosso l'auto-sanitizzazione delle variabili. Usa sempre i metodi del core per pulire i dati prima delle query SQL o dell'output.
## Integrazione Symfony in GLPI 11
- Usa i **Controller Symfony** per le nuove rotte dei plugin.
- Sfrutta il **Twig Template Engine** situato in `templates/` per la UI.
- Definisci i comandi CLI tramite il componente Console di Symfony.
-27
View File
@@ -1,27 +0,0 @@
#!/bin/bash
# Fix file permissions for GLPI plugin
echo "Fixing file permissions..."
# Fix ownership
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/Server.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/Profile.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/MassiveAction.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/AssetTab.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/hook.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/setup.php
# Replace hook.php with corrected version
if [ -f /var/www/glpi/plugins/urbackup/hook_corrected.php ]; then
sudo cp /var/www/glpi/plugins/urbackup/hook_corrected.php /var/www/glpi/plugins/urbackup/hook.php
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/hook.php
rm /var/www/glpi/plugins/urbackup/hook_corrected.php
fi
# Fix all src/ files
sudo chown www-data:www-data /var/www/glpi/plugins/urbackup/src/*.php
echo "Permissions fixed. Now add declare(strict_types=1) to:"
echo " - src/Server.php"
echo " - src/Profile.php"
echo " - src/MassiveAction.php"
echo " - src/AssetTab.php"
+73 -301
View File
@@ -1,322 +1,94 @@
# UrBackup Plugin - Memoria di Sviluppo
# MEMORY.md - Stato del Plugin UrBackup
## 📋 Informazioni Plugin
## Ultima modifica: 28/05/2026
- **Nome**: UrBackup
- **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
### Refactoring server.form.php e nuove tab (v0.4.7)
**Data**: 2026-05-21
**Descrizione**: Restrutturazione completa della pagina server con tab laterali e nuove funzionalità:
**Refactoring Server::showForm()**:
- Estratto `showFormFields(?int $ID)` da `showForm()` per permettere layout custom
- `showForm()` ora chiama header + fields + buttons in sequenza
**Tab laterali (server.form.php)**:
- Side tabs a sinistra (col-2) con `nav-pills flex-column`
- 4 tab: Server, Linked clients, Unlinked clients, Missing clients
- Tab persistence via URL hash (JS su `shown.bs.tab`)
**Missing clients tab**:
- Mostra asset GLPI con root location matching, non ancora collegati a ServerAsset e non presenti su UrBackup
- Colonne: Name (link all'asset), Entity, Location (completename), Inventory number, IP, State, User, Group
- Ricerca testuale client-side
- Ordinamento colonne click-to-sort vanilla JS
- Navigazione server spostata in server.form.php (sopra i tab, indipendente dalle sezioni)
**Bug fix namespace risolto**:
- `getCachedName()` usava `new $classname()` con stringa → PHP cercava `GlpiPlugin\Urbackup\Group` invece di `\Group`
- Fix: `$fqcn = '\\' . ltrim($classname, '\\')` per forzare global namespace
**Gruppo asset corretto**:
- Gruppo non in `glpi_computers.groups_id` (colonna inesistente)
- Letto da `glpi_groups_items` WHERE `type = 1` (GROUP_TYPE_NORMAL)
- Usa `completename` per gerarchia (es. "Helpdesk > Livello 1")
**Indirizzo IP asset**:
- Query JOIN: `glpi_ipaddresses → glpi_networknames → glpi_networkports`
- Primo IP valido per l'item
**Breadcrumb**:
- Cambiato da `'Assets'` a `'admin'` in `Html::header()` di server.php e server.form.php
- Mostra: Pagina principale > Amministrazione > UrBackup servers
**Fix port default per nuovi server**:
- `prepareInputForAdd()` ora setta default 55414 per `port`, 'http' per `protocol`, 1 per `is_active`, ecc.
- `prepareInputForUpdate()` normalizza anche valori vuoti
**File modificati**:
- `src/Server.php`: `showFormFields()`, `showMissingClientsTab()`, `getCachedName()` fix, `getAssetGroupName()`, `getAssetIp()`, `getCachedLocationName()`, `prepareInputForAdd/Update` default fix, icona `ti-cloud-up`
- `front/server.form.php`: Tab laterali, tab hash JS, breadcrumb admin, navigazione server
- `front/server.php`: Breadcrumb admin, rimosso pulsante "Add" manuale
- `src/Profile.php`: Icona `ti-cloud-up`
- `src/AssetTab.php`: Icona `ti-cloud-up`
- **Permessi**: `front/server.form.php` ora usa READ invece di UPDATE per accesso form; View search option + pulsanti Connect nascosti per READ; API username/password nascosti per READ
- **i18n**: aggiunte 17 stringhe mancanti con dominio `urbackup` a tutti i file e .po/.mo (it, en, de)
- **Bug fix**: ServerAsset colonne rimosse, asset.form.php usa disconnectAsset(), `declare(strict_types=1)` in 11 file
## 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()]);
}
-23
View File
@@ -1,23 +0,0 @@
{
"name": "finstral/glpi-urbackup-plugin",
"description": "UrBackup integration plugin for GLPI 11",
"type": "glpi-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.3"
},
"autoload": {
"psr-4": {
"GlpiPlugin\\Urbackup\\": "src/"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"extra": {
"glpi-plugin": {
"key": "urbackup"
}
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php
/**
* AJAX endpoint for API test
* Uses standard GLPI front page pattern
*/
require_once(__DIR__ . '/_check_webserver_config.php');
global $CFG_GLPI;
header("Content-Type: application/json; charset=UTF-8");
// Check login
Session::checkLoginUser();
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => 'No permission']);
exit;
}
$server_id = (int) (($_GET['id'] ?? $_POST['id'] ?? 0));
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
// Load plugin classes
$classes = ['Server', 'UrbackupApiClient'];
foreach ($classes as $class) {
$file = PLUGIN_URBACKUP_DIR . '/src/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
}
$server = new \GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+27 -6
View File
@@ -8,6 +8,7 @@ declare(strict_types=1);
* -------------------------------------------------------------------------
*/
use Glpi\Exception\Http\BadRequestHttpException;
use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
@@ -27,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', 'urbackup'));
throw new BadRequestHttpException(__('Invalid parameters', 'urbackup'));
}
if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) {
Html::displayValidationError(__('Item type not enabled for UrBackup', 'urbackup'));
throw new BadRequestHttpException(__('Item type not enabled for UrBackup', 'urbackup'));
}
$item = getItemForItemtype($itemtype);
@@ -48,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', 'urbackup'));
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', 'urbackup'));
Session::addMessageAfterRedirect(
__('Asset is already linked to a server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
$result = ServerAsset::createForAsset($itemtype, $items_id, $server_id);
@@ -63,7 +74,12 @@ if (isset($_POST['connect'])) {
$item->getFromDB($items_id);
Html::redirect($item->getLinkURL());
} else {
Html::displayValidationError(__('Failed to link asset to server', 'urbackup'));
Session::addMessageAfterRedirect(
__('Failed to link asset to server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
}
@@ -78,7 +94,12 @@ if (isset($_POST['disconnect'])) {
$item->getFromDB($items_id);
Html::redirect($item->getLinkURL());
} else {
Html::displayValidationError(__('Failed to disconnect asset from server', 'urbackup'));
Session::addMessageAfterRedirect(
__('Failed to disconnect asset from server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
}
}
-1
View File
@@ -3,7 +3,6 @@
declare(strict_types=1);
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);
-2
View File
@@ -4,8 +4,6 @@ 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));
-64
View File
@@ -1,64 +0,0 @@
<?php
/**
* Test API endpoint
* Works without GLPI session redirect
*/
define('PLUGIN_URBACKUP_DIR', __DIR__ . '/..');
define('GLPI_ROOT', dirname(__DIR__, 4));
// Load minimal GLPI
require_once GLPI_ROOT . '/inc/includes.php';
// Check session exists
if (!isset($_SESSION['glpiID']) || (int) $_SESSION['glpiID'] <= 0) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Forbidden - No right plugin_urbackup READ']);
exit;
}
$server_id = (int) (($_GET['id'] ?? $_POST['id'] ?? 0));
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
// Load plugin classes
$classes = ['Server', 'UrbackupApiClient'];
foreach ($classes as $class) {
$file = PLUGIN_URBACKUP_DIR . '/src/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
}
$server = new \GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
-42
View File
@@ -1,42 +0,0 @@
<?php
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use Html;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4));
}
include_once GLPI_ROOT . "/inc/includes.php";
if (!Profile::canCurrentUser(READ)) {
Html::displayRightError();
}
$ID = $_GET['id'] ?? 0;
if ($ID <= 0) {
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php');
}
$server = new Server();
if (!$server->getFromDB($ID)) {
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php');
}
Html::header(
$server->fields['name'] . ' - UrBackup',
'',
'Assets',
'GlpiPlugin\Urbackup\Server'
);
$server->display([
'id' => $ID,
'show_nav' => true,
'show_tabs' => true,
]);
Html::footer();
-43
View File
@@ -1,43 +0,0 @@
<?php
/**
* Test page - will work when accessed from within GLPI session
*/
$AJAX_INCLUDE = 1;
define('GLPI_ROOT', dirname(__DIR__, 4));
require_once GLPI_ROOT . '/inc/includes.php';
header('Content-Type: text/html; charset=UTF-8');
Html::header_nocache();
Session::checkLoginUser();
// Check rights
if (!Session::haveRight('plugin_urbackup', READ)) {
echo "<p style='color:red'>No READ permission on plugin_urbackup</p>";
Html::footer();
exit;
}
echo "<p style='color:green'>READ permission OK</p>";
// Get server ID
$server_id = (int) ($_GET['id'] ?? 0);
if ($server_id > 0) {
$server = new GlpiPlugin\Urbackup\Server();
if ($server->getFromDB($server_id)) {
echo "<p>Server: " . $server->fields['name'] . "</p>";
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
echo "<pre>" . print_r($result, true) . "</pre>";
} else {
echo "<p>Server not found</p>";
}
} else {
echo "<p>No server ID provided</p>";
}
Html::footer();
+82 -30
View File
@@ -8,6 +8,8 @@ declare(strict_types=1);
* -------------------------------------------------------------------------
*/
use Glpi\Asset\AssetDefinitionManager;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
@@ -29,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();
@@ -55,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;
@@ -153,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');
@@ -202,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',
@@ -262,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');
}
/**
@@ -297,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',
]);
$migration->addField($table, 'rightname', 'string', [
'value' => '',
'after' => 'profiles_id',
]);
$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');
// 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;
}
}
// Auto-enable UrBackup capacity on ALL Asset Definitions
// so the tab appears out of the box for any asset type.
if (class_exists(AssetDefinitionManager::class)) {
try {
$manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
if (!$definition->hasCapacity(UrBackupCapacity::class)) {
$definition->enableCapacity(UrBackupCapacity::class);
}
}
} catch (\Throwable $e) {
$migration->displayMessage(
__('Error enabling UrBackup capacity on definitions: ', 'urbackup') . $e->getMessage()
);
}
}
// Drop is_default column (no longer needed in capacity system)
if ($has_is_default) {
$migration->dropField($table, 'is_default');
}
}
+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())
-87
View File
@@ -1,87 +0,0 @@
/**
* UrBackup API Test JavaScript
*/
(function () {
'use strict';
function testApi(serverId, resultBox) {
if (!serverId || !resultBox) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/plugins/urbackup/ajax/server_test.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000;
xhr.onload = function () {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.success) {
resultBox.innerHTML = '<span class="text-success fw-bold"><i class="ti ti-check"></i> ' + 'API connection OK' + '</span>';
} else {
var message = data.message || 'Connection failed';
var isNetwork = /timeout|could not resolve|couldn't connect|connection refused|connection timed out|network is unreachable|no route to host|returned HTTP status/i.test(message);
var statusClass = isNetwork ? 'text-warning' : 'text-danger';
var icon = isNetwork ? 'ti-wifi-off' : 'ti-x';
var label = isNetwork ? 'Server unreachable' : 'API connection failed';
resultBox.innerHTML = '<span class="' + statusClass + ' fw-bold"><i class="ti ' + icon + '"></i> ' + label + '</span><br><small class="text-muted">' + message + '</small>';
}
} catch (e) {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> Error</span>';
}
} else {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> HTTP ' + xhr.status + '</span>';
}
};
xhr.ontimeout = function () {
resultBox.innerHTML = '<span class="text-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Connection timeout</small>';
};
xhr.onerror = function () {
resultBox.innerHTML = '<span class="text-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Network error</small>';
};
xhr.send('id=' + encodeURIComponent(serverId));
}
function initApiStatusCheck() {
var statusBox = document.getElementById('plugin-urbackup-api-status');
if (!statusBox) return;
if (statusBox._initialized) return;
statusBox._initialized = true;
var serverId = statusBox.getAttribute('data-server-id');
if (serverId) {
testApi(serverId, statusBox);
}
}
function initApiTestButtons() {
var buttons = document.querySelectorAll('.plugin-urbackup-test-api');
buttons.forEach(function (button) {
if (button._initialized) return;
button._initialized = true;
button.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var serverId = button.getAttribute('data-server-id');
var resultBox = document.getElementById('plugin-urbackup-api-test-result');
testApi(serverId, resultBox);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApiStatusCheck);
document.addEventListener('DOMContentLoaded', initApiTestButtons);
} else {
setTimeout(initApiStatusCheck, 100);
setTimeout(initApiTestButtons, 100);
}
})();
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server nicht gefunden"
msgid "Sorry. You cannot access this file directly."
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
msgid "No assets"
msgstr "Keine Assets"
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server not found"
msgid "Sorry. You cannot access this file directly."
msgstr "Sorry. You cannot access this file directly."
msgid "No assets"
msgstr "No assets"
+3
View File
@@ -207,3 +207,6 @@ msgstr "Server non trovato"
msgid "Sorry. You cannot access this file directly."
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
msgid "No assets"
msgstr "Nessun Asset"
+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."
+23 -19
View File
@@ -2,20 +2,29 @@
declare(strict_types=1);
// Force OPcache to reload this file when accessed via web
// Force OPcache to reload plugin files when accessed via web
if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) {
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');
@@ -52,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)) {
// Register tab on Computer (legacy, always enabled)
Plugin::registerClass(AssetTab::class, [
'addtabon' => $enabled_types,
'addtabon' => ['Computer'],
]);
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
// which is called by `bootDefinitions()` during kernel PostBoot event.
// This ensures tabs are registered on all definitions that have the
// UrBackup capacity enabled, even if they are created after plugin init.
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
$manager->registerCapacity(new UrBackupCapacity());
}
$PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php';
@@ -75,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();
}
/**
+28 -14
View File
@@ -57,11 +57,15 @@ 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 '';
}
@@ -228,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>";
@@ -259,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' => [],
@@ -305,6 +314,11 @@ class AssetTab extends CommonDBTM
$data['error'] = $e->getMessage();
}
$_SESSION[$cache_key] = [
'time' => time(),
'data' => $data,
];
return $data;
}
@@ -328,19 +342,19 @@ class AssetTab extends CommonDBTM
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
echo '<ul class="nav nav-tabs" id="urbackupTabs">';
echo '<li class="nav-item">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab">';
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">';
echo '<a class="nav-link" id="actions-tab" data-bs-toggle="tab" href="#actions" role="tab">';
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">';
echo '<a class="nav-link" id="logs-tab" data-bs-toggle="tab" href="#logs" role="tab">';
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>';
@@ -831,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;
}
@@ -869,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;
}
@@ -899,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;
}
}
}
+31 -280
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;
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
foreach ($manager->getDefinitions() as $definition) {
$classname = $definition->getAssetClassName();
if (!in_array($classname, $itemtypes, true)) {
$itemtypes[] = $classname;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_active' => 1,
],
'ORDER' => 'itemtype',
]);
foreach ($iterator as $row) {
$itemtype = (string) $row['itemtype'];
if ($itemtype !== '' && !in_array($itemtype, $types, true)) {
$types[] = $itemtype;
}
}
return $types;
}
/**
* 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>";
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
$enabled = self::isItemtypeEnabled($itemtype);
$is_default = ($itemtype === 'Computer');
$is_asset_definition = self::isAssetDefinition($itemtype);
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
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'>" . htmlspecialchars(__('Asset Definition', 'urbackup')) . "</span></td>";
} else {
// Legacy type
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . ']',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>" . ($is_default ? htmlspecialchars(__('Yes', 'urbackup')) : '') . "</td>";
echo "<td><span class='badge bg-secondary'>" . htmlspecialchars(__('Legacy', 'urbackup')) . "</span></td>";
}
echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>";
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</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);
}
}
}
+17 -2
View File
@@ -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' => [
+145 -14
View File
@@ -259,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'),
@@ -266,7 +274,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 9,
'id' => 10,
'table' => self::getTable(),
'field' => 'last_api_status',
'name' => __('Last API status', 'urbackup'),
@@ -274,7 +282,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 10,
'id' => 11,
'table' => self::getTable(),
'field' => 'last_api_check',
'name' => __('Last API check', 'urbackup'),
@@ -282,7 +290,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 11,
'id' => 12,
'table' => self::getTable(),
'field' => 'date_creation',
'name' => __('Creation date'),
@@ -290,7 +298,7 @@ class Server extends CommonDBTM
];
$tab[] = [
'id' => 12,
'id' => 13,
'table' => self::getTable(),
'field' => 'date_mod',
'name' => __('Last update'),
@@ -299,7 +307,7 @@ class Server extends CommonDBTM
if (Session::haveRight(self::$rightname, UPDATE)) {
$tab[] = [
'id' => 13,
'id' => 14,
'table' => self::getTable(),
'field' => 'id',
'name' => __('View', 'urbackup'),
@@ -350,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>";
@@ -433,12 +444,14 @@ class Server extends CommonDBTM
echo "</td>";
echo "</tr>";
$canUpdate = Session::haveRight(self::$rightname, UPDATE);
$canEdit = $ID > 0
? Session::haveRight(self::$rightname, UPDATE)
: Session::haveRight(self::$rightname, CREATE);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>";
echo "<td>";
if ($canUpdate) {
if ($canEdit) {
echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '',
'size' => 40,
@@ -451,7 +464,7 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>";
if ($canUpdate) {
if ($canEdit) {
echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>";
@@ -566,6 +579,10 @@ class Server extends CommonDBTM
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;
}
@@ -1125,6 +1142,8 @@ class Server extends CommonDBTM
$itemtypes = Config::getEnabledItemtypes();
$missingAssets = [];
$candidatesByType = [];
$totalAssets = 0;
foreach ($itemtypes as $itemtype) {
if (!class_exists($itemtype)) {
@@ -1147,6 +1166,7 @@ class Server extends CommonDBTM
]);
foreach ($iterator as $assetRow) {
$totalAssets++;
$name = (string) ($assetRow['name'] ?? '');
if ($name === '') {
continue;
@@ -1174,9 +1194,6 @@ class Server extends CommonDBTM
$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);
$groupName = self::getAssetGroupName($itemtype, $assetId, $cacheGroup);
$ip = self::getAssetIp($itemtype, $assetId);
$missingAssets[] = [
'itemtype' => $itemtype,
@@ -1185,18 +1202,36 @@ class Server extends CommonDBTM
'entity' => $entityName,
'location' => $locationName,
'otherserial' => (string) ($assetRow['otherserial'] ?? ''),
'ip' => $ip,
'state' => $stateName,
'user' => $userName,
'group' => $groupName,
];
$candidatesByType[$itemtype][] = $assetId;
}
}
$batchIps = self::batchLoadIps($candidatesByType);
$batchGroups = self::batchLoadGroups($candidatesByType);
foreach ($missingAssets as &$asset) {
$key = $asset['itemtype'] . ':' . $asset['items_id'];
$asset['ip'] = $batchIps[$key] ?? '';
$groupId = $batchGroups[$key] ?? 0;
$asset['group'] = $groupId > 0
? self::getCachedName('Group', $groupId, $cacheGroup)
: '';
}
unset($asset);
if (count($missingAssets) === 0) {
if ($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;
}
@@ -1325,6 +1360,102 @@ JAVASCRIPT;
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;
+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 %}