1 Commits

Author SHA1 Message Date
mariano 2c1676dd47 test 2026-05-20 13:05:55 +02:00
46 changed files with 1977 additions and 1478 deletions
+66
View File
@@ -0,0 +1,66 @@
# 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
@@ -0,0 +1,27 @@
#!/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"
+248 -73
View File
@@ -1,94 +1,269 @@
# MEMORY.md - Stato del Plugin UrBackup # UrBackup Plugin - Memoria di Sviluppo
## Ultima modifica: 28/05/2026 ## 📋 Informazioni Plugin
## GLPI 11 — Session::checkCSRF() breaking change - **Nome**: UrBackup
In GLPI 11, `Session::checkCSRF()` richiede il primo argomento `$data` (i dati POST da validare). In GLPI ≤10 accettava zero argomenti. - **Versione**: 0.4.2
Il listener globale `CheckCsrfListener` gestisce già il CSRF per tutte le richieste POST, ma il plugin chiamava `Session::checkCSRF()` senza argomenti causando errore fatale. - **Compatibilità**: GLPI 11.0.6+, PHP 8.3+
- **Namespace**: `GlpiPlugin\Urbackup`
### Fix applicati (tutti i file) ## ✅ Funzionalità Implementate
- 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
## Performance - Asset Definition vs Computer ### 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`
### Problema ### Struttura File
Gli Asset Definition custom (GLPI 11) sono 2-5x più lenti dei Computer nativi nel caricamento del tab UrBackup. - `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`)
### Causa ### Classi Principali
L'overhead è in GLPI 11 core, non nel plugin: | Classe | Descrizione |
1. **`Asset::__construct()`** — itera 30+ Capacity classi per ogni istanza |--------|-------------|
2. **`Asset::post_getFromDB()`** — decodifica JSON custom fields e li processa | `Config` | Configurazione plugin, tipi asset abilitati |
3. **`eval()` autoloading** — le classi concrete sono definite via `eval()` a runtime | `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 |
### Ottimizzazioni applicate ### Pagine Frontend
1. **`AssetTab.php::loadApiData()`** — letto `$item->fields['name']` direttamente invece di chiamare `ServerAsset::getAssetName()` che faceva una seconda `getFromDB()` ridondante - `front/config.form.php`: Pagina configurazione plugin
2. **`AssetTab.php::startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`, `showServerLinkedBlock()`** — stesso pattern, `$item->fields['name']` al posto di `getAssetName()` - `front/server.php`: Lista server UrBackup
3. **`Server.php::showMissingClientsTab()`** — batch loading IP e gruppi: `batchLoadIps()` (1 query per itemtype vs 1 per riga) + `batchLoadGroups()` (1 query per itemtype vs 1 per riga) - `front/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
## Architettura ### Interfacce GLPI
- `src/Capacity/UrBackupCapacity.php` — registra `AssetTab` via `CommonGLPI::registerStandardTab()` in `onClassBootstrap()` - ✅ Menu: Amministrazione → Server UrBackup
- `setup.php` — registra capacità, CSS, JS, hook - ✅ Tab: UrBackup su Computer e asset abilitati
- `src/AssetTab.php` — display tab content + tab interni (Stato/Azioni/Info-Log) - ✅ Configurazione: Abilitazione tipi asset (legacy + Asset Definition GLPI 11)
- `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
## Asset Tab Interni ## 🔧 Fix e Correzioni Applicate
- 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)
## Cache 1. **OPcache**: Funzioni install in setup.php (non in hook.php) per problema OPcache
- `UrbackupApiClient`: cache in-memory per `getStatus()` e `getClientSettings()` 2. **Duplicate profilerights**: Aggiunto check in `Profile::registerRights()` per evitare duplicati
- `AssetTab::loadApiData()`: cache sessione 30s (chiave: server_id + client_name) 3. **CSRF**: Include `inc/includes.php` nei file front per gestione CSRF
- API timeout: 30s, connect timeout: 5s 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.
## Bugfix: Campi API username/password non visibili in nuova installazione ### Modifica Test API Connection (v0.4.3)
### Problema **Data**: 2026-05-19
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.
### Cause (2 bug distinti) **Descrizione**: Modificato il comportamento del test API nel form del server:
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. - Rimosso il pulsante "Test API Connection" manuale
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. - Rimosse le righe "Last API status", "Last API message", "Last API check"
- Aggiunto controllo automatico della connessione API eseguito ad ogni visualizzazione del form
### Fix applicati **Stati visualizzati**:
1. **`Server.php::showFormFields()`** — Sostituito `$canUpdate` con `$canEdit`: per nuovi server (`$ID <= 0`) usa `CREATE`, per server esistenti usa `UPDATE`. - 🟢 **Verde** "Connessione API OK" - Connessione riuscita
2. **`Profile.php::installRights()`** — In assenza di sessione attiva (CLI), cerca il profilo "Super-Admin" tramite query diretta e gli assegna tutti i diritti. - 🔴 **Rosso** "Connessione API fallita" - Errore di autenticazione o configurazione
- 🟡 **Giallo** "Server irraggiungibile" - Problemi di rete (timeout, DNS, HTTP 4xx/5xx, SSL)
## Bugfix: Unknown column 'users_id' — Group::getDataItems() SQL error **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
### Problema ### API UrBackup - Endpoints Verificati
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: | Endpoint | Metodo | Note |
```sql |----------|--------|------|
WHERE ((... ) OR (`users_id` IN (SELECT `users_id` FROM `glpi_groups_users` WHERE `groups_id` IN ('7483')))) | `/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
``` ```
La colonna `users_id` non esisteva nella tabella `glpi_plugin_urbackup_servers`.
### Causa ## 📚 Riferimenti
`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.
### Fix - GLPI 11 Plugin Development: https://glpi-project.org/documentation/
1. **`install/mysql/plugin_urbackup-empty.sql`** — Aggiunta colonna `users_id INT UNSIGNED NOT NULL DEFAULT 0` dopo `locations_id` + indice - Plugin Example: https://github.com/pluginsGLPI/example
2. **`install/install.php`** — Aggiunto `$migration->addField('users_id', ...)` e `$migration->addKey('users_id')` nella migrazione del server - GLPI Inventory: https://github.com/glpi-project/glpi-inventory-plugin
3. **`src/Server.php`** — Aggiunto search option `id=8` per `User::getTable()` + auto-set di `users_id` da `$_SESSION['glpiID']` in `prepareInputForAdd()`
## Versione ---
- 0.7.0
## 0.7.0 — Pulizia, sicurezza e DB cleanup ## 🔌 API UrBackup - Riferimenti Ufficiali
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/) ### Documentazione UrBackup Web API
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 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`).
5. **SQL schema pulito**: Rimosse tabelle legacy dall'empty.sql
6. **PHP lint warnings**: Rimosse `use Html;` e `use Search;` superflue ### Risorse API Utilizzate
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 1. **Python Wrapper** (uroni/urbackup-server-python-web-api-wrapper)
9. **CSRF GLPI 11 fix**: `Session::checkCSRF()` richiede `$_POST` come argomento in GLPI 11; aggiunto `X-Glpi-Csrf-Token` header al JS AJAX - URL: https://github.com/uroni/urbackup-server-python-web-api-wrapper
10. **Session::isDebugActive()**: Metodo inesistente in GLPI 11; sostituito in `AssetTab.php:150` con `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE` - 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
+27 -120
View File
@@ -1,130 +1,37 @@
# UrBackup plugin for GLPI 11 # UrBackup Plugin for GLPI 11
[![GLPI](https://img.shields.io/badge/GLPI-11.0.6%2B-blue?logo=glpi)](https://glpi-project.org) Plugin **UrBackup** per GLPI 11, sviluppato in PHP 8.3, che consente la gestione
[![PHP](https://img.shields.io/badge/PHP-8.3%2B-777BB4?logo=php)](https://php.net) centralizzata dei server UrBackup e dei client direttamente dallinterfaccia GLPI.
[![License](https://img.shields.io/badge/License-GPL--2.0--or--later-green)](LICENSE)
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. 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).
## Features ---
- **Server management** Register UrBackup servers by location; test API connectivity. ## ✅ Compatibilità
- **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.
## Requirements | Componente | Versione |
|-----------|----------|
| GLPI | 11.x |
| PHP | ≥ 8.3 |
| Database | MySQL / MariaDB (GLPI standard) |
| Component | Version | ---
|-----------|---------|
| **GLPI** | >= 11.0.6, < 12.0.0 |
| **PHP** | >= 8.3.0 (8.4 supported) |
| **Database** | MySQL 5.7+ / MariaDB 10.5+ |
## Installation ## ✅ Funzionalità principali
1. **Download** the plugin and extract it to `GLPI_ROOT/plugins/urbackup/` ### 🔧 Configurazione plugin
2. **Install** via GLPI web interface or CLI: - Percorso: **Configurazione → Plugin → UrBackup**
- Asset supportati:
- Computer (sempre attivo)
- Altri asset GLPI configurabili
- Attivazione automatica tab e massive action sugli asset abilitati
```bash ---
php bin/console glpi:plugin:install urbackup
php bin/console glpi:plugin:activate urbackup
```
3. **Configure rights** Go to *Administration > Profiles*, select a profile, and set *UrBackup* rights (READ / UPDATE / CREATE / DELETE / PURGE). ### 🗄️ Gestione server UrBackup
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
@@ -0,0 +1,57 @@
<?php
/**
* AJAX endpoint for testing UrBackup API
*/
$AJAX_INCLUDE = 1;
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 3));
}
require_once GLPI_ROOT . "/inc/includes.php";
header("Content-Type: application/json; charset=UTF-8");
Html::header_nocache();
Session::checkLoginUser();
// Allow AJAX requests without CSRF token (internal plugin calls)
if (!Session::getCSRFToken()) {
// Allow for AJAX calls
}
if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => 'No permission']);
exit;
}
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit;
}
$server = new GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => 'Server not found']);
exit;
}
try {
$client = new GlpiPlugin\Urbackup\UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $server_id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
echo json_encode($result);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "glpi/urbackup-plugin", "name": "finstral/glpi-urbackup-plugin",
"description": "UrBackup integration plugin for GLPI 11", "description": "UrBackup integration plugin for GLPI 11",
"type": "glpi-plugin", "type": "glpi-plugin",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
+59
View File
@@ -0,0 +1,59 @@
<?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()]);
}
+15 -30
View File
@@ -1,14 +1,11 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
*/ */
use Glpi\Exception\Http\BadRequestHttpException;
use GlpiPlugin\Urbackup\AssetTab; use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
@@ -28,11 +25,11 @@ $itemtype = (string) ($_POST['itemtype'] ?? $_GET['itemtype'] ?? '');
$items_id = (int) ($_POST['items_id'] ?? $_GET['items_id'] ?? 0); $items_id = (int) ($_POST['items_id'] ?? $_GET['items_id'] ?? 0);
if ($itemtype === '' || $items_id <= 0) { if ($itemtype === '' || $items_id <= 0) {
throw new BadRequestHttpException(__('Invalid parameters', 'urbackup')); Html::displayValidationError(__('Invalid parameters'));
} }
if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) { if (!in_array($itemtype, Config::getEnabledItemtypes(), true)) {
throw new BadRequestHttpException(__('Item type not enabled for UrBackup', 'urbackup')); Html::displayValidationError(__('Item type not enabled for UrBackup'));
} }
$item = getItemForItemtype($itemtype); $item = getItemForItemtype($itemtype);
@@ -49,23 +46,13 @@ if (isset($_POST['connect'])) {
$server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0); $server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) { if ($server_id <= 0) {
Session::addMessageAfterRedirect( Html::displayValidationError(__('No server selected'));
__('No server selected', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
$link = ServerAsset::getLinkForAsset($itemtype, $items_id); $link = ServerAsset::getLinkForAsset($itemtype, $items_id);
if ($link !== null) { if ($link !== null) {
Session::addMessageAfterRedirect( Html::displayValidationError(__('Asset is already linked to a server'));
__('Asset is already linked to a server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
$result = ServerAsset::createForAsset($itemtype, $items_id, $server_id); $result = ServerAsset::createForAsset($itemtype, $items_id, $server_id);
@@ -74,12 +61,7 @@ if (isset($_POST['connect'])) {
$item->getFromDB($items_id); $item->getFromDB($items_id);
Html::redirect($item->getLinkURL()); Html::redirect($item->getLinkURL());
} else { } else {
Session::addMessageAfterRedirect( Html::displayValidationError(__('Failed to link asset to server'));
__('Failed to link asset to server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
} }
@@ -88,18 +70,21 @@ if (isset($_POST['disconnect'])) {
Html::displayRightError(); Html::displayRightError();
} }
$result = ServerAsset::disconnectAsset($itemtype, $items_id); global $DB;
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, false);
if ($link === null) {
Html::displayValidationError(__('Asset is not linked to any server'));
}
$result = $DB->delete('glpi_plugin_urbackup_serverassets', ['id' => (int) $link['id']]);
if ($result) { if ($result) {
$item->getFromDB($items_id); $item->getFromDB($items_id);
Html::redirect($item->getLinkURL()); Html::redirect($item->getLinkURL());
} else { } else {
Session::addMessageAfterRedirect( Html::displayValidationError(__('Failed to disconnect asset from server'));
__('Failed to disconnect asset from server', 'urbackup'),
false,
ERROR
);
Html::redirect($item->getLinkURL());
} }
} }
+1 -2
View File
@@ -1,8 +1,7 @@
<?php <?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use Html;
global $CFG_GLPI; global $CFG_GLPI;
+41
View File
@@ -0,0 +1,41 @@
<?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);
+21 -112
View File
@@ -1,10 +1,9 @@
<?php <?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset; use GlpiPlugin\Urbackup\ServerAsset;
use Html;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4)); define('GLPI_ROOT', dirname(__DIR__, 4));
@@ -12,7 +11,7 @@ if (!defined('GLPI_ROOT')) {
include_once GLPI_ROOT . "/inc/includes.php"; include_once GLPI_ROOT . "/inc/includes.php";
if (!Profile::canCurrentUser(READ)) { if (!Profile::canCurrentUser(UPDATE)) {
Html::displayRightError(); Html::displayRightError();
} }
@@ -49,120 +48,30 @@ $ID = $_GET['id'] ?? null;
Html::header( Html::header(
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'), $ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
'', '',
'admin', 'Assets',
'GlpiPlugin\Urbackup\Server' 'GlpiPlugin\Urbackup\Server'
); );
if ($ID > 0) { $server->showForm($ID);
global $DB;
$server->getFromDB($ID); if ($ID > 0 && $server->getFromDB($ID)) {
echo '<div class="card mt-4">';
echo '<div class="card-header">';
echo '<h5 class="mb-0">' . htmlspecialchars(__('Linked clients', 'urbackup')) . '</h5>';
echo '</div>';
echo '<div class="card-body">';
Server::showLinkedClientsTab($server);
echo '</div>';
echo '</div>';
$serverIterator = $DB->request([ echo '<div class="card mt-4">';
'FROM' => Server::getTable(), echo '<div class="card-header">';
'ORDER' => 'name', echo '<h5 class="mb-0">' . htmlspecialchars(__('Unlinked clients', 'urbackup')) . '</h5>';
]); echo '</div>';
$serverIds = []; echo '<div class="card-body">';
foreach ($serverIterator as $row) { Server::showUnlinkedClientsTab($server);
$serverIds[(int) $row['id']] = (string) $row['name']; echo '</div>';
} echo '</div>';
$serverIdKeys = array_keys($serverIds);
$currentIndex = array_search((int) $ID, $serverIdKeys, true);
$totalServers = count($serverIds);
$prevId = ($currentIndex !== false && $currentIndex > 0) ? $serverIdKeys[$currentIndex - 1] : null;
$nextId = ($currentIndex !== false && $currentIndex < $totalServers - 1) ? $serverIdKeys[$currentIndex + 1] : null;
$baseUrl = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
?>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<a href="<?php echo htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php'); ?>" class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('List')); ?>">
<i class="ti ti-list-search fs-2"></i>
</a>
<?php if ($prevId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $prevId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Previous')); ?>">
<i class="fs-2 ti ti-chevron-left"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary me-2 bs-invisible">
<i class="fs-2 ti ti-chevron-left"></i>
</span>
<?php endif; ?>
<span class="fw-bold mx-2"><?php echo htmlspecialchars(($currentIndex !== false ? $currentIndex + 1 : 1) . ' / ' . $totalServers); ?></span>
<?php if ($nextId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $nextId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary ms-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Next')); ?>">
<i class="fs-2 ti ti-chevron-right"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary ms-2 bs-invisible">
<i class="fs-2 ti ti-chevron-right"></i>
</span>
<?php endif; ?>
</div>
</div>
<div class="row">
<div class="col-2">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-server">
<?php echo htmlspecialchars(__('Server')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-linked">
<?php echo htmlspecialchars(__('Linked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-unlinked">
<?php echo htmlspecialchars(__('Unlinked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-missing">
<?php echo htmlspecialchars(__('Missing clients', 'urbackup')); ?>
</a>
</li>
</ul>
</div>
<div class="col-10">
<div class="tab-content">
<div class="tab-pane active" id="tab-server">
<?php $server->showForm($ID); ?>
</div>
<div class="tab-pane" id="tab-linked">
<?php Server::showLinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-unlinked">
<?php Server::showUnlinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-missing">
<?php Server::showMissingClientsTab($server); ?>
</div>
</div>
</div>
</div>
<?php
} else {
$server->showForm($ID);
} }
?>
<script>
$(function () {
var hash = window.location.hash;
if (hash) {
$('[data-bs-toggle="tab"][href="' + hash + '"]').tab('show');
}
$('[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
window.location.hash = $(this).attr('href');
});
});
</script>
<?php
Html::footer(); Html::footer();
+19 -5
View File
@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1);
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\Server;
use Html;
use Search;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4)); define('GLPI_ROOT', dirname(__DIR__, 4));
@@ -15,13 +15,27 @@ if (!Profile::canCurrentUser(READ)) {
Html::displayRightError(); Html::displayRightError();
} }
$can_read = Profile::canCurrentUser(READ);
$can_create = Profile::canCurrentUser(CREATE);
Html::header( Html::header(
__('UrBackup Servers', 'urbackup'), 'UrBackup Servers',
'', '',
'admin', 'Assets',
'GlpiPlugin\Urbackup\Server' ''
); );
if ($can_create) {
echo "<div class='center'>";
echo Html::link(
__('Add UrBackup server', 'urbackup'),
'/plugins/urbackup/front/server.form.php',
['class' => 'btn btn-primary']
);
echo "</div>";
echo "<br>";
}
Search::show('GlpiPlugin\Urbackup\Server', [ Search::show('GlpiPlugin\Urbackup\Server', [
'is_deleted' => 0, 'is_deleted' => 0,
'massiveaction' => true, 'massiveaction' => true,
+64
View File
@@ -0,0 +1,64 @@
<?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
@@ -0,0 +1,42 @@
<?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();
+3 -6
View File
@@ -1,7 +1,4 @@
<?php <?php
declare(strict_types=1);
/** /**
* AJAX endpoint for testing UrBackup API * AJAX endpoint for testing UrBackup API
*/ */
@@ -14,21 +11,21 @@ Html::header_nocache();
Session::checkLoginUser(); Session::checkLoginUser();
if (!Session::haveRight('plugin_urbackup', READ)) { if (!Session::haveRight('plugin_urbackup', READ)) {
echo json_encode(['success' => false, 'message' => __('No permission', 'urbackup')]); echo json_encode(['success' => false, 'message' => 'No permission']);
exit; exit;
} }
$server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0); $server_id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
if ($server_id <= 0) { if ($server_id <= 0) {
echo json_encode(['success' => false, 'message' => __('Invalid server ID', 'urbackup')]); echo json_encode(['success' => false, 'message' => 'Invalid server ID']);
exit; exit;
} }
$server = new GlpiPlugin\Urbackup\Server(); $server = new GlpiPlugin\Urbackup\Server();
if (!$server->getFromDB($server_id)) { if (!$server->getFromDB($server_id)) {
echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]); echo json_encode(['success' => false, 'message' => 'Server not found']);
exit; exit;
} }
+43
View File
@@ -0,0 +1,43 @@
<?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();
+39 -83
View File
@@ -1,20 +1,26 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
*
* Install/update process for GLPI 11.
*
* GLPI 11 Migration class does not expose createTable()/addTable().
* Compatible GLPI plugin pattern:
*
* - initial schema creation from SQL file using $DB->runFile()
* - schema evolution using Migration addField(), addKey(), executeMigration()
*
* -------------------------------------------------------------------------
*/ */
use Glpi\Asset\AssetDefinitionManager;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
die(__('Sorry. You cannot access this file directly.', 'urbackup')); die('Sorry. You cannot access this file directly.');
} }
/** /**
@@ -31,15 +37,10 @@ function plugin_urbackup_install_process(): bool
plugin_urbackup_install_create_initial_schema($migration); plugin_urbackup_install_create_initial_schema($migration);
plugin_urbackup_install_update_configs_table($migration); plugin_urbackup_install_update_configs_table($migration);
plugin_urbackup_install_update_assettypes_table($migration);
// Convert old assettype configurations to GLPI 11 capacity system
plugin_urbackup_install_convert_assettypes_to_capacities($migration);
plugin_urbackup_install_update_servers_table($migration); plugin_urbackup_install_update_servers_table($migration);
plugin_urbackup_install_update_serverassets_table($migration); plugin_urbackup_install_update_serverassets_table($migration);
plugin_urbackup_install_update_profiles_table($migration);
// Drop tables that are no longer used (replaced by Profile::registerRights() and Capacity system)
plugin_urbackup_install_drop_legacy_tables($migration);
$migration->executeMigration(); $migration->executeMigration();
@@ -62,8 +63,10 @@ function plugin_urbackup_install_create_initial_schema(Migration $migration): vo
$required_tables = [ $required_tables = [
'glpi_plugin_urbackup_configs', 'glpi_plugin_urbackup_configs',
'glpi_plugin_urbackup_assettypes',
'glpi_plugin_urbackup_servers', 'glpi_plugin_urbackup_servers',
'glpi_plugin_urbackup_serverassets', 'glpi_plugin_urbackup_serverassets',
'glpi_plugin_urbackup_profiles',
]; ];
$missing_table_found = false; $missing_table_found = false;
@@ -158,6 +161,11 @@ function plugin_urbackup_install_update_assettypes_table(Migration $migration):
'after' => 'itemtype', 'after' => 'itemtype',
]); ]);
$migration->addField($table, 'is_default', 'bool', [
'value' => 0,
'after' => 'is_active',
]);
$migration->addField($table, 'date_creation', 'timestamp'); $migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp'); $migration->addField($table, 'date_mod', 'timestamp');
@@ -202,11 +210,6 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
'after' => 'name', 'after' => 'name',
]); ]);
$migration->addField($table, 'users_id', 'integer', [
'value' => 0,
'after' => 'locations_id',
]);
$migration->addField($table, 'ip_address', 'string', [ $migration->addField($table, 'ip_address', 'string', [
'value' => '', 'value' => '',
'after' => 'locations_id', 'after' => 'locations_id',
@@ -267,9 +270,7 @@ function plugin_urbackup_install_update_servers_table(Migration $migration): voi
$migration->addKey($table, 'name'); $migration->addKey($table, 'name');
$migration->addKey($table, 'entities_id'); $migration->addKey($table, 'entities_id');
$migration->addKey($table, 'locations_id'); $migration->addKey($table, 'locations_id');
$migration->addKey($table, 'users_id');
$migration->addKey($table, 'is_active'); $migration->addKey($table, 'is_active');
$migration->addKey($table, ['locations_id', 'is_active'], 'location_active');
} }
/** /**
@@ -304,89 +305,44 @@ function plugin_urbackup_install_update_serverassets_table(Migration $migration)
'after' => 'itemtype', 'after' => 'itemtype',
]); ]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, 'plugin_urbackup_servers_id'); $migration->addKey($table, 'plugin_urbackup_servers_id');
$migration->addKey($table, ['itemtype', 'items_id'], 'item'); $migration->addKey($table, ['itemtype', 'items_id'], 'item');
} }
/** /**
* Drop legacy tables that are no longer used. * Update profiles table.
* *
* @param Migration $migration Migration instance * @param Migration $migration Migration instance
* *
* @return void * @return void
*/ */
function plugin_urbackup_install_drop_legacy_tables(Migration $migration): void function plugin_urbackup_install_update_profiles_table(Migration $migration): void
{ {
global $DB; global $DB;
// glpi_plugin_urbackup_profiles was replaced by Profile::registerRights() in 0.5.0
$table = 'glpi_plugin_urbackup_profiles'; $table = 'glpi_plugin_urbackup_profiles';
if ($DB->tableExists($table)) {
$migration->displayMessage(__('Dropping legacy glpi_plugin_urbackup_profiles table', 'urbackup'));
$DB->queryOrDie("DROP TABLE IF EXISTS `$table`");
}
// glpi_plugin_urbackup_assettypes was replaced by GLPI 11 Capacity system in 0.6.0
$table2 = 'glpi_plugin_urbackup_assettypes';
if ($DB->tableExists($table2)) {
$migration->displayMessage(__('Dropping legacy glpi_plugin_urbackup_assettypes table', 'urbackup'));
$DB->queryOrDie("DROP TABLE IF EXISTS `$table2`");
}
$migration->displayMessage(__('Legacy tables dropped successfully', 'urbackup'));
}
/**
* Convert old glpi_plugin_urbackup_assettypes to GLPI 11 capacity system
* and auto-enable UrBackup capacity on all existing Asset Definitions.
*
* After this migration, admins can disable UrBackup per Asset Definition
* via the native Capacities UI (Config > Asset definitions > Capacities).
*
* @param Migration $migration Migration instance
*
* @return void
*/
function plugin_urbackup_install_convert_assettypes_to_capacities(Migration $migration): void
{
global $DB;
$table = Config::getAssetTypesTable();
if (!$DB->tableExists($table)) { if (!$DB->tableExists($table)) {
return; return;
} }
// Check if is_default column exists $migration->addField($table, 'profiles_id', 'integer', [
$has_is_default = false; 'value' => 0,
foreach ($DB->listFields($table) as $col) { 'after' => 'id',
if ($col['Field'] === 'is_default') { ]);
$has_is_default = true;
break;
}
}
// Auto-enable UrBackup capacity on ALL Asset Definitions $migration->addField($table, 'rightname', 'string', [
// so the tab appears out of the box for any asset type. 'value' => '',
if (class_exists(AssetDefinitionManager::class)) { 'after' => 'profiles_id',
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) $migration->addField($table, 'rights', 'integer', [
if ($has_is_default) { 'value' => 0,
$migration->dropField($table, 'is_default'); 'after' => 'rightname',
} ]);
$migration->addField($table, 'date_creation', 'timestamp');
$migration->addField($table, 'date_mod', 'timestamp');
$migration->addKey($table, ['profiles_id', 'rightname'], 'profile_right');
} }
+23 -7
View File
@@ -8,13 +8,24 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_configs` (
KEY `name` (`name`) KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_assettypes` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`itemtype` VARCHAR(255) NOT NULL DEFAULT '',
`is_active` TINYINT NOT NULL DEFAULT 0,
`is_default` TINYINT NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `itemtype` (`itemtype`),
KEY `is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` ( CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`entities_id` INT UNSIGNED NOT NULL DEFAULT 0, `entities_id` INT UNSIGNED NOT NULL DEFAULT 0,
`is_recursive` TINYINT NOT NULL DEFAULT 0, `is_recursive` TINYINT NOT NULL DEFAULT 0,
`name` VARCHAR(255) NOT NULL DEFAULT '', `name` VARCHAR(255) NOT NULL DEFAULT '',
`locations_id` INT UNSIGNED NOT NULL DEFAULT 0, `locations_id` INT UNSIGNED NOT NULL DEFAULT 0,
`users_id` INT UNSIGNED NOT NULL DEFAULT 0,
`ip_address` VARCHAR(255) NOT NULL DEFAULT '', `ip_address` VARCHAR(255) NOT NULL DEFAULT '',
`port` INT UNSIGNED NOT NULL DEFAULT 55414, `port` INT UNSIGNED NOT NULL DEFAULT 55414,
`protocol` VARCHAR(10) NOT NULL DEFAULT 'http', `protocol` VARCHAR(10) NOT NULL DEFAULT 'http',
@@ -33,9 +44,7 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` (
KEY `name` (`name`), KEY `name` (`name`),
KEY `entities_id` (`entities_id`), KEY `entities_id` (`entities_id`),
KEY `locations_id` (`locations_id`), KEY `locations_id` (`locations_id`),
KEY `users_id` (`users_id`), KEY `is_active` (`is_active`)
KEY `is_active` (`is_active`),
KEY `location_active` (`locations_id`, `is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` ( CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
@@ -43,11 +52,18 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_serverassets` (
`plugin_urbackup_servers_id` INT UNSIGNED NOT NULL DEFAULT 0, `plugin_urbackup_servers_id` INT UNSIGNED NOT NULL DEFAULT 0,
`itemtype` VARCHAR(255) NOT NULL DEFAULT '', `itemtype` VARCHAR(255) NOT NULL DEFAULT '',
`items_id` INT UNSIGNED NOT NULL DEFAULT 0, `items_id` INT UNSIGNED NOT NULL DEFAULT 0,
`date_creation` TIMESTAMP NULL DEFAULT NULL,
`date_mod` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`), KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`),
KEY `item` (`itemtype`, `items_id`) KEY `item` (`itemtype`, `items_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- glpi_plugin_urbackup_profiles was dropped in 0.7.0 (replaced by Profile::registerRights()) 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;
+4 -3
View File
@@ -1,17 +1,18 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
*
* Uninstall process for GLPI 11.
* -------------------------------------------------------------------------
*/ */
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
die(__('Sorry. You cannot access this file directly.', 'urbackup')); die('Sorry. You cannot access this file directly.');
} }
/** /**
+87
View File
@@ -0,0 +1,87 @@
/**
* UrBackup API Test JavaScript
*/
(function () {
'use strict';
function testApi(serverId, resultBox) {
if (!serverId || !resultBox) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/plugins/urbackup/ajax/server_test.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000;
xhr.onload = function () {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.success) {
resultBox.innerHTML = '<span class="text-success fw-bold"><i class="ti ti-check"></i> ' + 'API connection OK' + '</span>';
} else {
var message = data.message || 'Connection failed';
var isNetwork = /timeout|could not resolve|couldn't connect|connection refused|connection timed out|network is unreachable|no route to host|returned HTTP status/i.test(message);
var statusClass = isNetwork ? 'text-warning' : 'text-danger';
var icon = isNetwork ? 'ti-wifi-off' : 'ti-x';
var label = isNetwork ? 'Server unreachable' : 'API connection failed';
resultBox.innerHTML = '<span class="' + statusClass + ' fw-bold"><i class="ti ' + icon + '"></i> ' + label + '</span><br><small class="text-muted">' + message + '</small>';
}
} catch (e) {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> Error</span>';
}
} else {
resultBox.innerHTML = '<span class="text-danger fw-bold"><i class="ti ti-x"></i> HTTP ' + xhr.status + '</span>';
}
};
xhr.ontimeout = function () {
resultBox.innerHTML = '<span class="text-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Connection timeout</small>';
};
xhr.onerror = function () {
resultBox.innerHTML = '<span class="text-warning fw-bold"><i class="ti ti-wifi-off"></i> Server unreachable</span><br><small class="text-muted">Network error</small>';
};
xhr.send('id=' + encodeURIComponent(serverId));
}
function initApiStatusCheck() {
var statusBox = document.getElementById('plugin-urbackup-api-status');
if (!statusBox) return;
if (statusBox._initialized) return;
statusBox._initialized = true;
var serverId = statusBox.getAttribute('data-server-id');
if (serverId) {
testApi(serverId, statusBox);
}
}
function initApiTestButtons() {
var buttons = document.querySelectorAll('.plugin-urbackup-test-api');
buttons.forEach(function (button) {
if (button._initialized) return;
button._initialized = true;
button.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var serverId = button.getAttribute('data-server-id');
var resultBox = document.getElementById('plugin-urbackup-api-test-result');
testApi(serverId, resultBox);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApiStatusCheck);
document.addEventListener('DOMContentLoaded', initApiTestButtons);
} else {
setTimeout(initApiStatusCheck, 100);
setTimeout(initApiTestButtons, 100);
}
})();
BIN
View File
Binary file not shown.
-54
View File
@@ -156,57 +156,3 @@ msgstr "Letztes Backup"
msgid "Show" msgid "Show"
msgstr "Anzeigen" msgstr "Anzeigen"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API-Fehler"
msgid "Unknown"
msgstr "Unbekannt"
msgid "Asset Definition"
msgstr "Asset-Definition"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "UrBackup-Server"
msgid "Invalid parameters"
msgstr "Ungültige Parameter"
msgid "Item type not enabled for UrBackup"
msgstr "Elementtyp für UrBackup nicht aktiviert"
msgid "No server selected"
msgstr "Kein Server ausgewählt"
msgid "Asset is already linked to a server"
msgstr "Asset ist bereits mit einem Server verknüpft"
msgid "Failed to link asset to server"
msgstr "Verknüpfung von Asset mit Server fehlgeschlagen"
msgid "Failed to disconnect asset from server"
msgstr "Trennung von Asset vom Server fehlgeschlagen"
msgid "No permission"
msgstr "Keine Berechtigung"
msgid "Invalid server ID"
msgstr "Ungültige Server-ID"
msgid "Server not found"
msgstr "Server nicht gefunden"
msgid "Sorry. You cannot access this file directly."
msgstr "Entschuldigung. Sie können nicht direkt auf diese Datei zugreifen."
msgid "No assets"
msgstr "Keine Assets"
BIN
View File
Binary file not shown.
-54
View File
@@ -156,57 +156,3 @@ msgstr "Last backup"
msgid "Show" msgid "Show"
msgstr "Show" msgstr "Show"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "API Error"
msgid "Unknown"
msgstr "Unknown"
msgid "Asset Definition"
msgstr "Asset Definition"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "UrBackup Servers"
msgid "Invalid parameters"
msgstr "Invalid parameters"
msgid "Item type not enabled for UrBackup"
msgstr "Item type not enabled for UrBackup"
msgid "No server selected"
msgstr "No server selected"
msgid "Asset is already linked to a server"
msgstr "Asset is already linked to a server"
msgid "Failed to link asset to server"
msgstr "Failed to link asset to server"
msgid "Failed to disconnect asset from server"
msgstr "Failed to disconnect asset from server"
msgid "No permission"
msgstr "No permission"
msgid "Invalid server ID"
msgstr "Invalid server ID"
msgid "Server not found"
msgstr "Server not found"
msgid "Sorry. You cannot access this file directly."
msgstr "Sorry. You cannot access this file directly."
msgid "No assets"
msgstr "No assets"
BIN
View File
Binary file not shown.
-54
View File
@@ -156,57 +156,3 @@ msgstr "Ultimo backup"
msgid "Show" msgid "Show"
msgstr "Mostra" msgstr "Mostra"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "API Error"
msgstr "Errore API"
msgid "Unknown"
msgstr "Sconosciuto"
msgid "Asset Definition"
msgstr "Definizione asset"
msgid "Legacy"
msgstr "Legacy"
msgid "UrBackup Servers"
msgstr "Server UrBackup"
msgid "Invalid parameters"
msgstr "Parametri non validi"
msgid "Item type not enabled for UrBackup"
msgstr "Tipo oggetto non abilitato per UrBackup"
msgid "No server selected"
msgstr "Nessun server selezionato"
msgid "Asset is already linked to a server"
msgstr "L'asset è già collegato a un server"
msgid "Failed to link asset to server"
msgstr "Collegamento asset al server fallito"
msgid "Failed to disconnect asset from server"
msgstr "Disconnessione asset dal server fallita"
msgid "No permission"
msgstr "Nessun permesso"
msgid "Invalid server ID"
msgstr "ID server non valido"
msgid "Server not found"
msgstr "Server non trovato"
msgid "Sorry. You cannot access this file directly."
msgstr "Spiacenti. Non puoi accedere direttamente a questo file."
msgid "No assets"
msgstr "Nessun Asset"
-43
View File
@@ -10,46 +10,3 @@
color: #b00; color: #b00;
font-weight: bold; font-weight: bold;
} }
#urbackupTabs .nav-link {
font-weight: 600;
border-width: 2px;
padding: 8px 18px;
margin-right: 4px;
}
#urbackupTabs .nav-link#state-tab {
color: #fff;
background-color: #4a4a4a;
border-color: #4a4a4a;
}
#urbackupTabs .nav-link#state-tab.active {
color: #fff;
background-color: #2d2d2d;
border-color: #2d2d2d;
}
#urbackupTabs .nav-link#actions-tab {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
#urbackupTabs .nav-link#actions-tab.active {
color: #fff;
background-color: #495057;
border-color: #495057;
}
#urbackupTabs .nav-link#logs-tab {
color: #fff;
background-color: #adb5bd;
border-color: #adb5bd;
}
#urbackupTabs .nav-link#logs-tab.active {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
-1
View File
@@ -10,7 +10,6 @@
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.php', true); xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.php', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-Glpi-Csrf-Token', getAjaxCsrfToken());
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.timeout = 8000; xhr.timeout = 8000;
+11
View File
@@ -0,0 +1,11 @@
#!/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."
+20 -26
View File
@@ -1,30 +1,19 @@
<?php <?php
declare(strict_types=1); // Force OPcache to reload this file when accessed via web
// Force OPcache to reload plugin files when accessed via web
if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) { if (PHP_SAPI !== 'cli' && function_exists('opcache_invalidate')) {
$files = new RecursiveIteratorIterator( opcache_invalidate(__FILE__, true);
new RecursiveDirectoryIterator(__DIR__, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if ($file->getExtension() === 'php') {
@opcache_invalidate($file->getRealPath(), true);
}
}
} }
use Glpi\Asset\AssetDefinitionManager;
use Glpi\Plugin\Hooks; use Glpi\Plugin\Hooks;
use GlpiPlugin\Urbackup\AssetTab; use GlpiPlugin\Urbackup\AssetTab;
use GlpiPlugin\Urbackup\Capacity\UrBackupCapacity;
use GlpiPlugin\Urbackup\Config; use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset; use GlpiPlugin\Urbackup\ServerAsset;
use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction; use GlpiPlugin\Urbackup\MassiveAction as PluginUrbackupMassiveAction;
define('PLUGIN_URBACKUP_VERSION', '0.7.0'); define('PLUGIN_URBACKUP_VERSION', '0.4.2');
define('PLUGIN_URBACKUP_MIN_GLPI', '11.0.0'); define('PLUGIN_URBACKUP_MIN_GLPI', '11.0.0');
define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99'); define('PLUGIN_URBACKUP_MAX_GLPI', '11.99.99');
@@ -61,19 +50,12 @@ function plugin_init_urbackup(): void
Plugin::registerClass(ServerAsset::class); Plugin::registerClass(ServerAsset::class);
Plugin::registerClass(PluginUrbackupMassiveAction::class); Plugin::registerClass(PluginUrbackupMassiveAction::class);
// Register tab on Computer (legacy, always enabled) // Register tab on Computer and other assets
$enabled_types = Config::getEnabledItemtypes();
if (!empty($enabled_types)) {
Plugin::registerClass(AssetTab::class, [ Plugin::registerClass(AssetTab::class, [
'addtabon' => ['Computer'], 'addtabon' => $enabled_types,
]); ]);
// Register UrBackupCapacity for GLPI 11 Asset Definition types.
// Tab registration happens inside `UrBackupCapacity::onClassBootstrap()`
// which is called by `bootDefinitions()` during kernel PostBoot event.
// This ensures tabs are registered on all definitions that have the
// UrBackup capacity enabled, even if they are created after plugin init.
if (class_exists(AssetDefinitionManager::class)) {
$manager = AssetDefinitionManager::getInstance();
$manager->registerCapacity(new UrBackupCapacity());
} }
$PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php'; $PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php';
@@ -91,6 +73,18 @@ function plugin_init_urbackup(): void
$PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [ $PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [
'public/js/urbackup.js', 'public/js/urbackup.js',
]; ];
$PLUGIN_HOOKS[Hooks::POST_INIT]['urbackup'] = 'plugin_urbackup_postinit';
}
/**
* Post init hook.
*
* @return void
*/
function plugin_urbackup_postinit(): void
{
Config::registerAssetTabs();
} }
/** /**
@@ -103,7 +97,7 @@ function plugin_version_urbackup(): array
return [ return [
'name' => __('UrBackup', 'urbackup'), 'name' => __('UrBackup', 'urbackup'),
'version' => PLUGIN_URBACKUP_VERSION, 'version' => PLUGIN_URBACKUP_VERSION,
'author' => 'Mariano Benzi', 'author' => 'Finstral',
'license' => 'GPL-2.0-or-later', 'license' => 'GPL-2.0-or-later',
'homepage' => '', 'homepage' => '',
'requirements' => [ 'requirements' => [
+22 -64
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
@@ -57,19 +55,15 @@ class AssetTab extends CommonDBTM
*/ */
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string
{ {
$itemtype = $item::class; if (!Config::isItemtypeEnabled($item::class)) {
$enabled = Config::isItemtypeEnabled($itemtype);
$hasRight = Profile::canCurrentUser(READ);
if (!$enabled) {
return ''; return '';
} }
if (!$hasRight) { if (!Profile::canCurrentUser(READ)) {
return ''; return '';
} }
return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-up'); return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-upload');
} }
/** /**
@@ -232,7 +226,7 @@ class AssetTab extends CommonDBTM
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>"; echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($item->fields['name'] ?? '')) . "</td>"; echo "<td>" . htmlspecialchars(ServerAsset::getAssetName($item::class, (int) $item->fields['id'])) . "</td>";
echo "</tr>"; echo "</tr>";
echo "</table>"; echo "</table>";
@@ -263,14 +257,9 @@ class AssetTab extends CommonDBTM
*/ */
private static function loadApiData(CommonDBTM $item, Server $server, array $link): array private static function loadApiData(CommonDBTM $item, Server $server, array $link): array
{ {
$client_name = (string) ($item->fields['name'] ?? ''); $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
$asset_ip = ServerAsset::extractAssetIp($item); $asset_ip = ServerAsset::extractAssetIp($item);
$cache_key = 'urbackup_data_' . $server->fields['id'] . '_' . $client_name;
if (isset($_SESSION[$cache_key]) && $_SESSION[$cache_key]['time'] > time() - 30) {
return $_SESSION[$cache_key]['data'];
}
$data = [ $data = [
'client_found' => false, 'client_found' => false,
'client_status' => [], 'client_status' => [],
@@ -314,11 +303,6 @@ class AssetTab extends CommonDBTM
$data['error'] = $e->getMessage(); $data['error'] = $e->getMessage();
} }
$_SESSION[$cache_key] = [
'time' => time(),
'data' => $data,
];
return $data; return $data;
} }
@@ -340,42 +324,15 @@ class AssetTab extends CommonDBTM
): void { ): void {
echo "<div class='plugin-urbackup-inner-tabs'>"; echo "<div class='plugin-urbackup-inner-tabs'>";
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE); echo "<h3>" . htmlspecialchars(__('State', 'urbackup')) . "</h3>";
echo '<ul class="nav nav-tabs" id="urbackupTabs" role="tablist">';
echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab" aria-selected="true">';
echo htmlspecialchars(__('State', 'urbackup'));
echo '</a></li>';
if ($canWrite) {
echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link" id="actions-tab" data-bs-toggle="tab" href="#actions" role="tab" aria-selected="false" tabindex="-1">';
echo htmlspecialchars(__('Actions', 'urbackup'));
echo '</a></li>';
}
echo '<li class="nav-item" role="presentation">';
echo '<a class="nav-link" id="logs-tab" data-bs-toggle="tab" href="#logs" role="tab" aria-selected="false" tabindex="-1">';
echo htmlspecialchars(__('Info / Log', 'urbackup'));
echo '</a></li>';
echo '</ul>';
echo '<div class="tab-content">';
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
self::showStateSection($server, $link, $api_data); self::showStateSection($server, $link, $api_data);
echo '</div>';
if ($canWrite) { echo "<h3>" . htmlspecialchars(__('Actions', 'urbackup')) . "</h3>";
echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
self::showActionsSection($item, $server, $link, $api_data); self::showActionsSection($item, $server, $link, $api_data);
echo '</div>';
}
echo '<div class="tab-pane fade" id="logs" role="tabpanel">'; echo "<h3>" . htmlspecialchars(__('Info / Log', 'urbackup')) . "</h3>";
self::showInfoLogSection($api_data); self::showInfoLogSection($api_data);
echo '</div>';
echo '</div>';
echo "</div>"; echo "</div>";
} }
@@ -470,6 +427,17 @@ class AssetTab extends CommonDBTM
echo "<table class='tab_cadre_fixe'>"; echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('Available actions', 'urbackup')) . "</th></tr>"; echo "<tr><th colspan='2'>" . htmlspecialchars(__('Available actions', 'urbackup')) . "</th></tr>";
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo htmlspecialchars(__('You do not have permission for UrBackup actions.', 'urbackup'));
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
if (!$api_data['client_found'] && Profile::canCurrentUser(CREATE)) { if (!$api_data['client_found'] && Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Create client in UrBackup', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('Create client in UrBackup', 'urbackup')) . "</td>";
@@ -497,20 +465,10 @@ class AssetTab extends CommonDBTM
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
echo "<div style='display:flex;gap:24px'>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('File backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('Image backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "</div>";
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
@@ -845,7 +803,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = (string) ($item->fields['name'] ?? ''); $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
@@ -883,7 +841,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = (string) ($item->fields['name'] ?? ''); $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
@@ -913,7 +871,7 @@ class AssetTab extends CommonDBTM
return false; return false;
} }
$client_name = (string) ($item->fields['name'] ?? ''); $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') { if ($client_name === '') {
return false; return false;
} }
-62
View File
@@ -1,62 +0,0 @@
<?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
@@ -0,0 +1,56 @@
<?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;
}
}
}
+281 -32
View File
@@ -11,7 +11,8 @@ declare(strict_types=1);
namespace GlpiPlugin\Urbackup; namespace GlpiPlugin\Urbackup;
use CommonDBTM; use CommonDBTM;
use Glpi\Asset\Asset; use Computer;
use Glpi\Asset\AssetDefinition;
use Glpi\Asset\AssetDefinitionManager; use Glpi\Asset\AssetDefinitionManager;
use DBmysql; use DBmysql;
use Html; use Html;
@@ -52,8 +53,67 @@ class Config extends CommonDBTM
*/ */
public static function ensureDefaultConfiguration(): void public static function ensureDefaultConfiguration(): void
{ {
// Computer is always enabled by default. self::ensureAssetType('Computer', true, true);
// Asset Definition types are managed via the native Capacities UI. }
/**
* Ensure an asset type is registered.
*
* @param string $itemtype Itemtype
* @param bool $is_active Active
* @param bool $is_default Default
*
* @return void
*/
public static function ensureAssetType(
string $itemtype,
bool $is_active = true,
bool $is_default = false
): void {
global $DB;
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return;
}
$existing = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
],
'LIMIT' => 1,
]);
if (count($existing) > 0) {
$row = $existing->current();
$DB->update(
$table,
[
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
],
[
'id' => (int) $row['id'],
]
);
return;
}
$DB->insert(
$table,
[
'itemtype' => $itemtype,
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_creation' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
]
);
} }
/** /**
@@ -75,6 +135,8 @@ class Config extends CommonDBTM
*/ */
public static function isItemtypeEnabled(string $itemtype): bool public static function isItemtypeEnabled(string $itemtype): bool
{ {
global $DB;
if ($itemtype === '') { if ($itemtype === '') {
return false; return false;
} }
@@ -83,15 +145,22 @@ class Config extends CommonDBTM
return true; return true;
} }
// All Asset subclasses are enabled by default. $table = self::getAssetTypesTable();
// The UrBackupCapacity is still registered for cleanup (onCapacityDisabled)
// and usage tracking in the Capacities UI, but visibility is always on if (!$DB->tableExists($table)) {
// so the tab appears out of the box on any Asset Definition. return false;
if (is_a($itemtype, Asset::class, true)) {
return true;
} }
return false; $iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
'is_active' => 1,
],
'LIMIT' => 1,
]);
return count($iterator) > 0;
} }
/** /**
@@ -101,19 +170,54 @@ class Config extends CommonDBTM
*/ */
public static function getEnabledItemtypes(): array public static function getEnabledItemtypes(): array
{ {
$itemtypes = ['Computer']; global $DB;
if (class_exists(AssetDefinitionManager::class)) { $types = ['Computer'];
$manager = AssetDefinitionManager::getInstance(); $table = self::getAssetTypesTable();
foreach ($manager->getDefinitions() as $definition) {
$classname = $definition->getAssetClassName(); if (!$DB->tableExists($table)) {
if (!in_array($classname, $itemtypes, true)) { return $types;
$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 $itemtypes; 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,
]);
}
} }
/** /**
@@ -128,32 +232,78 @@ class Config extends CommonDBTM
{ {
Session::checkRight('config', UPDATE); Session::checkRight('config', UPDATE);
echo "<form method='post' action='" . htmlspecialchars($options['target'] ?? '') . "'>";
echo "<div class='center'>"; echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>"; echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>"; echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
echo "<tr class='tab_bg_1'>"; echo "<tr>";
echo "<td><strong>" . htmlspecialchars(__('Computer', 'urbackup')) . "</strong></td>"; echo "<th>" . htmlspecialchars(__('Asset type', 'urbackup')) . "</th>";
echo "<td><span class='badge bg-success'>" . htmlspecialchars(__('Always enabled', 'urbackup')) . "</span></td>"; echo "<th>" . htmlspecialchars(__('Enabled', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Default', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type', 'urbackup')) . "</th>";
echo "</tr>"; echo "</tr>";
echo "</table>";
echo "<br>"; foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
echo "<div class='alert alert-info'>"; $enabled = self::isItemtypeEnabled($itemtype);
echo htmlspecialchars( $is_default = ($itemtype === 'Computer');
__('For Asset Definition types, enable/disable UrBackup via Config > Asset definitions > Capacities.', 'urbackup') $is_asset_definition = self::isAssetDefinition($itemtype);
);
echo "</div>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars($label) . "</td>";
if ($itemtype === 'Computer') {
echo "<td>" . htmlspecialchars(__('Always', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(__('Yes', 'urbackup')) . "</td>";
} elseif ($is_asset_definition) {
// GLPI 11 Asset Definition
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_active]',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_default]',
'checked' => $is_default,
]);
echo "</td>";
echo "<td><span class='badge bg-info'>Asset Definition</span></td>";
} else {
// Legacy type
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . ']',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>" . ($is_default ? htmlspecialchars(__('Yes', 'urbackup')) : '') . "</td>";
echo "<td><span class='badge bg-secondary'>Legacy</span></td>";
}
echo "</tr>";
}
echo "<tr>";
echo "<td colspan='4' class='center'>";
echo Html::submit(__('Save', 'urbackup'), [
'name' => 'update',
'class' => 'btn btn-primary',
]);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</div>"; echo "</div>";
Html::closeForm();
return true; return true;
} }
/** /**
* Save configuration. * Save configuration.
* *
* In the new capacity system, asset types are managed via the native Capacities UI.
* This method is kept for backward compatibility but does nothing.
*
* @param array<string, mixed> $input Input data * @param array<string, mixed> $input Input data
* *
* @return void * @return void
@@ -161,5 +311,104 @@ class Config extends CommonDBTM
public static function saveConfiguration(array $input): void public static function saveConfiguration(array $input): void
{ {
Session::checkRight('config', UPDATE); Session::checkRight('config', UPDATE);
$selected = $input['assettypes'] ?? [];
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
if ($itemtype === 'Computer') {
self::ensureAssetType('Computer', true, true);
continue;
}
// Check if it's an Asset Definition (new format with is_active/is_default)
if (self::isAssetDefinition($itemtype)) {
$is_active = isset($selected[$itemtype]['is_active']) && (bool) $selected[$itemtype]['is_active'];
$is_default = isset($selected[$itemtype]['is_default']) && (bool) $selected[$itemtype]['is_default'];
self::ensureAssetType($itemtype, $is_active, $is_default);
} else {
// Legacy format (simple checkbox)
$is_enabled = isset($selected[$itemtype]) && (bool) $selected[$itemtype];
self::ensureAssetType($itemtype, $is_enabled, false);
}
}
}
/**
* Get configurable asset types (legacy + GLPI 11 Asset Definition).
*
* @return array<string, string>
*/
public static function getConfigurableAssetTypes(): array
{
$types = [
'Computer' => Computer::getTypeName(Session::getPluralNumber()),
];
// Legacy types
$known_types = [
'Printer',
'Peripheral',
'NetworkEquipment',
'Phone',
'Monitor',
];
foreach ($known_types as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue;
}
$types[$itemtype] = $itemtype::getTypeName(Session::getPluralNumber());
}
// GLPI 11 Asset Definition
if (class_exists(AssetDefinitionManager::class)) {
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
// Access fields directly like CommonDBTM
$system_name = $definition->fields['system_name'] ?? '';
$name = $definition->fields['name'] ?? '';
if (!empty($system_name)) {
$types[$system_name] = !empty($name) ? $name : $system_name;
}
}
} catch (\Throwable $e) {
// If AssetDefinitionManager fails, skip
}
}
return $types;
}
/**
* Check if itemtype is a GLPI 11 Asset Definition.
*
* @param string $itemtype Itemtype or system name
*
* @return bool
*/
public static function isAssetDefinition(string $itemtype): bool
{
if (!class_exists(AssetDefinitionManager::class)) {
return false;
}
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
if (($definition->fields['system_name'] ?? '') === $itemtype) {
return true;
}
}
} catch (\Throwable $e) {
// If fails, return false
}
return false;
} }
} }
+191
View File
@@ -0,0 +1,191 @@
<?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
@@ -0,0 +1,45 @@
<?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
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Html;
use Session;
use Search;
class ServerController extends AbstractController
{
#[Route('/plugin/urbackup/servers', name: 'urbackup_server_list', methods: ['GET'])]
public function listServers(): void
{
if (!Server::canView()) {
Html::displayRightError();
}
Html::header(
Server::getTypeName(Session::getPluralNumber()),
$_SERVER['PHP_SELF'],
'admin',
Server::class
);
Search::show(Server::class);
Html::footer();
}
#[Route('/plugin/urbackup/server/{id}', name: 'urbackup_server_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function showServer(int $id): void
{
$server = new Server();
if ($id > 0) {
$server->check($id, READ);
} else {
$server->check(-1, CREATE);
$server->getEmpty();
}
$server->display(['id' => $id]);
}
#[Route('/plugin/urbackup/server/test/{id}', name: 'urbackup_server_test', methods: ['POST', 'GET'])]
public function testConnection(int $id = 0): JsonResponse
{
if (!Profile::canCurrentUser(READ)) {
return new JsonResponse([
'success' => false,
'message' => 'No permission',
], 403);
}
if ($id <= 0) {
$id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
}
if ($id <= 0) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid server ID',
], 400);
}
$server = new Server();
if (!$server->getFromDB($id)) {
return new JsonResponse([
'success' => false,
'message' => 'Server not found',
], 404);
}
try {
$client = new UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
return new JsonResponse($result);
} catch (Throwable $e) {
return new JsonResponse([
'success' => false,
'message' => $e->getMessage(),
], 500);
}
}
}
-2
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
+3 -18
View File
@@ -14,7 +14,7 @@ class Profile extends \Profile
public static function getIcon() public static function getIcon()
{ {
return 'ti ti-cloud-up'; return 'ti ti-server';
} }
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
@@ -73,22 +73,7 @@ class Profile extends \Profile
$profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0); $profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0);
if ($profiles_id > 0) { if ($profiles_id > 0) {
self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE | PURGE); self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE);
} else {
// CLI installation: no session available.
// Assign full rights to the Super-Admin profile.
global $DB;
$sa_iterator = $DB->request([
'FROM' => 'glpi_profiles',
'WHERE' => [
'name' => 'Super-Admin',
],
'LIMIT' => 1,
]);
foreach ($sa_iterator as $sa_profile) {
$profiles_id = (int) $sa_profile['id'];
self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE | PURGE);
}
} }
global $DB; global $DB;
@@ -98,7 +83,7 @@ class Profile extends \Profile
]); ]);
foreach ($all_profiles as $profile) { foreach ($all_profiles as $profile) {
if ((int) $profile['id'] !== $profiles_id) { if ($profile['id'] !== $profiles_id) {
$existing = $DB->request([ $existing = $DB->request([
'FROM' => 'glpi_profilerights', 'FROM' => 'glpi_profilerights',
'WHERE' => [ 'WHERE' => [
+19 -549
View File
@@ -1,7 +1,5 @@
<?php <?php
declare(strict_types=1);
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* UrBackup plugin for GLPI * UrBackup plugin for GLPI
@@ -14,12 +12,9 @@ use CommonDBTM;
use CommonGLPI; use CommonGLPI;
use Dropdown; use Dropdown;
use Entity; use Entity;
use Group;
use Html; use Html;
use Location; use Location;
use Session; use Session;
use State;
use User;
class Server extends CommonDBTM class Server extends CommonDBTM
{ {
@@ -156,7 +151,7 @@ class Server extends CommonDBTM
if (self::canView()) { if (self::canView()) {
$menu['title'] = self::getMenuName(); $menu['title'] = self::getMenuName();
$menu['page'] = self::getSearchURL(false); $menu['page'] = self::getSearchURL(false);
$menu['icon'] = 'ti ti-cloud-up'; $menu['icon'] = 'ti ti-server';
$menu['links']['search'] = self::getSearchURL(false); $menu['links']['search'] = self::getSearchURL(false);
@@ -259,14 +254,6 @@ class Server extends CommonDBTM
$tab[] = [ $tab[] = [
'id' => 8, 'id' => 8,
'table' => User::getTable(),
'field' => 'name',
'name' => User::getTypeName(1),
'datatype' => 'dropdown',
];
$tab[] = [
'id' => 9,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'is_active', 'field' => 'is_active',
'name' => __('Active'), 'name' => __('Active'),
@@ -274,7 +261,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 10, 'id' => 9,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'last_api_status', 'field' => 'last_api_status',
'name' => __('Last API status', 'urbackup'), 'name' => __('Last API status', 'urbackup'),
@@ -282,7 +269,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 11, 'id' => 10,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'last_api_check', 'field' => 'last_api_check',
'name' => __('Last API check', 'urbackup'), 'name' => __('Last API check', 'urbackup'),
@@ -290,7 +277,7 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 12, 'id' => 11,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'date_creation', 'field' => 'date_creation',
'name' => __('Creation date'), 'name' => __('Creation date'),
@@ -298,16 +285,15 @@ class Server extends CommonDBTM
]; ];
$tab[] = [ $tab[] = [
'id' => 13, 'id' => 12,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'date_mod', 'field' => 'date_mod',
'name' => __('Last update'), 'name' => __('Last update'),
'datatype' => 'datetime', 'datatype' => 'datetime',
]; ];
if (Session::haveRight(self::$rightname, UPDATE)) {
$tab[] = [ $tab[] = [
'id' => 14, 'id' => 13,
'table' => self::getTable(), 'table' => self::getTable(),
'field' => 'id', 'field' => 'id',
'name' => __('View', 'urbackup'), 'name' => __('View', 'urbackup'),
@@ -315,7 +301,6 @@ class Server extends CommonDBTM
'datatype' => 'raw', 'datatype' => 'raw',
'searchtype' => 'view', 'searchtype' => 'view',
]; ];
}
return $tab; return $tab;
} }
@@ -339,14 +324,7 @@ class Server extends CommonDBTM
$this->initForm($ID, $options); $this->initForm($ID, $options);
$this->showFormHeader($options); $this->showFormHeader($options);
$this->showFormFields($ID > 0 ? (int) $ID : null);
$this->showFormButtons($options);
return true;
}
public function showFormFields(?int $ID): void
{
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Name')) . "</td>"; echo "<td>" . htmlspecialchars(__('Name')) . "</td>";
echo "<td>"; echo "<td>";
@@ -358,10 +336,7 @@ class Server extends CommonDBTM
echo "<td>" . htmlspecialchars(__('Active')) . "</td>"; echo "<td>" . htmlspecialchars(__('Active')) . "</td>";
echo "<td>"; echo "<td>";
Dropdown::showYesNo( Dropdown::showYesNo('is_active', (int) ($this->fields['is_active'] ?? 1));
'is_active',
($ID !== null && $ID > 0) ? (int) ($this->fields['is_active'] ?? 1) : 1
);
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
@@ -399,8 +374,8 @@ class Server extends CommonDBTM
Dropdown::showFromArray( Dropdown::showFromArray(
'protocol', 'protocol',
[ [
'http' => __('HTTP', 'urbackup'), 'http' => 'HTTP',
'https' => __('HTTPS', 'urbackup'), 'https' => 'HTTPS',
], ],
[ [
'value' => $this->fields['protocol'] ?? 'http', 'value' => $this->fields['protocol'] ?? 'http',
@@ -444,33 +419,21 @@ class Server extends CommonDBTM
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
$canEdit = $ID > 0
? Session::haveRight(self::$rightname, UPDATE)
: Session::haveRight(self::$rightname, CREATE);
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
if ($canEdit) {
echo Html::input('api_username', [ echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '', 'value' => $this->fields['api_username'] ?? '',
'size' => 40, 'size' => 40,
'autocomplete' => 'off', 'autocomplete' => 'off',
]); ]);
} else {
echo htmlspecialchars($this->fields['api_username'] ?? '');
}
echo "</td>"; echo "</td>";
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
if ($canEdit) {
echo "<input type='password' name='api_password' value='" . echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) . htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>"; "' autocomplete='new-password'>";
} else {
echo '******';
}
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
@@ -501,7 +464,7 @@ class Server extends CommonDBTM
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
$apiStatus = (int) ($this->fields['last_api_status'] ?? 0); $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API connection status', 'urbackup')) . "<br><small class='text-muted'>" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . "</small></td>"; echo "<td>" . htmlspecialchars(__('API connection status', 'urbackup')) . "<br><small class='text-muted'>" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . "</small></td>";
echo "<td colspan='3'>"; echo "<td colspan='3'>";
@@ -516,6 +479,10 @@ class Server extends CommonDBTM
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
} }
$this->showFormButtons($options);
return true;
} }
/** /**
@@ -575,30 +542,6 @@ class Server extends CommonDBTM
public function prepareInputForAdd(mixed $input): mixed public function prepareInputForAdd(mixed $input): mixed
{ {
if (!is_array($input)) {
return $input;
}
if (!isset($input['users_id']) || $input['users_id'] <= 0) {
$input['users_id'] = (int) ($_SESSION['glpiID'] ?? 0);
}
if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) {
$input['port'] = 55414;
}
if (!isset($input['protocol']) || $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
$input['is_active'] = 1;
}
if (!isset($input['is_recursive']) || $input['is_recursive'] === '' || $input['is_recursive'] === null) {
$input['is_recursive'] = 0;
}
if (!isset($input['ignore_ssl']) || $input['ignore_ssl'] === '' || $input['ignore_ssl'] === null) {
$input['ignore_ssl'] = 0;
}
return $this->prepareInputForUpdate($input); return $this->prepareInputForUpdate($input);
} }
@@ -608,16 +551,6 @@ class Server extends CommonDBTM
return $input; return $input;
} }
if (isset($input['port']) && ($input['port'] === '' || $input['port'] === null)) {
$input['port'] = 55414;
}
if (isset($input['protocol']) && $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
$input['ignore_ssl'] = 0;
}
if (!empty($input['id']) && (int) $input['id'] > 0) { if (!empty($input['id']) && (int) $input['id'] > 0) {
$server = new self(); $server = new self();
if ($server->getFromDB((int) $input['id'])) { if ($server->getFromDB((int) $input['id'])) {
@@ -847,7 +780,7 @@ class Server extends CommonDBTM
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo '<div class="alert alert-danger">'; echo '<div class="alert alert-danger">';
echo htmlspecialchars(__('API Error', 'urbackup')) . ': ' . htmlspecialchars($e->getMessage()); echo 'API Error: ' . htmlspecialchars($e->getMessage());
echo '</div>'; echo '</div>';
return; return;
} }
@@ -856,6 +789,7 @@ class Server extends CommonDBTM
echo '<thead>'; echo '<thead>';
echo '<tr>'; echo '<tr>';
echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
@@ -906,14 +840,8 @@ class Server extends CommonDBTM
$itemUrl = $glpiItem ? $glpiItem->getLinkURL() : ''; $itemUrl = $glpiItem ? $glpiItem->getLinkURL() : '';
echo '<tr>'; echo '<tr>';
echo '<td>'; echo '<td>' . htmlspecialchars($itemTypeLabel . ' #' . $link['items_id']) . '</td>';
if ($itemUrl) { echo '<td>' . ($itemUrl ? '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>' : htmlspecialchars($clientName)) . '</td>';
echo '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>';
} else {
echo htmlspecialchars($clientName);
}
echo ' <small class="text-muted">(' . htmlspecialchars($itemTypeLabel) . ')</small>';
echo '</td>';
echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>'; echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>'; echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>'; echo '<td>' . $statusHtml . '</td>';
@@ -1021,8 +949,6 @@ class Server extends CommonDBTM
} }
} }
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
echo '<table class="table table-striped table-hover">'; echo '<table class="table table-striped table-hover">';
echo '<thead>'; echo '<thead>';
echo '<tr>'; echo '<tr>';
@@ -1031,15 +957,13 @@ class Server extends CommonDBTM
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>';
if ($canWrite) {
echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
}
echo '</tr>'; echo '</tr>';
echo '</thead>'; echo '</thead>';
echo '<tbody>'; echo '<tbody>';
foreach ($unlinkedClients as $uc) { foreach ($unlinkedClients as $uc) {
$clientName = (string) ($uc['name'] ?? __('Unknown', 'urbackup')); $clientName = (string) ($uc['name'] ?? 'Unknown');
$clientNameLower = strtolower($clientName); $clientNameLower = strtolower($clientName);
$online = $uc['online'] ?? null; $online = $uc['online'] ?? null;
@@ -1058,7 +982,6 @@ class Server extends CommonDBTM
echo '<td>' . $statusHtml . '</td>'; echo '<td>' . $statusHtml . '</td>';
echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>'; echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>';
echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</td>'; echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</td>';
if ($canWrite) {
echo '<td>'; echo '<td>';
if (isset($linkableAssets[$clientNameLower])) { if (isset($linkableAssets[$clientNameLower])) {
$match = $linkableAssets[$clientNameLower]; $match = $linkableAssets[$clientNameLower];
@@ -1076,463 +999,10 @@ class Server extends CommonDBTM
echo '<span class="text-muted">—</span>'; echo '<span class="text-muted">—</span>';
} }
echo '</td>'; echo '</td>';
}
echo '</tr>'; echo '</tr>';
} }
echo '</tbody>'; echo '</tbody>';
echo '</table>'; echo '</table>';
} }
public static function showMissingClientsTab(Server $server): void
{
global $DB;
$canWrite = Session::haveRight(self::$rightname, UPDATE) || Session::haveRight(self::$rightname, CREATE);
$apiStatus = (int) ($server->fields['last_api_status'] ?? 0);
if ($apiStatus !== 1) {
echo '<div class="alert alert-warning">';
echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup'));
echo '</div>';
return;
}
$serverLocationId = (int) ($server->fields['locations_id'] ?? 0);
if ($serverLocationId <= 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No location configured for this server.', 'urbackup'));
echo '</div>';
return;
}
$rootLocationId = LocationHelper::getRootLocationId($serverLocationId);
$linkedIterator = $DB->request([
'FROM' => ServerAsset::getTable(),
]);
$linkedAssetKeys = [];
foreach ($linkedIterator as $row) {
$linkedAssetKeys[$row['itemtype'] . ':' . $row['items_id']] = true;
}
try {
$client = new UrbackupApiClient($server);
$urbackupClients = $client->getStatus();
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
$urbackupClientNames = [];
foreach ($urbackupClients as $uc) {
$name = strtolower((string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? ''));
if ($name !== '') {
$urbackupClientNames[$name] = true;
}
}
$cacheEntity = [];
$cacheLocation = [];
$cacheState = [];
$cacheUser = [];
$cacheGroup = [];
$itemtypes = Config::getEnabledItemtypes();
$missingAssets = [];
$candidatesByType = [];
$totalAssets = 0;
foreach ($itemtypes as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
$assetItem = new $itemtype();
if (!$assetItem instanceof CommonDBTM) {
continue;
}
$table = $assetItem->getTable();
if (!$DB->tableExists($table)) {
continue;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_deleted' => 0,
],
]);
foreach ($iterator as $assetRow) {
$totalAssets++;
$name = (string) ($assetRow['name'] ?? '');
if ($name === '') {
continue;
}
$nameLower = strtolower($name);
$assetLocationId = (int) ($assetRow['locations_id'] ?? 0);
$assetRootLocationId = LocationHelper::getRootLocationId($assetLocationId);
if ($assetRootLocationId !== $rootLocationId) {
continue;
}
$key = $itemtype . ':' . $assetRow['id'];
$assetId = (int) $assetRow['id'];
if (isset($linkedAssetKeys[$key])) {
continue;
}
if (isset($urbackupClientNames[$nameLower])) {
continue;
}
$entityName = self::getCachedName('Entity', (int) ($assetRow['entities_id'] ?? 0), $cacheEntity);
$locationName = self::getCachedLocationName($assetLocationId, $cacheLocation);
$stateName = self::getCachedName('State', (int) ($assetRow['states_id'] ?? 0), $cacheState);
$userName = self::getCachedName('User', (int) ($assetRow['users_id'] ?? 0), $cacheUser);
$missingAssets[] = [
'itemtype' => $itemtype,
'items_id' => $assetId,
'name' => $name,
'entity' => $entityName,
'location' => $locationName,
'otherserial' => (string) ($assetRow['otherserial'] ?? ''),
'state' => $stateName,
'user' => $userName,
];
$candidatesByType[$itemtype][] = $assetId;
}
}
$batchIps = self::batchLoadIps($candidatesByType);
$batchGroups = self::batchLoadGroups($candidatesByType);
foreach ($missingAssets as &$asset) {
$key = $asset['itemtype'] . ':' . $asset['items_id'];
$asset['ip'] = $batchIps[$key] ?? '';
$groupId = $batchGroups[$key] ?? 0;
$asset['group'] = $groupId > 0
? self::getCachedName('Group', $groupId, $cacheGroup)
: '';
}
unset($asset);
if (count($missingAssets) === 0) {
if ($totalAssets === 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No assets', 'urbackup'));
echo '</div>';
} else {
echo '<div class="alert alert-success">';
echo htmlspecialchars(__('All assets in this location are linked or already on the UrBackup server.', 'urbackup'));
echo '</div>';
}
return;
}
echo '<div class="d-flex justify-content-end mb-2">';
echo '<input type="text" id="missing-search" class="form-control form-control-sm" placeholder="' . htmlspecialchars(__('Search...', 'urbackup')) . '" style="width:250px">';
echo '</div>';
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<table id="missing-table" class="table table-striped table-hover">';
echo '<thead><tr>';
echo '<th class="sortable" data-col="0">' . htmlspecialchars(__('Name')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="1">' . htmlspecialchars(Entity::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="2">' . htmlspecialchars(Location::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="3">' . htmlspecialchars(__('Inventory number')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="4">' . htmlspecialchars(__('IP address', 'urbackup')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="5">' . htmlspecialchars(State::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="6">' . htmlspecialchars(User::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="7">' . htmlspecialchars(Group::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
if ($canWrite) {
echo '<th data-col="8">' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
}
echo '</tr></thead>';
echo '<tbody>';
foreach ($missingAssets as $asset) {
$item = new $asset['itemtype']();
$itemLink = '';
if ($item instanceof CommonDBTM && $item->getFromDB($asset['items_id'])) {
$itemLink = $item->getLinkURL();
}
echo '<tr>';
echo '<td>';
if ($itemLink !== '') {
echo '<a href="' . htmlspecialchars($itemLink) . '">' . htmlspecialchars($asset['name']) . '</a>';
} else {
echo htmlspecialchars($asset['name']);
}
echo '</td>';
echo '<td>' . htmlspecialchars($asset['entity']) . '</td>';
echo '<td>' . htmlspecialchars($asset['location']) . '</td>';
echo '<td>' . htmlspecialchars($asset['otherserial']) . '</td>';
echo '<td>' . htmlspecialchars($asset['ip']) . '</td>';
echo '<td>' . htmlspecialchars($asset['state']) . '</td>';
echo '<td>' . htmlspecialchars($asset['user']) . '</td>';
echo '<td>' . htmlspecialchars($asset['group']) . '</td>';
if ($canWrite) {
echo '<td>';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $asset['itemtype']]);
echo Html::hidden('items_id', ['value' => $asset['items_id']]);
echo Html::hidden('id', ['value' => (int) $server->fields['id']]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">';
echo htmlspecialchars(__('Connect'));
echo '</button>';
Html::closeForm();
echo '</td>';
}
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo <<<'JAVASCRIPT'
<script>
$(function () {
var sortDir = {};
$('#missing-table th.sortable').on('click', function () {
var col = parseInt($(this).data('col'));
var $table = $('#missing-table');
var $tbody = $table.find('tbody');
var rows = $tbody.find('tr').get();
var dir = sortDir[col] === 'asc' ? 'desc' : 'asc';
sortDir[col] = dir;
rows.sort(function (a, b) {
var aVal = $(a).children('td').eq(col).text().trim().toLowerCase();
var bVal = $(b).children('td').eq(col).text().trim().toLowerCase();
if (aVal < bVal) return dir === 'asc' ? -1 : 1;
if (aVal > bVal) return dir === 'asc' ? 1 : -1;
return 0;
});
$.each(rows, function (i, row) { $tbody.append(row); });
$table.find('th .sort-arrow').text('');
$(this).find('.sort-arrow').text(dir === 'asc' ? '\u25B2' : '\u25BC');
});
$('#missing-search').on('keyup', function () {
var val = $(this).val().toLowerCase();
$('#missing-table tbody tr').each(function () {
var match = false;
$(this).children('td').each(function () {
if ($(this).text().toLowerCase().indexOf(val) > -1) {
match = true;
return false;
}
});
$(this).toggle(match);
});
});
});
</script>
JAVASCRIPT;
}
private static function getCachedName(string $classname, int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$fqcn = '\\' . ltrim($classname, '\\');
$obj = new $fqcn();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$cache[$id] = '';
}
}
return $cache[$id];
}
/**
* Batch-load IPs for multiple assets across itemtypes.
*
* @param array<string, list<int>> $candidatesByType itemtype => [items_id, ...]
*
* @return array<string, string> key "itemtype:items_id" => IP
*/
private static function batchLoadIps(array $candidatesByType): array
{
global $DB;
$ips = [];
foreach ($candidatesByType as $itemtype => $ids) {
if (count($ids) === 0) {
continue;
}
$iterator = $DB->request([
'SELECT' => ['np.items_id', 'ipa.name'],
'FROM' => 'glpi_ipaddresses AS ipa',
'INNER JOIN' => [
'glpi_networknames AS nn' => [
'ON' => [
'nn' => 'items_id',
'ipa' => 'id',
['AND' => ['ipa.itemtype' => 'NetworkName']],
],
],
'glpi_networkports AS np' => [
'ON' => [
'np' => 'id',
'nn' => 'items_id',
['AND' => ['nn.itemtype' => 'NetworkPort']],
],
],
],
'WHERE' => [
'np.itemtype' => $itemtype,
'np.items_id' => $ids,
],
]);
foreach ($iterator as $row) {
$ip = (string) ($row['name'] ?? '');
if ($ip === '') {
continue;
}
$key = $itemtype . ':' . $row['items_id'];
if (!isset($ips[$key])) {
$ips[$key] = $ip;
}
}
}
return $ips;
}
/**
* Batch-load group IDs for multiple assets across itemtypes.
*
* @param array<string, list<int>> $candidatesByType itemtype => [items_id, ...]
*
* @return array<string, int> key "itemtype:items_id" => groups_id
*/
private static function batchLoadGroups(array $candidatesByType): array
{
global $DB;
$groups = [];
foreach ($candidatesByType as $itemtype => $ids) {
if (count($ids) === 0) {
continue;
}
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $ids,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
]);
foreach ($iterator as $row) {
$key = $itemtype . ':' . $row['items_id'];
if (!isset($groups[$key])) {
$groups[$key] = (int) ($row['groups_id'] ?? 0);
}
}
}
return $groups;
}
private static function getAssetGroupName(string $itemtype, int $items_id, array &$cache): string
{
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $items_id,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$groupId = (int) ($row['groups_id'] ?? 0);
if ($groupId > 0) {
return self::getCachedName('Group', $groupId, $cache);
}
}
return '';
}
private static function getCachedLocationName(int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$obj = new Location();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$cache[$id] = '';
}
}
return $cache[$id];
}
public static function getAssetIp(string $itemtype, int $items_id): string
{
global $DB;
$iterator = $DB->request([
'SELECT' => ['ipa.name'],
'FROM' => 'glpi_ipaddresses AS ipa',
'INNER JOIN' => [
'glpi_networknames AS nn' => [
'ON' => [
'nn' => 'items_id',
'ipa' => 'id',
['AND' => ['ipa.itemtype' => 'NetworkName']],
],
],
'glpi_networkports AS np' => [
'ON' => [
'np' => 'id',
'nn' => 'items_id',
['AND' => ['nn.itemtype' => 'NetworkPort']],
],
],
],
'WHERE' => [
'np.itemtype' => $itemtype,
'np.items_id' => $items_id,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$ip = (string) ($row['name'] ?? '');
if ($ip !== '') {
return $ip;
}
}
return '';
}
} }
+14 -6
View File
@@ -79,6 +79,7 @@ class ServerAsset extends CommonDBTM
} }
$table = self::getTable(); $table = self::getTable();
$date = $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s');
$existing = self::getLinkForAsset($itemtype, $items_id, false); $existing = self::getLinkForAsset($itemtype, $items_id, false);
@@ -87,6 +88,10 @@ class ServerAsset extends CommonDBTM
$table, $table,
[ [
'plugin_urbackup_servers_id' => $server_id, 'plugin_urbackup_servers_id' => $server_id,
'client_name' => (string) ($item->fields['name'] ?? ''),
'client_ip' => self::extractAssetIp($item),
'is_active' => 1,
'date_mod' => $date,
], ],
[ [
'id' => (int) $existing['id'], 'id' => (int) $existing['id'],
@@ -238,40 +243,43 @@ class ServerAsset extends CommonDBTM
echo "<div class='center'>"; echo "<div class='center'>";
echo "<table class='tab_cadre_fixehov'>"; echo "<table class='tab_cadre_fixehov'>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('Linked assets', 'urbackup')) . "</th></tr>"; echo "<tr><th colspan='5'>" . htmlspecialchars(__('Linked assets', 'urbackup')) . "</th></tr>";
echo "<tr>"; echo "<tr>";
echo "<th>" . htmlspecialchars(__('Asset', 'urbackup')) . "</th>"; echo "<th>" . htmlspecialchars(__('Asset', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type')) . "</th>"; echo "<th>" . htmlspecialchars(__('Type')) . "</th>";
echo "<th>" . htmlspecialchars(__('IP address', 'urbackup')) . "</th>"; echo "<th>" . htmlspecialchars(__('IP address', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last file backup', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last image backup', 'urbackup')) . "</th>";
echo "</tr>"; echo "</tr>";
$iterator = $DB->request([ $iterator = $DB->request([
'FROM' => self::getTable(), 'FROM' => self::getTable(),
'WHERE' => [ 'WHERE' => [
'plugin_urbackup_servers_id' => (int) $item->fields['id'], 'plugin_urbackup_servers_id' => (int) $item->fields['id'],
'is_active' => 1,
], ],
'ORDER' => 'client_name',
]); ]);
foreach ($iterator as $row) { foreach ($iterator as $row) {
$asset_label = (string) $row['client_name'];
$itemtype = (string) $row['itemtype']; $itemtype = (string) $row['itemtype'];
$items_id = (int) $row['items_id']; $items_id = (int) $row['items_id'];
$asset_label = (string) $items_id;
$asset_ip = '';
if (class_exists($itemtype)) { if (class_exists($itemtype)) {
$asset = new $itemtype(); $asset = new $itemtype();
if ($asset instanceof CommonDBTM && $asset->getFromDB($items_id)) { if ($asset instanceof CommonDBTM && $asset->getFromDB($items_id)) {
$asset_label = $asset->getLink(); $asset_label = $asset->getLink();
$asset_ip = Server::getAssetIp($itemtype, $items_id);
} }
} }
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . $asset_label . "</td>"; echo "<td>" . $asset_label . "</td>";
echo "<td>" . htmlspecialchars($itemtype) . "</td>"; echo "<td>" . htmlspecialchars($itemtype) . "</td>";
echo "<td>" . htmlspecialchars($asset_ip ?: '-') . "</td>"; echo "<td>" . htmlspecialchars((string) $row['client_ip']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_file_backup']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_image_backup']) . "</td>";
echo "</tr>"; echo "</tr>";
} }
+7 -36
View File
@@ -27,9 +27,7 @@ class UrbackupApiClient
private bool $ignore_ssl; private bool $ignore_ssl;
private int $timeout = 30; private int $timeout = 10;
private int $connect_timeout = 5;
private string $session = ''; private string $session = '';
@@ -37,10 +35,6 @@ class UrbackupApiClient
private int $lastlogid = 0; private int $lastlogid = 0;
private ?array $cached_status = null;
private array $cached_settings = [];
private string $server_version = ''; private string $server_version = '';
private bool $is_version_2_4_or_higher = false; private bool $is_version_2_4_or_higher = false;
@@ -251,29 +245,21 @@ class UrbackupApiClient
*/ */
public function getStatus(): array public function getStatus(): array
{ {
if ($this->cached_status !== null) {
return $this->cached_status;
}
$data = $this->apiAction('status'); $data = $this->apiAction('status');
if (isset($data['status']) && is_array($data['status'])) { if (isset($data['status']) && is_array($data['status'])) {
$this->cached_status = array_values($data['status']); return array_values($data['status']);
return $this->cached_status;
} }
if (isset($data['clients']) && is_array($data['clients'])) { if (isset($data['clients']) && is_array($data['clients'])) {
$this->cached_status = array_values($data['clients']); return array_values($data['clients']);
return $this->cached_status;
} }
if (array_is_list($data)) { if (array_is_list($data)) {
$this->cached_status = $data; return $data;
return $this->cached_status;
} }
$this->cached_status = []; return [];
return $this->cached_status;
} }
/** /**
@@ -329,22 +315,16 @@ class UrbackupApiClient
return []; return [];
} }
if (isset($this->cached_settings[$client_id])) {
return $this->cached_settings[$client_id];
}
$data = $this->apiAction('settings', [ $data = $this->apiAction('settings', [
'sa' => 'clientsettings', 'sa' => 'clientsettings',
't_clientid' => $client_id, 't_clientid' => $client_id,
]); ]);
if (isset($data['settings']) && is_array($data['settings'])) { if (isset($data['settings']) && is_array($data['settings'])) {
$this->cached_settings[$client_id] = $data['settings']; return $data['settings'];
return $this->cached_settings[$client_id];
} }
$this->cached_settings[$client_id] = $data; return $data;
return $this->cached_settings[$client_id];
} }
/** /**
@@ -356,11 +336,6 @@ class UrbackupApiClient
* *
* @return bool * @return bool
*/ */
private function clearSettingsCache(int $client_id): void
{
unset($this->cached_settings[$client_id]);
}
public function changeClientSetting(string $client_name, string $key, mixed $value): bool public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{ {
$client_id = $this->getClientIdByName($client_name); $client_id = $this->getClientIdByName($client_name);
@@ -376,7 +351,6 @@ class UrbackupApiClient
$key => (string) $value, $key => (string) $value,
]); ]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -395,7 +369,6 @@ class UrbackupApiClient
$key => $value, $key => $value,
]); ]);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -418,7 +391,6 @@ class UrbackupApiClient
$data = $this->apiAction('settings', $params); $data = $this->apiAction('settings', $params);
$this->clearSettingsCache($client_id);
return $this->responseIsSuccess($data); return $this->responseIsSuccess($data);
} }
@@ -695,7 +667,6 @@ class UrbackupApiClient
$options = [ $options = [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout, CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => $this->connect_timeout,
CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl, CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl,
CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2, CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2,
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
+69
View File
@@ -0,0 +1,69 @@
<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
@@ -0,0 +1,39 @@
<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
@@ -0,0 +1,118 @@
{% 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
@@ -0,0 +1,51 @@
{% 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 %}