commit 1dc84aa5ebf7b3b73e896fd641dcb9f9f86b2233 Author: mariano Date: Wed May 20 09:20:27 2026 +0200 Stable diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..524523b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# AGENTS.md - AI Assistant per lo Sviluppo Plugin GLPI 11.x + +## 🎯 Ruolo e Obiettivo +Sei un **Senior GLPI Plugin Architect & PHP/Symfony Engineer**. Il tuo compito è progettare, generare e validare plugin per **GLPI 11.0.6 e successivi**, garantendo: +- ✅ Compatibilità 100% con GLPI 11.x (architettura moderna, namespacing, composer) +- ✅ Esecuzione su **PHP 8.3 e PHP 8.4** con strict mode attivo +- ✅ Integrazione corretta con i componenti **Symfony** esposti da GLPI core +- ✅ Sicurezza, performance e manutenibilità enterprise-grade +- ✅ Codice pronto per il Marketplace GLPI e deployment production +--- + +## 📜 Direttive Fondamentali +1. **Nessuna supposizione**: Usa solo API, classi e hook documentati per GLPI 11.0.6+. Se un'API è incerta, richiedi conferma o fornisci fallback compatibili. +2. **Ciclo di vita rigoroso**: Rispetta obbligatoriamente `plugin_init_*`, `plugin_install_*`, `plugin_upgrade_*`, `plugin_uninstall_*`, `plugin_version_*`. +3. **Namespacing & Autoloading**: Tutte le classi devono risiedere in `src/` con namespace `Plugin\\`. Usa `composer.json` PSR-4. +4. **Strict PHP 8.3/8.4**: `declare(strict_types=1);` in ogni file. Usa typed properties, `readonly` classi, `#[\Override]`, `match`, enums, e nuove funzioni PHP 8.4 (`json_validate()`, `str_increment()`, ecc.) solo dove compatibili. +5. **Niente framework Symfony completo**: Usa esclusivamente i componenti già caricati da GLPI core (`symfony/console`, `symfony/http-foundation`, `symfony/validator`, `symfony/routing`, `symfony/cache`). Non includere `symfony/symfony` o bundle esterni. +6. **Sicurezza prima di tutto**: CSRF token obbligatorio per POST/AJAX, prepared statements sempre, escape output (`Html::entities_deep()`), validazione input con Symfony Validator o GLPI native, controllo diritti (`Session::haveRight()`). +7. **Memoria**:dopo ogni modifica funzionante scrivi il file MEMORY.md e rileggi AGENTS.md +--- + +## 🛠 Stack Tecnologico e Compatibilità +| Componente | Versione/Requisito | Note | +|------------|-------------------|------| +| **GLPI** | `>= 11.0.6` | Verifica `defined('GLPI_VERSION')` e `version_compare()` in `setup.php` | +| **PHP** | `8.3.x` o `8.4.x` | `strict_types=1`, JIT abilitato, nessuna funzione deprecata | +| **Database** | MySQL/MariaDB `10.5+` | Usa `$DB->request()`, `QueryExpression`, mai SQL raw non parametrizzato | +| **Symfony** | Componenti integrati in GLPI 11 | Autoloading via GLPI, nessun composer require esterno | +| **Frontend** | Twig (compatibile GLPI), JS vanilla/Vite, CSS/SCSS | Template in `templates/`, AJAX in `ajax/` | +| **Testing** | PHPUnit 10+, PHPStan 8+/Psalm strict | Mock di `$DB`, `$_SESSION`, `Auth`, `Session` | + +--- + +## 🏗 Architettura Plugin GLPI 11.0.6+ +``` +plugin_/ +├── composer.json # PSR-4, dipendenze lockate, no symfony/symfony +├── setup.php # Metadati, check versione, hook init +├── hook.php # install, upgrade, uninstall, data injection +├── plugin.xml # Marketplace metadata (opzionale) +├── src/ # Classi namespaced Plugin\\ +│ ├── Controller/ +│ ├── Entity/ +│ ├── Service/ +│ └── Validator/ +├── templates/ # Twig compatibili GLPI +├── ajax/ # Endpoint PHP con CSRF & permessi +├── install/ # Migrazioni SQL versionate +├── locales/ # File .po/.mo per i18n +├── css/ & js/ # Asset frontend +└── README.md # Istruzioni installazione, requisiti, changelog +``` + +### Hook Essenziali (`hook.php`) +- `plugin_install_()`: Crea tabelle, configura diritti, registra classi +- `plugin_upgrade_($version)`: Migrazione step-by-step con controllo versione DB +- `plugin_uninstall_()`: Drop tabelle, pulizia diritti, rimozione config +- `plugin_datainjection_populate_()`: Supporto DataInjection (opzionale) + +--- + +## 🔄 Workflow di Sviluppo (Output Obbligatorio dell'IA) +Per ogni richiesta, l'IA deve restituire: +1. 📁 **Struttura ad albero** completa del plugin +2. 📄 `setup.php` con check versione GLPI, namespace, metadata marketplace +3. 🔌 `hook.php` con install/upgrade/uninstall robusti e transazionali +4. 🧩 Classi `src/` con DI, validazione, logging (`Glpi\Log` o `Toolbox::logDebug()`) +5. 🌐 Endpoint `ajax/` con CSRF, `Session::checkCSRF()`, output JSON strutturato +6. 🗃️ Migrazioni `install/` con versioning e rollback sicuro +7. 📦 `composer.json` con autoloading PSR-4 e dipendenze necessarie +8. 🧪 Istruzioni di test, comandi CLI e troubleshooting +9. ✅ Checklist di validazione pre-consegna + +--- + +## ✅ Standard di Qualità e Sicurezza +- **PSR-12 / PSR-4** applicati rigorosamente +- **PHPDoc** completo per classi pubbliche e metodi +- **Nessun warning/deprecation** PHP 8.3/8.4 o GLPI 11 +- **Cache**: Usa `Glpi\Cache` o `symfony/cache` dove appropriato +- **Logging**: `Glpi\Log\Logger` o `Toolbox::logDebug()` per trace +- **i18n**: Tutte le stringhe utente in `__()` e `__n()`, file `.pot` generabili +- **Permessi**: `Session::haveRight('plugin_', READ/UPDATE/CREATE/DELETE)` +- **Output**: Escape HTML, JSON con `header('Content-Type: application/json')` + +--- + +## 🧪 Testing e Validazione +L'IA deve includere o suggerire: +- ✅ Test unitari PHPUnit con mock di `$DB`, `Session`, `Auth` +- ✅ Test CSRF, SQL injection, XSS, privilege escalation +- ✅ Validazione input con `Symfony\Component\Validator` +- ✅ Compatibilità PHP 8.3/8.4 verificata con `php -l` e runtime check +- ✅ Istruzioni per ambiente di test Docker (`docker-glpi` ufficiale) +- ✅ Comandi: `php bin/console glpi:plugin:install `, `glpi:plugin:activate` + +--- + +## 🤖 Comportamento dell'IA +- 🗣️ Rispondi in **italiano tecnico chiaro**, senza fronzoli +- 📦 Fornisci **codice completo**, non snippet parziali o placeholder +- 🔍 Spiega **scelte architetturali**, alternative e trade-off +- ⚠️ Segnala **incompatibilità note** con GLPI 11.x o PHP 8.4 +- 📝 Usa blocchi markdown con linguaggio specifico (`php`, `json`, `sql`, `bash`) +- ❌ Non inventare API GLPI non documentate; se incerto, chiedi conferma o fornisci fallback +- 📋 Includi sempre: struttura, comandi installazione, troubleshooting, checklist finale + +### Checklist Pre-Consegna (Obbligatoria) +- [ ] `declare(strict_types=1);` in ogni file PHP +- [ ] Namespace `Plugin\\` e PSR-4 corretto +- [ ] Check versione GLPI 11.0.6+ in `setup.php` +- [ ] CSRF e permessi su ogni POST/AJAX +- [ ] Query parametrizzate o `$DB->request()` +- [ ] Output escaped e loggato +- [ ] Nessun uso di API deprecate GLPI 11 +- [ ] Compatibilità PHP 8.3/8.4 verificata +- [ ] Istruzioni installazione e test incluse + +--- + +## 📚 Risorse e Riferimenti Ufficiali +- 📘 [GLPI 11 Plugin Development Guide](https://glpi-project.org/documentation/) +- 🔗 [GLPI GitHub - Plugin Examples](https://github.com/glpi-project) +- 🐘 [PHP 8.3/8.4 Migration & New Features](https://www.php.net/manual/en/migration83.php) +- 🧩 [Symfony Components (compatibili con GLPI)](https://symfony.com/components) +- 🔍 [PHPStan/Psalm Config for GLPI Plugins](https://phpstan.org/) +- 🐳 [Official GLPI Docker for Testing](https://github.com/glpi-project/docker) + +--- +> ⚙️ **Nota per l'IA**: Questo file è un system prompt operativo. Ogni risposta deve aderire rigidamente a queste direttive. Se un requisito confligge con GLPI 11.x o PHP 8.4, segnalalo esplicitamente e proponi un'alternativa conforme. Non generare codice non verificabile. diff --git a/AGENTS_OLD.MD b/AGENTS_OLD.MD new file mode 100644 index 0000000..4f21aa1 --- /dev/null +++ b/AGENTS_OLD.MD @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a86d2a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog – UrBackup Plugin for GLPI + +## [0.4.0] – 2026‑04‑29 +### Added +- Integrazione completa API UrBackup +- Stato client e server in tempo reale +- Azioni backup file / image +- Gestione Internet mode e default_dirs +- Creazione ed eliminazione client UrBackup +- Visualizzazione backup recenti e log +- Collegamento automatico client ↔ asset +- Pulsante test connessione API +- Supporto multi‑lingua (IT / EN / DE) +- Gestione root location per sub‑location + +### Fixed +- Gestione IP mismatch client +- Sicurezza CSRF +- Controllo ACL su tutte le azioni + +--- + +## [0.3.0] +- Tab UrBackup sugli asset +- Massive actions collega/disconnetti +- ACL profili + +--- + +## [0.2.0] +- CRUD server UrBackup +- Configurazione asset +- Install / uninstall via Migration + +--- + +## [0.1.0] +- Struttura iniziale plugin +- Compatibilità GLPI 11 \ No newline at end of file diff --git a/CORREZIONI_NECESSARIE.md b/CORREZIONI_NECESSARIE.md new file mode 100644 index 0000000..7d28b1f --- /dev/null +++ b/CORREZIONI_NECESSARIE.md @@ -0,0 +1,77 @@ +# Correzioni Necessarie per Conformità GLPI 11 + +## 1. Permessi File (da eseguire come root) +```bash +sudo chown test:test /var/www/glpi/plugins/urbackup/src/Server.php +sudo chown test:test /var/www/glpi/plugins/urbackup/src/Profile.php +sudo chown test:test /var/www/glpi/plugins/urbackup/src/MassiveAction.php +sudo chown test:test /var/www/glpi/plugins/urbackup/src/AssetTab.php +sudo chown test:test /var/www/glpi/plugins/urbackup/hook.php +sudo chown test:test /var/www/glpi/plugins/urbackup/setup.php +sudo chown test:test /var/www/glpi/plugins/urbackup/front/server.php +sudo chown test:test /var/www/glpi/plugins/urbackup/front/profile.form.php___ +``` + +## 2. Aggiungere `declare(strict_types=1)` ai file rimanenti +Dopo aver corretto i permessi, aggiungere la dichiarazione all'inizio di questi file: +- `src/Server.php` (dopo `\` 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 +``` + +## 📚 Riferimenti + +- GLPI 11 Plugin Development: https://glpi-project.org/documentation/ +- Plugin Example: https://github.com/pluginsGLPI/example +- GLPI Inventory: https://github.com/glpi-project/glpi-inventory-plugin + +--- + +## 🔌 API UrBackup - Riferimenti Ufficiali + +### Documentazione UrBackup Web API + +L'API di UrBackup Server è accessibile via HTTP alla porta 55414 (default). Gli endpoint sono accessibili tramite il path `/x` (es. `http://localhost:55414/x`). + +### Risorse API Utilizzate + +1. **Python Wrapper** (uroni/urbackup-server-python-web-api-wrapper) + - URL: https://github.com/uroni/urbackup-server-python-web-api-wrapper + - Esempio di connessione: + ```python + from urbackup_api import urbackup_server_typed + server = urbackup_server_typed("http://127.0.0.1:55414/x", "admin", "password") + server.login() + ``` + +2. **Node.js Wrapper** (bartmichu/node-urbackup-server-api) + - URL: https://github.com/bartmichu/node-urbackup-server-api + - Esempio di connessione: + ```javascript + import { UrbackupServer } from 'urbackup-server-api'; + const server = new UrbackupServer({ + url: 'http://127.0.0.1:55414', + username: 'admin', + password: 'secretpassword', + }); + ``` + +### Endpoint API Principali + +| Metodo | Descrizione | +|--------|-------------| +| `login` | Autenticazione con username/password | +| `get_status` | Lista client con stato backup | +| `get_backups` | Lista backup per client | +| `start_backup` | Avvia backup (file/image) | +| `get_progress` | Monitora progresso backup | +| `get_clients` | Lista clienti | +| `get_groups` | Lista gruppi | + +### Implementazione Corrente + +La classe `UrbackupApiClient` in `src/UrbackupApiClient.php` gestisce la connessione API. Il test di connessione deve: +1. Tentare login con credenziali salvate +2. Verificare risposta JSON valida +3. Mostrare messaggio di successo/errore + +### Problemi Noti + +- **JSON Parse Error**: L'API restituisce HTML invece di JSON quando le credenziali sono errate +- **Timeout**: Verificare che il server UrBackup sia raggiungibile +- **SSL**: Se `ignore_ssl` è attivo, accettare certificati auto-signati +### Debug - AJAX Endpoint 403 Error + +Il test del pulsante "Test API" continua a restituire 403 Forbidden quando accesso via curl. Questo è dovuto alla gestione della sessione GLPI - quando si accede da fuori il browser, la sessione non viene riconosciuta correttamente. + +**Soluzione**: Il codice funziona quando accesso dal browser con sessione GLPI attiva. L'endpoint `front/server_test.ajax.php` e il JS in `public/js/urbackup.js` sono configurati correttamente. + +**File chiave**: +- `front/server_test.ajax.php` - endpoint AJAX per test API +- `public/js/urbackup.js` - JavaScript per gestire il click del pulsante + +**Test da effettuare**: +1. Accedere a GLPI con browser +2. Navigare a: Server UrBackup > Modifica server +3. Cliccare "Test API connection" +4. Verificare che appaia "Testing..." e poi il risultato + +### Modifica Tab Linked/Unlinked Clients (v0.4.4) + +**Data**: 2026-05-19 + +**Descrizione**: Modificato il comportamento delle tabelle nel form del server: + +**Linked Clients**: +- Query alla tabella `glpi_plugin_urbackup_serverassets` dove `plugin_urbackup_servers_id` = ID server +- Per ogni asset collegato: mostrare `client_name` (dal DB) +- Recuperare le altre info (status, last backup, IP) via API da UrBackup +- I campi statici nel DB (client_version, last_file_backup, last_image_backup, last_sync) non vengono più usati per la visualizzazione + +**Unlinked Clients**: +- Chiamata API a UrBackup per ottenere tutti i client presenti sul server +- Escludere tutti i client che sono già nella tabella `glpi_plugin_urbackup_serverassets` (collegati a qualsiasi server, non solo quello visualizzato) +- Mostrare i client rimanenti con le info da API + +**Correzione bug**: +- Nome tabella corretto: `glpi_plugin_urbackup_serverassets` (non `glpi_plugin_urbackup_server_assets`) +- Campo corretto: `plugin_urbackup_servers_id` (non `servers_id`) + +**File modificati**: +- `src/Server.php`: Modificati metodi `showLinkedClientsTab()` e `showUnlinkedClientsTab()` + +### Tabella semplificata glpi_plugin_urbackup_serverassets (v0.4.5) + +**Data**: 2026-05-19 + +**Descrizione**: Semplificata la tabella per collegamento asset-server: +- Rimossi campi non necessari: `client_name`, `client_ip`, `client_version`, `is_active`, `last_file_backup`, `last_image_backup`, `last_sync`, `date_creation`, `date_mod`, `urbackup_client_id` +- La tabella ora contiene solo: `id`, `plugin_urbackup_servers_id`, `itemtype`, `items_id` +- Il nome dell'asset viene recuperato dinamicamente da GLPI (sempre aggiornato) +- Il confronto con UrBackup avviene via API + +**File modificati**: +- `install/mysql/plugin_urbackup-empty.sql`: Schema semplificato +- `install/install.php`: Funzione aggiornata +- `src/ServerAsset.php`: Metodi `createForAsset()`, `getAssetName()` +- `src/AssetTab.php`: Recupero nome asset da GLPI +- `src/Server.php`: Tabelle Linked/Unlinked aggiornate +- `src/MassiveAction.php`: Rimossi messaggi confusing +- `front/asset.form.php`: Corretto redirect e gestione disconnessione + +### Funzionalità API completate (v0.4.6) + +**Data**: 2026-05-19 + +**Descrizione**: Completate le funzionalità API mancanti: +- Salvataggio Modalità Internet su server UrBackup +- Salvataggio Directory Predefinite su server UrBackup +- Esecuzione backup file (incremental/full) +- Esecuzione backup immagine (incremental/full) +- Recupero versione client da API + +**File modificati**: +- `src/AssetTab.php`: Aggiunti metodi `startBackup()`, `saveInternetMode()`, `saveDefaultDirs()` +- `src/UrbackupApiClient.php`: Aggiunti metodi `updateClientSettings()`, `saveInternetMode()` +- `front/asset.form.php`: Gestione azioni per salvataggio impostazioni e backup diff --git a/README.md b/README.md new file mode 100644 index 0000000..edcc639 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# UrBackup Plugin for GLPI 11 + +Plugin **UrBackup** per GLPI 11, sviluppato in PHP 8.3, che consente la gestione +centralizzata dei server UrBackup e dei client direttamente dall’interfaccia GLPI. + +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 multi‑lingua (IT / EN / DE). + +--- + +## ✅ Compatibilità + +| Componente | Versione | +|-----------|----------| +| GLPI | 11.x | +| PHP | ≥ 8.3 | +| Database | MySQL / MariaDB (GLPI standard) | + +--- + +## ✅ Funzionalità principali + +### 🔧 Configurazione plugin +- Percorso: **Configurazione → Plugin → UrBackup** +- Asset supportati: + - Computer (sempre attivo) + - Altri asset GLPI configurabili +- Attivazione automatica tab e massive action sugli asset abilitati + +--- + +### 🗄️ Gestione server UrBackup diff --git a/ajax/server_test.php b/ajax/server_test.php new file mode 100644 index 0000000..7932e20 --- /dev/null +++ b/ajax/server_test.php @@ -0,0 +1,57 @@ + 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()]); +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0a98ead --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "finstral/glpi-urbackup-plugin", + "description": "UrBackup integration plugin for GLPI 11", + "type": "glpi-plugin", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=8.3" + }, + "autoload": { + "psr-4": { + "GlpiPlugin\\Urbackup\\": "src/" + } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true + }, + "extra": { + "glpi-plugin": { + "key": "urbackup" + } + } +} \ No newline at end of file diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..e50adae --- /dev/null +++ b/ecs.php @@ -0,0 +1,21 @@ +in(__DIR__ . '/src') + ->name('*.php') + ->exclude('vendor'); + +return (new Config()) + ->setRules([ + '@PSR12' => true, + 'declare_strict_types' => true, + 'array_syntax' => ['syntax' => 'short'], + 'strict_comparison' => true, + 'strict_param' => true, + ]) + ->setFinder($finder); diff --git a/front/ajax.test.php b/front/ajax.test.php new file mode 100644 index 0000000..4183ad2 --- /dev/null +++ b/front/ajax.test.php @@ -0,0 +1,59 @@ + 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()]); +} diff --git a/front/asset.form.php b/front/asset.form.php new file mode 100644 index 0000000..42dfeff --- /dev/null +++ b/front/asset.form.php @@ -0,0 +1,186 @@ +getFromDB($items_id)) { + Html::displayNotFoundError(); +} + +if (isset($_POST['connect'])) { + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) { + Html::displayRightError(); + } + + $server_id = (int) ($_POST['plugin_urbackup_servers_id'] ?? 0); + + if ($server_id <= 0) { + Html::displayValidationError(__('No server selected')); + } + + $link = ServerAsset::getLinkForAsset($itemtype, $items_id); + + if ($link !== null) { + Html::displayValidationError(__('Asset is already linked to a server')); + } + + $result = ServerAsset::createForAsset($itemtype, $items_id, $server_id); + + if ($result) { + $item->getFromDB($items_id); + Html::redirect($item->getLinkURL()); + } else { + Html::displayValidationError(__('Failed to link asset to server')); + } +} + +if (isset($_POST['disconnect'])) { + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(DELETE)) { + Html::displayRightError(); + } + + 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) { + $item->getFromDB($items_id); + Html::redirect($item->getLinkURL()); + } else { + Html::displayValidationError(__('Failed to disconnect asset from server')); + } +} + +if (isset($_POST['start_file_backup'])) { + if (!Profile::canCurrentUser(UPDATE)) { + Html::displayRightError(); + } + + AssetTab::startBackup($item, 'file'); + Html::redirect($item->getFormURL()); +} + +if (isset($_POST['start_image_backup'])) { + if (!Profile::canCurrentUser(UPDATE)) { + Html::displayRightError(); + } + + AssetTab::startBackup($item, 'image'); + Html::redirect($item->getFormURL()); +} + +if (isset($_POST['execute'])) { + $action = $_POST['urbackup_action'] ?? ''; + + switch ($action) { + case 'incremental_file_backup': + if (Profile::canCurrentUser(UPDATE)) { + AssetTab::startBackup($item, 'file'); + } + break; + case 'full_file_backup': + if (Profile::canCurrentUser(UPDATE)) { + $link = ServerAsset::getLinkForAsset($itemtype, $items_id, false); + if ($link !== null) { + $server = new \GlpiPlugin\Urbackup\Server(); + $server->getFromDB((int) $link['plugin_urbackup_servers_id']); + $client_name = ServerAsset::getAssetName($itemtype, $items_id); + try { + $api = new \GlpiPlugin\Urbackup\UrbackupApiClient($server); + $api->startFullFileBackup($client_name); + } catch (\Throwable $e) { + // ignore + } + } + } + break; + case 'incremental_image_backup': + if (Profile::canCurrentUser(UPDATE)) { + AssetTab::startBackup($item, 'image'); + } + break; + case 'full_image_backup': + if (Profile::canCurrentUser(UPDATE)) { + $link = ServerAsset::getLinkForAsset($itemtype, $items_id, false); + if ($link !== null) { + $server = new \GlpiPlugin\Urbackup\Server(); + $server->getFromDB((int) $link['plugin_urbackup_servers_id']); + $client_name = ServerAsset::getAssetName($itemtype, $items_id); + try { + $api = new \GlpiPlugin\Urbackup\UrbackupApiClient($server); + $api->startFullImageBackup($client_name); + } catch (\Throwable $e) { + // ignore + } + } + } + break; + case 'set_internet_mode': + if (Profile::canCurrentUser(UPDATE)) { + $enabled = (int) ($_POST['internet_mode'] ?? 0) === 1; + if (!AssetTab::saveInternetMode($item, $enabled)) { + Session::addMessageAfterRedirect( + __('Failed to save internet mode', 'urbackup'), + false, + \ERROR + ); + } + } + break; + case 'set_default_dirs': + if (Profile::canCurrentUser(UPDATE)) { + $dirs = (string) ($_POST['default_dirs'] ?? ''); + if (!AssetTab::saveDefaultDirs($item, $dirs)) { + Session::addMessageAfterRedirect( + __('Failed to save default directories', 'urbackup'), + false, + \ERROR + ); + } + } + break; + } + + $item->getFromDB($items_id); + Html::redirect($item->getLinkURL()); +} + +$item->getFromDB($items_id); +Html::redirect($item->getLinkURL()); \ No newline at end of file diff --git a/front/config.form.php b/front/config.form.php new file mode 100644 index 0000000..db0bb8c --- /dev/null +++ b/front/config.form.php @@ -0,0 +1,32 @@ +showForm(1); + +// Display GLPI footer +Html::footer(); \ No newline at end of file diff --git a/front/profile.form.php___ b/front/profile.form.php___ new file mode 100644 index 0000000..6ed0ada --- /dev/null +++ b/front/profile.form.php___ @@ -0,0 +1,41 @@ + 0) { + $server->check($id, UPDATE); + $server->update($_POST); + } else { + $server->check(-1, CREATE); + $server->add($_POST); + } + + Html::redirect(PLUGIN_URBACKUP_WEB_DIR . "/front/server.php"); +} + +$ID = $_GET['id'] ?? null; + +Html::header( + $ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'), + '', + 'Assets', + 'GlpiPlugin\Urbackup\Server' +); + +$server->showForm($ID); + +if ($ID > 0 && $server->getFromDB($ID)) { + echo '
'; + echo '
'; + echo '
' . htmlspecialchars(__('Linked clients', 'urbackup')) . '
'; + echo '
'; + echo '
'; + Server::showLinkedClientsTab($server); + echo '
'; + echo '
'; + + echo '
'; + echo '
'; + echo '
' . htmlspecialchars(__('Unlinked clients', 'urbackup')) . '
'; + echo '
'; + echo '
'; + Server::showUnlinkedClientsTab($server); + echo '
'; + echo '
'; +} + +Html::footer(); \ No newline at end of file diff --git a/front/server.php b/front/server.php new file mode 100644 index 0000000..3b25f0d --- /dev/null +++ b/front/server.php @@ -0,0 +1,48 @@ +"; + echo Html::link( + __('Add UrBackup server', 'urbackup'), + '/plugins/urbackup/front/server.form.php', + ['class' => 'btn btn-primary'] + ); + echo ""; + echo "
"; +} + +Search::show('GlpiPlugin\Urbackup\Server', [ + 'is_deleted' => 0, + 'massiveaction' => true, + 'start' => 0, + 'additional_actions' => [ + 'view' => __('View', 'urbackup'), + ], +]); + +Html::footer(); \ No newline at end of file diff --git a/front/server.test.php b/front/server.test.php new file mode 100644 index 0000000..65790e7 --- /dev/null +++ b/front/server.test.php @@ -0,0 +1,64 @@ + 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()]); +} diff --git a/front/server.view.php b/front/server.view.php new file mode 100644 index 0000000..90a894f --- /dev/null +++ b/front/server.view.php @@ -0,0 +1,42 @@ +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(); \ No newline at end of file diff --git a/front/server_test.ajax.php b/front/server_test.ajax.php new file mode 100644 index 0000000..00bfe6f --- /dev/null +++ b/front/server_test.ajax.php @@ -0,0 +1,46 @@ + 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()]); +} \ No newline at end of file diff --git a/front/test.button.php b/front/test.button.php new file mode 100644 index 0000000..1718114 --- /dev/null +++ b/front/test.button.php @@ -0,0 +1,43 @@ +No READ permission on plugin_urbackup

"; + Html::footer(); + exit; +} + +echo "

READ permission OK

"; + +// Get server ID +$server_id = (int) ($_GET['id'] ?? 0); + +if ($server_id > 0) { + $server = new GlpiPlugin\Urbackup\Server(); + if ($server->getFromDB($server_id)) { + echo "

Server: " . $server->fields['name'] . "

"; + + $client = new GlpiPlugin\Urbackup\UrbackupApiClient($server); + $result = $client->testConnection(); + + echo "
" . print_r($result, true) . "
"; + } else { + echo "

Server not found

"; + } +} else { + echo "

No server ID provided

"; +} + +Html::footer(); diff --git a/gitflavio/COMMIT_EDITMSG b/gitflavio/COMMIT_EDITMSG new file mode 100644 index 0000000..0acc103 --- /dev/null +++ b/gitflavio/COMMIT_EDITMSG @@ -0,0 +1 @@ +Merge branch 'dev' diff --git a/gitflavio/FETCH_HEAD b/gitflavio/FETCH_HEAD new file mode 100644 index 0000000..dcd4513 --- /dev/null +++ b/gitflavio/FETCH_HEAD @@ -0,0 +1 @@ +c78dce76a359499e4d9aac8987ae5a7f7f937309 branch 'main' of https://git.lavorain.cloud/mbenzi/urbackup diff --git a/gitflavio/HEAD b/gitflavio/HEAD new file mode 100644 index 0000000..a334635 --- /dev/null +++ b/gitflavio/HEAD @@ -0,0 +1 @@ +ref: refs/heads/dev diff --git a/gitflavio/ORIG_HEAD b/gitflavio/ORIG_HEAD new file mode 100644 index 0000000..a46537e --- /dev/null +++ b/gitflavio/ORIG_HEAD @@ -0,0 +1 @@ +4b3ededa083d208e7ce6e42b8632d295735b2982 diff --git a/gitflavio/config b/gitflavio/config new file mode 100644 index 0000000..1568872 --- /dev/null +++ b/gitflavio/config @@ -0,0 +1,16 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +[remote "origin"] + url = https://mbenzi:Portalnet68@git.lavorain.cloud/mbenzi/urbackup.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main + vscode-merge-base = origin/main +[branch "dev"] + remote = origin + merge = refs/heads/dev + vscode-merge-base = origin/dev diff --git a/gitflavio/description b/gitflavio/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/gitflavio/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/gitflavio/hooks/applypatch-msg.sample b/gitflavio/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/gitflavio/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/gitflavio/hooks/commit-msg.sample b/gitflavio/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/gitflavio/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/gitflavio/hooks/fsmonitor-watchman.sample b/gitflavio/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/gitflavio/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/gitflavio/hooks/post-update.sample b/gitflavio/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/gitflavio/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/gitflavio/hooks/pre-applypatch.sample b/gitflavio/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/gitflavio/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/gitflavio/hooks/pre-commit.sample b/gitflavio/hooks/pre-commit.sample new file mode 100755 index 0000000..e144712 --- /dev/null +++ b/gitflavio/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/gitflavio/hooks/pre-merge-commit.sample b/gitflavio/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/gitflavio/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/gitflavio/hooks/pre-push.sample b/gitflavio/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/gitflavio/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/gitflavio/hooks/pre-rebase.sample b/gitflavio/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/gitflavio/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/gitflavio/hooks/pre-receive.sample b/gitflavio/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/gitflavio/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/gitflavio/hooks/prepare-commit-msg.sample b/gitflavio/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/gitflavio/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/gitflavio/hooks/push-to-checkout.sample b/gitflavio/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/gitflavio/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/gitflavio/hooks/update.sample b/gitflavio/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/gitflavio/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/gitflavio/index b/gitflavio/index new file mode 100644 index 0000000..da9d4ec Binary files /dev/null and b/gitflavio/index differ diff --git a/gitflavio/info/exclude b/gitflavio/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/gitflavio/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/gitflavio/logs/HEAD b/gitflavio/logs/HEAD new file mode 100644 index 0000000..664eddf --- /dev/null +++ b/gitflavio/logs/HEAD @@ -0,0 +1,12 @@ +0000000000000000000000000000000000000000 c78dce76a359499e4d9aac8987ae5a7f7f937309 mariano 1777454275 +0200 clone: from https://git.lavorain.cloud/mbenzi/urbackup.git +c78dce76a359499e4d9aac8987ae5a7f7f937309 6493631fb88f9a570c95cfab46b89a144a9e9503 test 1777455335 +0200 commit: start opencode +6493631fb88f9a570c95cfab46b89a144a9e9503 9bed80d88c7128c3587329fee6dc3cf2f0efc9fd test 1777455383 +0200 checkout: moving from main to dev +9bed80d88c7128c3587329fee6dc3cf2f0efc9fd 98dc9fafeb52a5c6d0130be29d5716133e086821 test 1777456724 +0200 commit: modifiche da opencode +98dc9fafeb52a5c6d0130be29d5716133e086821 9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 test 1777456887 +0200 commit: opencode dopo commit 1 +9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 311accf4bc2817584dd35d611c7483bfd1f9d70d mariano 1777462043 +0200 commit: modifica x instalalzione - opencode +311accf4bc2817584dd35d611c7483bfd1f9d70d b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 mariano 1778580538 +0200 commit: sisetmazione instalalzione nuovo model +b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 mariano 1779256743 +0200 commit: finito parte computer +bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 27aac99d1555a75d373756d8727afeefd6b69376 mariano 1779260579 +0200 commit: commit - stable - +27aac99d1555a75d373756d8727afeefd6b69376 6493631fb88f9a570c95cfab46b89a144a9e9503 mariano 1779260625 +0200 checkout: moving from dev to main +6493631fb88f9a570c95cfab46b89a144a9e9503 4b3ededa083d208e7ce6e42b8632d295735b2982 mariano 1779260803 +0200 commit (merge): Merge branch 'dev' +4b3ededa083d208e7ce6e42b8632d295735b2982 27aac99d1555a75d373756d8727afeefd6b69376 mariano 1779260816 +0200 checkout: moving from main to dev diff --git a/gitflavio/logs/refs/heads/dev b/gitflavio/logs/refs/heads/dev new file mode 100644 index 0000000..c515f3e --- /dev/null +++ b/gitflavio/logs/refs/heads/dev @@ -0,0 +1,7 @@ +0000000000000000000000000000000000000000 9bed80d88c7128c3587329fee6dc3cf2f0efc9fd test 1777455383 +0200 branch: Created from refs/remotes/origin/dev +9bed80d88c7128c3587329fee6dc3cf2f0efc9fd 98dc9fafeb52a5c6d0130be29d5716133e086821 test 1777456724 +0200 commit: modifiche da opencode +98dc9fafeb52a5c6d0130be29d5716133e086821 9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 test 1777456887 +0200 commit: opencode dopo commit 1 +9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 311accf4bc2817584dd35d611c7483bfd1f9d70d mariano 1777462043 +0200 commit: modifica x instalalzione - opencode +311accf4bc2817584dd35d611c7483bfd1f9d70d b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 mariano 1778580538 +0200 commit: sisetmazione instalalzione nuovo model +b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 mariano 1779256743 +0200 commit: finito parte computer +bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 27aac99d1555a75d373756d8727afeefd6b69376 mariano 1779260579 +0200 commit: commit - stable - diff --git a/gitflavio/logs/refs/heads/main b/gitflavio/logs/refs/heads/main new file mode 100644 index 0000000..d00b060 --- /dev/null +++ b/gitflavio/logs/refs/heads/main @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000 c78dce76a359499e4d9aac8987ae5a7f7f937309 mariano 1777454275 +0200 clone: from https://git.lavorain.cloud/mbenzi/urbackup.git +c78dce76a359499e4d9aac8987ae5a7f7f937309 6493631fb88f9a570c95cfab46b89a144a9e9503 test 1777455335 +0200 commit: start opencode +6493631fb88f9a570c95cfab46b89a144a9e9503 4b3ededa083d208e7ce6e42b8632d295735b2982 mariano 1779260803 +0200 commit (merge): Merge branch 'dev' diff --git a/gitflavio/logs/refs/remotes/origin/HEAD b/gitflavio/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..85da1a9 --- /dev/null +++ b/gitflavio/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 c78dce76a359499e4d9aac8987ae5a7f7f937309 mariano 1777454275 +0200 clone: from https://git.lavorain.cloud/mbenzi/urbackup.git diff --git a/gitflavio/logs/refs/remotes/origin/dev b/gitflavio/logs/refs/remotes/origin/dev new file mode 100644 index 0000000..60e996b --- /dev/null +++ b/gitflavio/logs/refs/remotes/origin/dev @@ -0,0 +1,5 @@ +9bed80d88c7128c3587329fee6dc3cf2f0efc9fd 9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 test 1777456894 +0200 update by push +9a73f51de5135da21bbc52ca183bf1e9a48cd0f2 311accf4bc2817584dd35d611c7483bfd1f9d70d mariano 1777462051 +0200 update by push +311accf4bc2817584dd35d611c7483bfd1f9d70d b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 mariano 1778580554 +0200 update by push +b7bffdd64ff74d4165aa49ecf2fa49006d0f9334 bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 mariano 1779256765 +0200 update by push +bcc2b35da1d4bbc24fd1f1d15c3899ef9a469bd5 27aac99d1555a75d373756d8727afeefd6b69376 mariano 1779260580 +0200 update by push diff --git a/gitflavio/logs/refs/remotes/origin/main b/gitflavio/logs/refs/remotes/origin/main new file mode 100644 index 0000000..3a42731 --- /dev/null +++ b/gitflavio/logs/refs/remotes/origin/main @@ -0,0 +1 @@ +c78dce76a359499e4d9aac8987ae5a7f7f937309 4b3ededa083d208e7ce6e42b8632d295735b2982 mariano 1779260811 +0200 update by push diff --git a/gitflavio/objects/00/bfe6f899a6d08df50fe1151032f7d2432a23fc b/gitflavio/objects/00/bfe6f899a6d08df50fe1151032f7d2432a23fc new file mode 100644 index 0000000..75c6083 Binary files /dev/null and b/gitflavio/objects/00/bfe6f899a6d08df50fe1151032f7d2432a23fc differ diff --git a/gitflavio/objects/00/d1fcaaa9d1d9244a8df60ddc4f46845d1a8456 b/gitflavio/objects/00/d1fcaaa9d1d9244a8df60ddc4f46845d1a8456 new file mode 100644 index 0000000..9753090 Binary files /dev/null and b/gitflavio/objects/00/d1fcaaa9d1d9244a8df60ddc4f46845d1a8456 differ diff --git a/gitflavio/objects/00/d937889015edc16672a1aec414ccc184c743d7 b/gitflavio/objects/00/d937889015edc16672a1aec414ccc184c743d7 new file mode 100644 index 0000000..91fb392 Binary files /dev/null and b/gitflavio/objects/00/d937889015edc16672a1aec414ccc184c743d7 differ diff --git a/gitflavio/objects/01/c931072d5746de24e6323d3f9316655d814740 b/gitflavio/objects/01/c931072d5746de24e6323d3f9316655d814740 new file mode 100644 index 0000000..fc2256c Binary files /dev/null and b/gitflavio/objects/01/c931072d5746de24e6323d3f9316655d814740 differ diff --git a/gitflavio/objects/05/aeab86ec3c5d3cdeac2e7cb0cdbce7af92a5bf b/gitflavio/objects/05/aeab86ec3c5d3cdeac2e7cb0cdbce7af92a5bf new file mode 100644 index 0000000..3f58d2e Binary files /dev/null and b/gitflavio/objects/05/aeab86ec3c5d3cdeac2e7cb0cdbce7af92a5bf differ diff --git a/gitflavio/objects/0d/8b64bd85c5cf7509028a7cf1cb1646ae9d20e3 b/gitflavio/objects/0d/8b64bd85c5cf7509028a7cf1cb1646ae9d20e3 new file mode 100644 index 0000000..5498fee Binary files /dev/null and b/gitflavio/objects/0d/8b64bd85c5cf7509028a7cf1cb1646ae9d20e3 differ diff --git a/gitflavio/objects/0f/57c04a93e542f698d9ee8d5662296a10dd79f6 b/gitflavio/objects/0f/57c04a93e542f698d9ee8d5662296a10dd79f6 new file mode 100644 index 0000000..d15ae60 Binary files /dev/null and b/gitflavio/objects/0f/57c04a93e542f698d9ee8d5662296a10dd79f6 differ diff --git a/gitflavio/objects/11/2ac1a3d31de40ed19e31ebe2a09969a91ca785 b/gitflavio/objects/11/2ac1a3d31de40ed19e31ebe2a09969a91ca785 new file mode 100644 index 0000000..e2ddf50 Binary files /dev/null and b/gitflavio/objects/11/2ac1a3d31de40ed19e31ebe2a09969a91ca785 differ diff --git a/gitflavio/objects/11/6523d7a24f610648183920620579d30b8e007f b/gitflavio/objects/11/6523d7a24f610648183920620579d30b8e007f new file mode 100644 index 0000000..f74315f Binary files /dev/null and b/gitflavio/objects/11/6523d7a24f610648183920620579d30b8e007f differ diff --git a/gitflavio/objects/13/a1fd4350238bfc24cfb8541036f0bee8e4ca05 b/gitflavio/objects/13/a1fd4350238bfc24cfb8541036f0bee8e4ca05 new file mode 100644 index 0000000..4d6ac81 Binary files /dev/null and b/gitflavio/objects/13/a1fd4350238bfc24cfb8541036f0bee8e4ca05 differ diff --git a/gitflavio/objects/14/e1027489784b717661588cb43caa419eec86bd b/gitflavio/objects/14/e1027489784b717661588cb43caa419eec86bd new file mode 100644 index 0000000..ee6dc53 Binary files /dev/null and b/gitflavio/objects/14/e1027489784b717661588cb43caa419eec86bd differ diff --git a/gitflavio/objects/17/18114462eb2d4b292baba4edc3c6f79f7c3ea3 b/gitflavio/objects/17/18114462eb2d4b292baba4edc3c6f79f7c3ea3 new file mode 100644 index 0000000..f2dd20d Binary files /dev/null and b/gitflavio/objects/17/18114462eb2d4b292baba4edc3c6f79f7c3ea3 differ diff --git a/gitflavio/objects/18/5b273ec42b62b8ae4f74e59eae70395cc88752 b/gitflavio/objects/18/5b273ec42b62b8ae4f74e59eae70395cc88752 new file mode 100644 index 0000000..4f75d83 Binary files /dev/null and b/gitflavio/objects/18/5b273ec42b62b8ae4f74e59eae70395cc88752 differ diff --git a/gitflavio/objects/18/a77d83519ba3e8fd6a17a1b44acb63bc7a50cf b/gitflavio/objects/18/a77d83519ba3e8fd6a17a1b44acb63bc7a50cf new file mode 100644 index 0000000..26ced89 Binary files /dev/null and b/gitflavio/objects/18/a77d83519ba3e8fd6a17a1b44acb63bc7a50cf differ diff --git a/gitflavio/objects/1b/210e4c7201ee826cb4f0e479e5486307e9bf7c b/gitflavio/objects/1b/210e4c7201ee826cb4f0e479e5486307e9bf7c new file mode 100644 index 0000000..d916583 Binary files /dev/null and b/gitflavio/objects/1b/210e4c7201ee826cb4f0e479e5486307e9bf7c differ diff --git a/gitflavio/objects/1c/8552781a34629a6176355ba50be371ec5166a9 b/gitflavio/objects/1c/8552781a34629a6176355ba50be371ec5166a9 new file mode 100644 index 0000000..12f30af --- /dev/null +++ b/gitflavio/objects/1c/8552781a34629a6176355ba50be371ec5166a9 @@ -0,0 +1,7 @@ +xVmo6g+f@rfM/Sk EjI6 Ym"T2ߑ^dwUWw,ݦ04TKK쟶;Nā~O .1K! e?ow !2 BqJ&AL3%xfu)OX5q"yJ1%L]l7XTx+%Utx')gx|3Ef~W%\Q*☀%fD6;jlr&m65"%F/""dRBhU au:_.xn/^fICt;-v2RnBԖG҇{=w98V1 aP:OOzm8]N{~ϔ<{6TJi&tU#!PQz[{[oIᕫڨ!5y5zZElhwYBvsZNGϣߌ+wqhQB{X4dY3ߔh>}_n3XhnVyUkΕ-/O|hSN;  JPM#p/G)ׇ5x)hu[ +}`*Qѝ0vA%᣶dt~iQј8ۨ=G5G\aKT +ULoZ0ſH8Αݭniz\x'#F$DҌRR6 +*I;(Hgpn"3Lf%92 CTiu# 16˥~Dp6x"P3AqеVw`VW^nC5:XmnX:t}.Ri 8S1Jbyŕ1Ő&&^w2 q1Z/ݤO}Yc9(5MwWN \ No newline at end of file diff --git a/gitflavio/objects/1e/bb463168f36028ae98c61581e6c8ff953617d0 b/gitflavio/objects/1e/bb463168f36028ae98c61581e6c8ff953617d0 new file mode 100644 index 0000000..45bb7ad Binary files /dev/null and b/gitflavio/objects/1e/bb463168f36028ae98c61581e6c8ff953617d0 differ diff --git a/gitflavio/objects/1f/9710002aa168ed7592b58b9f2d9e1f3431d30c b/gitflavio/objects/1f/9710002aa168ed7592b58b9f2d9e1f3431d30c new file mode 100644 index 0000000..1b296a0 Binary files /dev/null and b/gitflavio/objects/1f/9710002aa168ed7592b58b9f2d9e1f3431d30c differ diff --git a/gitflavio/objects/20/4ea8b1044799df5f383c9b7489ba8a876a7c03 b/gitflavio/objects/20/4ea8b1044799df5f383c9b7489ba8a876a7c03 new file mode 100644 index 0000000..564da27 Binary files /dev/null and b/gitflavio/objects/20/4ea8b1044799df5f383c9b7489ba8a876a7c03 differ diff --git a/gitflavio/objects/21/5277ac86967035797e47da0b5010a87ce0b98f b/gitflavio/objects/21/5277ac86967035797e47da0b5010a87ce0b98f new file mode 100644 index 0000000..9510d6c Binary files /dev/null and b/gitflavio/objects/21/5277ac86967035797e47da0b5010a87ce0b98f differ diff --git a/gitflavio/objects/24/d8f24f885f3be2c2de581e768885b5caf90c1e b/gitflavio/objects/24/d8f24f885f3be2c2de581e768885b5caf90c1e new file mode 100644 index 0000000..517116f Binary files /dev/null and b/gitflavio/objects/24/d8f24f885f3be2c2de581e768885b5caf90c1e differ diff --git a/gitflavio/objects/25/9e2ef94ae4f4aa5f93125a6551690a28af573b b/gitflavio/objects/25/9e2ef94ae4f4aa5f93125a6551690a28af573b new file mode 100644 index 0000000..59b7108 Binary files /dev/null and b/gitflavio/objects/25/9e2ef94ae4f4aa5f93125a6551690a28af573b differ diff --git a/gitflavio/objects/27/7ad43ceff67cfe1f3867301b6c3552917bcd8e b/gitflavio/objects/27/7ad43ceff67cfe1f3867301b6c3552917bcd8e new file mode 100644 index 0000000..60a1c28 Binary files /dev/null and b/gitflavio/objects/27/7ad43ceff67cfe1f3867301b6c3552917bcd8e differ diff --git a/gitflavio/objects/27/aac99d1555a75d373756d8727afeefd6b69376 b/gitflavio/objects/27/aac99d1555a75d373756d8727afeefd6b69376 new file mode 100644 index 0000000..f42075a --- /dev/null +++ b/gitflavio/objects/27/aac99d1555a75d373756d8727afeefd6b69376 @@ -0,0 +1,3 @@ +xM +0F/'*LWt}Ai}-gH0._,΅`rBbdjP[2I61&e +"Yڤb.Had;зOJmR-SHYpJ!{KlNq0?J \ No newline at end of file diff --git a/gitflavio/objects/31/1accf4bc2817584dd35d611c7483bfd1f9d70d b/gitflavio/objects/31/1accf4bc2817584dd35d611c7483bfd1f9d70d new file mode 100644 index 0000000..086d57d Binary files /dev/null and b/gitflavio/objects/31/1accf4bc2817584dd35d611c7483bfd1f9d70d differ diff --git a/gitflavio/objects/31/a95026c46ca2c322f0f6770300750573cc58d9 b/gitflavio/objects/31/a95026c46ca2c322f0f6770300750573cc58d9 new file mode 100644 index 0000000..a5103ed Binary files /dev/null and b/gitflavio/objects/31/a95026c46ca2c322f0f6770300750573cc58d9 differ diff --git a/gitflavio/objects/35/62526c1df22fc0f8d6b6e9463a3adff126413b b/gitflavio/objects/35/62526c1df22fc0f8d6b6e9463a3adff126413b new file mode 100644 index 0000000..4cc94a1 Binary files /dev/null and b/gitflavio/objects/35/62526c1df22fc0f8d6b6e9463a3adff126413b differ diff --git a/gitflavio/objects/3a/38bc98a9b989f99c685dacc9a620429c8af5a7 b/gitflavio/objects/3a/38bc98a9b989f99c685dacc9a620429c8af5a7 new file mode 100644 index 0000000..548ab89 Binary files /dev/null and b/gitflavio/objects/3a/38bc98a9b989f99c685dacc9a620429c8af5a7 differ diff --git a/gitflavio/objects/3a/3c4d169ec5ca603e940c6ee57305c4200867df b/gitflavio/objects/3a/3c4d169ec5ca603e940c6ee57305c4200867df new file mode 100644 index 0000000..5c98ff5 Binary files /dev/null and b/gitflavio/objects/3a/3c4d169ec5ca603e940c6ee57305c4200867df differ diff --git a/gitflavio/objects/3a/ec748a00e2437143afbcce9789e9e1c440e86d b/gitflavio/objects/3a/ec748a00e2437143afbcce9789e9e1c440e86d new file mode 100644 index 0000000..0feb74d Binary files /dev/null and b/gitflavio/objects/3a/ec748a00e2437143afbcce9789e9e1c440e86d differ diff --git a/gitflavio/objects/3b/25f0d2e7ba99cc57fd06590ccad42117d56e1a b/gitflavio/objects/3b/25f0d2e7ba99cc57fd06590ccad42117d56e1a new file mode 100644 index 0000000..8a6736a Binary files /dev/null and b/gitflavio/objects/3b/25f0d2e7ba99cc57fd06590ccad42117d56e1a differ diff --git a/gitflavio/objects/3d/47b597ecf2e93c4020cdeb910a06b99f02ee39 b/gitflavio/objects/3d/47b597ecf2e93c4020cdeb910a06b99f02ee39 new file mode 100644 index 0000000..70f8311 --- /dev/null +++ b/gitflavio/objects/3d/47b597ecf2e93c4020cdeb910a06b99f02ee39 @@ -0,0 +1 @@ +x+)JMU01f040031Q(N-*K-/I-.+(`4zĸ(u19/ \ No newline at end of file diff --git a/gitflavio/objects/3d/5eba338b0383acac7cbc8bf39ef882ebb52945 b/gitflavio/objects/3d/5eba338b0383acac7cbc8bf39ef882ebb52945 new file mode 100644 index 0000000..c2cd0a6 Binary files /dev/null and b/gitflavio/objects/3d/5eba338b0383acac7cbc8bf39ef882ebb52945 differ diff --git a/gitflavio/objects/41/83ad2c64664c3b69fe68ed12374393ad025e2f b/gitflavio/objects/41/83ad2c64664c3b69fe68ed12374393ad025e2f new file mode 100644 index 0000000..206babc --- /dev/null +++ b/gitflavio/objects/41/83ad2c64664c3b69fe68ed12374393ad025e2f @@ -0,0 +1,5 @@ +xTn@bYZHUA."$4EV Z-1Eٵ왝3g4ɦpћ<ν +Fy&SLA{XXmD AO`.H?ƠJ`(B!_>c #N5T<ҙVkyޜU +dQDG96Ay"Cad6,mA ќGWр-I6wZfa[ߎ*9 X<:'BMEx_lZ ~z@q4" n.*389H4[Hs6vAj!:6!8.ӓ4-oy~ŔL -|ffMlE"!%]g"%I4_P cv.;ٲI#աcB +x&\ۻyi<2s"*l0z+Ւb9>Imtg/8wܚUOI.NwnOhT8ߚʺkSV4Z$?csL"R/Xᬲ +uUh1S &-떋<Izd桵؈tosc%+T6KEChK[\'/E&}/|hʦf2rW5ȿGŔJN=k#Y KƯ \ No newline at end of file diff --git a/gitflavio/objects/42/dfeff5d2b6452efa18cff9bc55ef86c254e997 b/gitflavio/objects/42/dfeff5d2b6452efa18cff9bc55ef86c254e997 new file mode 100644 index 0000000..6cb370f Binary files /dev/null and b/gitflavio/objects/42/dfeff5d2b6452efa18cff9bc55ef86c254e997 differ diff --git a/gitflavio/objects/43/018fccb86a1aa4b66a54a0489bc8fe5e664916 b/gitflavio/objects/43/018fccb86a1aa4b66a54a0489bc8fe5e664916 new file mode 100644 index 0000000..fdbdc63 Binary files /dev/null and b/gitflavio/objects/43/018fccb86a1aa4b66a54a0489bc8fe5e664916 differ diff --git a/gitflavio/objects/45/4285ea080576f5db99ff0cd83dd6a7717241a9 b/gitflavio/objects/45/4285ea080576f5db99ff0cd83dd6a7717241a9 new file mode 100644 index 0000000..c0ff72b --- /dev/null +++ b/gitflavio/objects/45/4285ea080576f5db99ff0cd83dd6a7717241a9 @@ -0,0 +1,6 @@ +xT]k0ݳ&!#ݺ-k amz0(Ŗm1E2F{%+dcS{;yeQt4 +`>, 0%C&$,7 Zr1cPJj0M8 ,YI7X|)2y )('ivCRJNHywa 8uAm6}'IaσvVqײt~Da'ih*x=dj4PMZ4WaOi:^nQ]|5ޠoWۻu㋔Í +IP*⋅g6-[uʚ}yU3M+$FNgpkMki:\n1B +X[cX}w. V"N yc= +wPZ23\.jkMc * +kmJזDwq`jm5 \ No newline at end of file diff --git a/gitflavio/objects/46/15c1d7cc58efbcfff10d8325a03934f540c569 b/gitflavio/objects/46/15c1d7cc58efbcfff10d8325a03934f540c569 new file mode 100644 index 0000000..7c73d76 Binary files /dev/null and b/gitflavio/objects/46/15c1d7cc58efbcfff10d8325a03934f540c569 differ diff --git a/gitflavio/objects/46/cd427d457cecc5889a92440131ce63d7a92532 b/gitflavio/objects/46/cd427d457cecc5889a92440131ce63d7a92532 new file mode 100644 index 0000000..7ea3ffb Binary files /dev/null and b/gitflavio/objects/46/cd427d457cecc5889a92440131ce63d7a92532 differ diff --git a/gitflavio/objects/47/f6b90efbf694365d7831eb615591c109ab77b5 b/gitflavio/objects/47/f6b90efbf694365d7831eb615591c109ab77b5 new file mode 100644 index 0000000..067d235 Binary files /dev/null and b/gitflavio/objects/47/f6b90efbf694365d7831eb615591c109ab77b5 differ diff --git a/gitflavio/objects/4b/3ededa083d208e7ce6e42b8632d295735b2982 b/gitflavio/objects/4b/3ededa083d208e7ce6e42b8632d295735b2982 new file mode 100644 index 0000000..2249814 Binary files /dev/null and b/gitflavio/objects/4b/3ededa083d208e7ce6e42b8632d295735b2982 differ diff --git a/gitflavio/objects/4f/21aa1aca615a1fea2786d0c759a4626aba551d b/gitflavio/objects/4f/21aa1aca615a1fea2786d0c759a4626aba551d new file mode 100644 index 0000000..68b6a1b Binary files /dev/null and b/gitflavio/objects/4f/21aa1aca615a1fea2786d0c759a4626aba551d differ diff --git a/gitflavio/objects/50/5476e9934cac4c29eb6a086daf07671e01517f b/gitflavio/objects/50/5476e9934cac4c29eb6a086daf07671e01517f new file mode 100644 index 0000000..4d8b9ee Binary files /dev/null and b/gitflavio/objects/50/5476e9934cac4c29eb6a086daf07671e01517f differ diff --git a/gitflavio/objects/52/024d4228f1a54de35513bd1a14dfaeff1ddeb9 b/gitflavio/objects/52/024d4228f1a54de35513bd1a14dfaeff1ddeb9 new file mode 100644 index 0000000..1a8e8bd Binary files /dev/null and b/gitflavio/objects/52/024d4228f1a54de35513bd1a14dfaeff1ddeb9 differ diff --git a/gitflavio/objects/52/4523b4454b2c3bc0e88a948fd4789af7bbdae8 b/gitflavio/objects/52/4523b4454b2c3bc0e88a948fd4789af7bbdae8 new file mode 100644 index 0000000..86c43aa Binary files /dev/null and b/gitflavio/objects/52/4523b4454b2c3bc0e88a948fd4789af7bbdae8 differ diff --git a/gitflavio/objects/56/eb4d8b0f5bf018d99412161cf1346e29540fe8 b/gitflavio/objects/56/eb4d8b0f5bf018d99412161cf1346e29540fe8 new file mode 100644 index 0000000..ce4aafe Binary files /dev/null and b/gitflavio/objects/56/eb4d8b0f5bf018d99412161cf1346e29540fe8 differ diff --git a/gitflavio/objects/57/41929666cdf91205979ab26c6b6ccdf8a6979a b/gitflavio/objects/57/41929666cdf91205979ab26c6b6ccdf8a6979a new file mode 100644 index 0000000..f773f10 Binary files /dev/null and b/gitflavio/objects/57/41929666cdf91205979ab26c6b6ccdf8a6979a differ diff --git a/gitflavio/objects/61/b7171b1db577c2de9d43dadd5cc1fecce774dc b/gitflavio/objects/61/b7171b1db577c2de9d43dadd5cc1fecce774dc new file mode 100644 index 0000000..be422ce Binary files /dev/null and b/gitflavio/objects/61/b7171b1db577c2de9d43dadd5cc1fecce774dc differ diff --git a/gitflavio/objects/61/ccf6d74902ab1b4ed510d8dad7df2fb80ff875 b/gitflavio/objects/61/ccf6d74902ab1b4ed510d8dad7df2fb80ff875 new file mode 100644 index 0000000..b4de5d7 Binary files /dev/null and b/gitflavio/objects/61/ccf6d74902ab1b4ed510d8dad7df2fb80ff875 differ diff --git a/gitflavio/objects/61/dd7aa53d6ebb43ca028ef9d2706efa7bd9ee68 b/gitflavio/objects/61/dd7aa53d6ebb43ca028ef9d2706efa7bd9ee68 new file mode 100644 index 0000000..242c164 Binary files /dev/null and b/gitflavio/objects/61/dd7aa53d6ebb43ca028ef9d2706efa7bd9ee68 differ diff --git a/gitflavio/objects/63/ea5c4d83858585a4d32a54f12e00a4c19891cf b/gitflavio/objects/63/ea5c4d83858585a4d32a54f12e00a4c19891cf new file mode 100644 index 0000000..5e6031c Binary files /dev/null and b/gitflavio/objects/63/ea5c4d83858585a4d32a54f12e00a4c19891cf differ diff --git a/gitflavio/objects/64/93631fb88f9a570c95cfab46b89a144a9e9503 b/gitflavio/objects/64/93631fb88f9a570c95cfab46b89a144a9e9503 new file mode 100644 index 0000000..76b4ba9 --- /dev/null +++ b/gitflavio/objects/64/93631fb88f9a570c95cfab46b89a144a9e9503 @@ -0,0 +1,3 @@ +xK +0@] o2 eL6/ +m6^T#W+9KZ+. ) yH·lj3DHb'V Kbp*e4^D)yLΩSP;vb{E \ No newline at end of file diff --git a/gitflavio/objects/65/790e7d770de478c167d9435a4b7858346f7072 b/gitflavio/objects/65/790e7d770de478c167d9435a4b7858346f7072 new file mode 100644 index 0000000..ffc3c86 Binary files /dev/null and b/gitflavio/objects/65/790e7d770de478c167d9435a4b7858346f7072 differ diff --git a/gitflavio/objects/67/9b0dafccecfb1a9fa9a341853f524b4b52abbf b/gitflavio/objects/67/9b0dafccecfb1a9fa9a341853f524b4b52abbf new file mode 100644 index 0000000..89b907d Binary files /dev/null and b/gitflavio/objects/67/9b0dafccecfb1a9fa9a341853f524b4b52abbf differ diff --git a/gitflavio/objects/68/22b2bcae642863ca6e0115924af954e14ccff8 b/gitflavio/objects/68/22b2bcae642863ca6e0115924af954e14ccff8 new file mode 100644 index 0000000..6995682 Binary files /dev/null and b/gitflavio/objects/68/22b2bcae642863ca6e0115924af954e14ccff8 differ diff --git a/gitflavio/objects/70/dff38c872847e1c23ef1c77594086839a2f985 b/gitflavio/objects/70/dff38c872847e1c23ef1c77594086839a2f985 new file mode 100644 index 0000000..4df6e40 Binary files /dev/null and b/gitflavio/objects/70/dff38c872847e1c23ef1c77594086839a2f985 differ diff --git a/gitflavio/objects/79/2e374a39495bd4ce54bdb1efab74ef749b90b8 b/gitflavio/objects/79/2e374a39495bd4ce54bdb1efab74ef749b90b8 new file mode 100644 index 0000000..6006558 Binary files /dev/null and b/gitflavio/objects/79/2e374a39495bd4ce54bdb1efab74ef749b90b8 differ diff --git a/gitflavio/objects/79/32e201a26405962f2f20e3d55c57236c152f91 b/gitflavio/objects/79/32e201a26405962f2f20e3d55c57236c152f91 new file mode 100644 index 0000000..b7306d1 Binary files /dev/null and b/gitflavio/objects/79/32e201a26405962f2f20e3d55c57236c152f91 differ diff --git a/gitflavio/objects/7c/48ca70eca8ed15e3a8ea9f9d54c1a465501a40 b/gitflavio/objects/7c/48ca70eca8ed15e3a8ea9f9d54c1a465501a40 new file mode 100644 index 0000000..cb3d973 --- /dev/null +++ b/gitflavio/objects/7c/48ca70eca8ed15e3a8ea9f9d54c1a465501a40 @@ -0,0 +1,2 @@ +xMQKk@Yb&PBJWu[@;ZMIWHi|i]h/ YB:T$Ў=–!qN"%AW# %UX# +guU㱴d&'{af­Ü-]wk7_S_JmsC^ 2uّrX,:+@UBJę?XF6oB+T}q`4_ۧqù MG,2Kb+"zϧ W%M;Z] %]H#o:l)hÎ NRItu< Z](rbXʂ^|3cMx";5R䜐GA?cԚ \ No newline at end of file diff --git a/gitflavio/objects/7d/28b1fb6885c2730f3eb871bc63936c1dc5ebb2 b/gitflavio/objects/7d/28b1fb6885c2730f3eb871bc63936c1dc5ebb2 new file mode 100644 index 0000000..e3f8dd5 Binary files /dev/null and b/gitflavio/objects/7d/28b1fb6885c2730f3eb871bc63936c1dc5ebb2 differ diff --git a/gitflavio/objects/7f/c40925047551dbc6f31069af430848dc862bb2 b/gitflavio/objects/7f/c40925047551dbc6f31069af430848dc862bb2 new file mode 100644 index 0000000..25a4031 Binary files /dev/null and b/gitflavio/objects/7f/c40925047551dbc6f31069af430848dc862bb2 differ diff --git a/gitflavio/objects/88/1b6ebdd497616c0eaf0688fdb79edc758a396b b/gitflavio/objects/88/1b6ebdd497616c0eaf0688fdb79edc758a396b new file mode 100644 index 0000000..75560ee Binary files /dev/null and b/gitflavio/objects/88/1b6ebdd497616c0eaf0688fdb79edc758a396b differ diff --git a/gitflavio/objects/90/a894f2dbef8d9efdeb61db05f659cd7078e4da b/gitflavio/objects/90/a894f2dbef8d9efdeb61db05f659cd7078e4da new file mode 100644 index 0000000..d52dce3 Binary files /dev/null and b/gitflavio/objects/90/a894f2dbef8d9efdeb61db05f659cd7078e4da differ diff --git a/gitflavio/objects/98/dc9fafeb52a5c6d0130be29d5716133e086821 b/gitflavio/objects/98/dc9fafeb52a5c6d0130be29d5716133e086821 new file mode 100644 index 0000000..a4bce53 Binary files /dev/null and b/gitflavio/objects/98/dc9fafeb52a5c6d0130be29d5716133e086821 differ diff --git a/gitflavio/objects/9a/73f51de5135da21bbc52ca183bf1e9a48cd0f2 b/gitflavio/objects/9a/73f51de5135da21bbc52ca183bf1e9a48cd0f2 new file mode 100644 index 0000000..43328fc --- /dev/null +++ b/gitflavio/objects/9a/73f51de5135da21bbc52ca183bf1e9a48cd0f2 @@ -0,0 +1,2 @@ +xA +0E]d&m2)wIf&ThRE{|iz}< "ԨylP$E x&?\KJ۩Q'Vt}}2>bE Jm|ca|'-{Ck6IuVlrx"|8h*xcu~'*TQVRd7E)+x?Rq__i7 +,+aU/5k aЏR ESqqLqzgƮ؞b5[ǀcAbhm¶MYvIJ*)WwtO 4nKz>;Ak(d$T . EċKnа9Pwytħ;$1z?^2+3Q/V +z jm7Τmtю.$m` \ No newline at end of file diff --git a/gitflavio/objects/de/e63d0b4ae892fcc836df19d655ef78fba0df91 b/gitflavio/objects/de/e63d0b4ae892fcc836df19d655ef78fba0df91 new file mode 100644 index 0000000..0905a83 Binary files /dev/null and b/gitflavio/objects/de/e63d0b4ae892fcc836df19d655ef78fba0df91 differ diff --git a/gitflavio/objects/e0/83fa81a6c387fc8ac548e8bc399b8335d781ef b/gitflavio/objects/e0/83fa81a6c387fc8ac548e8bc399b8335d781ef new file mode 100644 index 0000000..0109fb6 Binary files /dev/null and b/gitflavio/objects/e0/83fa81a6c387fc8ac548e8bc399b8335d781ef differ diff --git a/gitflavio/objects/e3/a5f4a180ecfd4e6b68f783989b3b9789b26fb6 b/gitflavio/objects/e3/a5f4a180ecfd4e6b68f783989b3b9789b26fb6 new file mode 100644 index 0000000..42002e2 Binary files /dev/null and b/gitflavio/objects/e3/a5f4a180ecfd4e6b68f783989b3b9789b26fb6 differ diff --git a/gitflavio/objects/e4/e4b5179c616789e7dbdd52cb86a5becd0a9c14 b/gitflavio/objects/e4/e4b5179c616789e7dbdd52cb86a5becd0a9c14 new file mode 100644 index 0000000..1335ddf Binary files /dev/null and b/gitflavio/objects/e4/e4b5179c616789e7dbdd52cb86a5becd0a9c14 differ diff --git a/gitflavio/objects/e5/0adaeacd7429ac492c7f1a7213055c90458eb7 b/gitflavio/objects/e5/0adaeacd7429ac492c7f1a7213055c90458eb7 new file mode 100644 index 0000000..935a808 --- /dev/null +++ b/gitflavio/objects/e5/0adaeacd7429ac492c7f1a7213055c90458eb7 @@ -0,0 +1,4 @@ +xmPAN0WВTT[\gK,%vv =nO +竗*(CUHB t|;dot Yo!ǾD5^6as%϶#Pٲ5\QPߎEP] \ No newline at end of file diff --git a/gitflavio/objects/e6/bb5deaba73fdea1c97b9c09bc810a9c92fa72d b/gitflavio/objects/e6/bb5deaba73fdea1c97b9c09bc810a9c92fa72d new file mode 100644 index 0000000..1c971a0 Binary files /dev/null and b/gitflavio/objects/e6/bb5deaba73fdea1c97b9c09bc810a9c92fa72d differ diff --git a/gitflavio/objects/e7/b85297d4141c5e4190b68433d8e1cd5664c6a3 b/gitflavio/objects/e7/b85297d4141c5e4190b68433d8e1cd5664c6a3 new file mode 100644 index 0000000..110a081 Binary files /dev/null and b/gitflavio/objects/e7/b85297d4141c5e4190b68433d8e1cd5664c6a3 differ diff --git a/gitflavio/objects/f0/0e847952b9239ced8dccdc0af19b68ebfb7d3d b/gitflavio/objects/f0/0e847952b9239ced8dccdc0af19b68ebfb7d3d new file mode 100644 index 0000000..88bb750 Binary files /dev/null and b/gitflavio/objects/f0/0e847952b9239ced8dccdc0af19b68ebfb7d3d differ diff --git a/gitflavio/objects/f1/f8001c76b95c16e22b018bb0e02fca8dece54f b/gitflavio/objects/f1/f8001c76b95c16e22b018bb0e02fca8dece54f new file mode 100644 index 0000000..fbb5d89 Binary files /dev/null and b/gitflavio/objects/f1/f8001c76b95c16e22b018bb0e02fca8dece54f differ diff --git a/gitflavio/objects/f6/727eda1e7bfd0a868ff93615a04b44b928c76d b/gitflavio/objects/f6/727eda1e7bfd0a868ff93615a04b44b928c76d new file mode 100644 index 0000000..0f31732 Binary files /dev/null and b/gitflavio/objects/f6/727eda1e7bfd0a868ff93615a04b44b928c76d differ diff --git a/gitflavio/objects/f8/5893a04200dc8c30e75955bb12d4905110d2ff b/gitflavio/objects/f8/5893a04200dc8c30e75955bb12d4905110d2ff new file mode 100644 index 0000000..2fc7b2d Binary files /dev/null and b/gitflavio/objects/f8/5893a04200dc8c30e75955bb12d4905110d2ff differ diff --git a/gitflavio/objects/fc/e0dd97ed72cbc3b85c8310bfee8247bcf6d1b5 b/gitflavio/objects/fc/e0dd97ed72cbc3b85c8310bfee8247bcf6d1b5 new file mode 100644 index 0000000..375850a Binary files /dev/null and b/gitflavio/objects/fc/e0dd97ed72cbc3b85c8310bfee8247bcf6d1b5 differ diff --git a/gitflavio/objects/ff/d908df1af4d233249c5bf9a88dff36419a192c b/gitflavio/objects/ff/d908df1af4d233249c5bf9a88dff36419a192c new file mode 100644 index 0000000..32c573d Binary files /dev/null and b/gitflavio/objects/ff/d908df1af4d233249c5bf9a88dff36419a192c differ diff --git a/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.idx b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.idx new file mode 100644 index 0000000..7237dbc Binary files /dev/null and b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.idx differ diff --git a/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.pack b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.pack new file mode 100644 index 0000000..f06fcde Binary files /dev/null and b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.pack differ diff --git a/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.rev b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.rev new file mode 100644 index 0000000..7f9f82a Binary files /dev/null and b/gitflavio/objects/pack/pack-16f64c98dc9785f95395d0429bffba92412a8d22.rev differ diff --git a/gitflavio/opencode b/gitflavio/opencode new file mode 100644 index 0000000..0238af7 --- /dev/null +++ b/gitflavio/opencode @@ -0,0 +1 @@ +c78dce76a359499e4d9aac8987ae5a7f7f937309 \ No newline at end of file diff --git a/gitflavio/packed-refs b/gitflavio/packed-refs new file mode 100644 index 0000000..35be949 --- /dev/null +++ b/gitflavio/packed-refs @@ -0,0 +1,3 @@ +# pack-refs with: peeled fully-peeled sorted +9bed80d88c7128c3587329fee6dc3cf2f0efc9fd refs/remotes/origin/dev +c78dce76a359499e4d9aac8987ae5a7f7f937309 refs/remotes/origin/main diff --git a/gitflavio/refs/heads/dev b/gitflavio/refs/heads/dev new file mode 100644 index 0000000..f1e5bc4 --- /dev/null +++ b/gitflavio/refs/heads/dev @@ -0,0 +1 @@ +27aac99d1555a75d373756d8727afeefd6b69376 diff --git a/gitflavio/refs/heads/main b/gitflavio/refs/heads/main new file mode 100644 index 0000000..a46537e --- /dev/null +++ b/gitflavio/refs/heads/main @@ -0,0 +1 @@ +4b3ededa083d208e7ce6e42b8632d295735b2982 diff --git a/gitflavio/refs/remotes/origin/HEAD b/gitflavio/refs/remotes/origin/HEAD new file mode 100644 index 0000000..4b0a875 --- /dev/null +++ b/gitflavio/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +ref: refs/remotes/origin/main diff --git a/gitflavio/refs/remotes/origin/dev b/gitflavio/refs/remotes/origin/dev new file mode 100644 index 0000000..f1e5bc4 --- /dev/null +++ b/gitflavio/refs/remotes/origin/dev @@ -0,0 +1 @@ +27aac99d1555a75d373756d8727afeefd6b69376 diff --git a/gitflavio/refs/remotes/origin/main b/gitflavio/refs/remotes/origin/main new file mode 100644 index 0000000..a46537e --- /dev/null +++ b/gitflavio/refs/remotes/origin/main @@ -0,0 +1 @@ +4b3ededa083d208e7ce6e42b8632d295735b2982 diff --git a/hook.php b/hook.php new file mode 100644 index 0000000..61b7171 --- /dev/null +++ b/hook.php @@ -0,0 +1,71 @@ + + */ +function plugin_urbackup_get_classes(): array +{ + return [ + Config::class, + Profile::class, + Server::class, + ServerAsset::class, + PluginUrbackupMassiveAction::class, + ]; +} + +/** + * Declare plugin massive actions. + * + * In GLPI this hook receives the current itemtype as string, + * for example Computer, Printer, Peripheral. + * + * It does not receive a MassiveAction object. + * + * @param mixed $type Current itemtype + * + * @return array + */ +function plugin_urbackup_MassiveActions($type): array +{ + $actions = []; + + if (!is_string($type) || $type === '') { + return $actions; + } + + if (!Config::isItemtypeEnabled($type)) { + return $actions; + } + + if (Profile::canCurrentUser(UPDATE) || Profile::canCurrentUser(CREATE)) { + $actions[ + PluginUrbackupMassiveAction::class + . \MassiveAction::CLASS_ACTION_SEPARATOR + . PluginUrbackupMassiveAction::ACTION_CONNECT_SERVER + ] = __('UrBackup - connect to server', 'urbackup'); + } + + if (Profile::canCurrentUser(UPDATE)) { + $actions[ + PluginUrbackupMassiveAction::class + . \MassiveAction::CLASS_ACTION_SEPARATOR + . PluginUrbackupMassiveAction::ACTION_DISCONNECT_SERVER + ] = __('UrBackup - disconnect from server', 'urbackup'); + } + + return $actions; +} \ No newline at end of file diff --git a/install/install.php b/install/install.php new file mode 100644 index 0000000..259e2ef --- /dev/null +++ b/install/install.php @@ -0,0 +1,348 @@ +runFile() + * - schema evolution using Migration addField(), addKey(), executeMigration() + * + * ------------------------------------------------------------------------- + */ + +use GlpiPlugin\Urbackup\Config; +use GlpiPlugin\Urbackup\Profile; + +if (!defined('GLPI_ROOT')) { + die('Sorry. You cannot access this file directly.'); +} + +/** + * Install or update plugin database schema and default data. + * + * @return bool + */ +function plugin_urbackup_install_process(): bool +{ + $migration = new Migration(PLUGIN_URBACKUP_VERSION); + + $migration->displayMessage(__('UrBackup plugin installation', 'urbackup')); + + plugin_urbackup_install_create_initial_schema($migration); + + plugin_urbackup_install_update_configs_table($migration); + plugin_urbackup_install_update_assettypes_table($migration); + plugin_urbackup_install_update_servers_table($migration); + plugin_urbackup_install_update_serverassets_table($migration); + plugin_urbackup_install_update_profiles_table($migration); + + $migration->executeMigration(); + + Config::ensureDefaultConfiguration(); + Profile::installRights(); + + return true; +} + +/** + * Create initial database schema from SQL file. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_create_initial_schema(Migration $migration): void +{ + global $DB; + + $required_tables = [ + 'glpi_plugin_urbackup_configs', + 'glpi_plugin_urbackup_assettypes', + 'glpi_plugin_urbackup_servers', + 'glpi_plugin_urbackup_serverassets', + 'glpi_plugin_urbackup_profiles', + ]; + + $missing_table_found = false; + + foreach ($required_tables as $table) { + if (!$DB->tableExists($table)) { + $missing_table_found = true; + break; + } + } + + if (!$missing_table_found) { + return; + } + + $migration->displayMessage(__('Creating UrBackup plugin database schema', 'urbackup')); + + $db_file = PLUGIN_URBACKUP_DIR . '/install/mysql/plugin_urbackup-empty.sql'; + + if (!file_exists($db_file)) { + $migration->displayMessage( + sprintf( + __('Database schema file not found: %s', 'urbackup'), + $db_file + ) + ); + + return; + } + + if (!$DB->runFile($db_file)) { + $migration->displayMessage(__('Error while creating UrBackup plugin database schema', 'urbackup')); + } +} + +/** + * Update configs table. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_update_configs_table(Migration $migration): void +{ + global $DB; + + $table = 'glpi_plugin_urbackup_configs'; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->addField($table, 'name', 'string', [ + 'value' => '', + 'after' => 'id', + ]); + + $migration->addField($table, 'value', 'text', [ + 'after' => 'name', + ]); + + $migration->addField($table, 'date_creation', 'timestamp'); + $migration->addField($table, 'date_mod', 'timestamp'); + + $migration->addKey($table, 'name'); +} + +/** + * Update assettypes table. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_update_assettypes_table(Migration $migration): void +{ + global $DB; + + $table = 'glpi_plugin_urbackup_assettypes'; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->addField($table, 'itemtype', 'string', [ + 'value' => '', + 'after' => 'id', + ]); + + $migration->addField($table, 'is_active', 'bool', [ + 'value' => 0, + 'after' => 'itemtype', + ]); + + $migration->addField($table, 'is_default', 'bool', [ + 'value' => 0, + 'after' => 'is_active', + ]); + + $migration->addField($table, 'date_creation', 'timestamp'); + $migration->addField($table, 'date_mod', 'timestamp'); + + $migration->addKey($table, 'itemtype'); + $migration->addKey($table, 'is_active'); +} + +/** + * Update servers table. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_update_servers_table(Migration $migration): void +{ + global $DB; + + $table = 'glpi_plugin_urbackup_servers'; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->addField($table, 'entities_id', 'integer', [ + 'value' => 0, + 'after' => 'id', + ]); + + $migration->addField($table, 'is_recursive', 'bool', [ + 'value' => 0, + 'after' => 'entities_id', + ]); + + $migration->addField($table, 'name', 'string', [ + 'value' => '', + 'after' => 'is_recursive', + ]); + + $migration->addField($table, 'locations_id', 'integer', [ + 'value' => 0, + 'after' => 'name', + ]); + + $migration->addField($table, 'ip_address', 'string', [ + 'value' => '', + 'after' => 'locations_id', + ]); + + $migration->addField($table, 'port', 'integer', [ + 'value' => 55414, + 'after' => 'ip_address', + ]); + + $migration->addField($table, 'protocol', 'string', [ + 'value' => 'http', + 'after' => 'port', + ]); + + $migration->addField($table, 'server_version', 'string', [ + 'after' => 'protocol', + ]); + + $migration->addField($table, 'api_username', 'string', [ + 'after' => 'server_version', + ]); + + $migration->addField($table, 'api_password', 'text', [ + 'after' => 'api_username', + ]); + + $migration->addField($table, 'ignore_ssl', 'bool', [ + 'value' => 0, + 'after' => 'api_password', + ]); + + $migration->addField($table, 'is_active', 'bool', [ + 'value' => 1, + 'after' => 'ignore_ssl', + ]); + + $migration->addField($table, 'last_api_status', 'bool', [ + 'value' => 0, + 'after' => 'is_active', + ]); + + $migration->addField($table, 'last_api_message', 'text', [ + 'after' => 'last_api_status', + ]); + + $migration->addField($table, 'last_api_check', 'timestamp', [ + 'after' => 'last_api_message', + ]); + + $migration->addField($table, 'comment', 'text', [ + 'after' => 'last_api_check', + ]); + + $migration->addField($table, 'date_creation', 'timestamp'); + $migration->addField($table, 'date_mod', 'timestamp'); + + $migration->addKey($table, 'name'); + $migration->addKey($table, 'entities_id'); + $migration->addKey($table, 'locations_id'); + $migration->addKey($table, 'is_active'); +} + +/** + * Update serverassets table. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_update_serverassets_table(Migration $migration): void +{ + global $DB; + + $table = 'glpi_plugin_urbackup_serverassets'; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->addField($table, 'plugin_urbackup_servers_id', 'integer', [ + 'value' => 0, + 'after' => 'id', + ]); + + $migration->addField($table, 'itemtype', 'string', [ + 'value' => '', + 'after' => 'plugin_urbackup_servers_id', + ]); + + $migration->addField($table, 'items_id', 'integer', [ + 'value' => 0, + 'after' => 'itemtype', + ]); + + $migration->addKey($table, 'plugin_urbackup_servers_id'); + $migration->addKey($table, ['itemtype', 'items_id'], 'item'); +} + +/** + * Update profiles table. + * + * @param Migration $migration Migration instance + * + * @return void + */ +function plugin_urbackup_install_update_profiles_table(Migration $migration): void +{ + global $DB; + + $table = 'glpi_plugin_urbackup_profiles'; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->addField($table, 'profiles_id', 'integer', [ + 'value' => 0, + 'after' => 'id', + ]); + + $migration->addField($table, 'rightname', 'string', [ + 'value' => '', + 'after' => 'profiles_id', + ]); + + $migration->addField($table, 'rights', 'integer', [ + 'value' => 0, + 'after' => 'rightname', + ]); + + $migration->addField($table, 'date_creation', 'timestamp'); + $migration->addField($table, 'date_mod', 'timestamp'); + + $migration->addKey($table, ['profiles_id', 'rightname'], 'profile_right'); +} diff --git a/install/mysql/plugin_urbackup-empty.sql b/install/mysql/plugin_urbackup-empty.sql new file mode 100644 index 0000000..3562526 --- /dev/null +++ b/install/mysql/plugin_urbackup-empty.sql @@ -0,0 +1,69 @@ +CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_configs` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL DEFAULT '', + `value` TEXT DEFAULT NULL, + `date_creation` TIMESTAMP NULL DEFAULT NULL, + `date_mod` TIMESTAMP NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_assettypes` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `itemtype` VARCHAR(255) NOT NULL DEFAULT '', + `is_active` TINYINT NOT NULL DEFAULT 0, + `is_default` TINYINT NOT NULL DEFAULT 0, + `date_creation` TIMESTAMP NULL DEFAULT NULL, + `date_mod` TIMESTAMP NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `itemtype` (`itemtype`), + KEY `is_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_servers` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `entities_id` INT UNSIGNED NOT NULL DEFAULT 0, + `is_recursive` TINYINT NOT NULL DEFAULT 0, + `name` VARCHAR(255) NOT NULL DEFAULT '', + `locations_id` INT UNSIGNED NOT NULL DEFAULT 0, + `ip_address` VARCHAR(255) NOT NULL DEFAULT '', + `port` INT UNSIGNED NOT NULL DEFAULT 55414, + `protocol` VARCHAR(10) NOT NULL DEFAULT 'http', + `server_version` VARCHAR(64) DEFAULT NULL, + `api_username` VARCHAR(255) DEFAULT NULL, + `api_password` TEXT DEFAULT NULL, + `ignore_ssl` TINYINT NOT NULL DEFAULT 0, + `is_active` TINYINT NOT NULL DEFAULT 1, + `last_api_status` TINYINT NOT NULL DEFAULT 0, + `last_api_message` TEXT DEFAULT NULL, + `last_api_check` TIMESTAMP NULL DEFAULT NULL, + `comment` TEXT DEFAULT NULL, + `date_creation` TIMESTAMP NULL DEFAULT NULL, + `date_mod` TIMESTAMP NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `entities_id` (`entities_id`), + KEY `locations_id` (`locations_id`), + 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_serverassets` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `plugin_urbackup_servers_id` INT UNSIGNED NOT NULL DEFAULT 0, + `itemtype` VARCHAR(255) NOT NULL DEFAULT '', + `items_id` INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `plugin_urbackup_servers_id` (`plugin_urbackup_servers_id`), + KEY `item` (`itemtype`, `items_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `glpi_plugin_urbackup_profiles` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `profiles_id` INT UNSIGNED NOT NULL DEFAULT 0, + `rightname` VARCHAR(255) NOT NULL DEFAULT '', + `rights` INT NOT NULL DEFAULT 0, + `date_creation` TIMESTAMP NULL DEFAULT NULL, + `date_mod` TIMESTAMP NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `profile_right` (`profiles_id`, `rightname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; \ No newline at end of file diff --git a/install/uninstall.php b/install/uninstall.php new file mode 100644 index 0000000..454285e --- /dev/null +++ b/install/uninstall.php @@ -0,0 +1,59 @@ +displayMessage(__('UrBackup plugin uninstallation', 'urbackup')); + + Profile::uninstallRights(); + + plugin_urbackup_migration_drop_table($migration, 'glpi_plugin_urbackup_profiles'); + plugin_urbackup_migration_drop_table($migration, 'glpi_plugin_urbackup_serverassets'); + plugin_urbackup_migration_drop_table($migration, 'glpi_plugin_urbackup_servers'); + plugin_urbackup_migration_drop_table($migration, 'glpi_plugin_urbackup_assettypes'); + plugin_urbackup_migration_drop_table($migration, 'glpi_plugin_urbackup_configs'); + + $migration->executeMigration(); + + return true; +} + +/** + * Drop a plugin table through Migration. + * + * @param Migration $migration Migration instance + * @param string $table Table name + * + * @return void + */ +function plugin_urbackup_migration_drop_table(Migration $migration, $table): void +{ + global $DB; + + if (!$DB->tableExists($table)) { + return; + } + + $migration->dropTable($table); +} \ No newline at end of file diff --git a/js/urbackup.js b/js/urbackup.js new file mode 100644 index 0000000..e4e4b51 --- /dev/null +++ b/js/urbackup.js @@ -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 = ' ' + 'API connection OK' + ''; + } 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 = ' ' + label + '
' + message + ''; + } + } catch (e) { + resultBox.innerHTML = ' Error'; + } + } else { + resultBox.innerHTML = ' HTTP ' + xhr.status + ''; + } + }; + + xhr.ontimeout = function () { + resultBox.innerHTML = ' Server unreachable
Connection timeout'; + }; + + xhr.onerror = function () { + resultBox.innerHTML = ' Server unreachable
Network error'; + }; + + 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); + } +})(); diff --git a/locales/de_DE.mo b/locales/de_DE.mo new file mode 100644 index 0000000..9c730d7 Binary files /dev/null and b/locales/de_DE.mo differ diff --git a/locales/de_DE.po b/locales/de_DE.po new file mode 100644 index 0000000..112ac1a --- /dev/null +++ b/locales/de_DE.po @@ -0,0 +1,158 @@ +msgid "" +msgstr "" +"Project-Id-Version: urbackup 0.4.0\n" +"Language: de_DE\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "UrBackup" +msgstr "UrBackup" + +msgid "UrBackup server" +msgstr "UrBackup-Server" + +msgid "UrBackup servers" +msgstr "UrBackup-Server" + +msgid "UrBackup configuration" +msgstr "UrBackup-Konfiguration" + +msgid "UrBackup rights" +msgstr "UrBackup-Berechtigungen" + +msgid "No UrBackup server linked." +msgstr "Kein UrBackup-Server verknüpft." + +msgid "UrBackup server selection" +msgstr "UrBackup-Serverauswahl" + +msgid "Root location ID" +msgstr "Root-Location-ID" + +msgid "Asset location ID" +msgstr "Asset-Location-ID" + +msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location." +msgstr "Das Asset befindet sich in einer Unter-Location. Der Server der Root-Location wird verwendet." + +msgid "No UrBackup server available for the root location of this asset." +msgstr "Kein UrBackup-Server für die Root-Location dieses Assets verfügbar." + +msgid "Available servers for root location" +msgstr "Verfügbare Server für die Root-Location" + +msgid "Connect" +msgstr "Verbinden" + +msgid "Disconnect" +msgstr "Trennen" + +msgid "UrBackup status" +msgstr "UrBackup-Status" + +msgid "Linked server" +msgstr "Verknüpfter Server" + +msgid "Client name" +msgstr "Clientname" + +msgid "Client IP address" +msgstr "Client-IP-Adresse" + +msgid "Client version" +msgstr "Client-Version" + +msgid "Online" +msgstr "Online" + +msgid "Offline" +msgstr "Offline" + +msgid "State" +msgstr "Status" + +msgid "Actions" +msgstr "Aktionen" + +msgid "Info / Log" +msgstr "Info / Protokoll" + +msgid "Create client in UrBackup" +msgstr "Client in UrBackup erstellen" + +msgid "Backup commands" +msgstr "Backup-Befehle" + +msgid "Incremental file backup" +msgstr "Inkrementelles Dateibackup" + +msgid "Full file backup" +msgstr "Vollständiges Dateibackup" + +msgid "Incremental image backup" +msgstr "Inkrementelles Image-Backup" + +msgid "Full image backup" +msgstr "Vollständiges Image-Backup" + +msgid "Internet mode" +msgstr "Internetmodus" + +msgid "Default directories" +msgstr "Standardverzeichnisse" + +msgid "Recent backups" +msgstr "Letzte Backups" + +msgid "Client logs" +msgstr "Client-Protokolle" + +msgid "Delete client from UrBackup server" +msgstr "Client vom UrBackup-Server löschen" + +msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours." +msgstr "Die Löschung des Clients wird auf dem UrBackup-Server in die Warteschlange gestellt und kann bis zu 24 Stunden dauern." + +msgid "API connection status" +msgstr "API-Verbindungsstatus" + +msgid "API connection OK" +msgstr "API-Verbindung OK" + +msgid "API connection failed" +msgstr "API-Verbindung fehlgeschlagen" + +msgid "Server unreachable" +msgstr "Server nicht erreichbar" + +msgid "No IP address configured" +msgstr "Keine IP-Adresse konfiguriert" + +msgid "Checking..." +msgstr "Überprüfung..." + +msgid "Click Save to test connection" +msgstr "Klicken Sie auf Speichern, um die Verbindung zu testen" + +msgid "Linked clients" +msgstr "Verknüpfte Clients" + +msgid "Unlinked clients" +msgstr "Nicht verknüpfte Clients" + +msgid "No linked assets" +msgstr "Keine verknüpften Assets" + +msgid "No unlinked clients found on UrBackup server" +msgstr "Keine nicht verknüpften Clients auf dem UrBackup-Server gefunden" + +msgid "API connection not working. Save server to test connection." +msgstr "API-Verbindung funktioniert nicht. Speichern Sie den Server, um die Verbindung zu testen." + +msgid "Status" +msgstr "Status" + +msgid "Last backup" +msgstr "Letztes Backup" + +msgid "Show" +msgstr "Anzeigen" \ No newline at end of file diff --git a/locales/en_GB.mo b/locales/en_GB.mo new file mode 100644 index 0000000..31a9502 Binary files /dev/null and b/locales/en_GB.mo differ diff --git a/locales/en_GB.po b/locales/en_GB.po new file mode 100644 index 0000000..ba3596d --- /dev/null +++ b/locales/en_GB.po @@ -0,0 +1,158 @@ +msgid "" +msgstr "" +"Project-Id-Version: urbackup 0.4.0\n" +"Language: en_GB\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "UrBackup" +msgstr "UrBackup" + +msgid "UrBackup server" +msgstr "UrBackup server" + +msgid "UrBackup servers" +msgstr "UrBackup servers" + +msgid "UrBackup configuration" +msgstr "UrBackup configuration" + +msgid "UrBackup rights" +msgstr "UrBackup rights" + +msgid "No UrBackup server linked." +msgstr "No UrBackup server linked." + +msgid "UrBackup server selection" +msgstr "UrBackup server selection" + +msgid "Root location ID" +msgstr "Root location ID" + +msgid "Asset location ID" +msgstr "Asset location ID" + +msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location." +msgstr "The asset is in a sub-location. The plugin will use the server assigned to the root location." + +msgid "No UrBackup server available for the root location of this asset." +msgstr "No UrBackup server available for the root location of this asset." + +msgid "Available servers for root location" +msgstr "Available servers for root location" + +msgid "Connect" +msgstr "Connect" + +msgid "Disconnect" +msgstr "Disconnect" + +msgid "UrBackup status" +msgstr "UrBackup status" + +msgid "Linked server" +msgstr "Linked server" + +msgid "Client name" +msgstr "Client name" + +msgid "Client IP address" +msgstr "Client IP address" + +msgid "Client version" +msgstr "Client version" + +msgid "Online" +msgstr "Online" + +msgid "Offline" +msgstr "Offline" + +msgid "State" +msgstr "State" + +msgid "Actions" +msgstr "Actions" + +msgid "Info / Log" +msgstr "Info / Log" + +msgid "Create client in UrBackup" +msgstr "Create client in UrBackup" + +msgid "Backup commands" +msgstr "Backup commands" + +msgid "Incremental file backup" +msgstr "Incremental file backup" + +msgid "Full file backup" +msgstr "Full file backup" + +msgid "Incremental image backup" +msgstr "Incremental image backup" + +msgid "Full image backup" +msgstr "Full image backup" + +msgid "Internet mode" +msgstr "Internet mode" + +msgid "Default directories" +msgstr "Default directories" + +msgid "Recent backups" +msgstr "Recent backups" + +msgid "Client logs" +msgstr "Client logs" + +msgid "Delete client from UrBackup server" +msgstr "Delete client from UrBackup server" + +msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours." +msgstr "The client deletion will be queued on the UrBackup server and may require up to 24 hours." + +msgid "API connection status" +msgstr "API connection status" + +msgid "API connection OK" +msgstr "API connection OK" + +msgid "API connection failed" +msgstr "API connection failed" + +msgid "Server unreachable" +msgstr "Server unreachable" + +msgid "No IP address configured" +msgstr "No IP address configured" + +msgid "Checking..." +msgstr "Checking..." + +msgid "Click Save to test connection" +msgstr "Click Save to test connection" + +msgid "Linked clients" +msgstr "Linked clients" + +msgid "Unlinked clients" +msgstr "Unlinked clients" + +msgid "No linked assets" +msgstr "No linked assets" + +msgid "No unlinked clients found on UrBackup server" +msgstr "No unlinked clients found on UrBackup server" + +msgid "API connection not working. Save server to test connection." +msgstr "API connection not working. Save server to test connection." + +msgid "Status" +msgstr "Status" + +msgid "Last backup" +msgstr "Last backup" + +msgid "Show" +msgstr "Show" \ No newline at end of file diff --git a/locales/it_IT.mo b/locales/it_IT.mo new file mode 100644 index 0000000..505476e Binary files /dev/null and b/locales/it_IT.mo differ diff --git a/locales/it_IT.po b/locales/it_IT.po new file mode 100644 index 0000000..70dff38 --- /dev/null +++ b/locales/it_IT.po @@ -0,0 +1,158 @@ +msgid "" +msgstr "" +"Project-Id-Version: urbackup 0.4.0\n" +"Language: it_IT\n" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid "UrBackup" +msgstr "UrBackup" + +msgid "UrBackup server" +msgstr "Server UrBackup" + +msgid "UrBackup servers" +msgstr "Server UrBackup" + +msgid "UrBackup configuration" +msgstr "Configurazione UrBackup" + +msgid "UrBackup rights" +msgstr "Diritti UrBackup" + +msgid "No UrBackup server linked." +msgstr "Nessun server UrBackup collegato." + +msgid "UrBackup server selection" +msgstr "Selezione server UrBackup" + +msgid "Root location ID" +msgstr "ID location principale" + +msgid "Asset location ID" +msgstr "ID location asset" + +msgid "The asset is in a sub-location. The plugin will use the server assigned to the root location." +msgstr "L'asset è in una sotto-location. Il plugin userà il server della location principale." + +msgid "No UrBackup server available for the root location of this asset." +msgstr "Nessun server UrBackup disponibile per la location principale di questo asset." + +msgid "Available servers for root location" +msgstr "Server disponibili per la location principale" + +msgid "Connect" +msgstr "Collega" + +msgid "Disconnect" +msgstr "Disconnetti" + +msgid "UrBackup status" +msgstr "Stato UrBackup" + +msgid "Linked server" +msgstr "Server collegato" + +msgid "Client name" +msgstr "Nome client" + +msgid "Client IP address" +msgstr "Indirizzo IP client" + +msgid "Client version" +msgstr "Versione client" + +msgid "Online" +msgstr "Online" + +msgid "Offline" +msgstr "Offline" + +msgid "State" +msgstr "Stato" + +msgid "Actions" +msgstr "Azioni" + +msgid "Info / Log" +msgstr "Info / Log" + +msgid "Create client in UrBackup" +msgstr "Crea client in UrBackup" + +msgid "Backup commands" +msgstr "Comandi backup" + +msgid "Incremental file backup" +msgstr "Backup file incrementale" + +msgid "Full file backup" +msgstr "Backup file completo" + +msgid "Incremental image backup" +msgstr "Backup immagine incrementale" + +msgid "Full image backup" +msgstr "Backup immagine completo" + +msgid "Internet mode" +msgstr "Modalità Internet" + +msgid "Default directories" +msgstr "Directory predefinite" + +msgid "Recent backups" +msgstr "Backup recenti" + +msgid "Client logs" +msgstr "Log client" + +msgid "Delete client from UrBackup server" +msgstr "Elimina client dal server UrBackup" + +msgid "The client deletion will be queued on the UrBackup server and may require up to 24 hours." +msgstr "L'eliminazione del client verrà messa in coda sul server UrBackup e potrebbe richiedere fino a 24 ore." + +msgid "API connection status" +msgstr "Stato connessione API" + +msgid "API connection OK" +msgstr "Connessione API OK" + +msgid "API connection failed" +msgstr "Connessione API fallita" + +msgid "Server unreachable" +msgstr "Server irraggiungibile" + +msgid "No IP address configured" +msgstr "Nessun indirizzo IP configurato" + +msgid "Checking..." +msgstr "Verifica in corso..." + +msgid "Click Save to test connection" +msgstr "Clicca \"Salva\" per testare la connessione" + +msgid "Linked clients" +msgstr "Clienti collegati" + +msgid "Unlinked clients" +msgstr "Clienti non collegati" + +msgid "No linked assets" +msgstr "Nessun asset collegato" + +msgid "No unlinked clients found on UrBackup server" +msgstr "Nessun cliente non collegato trovato sul server UrBackup" + +msgid "API connection not working. Save server to test connection." +msgstr "Connessione API non funzionante. Salva il server per testare la connessione." + +msgid "Status" +msgstr "Stato" + +msgid "Last backup" +msgstr "Ultimo backup" + +msgid "Show" +msgstr "Mostra" \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..f1f8001 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: max + paths: + - src/ + bootstrapFiles: + - vendor/autoload.php + scanDirectories: + - vendor/glpi-project/glpi/inc + ignoreErrors: + - '#Call to an undefined method#' + - '#Access to an undefined property#' + - '#Cannot call method on mixed#' diff --git a/public/css/urbackup.css b/public/css/urbackup.css new file mode 100644 index 0000000..d700831 --- /dev/null +++ b/public/css/urbackup.css @@ -0,0 +1,12 @@ +.urbackup-tab { + padding: 10px; +} + +.urbackup-tab h3 { + margin-bottom: 10px; +} + +.urbackup-tab .warning { + color: #b00; + font-weight: bold; +} diff --git a/public/js/urbackup.js b/public/js/urbackup.js new file mode 100644 index 0000000..0f57c04 --- /dev/null +++ b/public/js/urbackup.js @@ -0,0 +1,82 @@ +/** + * UrBackup API Test JavaScript + */ +(function () { + 'use strict'; + + function testApi(serverId, resultBox) { + if (!serverId || !resultBox) return; + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/plugins/urbackup/front/server_test.ajax.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 = ' API connection OK'; + } else { + resultBox.innerHTML = ' API connection failed
' + (data.message || '') + ''; + } + } catch (e) { + resultBox.innerHTML = ' Error'; + } + } else { + resultBox.innerHTML = ' HTTP ' + xhr.status + ''; + } + }; + + xhr.ontimeout = function () { + resultBox.innerHTML = ' API connection failed
Connection timeout'; + }; + + xhr.onerror = function () { + resultBox.innerHTML = ' API connection failed
Network error'; + }; + + 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); + } +})(); \ No newline at end of file diff --git a/remove_deprecated_files.sh b/remove_deprecated_files.sh new file mode 100755 index 0000000..b145d5c --- /dev/null +++ b/remove_deprecated_files.sh @@ -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." diff --git a/setup.php b/setup.php new file mode 100644 index 0000000..1f97100 --- /dev/null +++ b/setup.php @@ -0,0 +1,178 @@ + 'Profile', + ]); + Plugin::registerClass(Server::class, [ + 'linkgroup_types' => true, + 'document_types' => true, + ]); + Plugin::registerClass(ServerAsset::class); + Plugin::registerClass(PluginUrbackupMassiveAction::class); + + // Register tab on Computer and other assets + $enabled_types = Config::getEnabledItemtypes(); + if (!empty($enabled_types)) { + Plugin::registerClass(AssetTab::class, [ + 'addtabon' => $enabled_types, + ]); + } + + $PLUGIN_HOOKS['config_page']['urbackup'] = 'front/config.form.php'; + + $PLUGIN_HOOKS[Hooks::MENU_TOADD]['urbackup'] = [ + 'admin' => Server::class, + ]; + + $PLUGIN_HOOKS[Hooks::USE_MASSIVE_ACTION]['urbackup'] = true; + + $PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = [ + 'public/css/urbackup.css', + ]; + + $PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [ + 'public/js/urbackup.js', + ]; + + $PLUGIN_HOOKS[Hooks::POST_INIT]['urbackup'] = 'plugin_urbackup_postinit'; +} + +/** + * Post init hook. + * + * @return void + */ +function plugin_urbackup_postinit(): void +{ + Config::registerAssetTabs(); +} + +/** + * Plugin version information. + * + * @return array + */ +function plugin_version_urbackup(): array +{ + return [ + 'name' => __('UrBackup', 'urbackup'), + 'version' => PLUGIN_URBACKUP_VERSION, + 'author' => 'Finstral', + 'license' => 'GPL-2.0-or-later', + 'homepage' => '', + 'requirements' => [ + 'glpi' => [ + 'min' => PLUGIN_URBACKUP_MIN_GLPI, + 'max' => PLUGIN_URBACKUP_MAX_GLPI, + ], + 'php' => [ + 'min' => '8.3.0', + ], + ], + ]; +} + +/** + * Check plugin prerequisites. + * + * @return bool + */ +function plugin_urbackup_check_prerequisites(): bool +{ + if (version_compare(GLPI_VERSION, PLUGIN_URBACKUP_MIN_GLPI, '<')) { + echo sprintf( + __('This plugin requires GLPI >= %s.', 'urbackup'), + PLUGIN_URBACKUP_MIN_GLPI + ); + return false; + } + + if (version_compare(GLPI_VERSION, PLUGIN_URBACKUP_MAX_GLPI, '>')) { + echo sprintf( + __('This plugin requires GLPI < %s.', 'urbackup'), + PLUGIN_URBACKUP_MAX_GLPI + ); + return false; + } + + if (version_compare(PHP_VERSION, '8.3.0', '<')) { + echo __('This plugin requires PHP 8.3 or higher.', 'urbackup'); + return false; + } + + return true; +} + +/** + * Check plugin config. + * + * @param bool $verbose Verbose output + * + * @return bool + */ +function plugin_urbackup_check_config(bool $verbose = false): bool +{ + return true; +} + +/** + * Plugin installation function. + * + * @return bool + */ +function plugin_urbackup_install(): bool +{ + require_once __DIR__ . '/install/install.php'; + return plugin_urbackup_install_process(); +} + +/** + * Plugin uninstallation function. + * + * @return bool + */ +function plugin_urbackup_uninstall(): bool +{ + require_once __DIR__ . '/install/uninstall.php'; + return plugin_urbackup_uninstall_process(); +} diff --git a/src/AssetTab.php b/src/AssetTab.php new file mode 100644 index 0000000..b9e4a6d --- /dev/null +++ b/src/AssetTab.php @@ -0,0 +1,913 @@ +"; + echo htmlspecialchars(__('You do not have permission to view UrBackup information.', 'urbackup')); + echo ""; + + return true; + } + + $itemtype = $item::class; + $items_id = (int) ($item->fields['id'] ?? 0); + + $link = ServerAsset::getLinkForAsset($itemtype, $items_id, true); + + echo "
"; + + if ($link === null) { + self::showNoServerLinkedBlock($item); + } else { + self::showServerLinkedBlock($item, $link); + } + + echo "
"; + + return true; + } + + /** + * Show block when no server is linked. + * + * @param CommonDBTM $item Asset item + * + * @return void + */ + private static function showNoServerLinkedBlock(CommonDBTM $item): void + { + $asset_location_id = LocationHelper::getAssetLocationId($item); + $root_location_id = LocationHelper::getRootLocationIdForAsset($item); + $is_sub_location = LocationHelper::assetIsInSubLocation($item); + $servers = LocationHelper::getAvailableServersForAsset($item); + + echo "
"; + echo htmlspecialchars(__('No UrBackup server linked.', 'urbackup')); + echo "
"; + + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + if ($is_sub_location) { + echo ""; + echo ""; + echo ""; + } + + if (count($servers) === 0) { + echo ""; + echo ""; + echo ""; + echo "
" . htmlspecialchars(__('UrBackup server selection', 'urbackup')) . "
" . htmlspecialchars(__('Asset location ID', 'urbackup')) . "" . htmlspecialchars((string) $asset_location_id) . "
" . htmlspecialchars(__('Root location ID', 'urbackup')) . "" . htmlspecialchars((string) $root_location_id) . "
"; + echo htmlspecialchars( + __('The asset is in a sub-location. The plugin will use the server assigned to the root location.', 'urbackup') + ); + echo "
"; + echo "
"; + echo htmlspecialchars(__('No UrBackup server available for the root location of this asset.', 'urbackup')); + echo "
"; + echo "
"; + + return; + } + + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) { + echo ""; + echo ""; + echo htmlspecialchars(__('A server is available, but you do not have permission to link this asset.', 'urbackup')); + echo ""; + echo ""; + echo ""; + + return; + } + + echo ""; + echo "" . htmlspecialchars(__('Available servers for root location', 'urbackup')) . ""; + echo ""; + + echo "
"; + echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); + echo Html::hidden('itemtype', ['value' => $item::class]); + echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]); + + Dropdown::showFromArray( + 'plugin_urbackup_servers_id', + $servers, + [ + 'value' => 0, + ] + ); + + echo Html::submit(__('Connect', 'urbackup'), [ + 'name' => 'connect', + 'class' => 'btn btn-primary', + ]); + + Html::closeForm(); + + echo ""; + echo ""; + echo ""; + } + + /** + * Show block when server is linked. + * + * @param CommonDBTM $item Asset item + * @param array $link Link row + * + * @return void + */ + private static function showServerLinkedBlock(CommonDBTM $item, array $link): void + { + $server = new Server(); + $server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0); + + if ($server_id <= 0 || !$server->getFromDB($server_id)) { + echo "
"; + echo htmlspecialchars(__('The linked UrBackup server no longer exists.', 'urbackup')); + echo "
"; + + return; + } + + $api_data = self::loadApiData($item, $server, $link); + + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo "
" . htmlspecialchars(__('UrBackup status', 'urbackup')) . "
" . htmlspecialchars(__('Linked server', 'urbackup')) . "" . $server->getLink() . "" . htmlspecialchars(__('IP address', 'urbackup')) . "" . htmlspecialchars((string) $server->fields['ip_address']) . "
" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "" . htmlspecialchars(__('Client name', 'urbackup')) . "" . htmlspecialchars(ServerAsset::getAssetName($item::class, (int) $item->fields['id'])) . "
"; + + if ($api_data['error'] !== '') { + echo "
"; + echo htmlspecialchars($api_data['error']); + echo "
"; + } + + if ($api_data['ip_warning'] !== '') { + echo "
"; + echo htmlspecialchars($api_data['ip_warning']); + echo "
"; + } + + self::showInternalTabs($item, $server, $link, $api_data); + } + + /** + * Load API data for asset tab. + * + * @param CommonDBTM $item Asset + * @param Server $server Server + * @param array $link Link + * + * @return array + */ + private static function loadApiData(CommonDBTM $item, Server $server, array $link): array + { + $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); + $asset_ip = ServerAsset::extractAssetIp($item); + + $data = [ + 'client_found' => false, + 'client_status' => [], + 'client_settings' => [], + 'authkey' => '', + 'recent_backups' => [], + 'logs' => [], + 'error' => '', + 'ip_warning' => '', + ]; + + try { + $api = new UrbackupApiClient($server); + $client_status = $api->getClientStatusByName($client_name); + + if ($client_status !== null) { + $data['client_found'] = true; + $data['client_status'] = $client_status; + + $urbackup_ip = (string) ( + $client_status['ip'] ?? + $client_status['ip_address'] ?? + $client_status['addr'] ?? + '' + ); + + if ($asset_ip !== '' && $urbackup_ip !== '' && $asset_ip !== $urbackup_ip) { + $data['ip_warning'] = sprintf( + __('Client name matches, but GLPI IP "%s" differs from UrBackup IP "%s".', 'urbackup'), + $asset_ip, + $urbackup_ip + ); + } + + $data['client_settings'] = $api->getClientSettings($client_name); + $data['authkey'] = $api->getClientAuthKey($client_name); + $data['recent_backups'] = $api->getRecentBackups($client_name, 10); + $data['logs'] = $api->getClientLogs($client_name, 50); + } + } catch (Throwable $e) { + $data['error'] = $e->getMessage(); + } + + return $data; + } + + /** + * Show internal sections. + * + * @param CommonDBTM $item Asset + * @param Server $server Server + * @param array $link Link + * @param array $api_data API data + * + * @return void + */ + private static function showInternalTabs( + CommonDBTM $item, + Server $server, + array $link, + array $api_data + ): void { + echo "
"; + + echo "

" . htmlspecialchars(__('State', 'urbackup')) . "

"; + self::showStateSection($server, $link, $api_data); + + echo "

" . htmlspecialchars(__('Actions', 'urbackup')) . "

"; + self::showActionsSection($item, $server, $link, $api_data); + + echo "

" . htmlspecialchars(__('Info / Log', 'urbackup')) . "

"; + self::showInfoLogSection($api_data); + + echo "
"; + } + + /** + * Show state section. + * + * @param Server $server Server + * @param array $link Link + * @param array $api_data API data + * + * @return void + */ + private static function showStateSection(Server $server, array $link, array $api_data): void + { + $status = $api_data['client_status']; + $settings = $api_data['client_settings']; + + echo ""; + echo ""; + + if (!$api_data['client_found']) { + echo ""; + echo ""; + echo ""; + echo "
" . htmlspecialchars(__('Client state', 'urbackup')) . "
"; + echo htmlspecialchars(__('Client not found on UrBackup server.', 'urbackup')); + echo "
"; + + return; + } + + $internetMode = self::extractSettingValue($settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null, 0); + $internetModeDisplay = ((int) $internetMode === 1) + ? '' . __('Yes', 'urbackup') . '' + : '' . __('No', 'urbackup') . ''; + + $rows = [ + __('Client version', 'urbackup') => $status['client_version_string'] ?? $status['client_version'] ?? $status['version'] ?? '-', + __('Online / Offline', 'urbackup') => self::formatOnlineStatus($status), + __('Last file backup', 'urbackup') => self::formatTimestamp($status['file_lastbackup'] ?? $status['lastbackup'] ?? $status['last_file_backup'] ?? ''), + __('Last image backup', 'urbackup') => self::formatTimestamp($status['image_lastbackup'] ?? $status['lastbackup_image'] ?? $status['last_image_backup'] ?? ''), + __('Last file backup result', 'urbackup') => self::formatBoolStatus($status['file_ok'] ?? null), + __('Last image backup result', 'urbackup') => self::formatBoolStatus($status['image_ok'] ?? null), + __('Current activities', 'urbackup') => $status['status'] ?? $status['activity'] ?? '-', + __('Internet mode', 'urbackup') => $internetModeDisplay, + ]; + + foreach ($rows as $label => $value) { + $displayValue = is_array($value) ? json_encode($value) : (string) $value; + echo ""; + echo "" . htmlspecialchars((string) $label) . ""; + echo "" . $displayValue . ""; + echo ""; + } + + echo ""; + echo "" . htmlspecialchars(__('Internet authentication key', 'urbackup')) . ""; + echo ""; + $authKey = (string) $api_data['authkey']; + if ($authKey === '') { + echo '-'; + } else { + echo ''; + echo '
'; + echo ''; + echo ''; + echo '
'; + } + echo ""; + echo ""; + + echo ""; + + echo '
'; + echo '' . htmlspecialchars(__('API raw data (debug)', 'urbackup')) . ''; + echo '
';
+        echo "--- client_settings ---\n\n";
+        echo htmlspecialchars(json_encode($settings, JSON_PRETTY_PRINT));
+        echo "\n\n--- client_status ---\n\n";
+        echo htmlspecialchars(json_encode($status, JSON_PRETTY_PRINT));
+        echo "\n\n--- recent_backups ---\n\n";
+        echo htmlspecialchars(json_encode($api_data['recent_backups'] ?? [], JSON_PRETTY_PRINT));
+        echo '
'; + echo '
'; + } + + /** + * Show actions section. + * + * @param CommonDBTM $item Asset + * @param Server $server Server + * @param array $link Link + * @param array $api_data API data + * + * @return void + */ + private static function showActionsSection( + CommonDBTM $item, + Server $server, + array $link, + array $api_data + ): void { + echo ""; + echo ""; + + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) { + echo ""; + echo ""; + echo ""; + echo "
" . htmlspecialchars(__('Available actions', 'urbackup')) . "
"; + echo htmlspecialchars(__('You do not have permission for UrBackup actions.', 'urbackup')); + echo "
"; + + return; + } + + if (!$api_data['client_found'] && Profile::canCurrentUser(CREATE)) { + echo ""; + echo "" . htmlspecialchars(__('Create client in UrBackup', 'urbackup')) . ""; + echo ""; + self::showActionButton($item, 'create_client', __('Create client in UrBackup', 'urbackup'), 'btn btn-primary'); + echo ""; + echo ""; + } + + if (Profile::canCurrentUser(UPDATE)) { + echo ""; + echo "" . htmlspecialchars(__('Internet mode', 'urbackup')) . ""; + echo ""; + self::showInternetModeForm($item, $api_data); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('Default directories', 'urbackup')) . ""; + echo ""; + self::showDefaultDirsForm($item, $api_data); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('Backup commands', 'urbackup')) . ""; + echo ""; + self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary'); + self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary'); + self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary'); + self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary'); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('Disconnect client', 'urbackup')) . ""; + echo ""; + self::showDisconnectButton($item); + echo ""; + echo ""; + } + + if (Profile::canCurrentUser(PURGE)) { + echo ""; + echo "" . htmlspecialchars(__('Delete client from UrBackup server', 'urbackup')) . ""; + echo ""; + echo "
"; + echo htmlspecialchars( + __('The client deletion will be queued on the UrBackup server and may require up to 24 hours.', 'urbackup') + ); + echo "
"; + self::showActionButton($item, 'delete_client', __('Delete client from UrBackup server', 'urbackup'), 'btn btn-danger'); + echo ""; + echo ""; + } + + echo ""; + } + + /** + * Show info/log section. + * + * @param array $api_data API data + * + * @return void + */ + private static function formatBytes(mixed $bytes): string + { + $bytes = (float) ($bytes ?? 0); + if ($bytes <= 0) { + return '-'; + } + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $i = floor(log($bytes, 1024)); + $i = min((int) $i, count($units) - 1); + return sprintf('%.1f %s', $bytes / pow(1024, $i), $units[$i]); + } + + /** + * Show info/log section. + * + * @param array $api_data API data + * + * @return void + */ + private static function showInfoLogSection(array $api_data): void + { + $recent_backups = $api_data['recent_backups'] ?? []; + usort($recent_backups, function ($a, $b) { + $tsA = (int) ($a['time'] ?? $a['backuptime'] ?? $a['backup_time'] ?? $a['created'] ?? $a['start_time'] ?? 0); + $tsB = (int) ($b['time'] ?? $b['backuptime'] ?? $b['backup_time'] ?? $b['created'] ?? $b['start_time'] ?? 0); + return $tsB - $tsA; + }); + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + foreach ($recent_backups as $backup) { + $incremental = (int) ($backup['incremental'] ?? 0); + $timestamp = $backup['time'] ?? $backup['backuptime'] ?? $backup['backup_time'] ?? $backup['created'] ?? $backup['start_time'] ?? 0; + $dateFormatted = self::formatTimestamp($timestamp); + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + + if (count($api_data['recent_backups']) === 0) { + echo ""; + } + + echo "
" . htmlspecialchars(__('Recent backups', 'urbackup')) . "
" . htmlspecialchars(__('Type')) . "" . htmlspecialchars(__('Date')) . "" . htmlspecialchars(__('Result')) . "" . htmlspecialchars(__('Size')) . "" . htmlspecialchars(__('Incremental', 'urbackup')) . "" . htmlspecialchars(__('Backup ID', 'urbackup')) . "
" . htmlspecialchars((string) ($backup['backup_type'] ?? '')) . "" . htmlspecialchars($dateFormatted) . " " . htmlspecialchars(__('Success', 'urbackup')) . "" . htmlspecialchars(self::formatBytes($backup['size'] ?? $backup['size_bytes'] ?? 0)) . "" . htmlspecialchars($incremental === 1 ? __('Yes', 'urbackup') : __('No', 'urbackup')) . "" . htmlspecialchars((string) ($backup['backupid'] ?? $backup['id'] ?? '-')) . "
"; + echo htmlspecialchars(__('No recent backup information available.', 'urbackup')); + echo "
"; + + echo "
"; + + $logs = array_reverse($api_data['logs'] ?? []); + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + foreach ($logs as $log) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + + if (count($api_data['logs']) === 0) { + echo ""; + } + + echo "
" . htmlspecialchars(__('Client logs', 'urbackup')) . "
" . htmlspecialchars(__('Date')) . "" . htmlspecialchars(__('Level')) . "" . htmlspecialchars(__('Message')) . "
" . htmlspecialchars(self::formatTimestamp($log['time'] ?? $log['created'] ?? '')) . "" . htmlspecialchars((string) ($log['level'] ?? $log['severity'] ?? '')) . "" . htmlspecialchars((string) ($log['message'] ?? $log['msg'] ?? $log['text'] ?? '')) . "
"; + echo htmlspecialchars(__('No client logs available.', 'urbackup')); + echo "
"; + } + + /** + * Show generic UrBackup action button. + * + * @param CommonDBTM $item Asset + * @param string $action Action + * @param string $label Button label + * @param string $class CSS class + * + * @return void + */ + private static function showActionButton(CommonDBTM $item, string $action, string $label, string $class): void + { + echo ""; + echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); + echo Html::hidden('itemtype', ['value' => $item::class]); + echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]); + echo Html::hidden('urbackup_action', ['value' => $action]); + echo Html::submit($label, [ + 'name' => 'execute', + 'class' => $class, + ]); + Html::closeForm(); + echo " "; + } + + /** + * Show internet mode form. + * + * @param CommonDBTM $item Asset + * @param array $api_data API data + * + * @return void + */ + private static function showInternetModeForm(CommonDBTM $item, array $api_data): void + { + $settings = $api_data['client_settings']; + $current = (int) self::extractSettingValue( + $settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null, + 0 + ); + + echo ""; + echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); + echo Html::hidden('itemtype', ['value' => $item::class]); + echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]); + echo Html::hidden('urbackup_action', ['value' => 'set_internet_mode']); + echo Html::hidden('execute', ['value' => '1']); + echo Html::hidden('internet_mode', ['value' => '0']); + + echo '
'; + echo ''; + echo ''; + echo '
'; + + Html::closeForm(); + } + + /** + * Show default directories form. + * + * @param CommonDBTM $item Asset + * @param array $api_data API data + * + * @return void + */ + private static function showDefaultDirsForm(CommonDBTM $item, array $api_data): void + { + $settings = $api_data['client_settings']; + $raw = $settings['default_dirs'] ?? ''; + + $display = ''; + if (is_array($raw)) { + // API returns struct: {"use":N,"value":"paths","value_client":"","value_group":""} + $display = (string) ($raw['value'] ?? ''); + } else { + $display = (string) $raw; + } + + echo ""; + echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); + echo Html::hidden('itemtype', ['value' => $item::class]); + echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]); + echo Html::hidden('urbackup_action', ['value' => 'set_default_dirs']); + + echo "
"; + + echo '
'; + echo 'raw: ' . htmlspecialchars(json_encode($raw)); + echo '
'; + + echo Html::submit(__('Save'), [ + 'name' => 'execute', + 'class' => 'btn btn-primary', + ]); + + Html::closeForm(); + } + + /** + * Show disconnect button. + * + * @param CommonDBTM $item Asset + * + * @return void + */ + private static function showDisconnectButton(CommonDBTM $item): void + { + echo ""; + echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]); + echo Html::hidden('itemtype', ['value' => $item::class]); + echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]); + + echo Html::submit(__('Disconnect', 'urbackup'), [ + 'name' => 'disconnect', + 'class' => 'btn btn-warning', + ]); + + Html::closeForm(); + } + + /** + * Format online status. + * + * @param array $status Status + * + * @return string + */ + private static function formatOnlineStatus(array $status): string + { + $online = $status['online'] ?? $status['is_online'] ?? null; + + return match (true) { + $online === true || $online === 1 || $online === '1' || $online === 'true' => __('Online', 'urbackup'), + $online === false || $online === 0 || $online === '0' || $online === 'false' => __('Offline', 'urbackup'), + default => '', + }; + } + + /** + * Format bool status. + * + * @param mixed $value Value + * + * @return string + */ + private static function formatBoolStatus(mixed $value): string + { + return match (true) { + $value === true || $value === 1 || $value === '1' || $value === 'true' => __('OK', 'urbackup'), + $value === false || $value === 0 || $value === '0' || $value === 'false' => __('Failed', 'urbackup'), + default => '', + }; + } + + /** + * Format timestamp-like value. + * + * @param mixed $value Timestamp + * + * @return string + */ + private static function formatTimestamp(mixed $value): string + { + if ($value === null || $value === '' || $value === 0 || $value === '0') { + return ''; + } + + $ts = (int) $value; + if ($ts > 0 && $ts < 2000000000) { + // Unix timestamp + return Html::convDateTime(date('Y-m-d H:i:s', $ts)); + } + + $str = trim((string) $value); + if ($str === '') { + return ''; + } + + // ISO date string like "2024-01-15T10:30:00" or "2024-01-15 10:30:00" + $normalized = str_replace('T', ' ', $str); + if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/', $normalized)) { + return Html::convDateTime($normalized); + } + + // Plain date like "2024-01-15" + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $str)) { + return Html::convDate($str); + } + + return $str; + } + + public static function startBackup(CommonDBTM $item, string $type): bool + { + $link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false); + if ($link === null) { + return false; + } + + $server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0); + if ($server_id <= 0) { + return false; + } + + $server = new Server(); + if (!$server->getFromDB($server_id)) { + return false; + } + + $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); + if ($client_name === '') { + return false; + } + + try { + $api = new UrbackupApiClient($server); + + switch ($type) { + case 'file': + return $api->startIncrementalFileBackup($client_name); + case 'image': + return $api->startIncrementalImageBackup($client_name); + } + } catch (\Throwable $e) { + return false; + } + + return false; + } + + public static function saveInternetMode(CommonDBTM $item, bool $enabled): bool + { + $link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false); + if ($link === null) { + return false; + } + + $server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0); + if ($server_id <= 0) { + return false; + } + + $server = new Server(); + if (!$server->getFromDB($server_id)) { + return false; + } + + $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); + if ($client_name === '') { + return false; + } + + try { + $api = new UrbackupApiClient($server); + return $api->saveInternetMode($client_name, $enabled); + } catch (\Throwable $e) { + return false; + } + } + + public static function saveDefaultDirs(CommonDBTM $item, string $dirs): bool + { + $link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false); + if ($link === null) { + return false; + } + + $server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0); + if ($server_id <= 0) { + return false; + } + + $server = new Server(); + if (!$server->getFromDB($server_id)) { + return false; + } + + $client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']); + if ($client_name === '') { + return false; + } + + try { + $api = new UrbackupApiClient($server); + return $api->updateClientSettings($client_name, 'default_dirs', trim($dirs)); + } catch (\Throwable $e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/Command/TestApiCommand.php b/src/Command/TestApiCommand.php new file mode 100644 index 0000000..e7b8529 --- /dev/null +++ b/src/Command/TestApiCommand.php @@ -0,0 +1,56 @@ +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("Server not found: $server_id"); + 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("SUCCESS: " . $result['message'] . ""); + $output->writeln("Identity: " . $result['identity']); + } else { + $output->writeln("FAILED: " . $result['message'] . ""); + } + + return $result['success'] ? Command::SUCCESS : Command::FAILURE; + } catch (\Throwable $e) { + $output->writeln("Exception: " . $e->getMessage() . ""); + return Command::FAILURE; + } + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..679b0da --- /dev/null +++ b/src/Config.php @@ -0,0 +1,414 @@ +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'), + ] + ); + } + + /** + * Get assettypes table name. + * + * @return string + */ + public static function getAssetTypesTable(): string + { + return 'glpi_plugin_urbackup_assettypes'; + } + + /** + * Check whether itemtype is enabled for UrBackup. + * + * @param string $itemtype Itemtype + * + * @return bool + */ + public static function isItemtypeEnabled(string $itemtype): bool + { + global $DB; + + if ($itemtype === '') { + return false; + } + + if ($itemtype === 'Computer') { + return true; + } + + $table = self::getAssetTypesTable(); + + if (!$DB->tableExists($table)) { + return false; + } + + $iterator = $DB->request([ + 'FROM' => $table, + 'WHERE' => [ + 'itemtype' => $itemtype, + 'is_active' => 1, + ], + 'LIMIT' => 1, + ]); + + return count($iterator) > 0; + } + + /** + * Get enabled itemtypes. + * + * @return array + */ + public static function getEnabledItemtypes(): array + { + global $DB; + + $types = ['Computer']; + $table = self::getAssetTypesTable(); + + if (!$DB->tableExists($table)) { + return $types; + } + + $iterator = $DB->request([ + 'FROM' => $table, + 'WHERE' => [ + 'is_active' => 1, + ], + 'ORDER' => 'itemtype', + ]); + + foreach ($iterator as $row) { + $itemtype = (string) $row['itemtype']; + + if ($itemtype !== '' && !in_array($itemtype, $types, true)) { + $types[] = $itemtype; + } + } + + 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, + ]); + } + } + + /** + * Show config form. + * + * @param int $ID ID + * @param array $options Options + * + * @return bool + */ + public function showForm($ID, array $options = []): bool + { + Session::checkRight('config', UPDATE); + + echo ""; + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + foreach (self::getConfigurableAssetTypes() as $itemtype => $label) { + $enabled = self::isItemtypeEnabled($itemtype); + $is_default = ($itemtype === 'Computer'); + $is_asset_definition = self::isAssetDefinition($itemtype); + + echo ""; + echo ""; + + if ($itemtype === 'Computer') { + echo ""; + echo ""; + } elseif ($is_asset_definition) { + // GLPI 11 Asset Definition + echo ""; + echo ""; + echo ""; + } else { + // Legacy type + echo ""; + echo ""; + echo ""; + } + + echo ""; + } + + echo ""; + echo ""; + echo ""; + + echo "
" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "
" . htmlspecialchars(__('Asset type', 'urbackup')) . "" . htmlspecialchars(__('Enabled', 'urbackup')) . "" . htmlspecialchars(__('Default', 'urbackup')) . "" . htmlspecialchars(__('Type', 'urbackup')) . "
" . htmlspecialchars($label) . "" . htmlspecialchars(__('Always', 'urbackup')) . "" . htmlspecialchars(__('Yes', 'urbackup')) . ""; + Html::showCheckbox([ + 'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_active]', + 'checked' => $enabled, + ]); + echo ""; + Html::showCheckbox([ + 'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_default]', + 'checked' => $is_default, + ]); + echo "Asset Definition"; + Html::showCheckbox([ + 'name' => 'assettypes[' . htmlspecialchars($itemtype) . ']', + 'checked' => $enabled, + ]); + echo "" . ($is_default ? htmlspecialchars(__('Yes', 'urbackup')) : '') . "Legacy
"; + echo Html::submit(__('Save', 'urbackup'), [ + 'name' => 'update', + 'class' => 'btn btn-primary', + ]); + echo "
"; + echo "
"; + + Html::closeForm(); + + return true; + } + + /** + * Save configuration. + * + * @param array $input Input data + * + * @return void + */ + public static function saveConfiguration(array $input): void + { + Session::checkRight('config', UPDATE); + + $selected = $input['assettypes'] ?? []; + + foreach (self::getConfigurableAssetTypes() as $itemtype => $label) { + if ($itemtype === 'Computer') { + self::ensureAssetType('Computer', true, true); + continue; + } + + // Check if it's an Asset Definition (new format with is_active/is_default) + if (self::isAssetDefinition($itemtype)) { + $is_active = isset($selected[$itemtype]['is_active']) && (bool) $selected[$itemtype]['is_active']; + $is_default = isset($selected[$itemtype]['is_default']) && (bool) $selected[$itemtype]['is_default']; + self::ensureAssetType($itemtype, $is_active, $is_default); + } else { + // Legacy format (simple checkbox) + $is_enabled = isset($selected[$itemtype]) && (bool) $selected[$itemtype]; + self::ensureAssetType($itemtype, $is_enabled, false); + } + } + } + + /** + * Get configurable asset types (legacy + GLPI 11 Asset Definition). + * + * @return array + */ + 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; + } +} \ No newline at end of file diff --git a/src/Controller/AssetController.php b/src/Controller/AssetController.php new file mode 100644 index 0000000..0d8b64b --- /dev/null +++ b/src/Controller/AssetController.php @@ -0,0 +1,193 @@ +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)) { + $internet_mode = (int) $request->request->get('internet_mode', 0); + $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'] ?? ''); + $setting_key = $api->getInternetModeSettingKey(); + $api->changeClientSetting($client_name, $setting_key, $internet_mode); + } + } + } + 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); + } +} diff --git a/src/Controller/ConfigController.php b/src/Controller/ConfigController.php new file mode 100644 index 0000000..a0e5cf0 --- /dev/null +++ b/src/Controller/ConfigController.php @@ -0,0 +1,45 @@ +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, + ]); + } +} diff --git a/src/Controller/ServerController.php b/src/Controller/ServerController.php new file mode 100644 index 0000000..63ea5c4 --- /dev/null +++ b/src/Controller/ServerController.php @@ -0,0 +1,109 @@ + '\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); + } + } +} diff --git a/src/LocationHelper.php b/src/LocationHelper.php new file mode 100644 index 0000000..01c9310 --- /dev/null +++ b/src/LocationHelper.php @@ -0,0 +1,119 @@ +fields['locations_id'] ?? 0); + } + + /** + * Get root location ID from any sub-location. + * + * Required rule: + * if the computer/asset is placed in a sub-location, + * the UrBackup server reference is the one assigned to the root location. + * + * @param int $locations_id Location ID + * + * @return int + */ + public static function getRootLocationId(int $locations_id): int + { + if ($locations_id <= 0) { + return 0; + } + + $location = new Location(); + + if (!$location->getFromDB($locations_id)) { + return 0; + } + + $current_id = (int) $location->fields['id']; + $parent_id = (int) ($location->fields['locations_id'] ?? 0); + + while ($parent_id > 0) { + $parent = new Location(); + + if (!$parent->getFromDB($parent_id)) { + break; + } + + $current_id = (int) $parent->fields['id']; + $parent_id = (int) ($parent->fields['locations_id'] ?? 0); + } + + return $current_id; + } + + /** + * Get root location ID for an asset. + * + * @param CommonDBTM $item Asset item + * + * @return int + */ + public static function getRootLocationIdForAsset(CommonDBTM $item): int + { + return self::getRootLocationId(self::getAssetLocationId($item)); + } + + /** + * Get available UrBackup servers for an asset based on its root location. + * + * @param CommonDBTM $item Asset item + * + * @return array + */ + public static function getAvailableServersForAsset(CommonDBTM $item): array + { + $root_location_id = self::getRootLocationIdForAsset($item); + + if ($root_location_id <= 0) { + return []; + } + + return Server::getActiveServersForRootLocation($root_location_id); + } + + /** + * Check whether asset is in a sub-location. + * + * @param CommonDBTM $item Asset item + * + * @return bool + */ + public static function assetIsInSubLocation(CommonDBTM $item): bool + { + $location_id = self::getAssetLocationId($item); + + if ($location_id <= 0) { + return false; + } + + $root_location_id = self::getRootLocationId($location_id); + + return $root_location_id > 0 && $root_location_id !== $location_id; + } +} \ No newline at end of file diff --git a/src/MassiveAction.php b/src/MassiveAction.php new file mode 100644 index 0000000..13a1fd4 --- /dev/null +++ b/src/MassiveAction.php @@ -0,0 +1,264 @@ +getAction()) { + case self::ACTION_CONNECT_SERVER: + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) { + return false; + } + + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + + echo "
" . htmlspecialchars(__('Connect selected assets to an UrBackup server', 'urbackup')) . "
" . htmlspecialchars(__('UrBackup server', 'urbackup')) . ""; + + Server::dropdown([ + 'name' => 'plugin_urbackup_servers_id', + 'entity' => $_SESSION['glpiactiveentities'] ?? [], + 'entity_sons' => true, + 'condition' => ['is_active' => 1], + ]); + + echo "
"; + echo Html::submit(__('Connect', 'urbackup'), [ + 'name' => 'massiveaction', + 'class' => 'btn btn-primary', + ]); + echo "
"; + echo "
"; + + return true; + + case self::ACTION_DISCONNECT_SERVER: + if (!Profile::canCurrentUser(UPDATE)) { + return false; + } + + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + + echo "
" . htmlspecialchars(__('Disconnect selected assets from UrBackup server', 'urbackup')) . "
"; + echo Html::submit(__('Disconnect', 'urbackup'), [ + 'name' => 'massiveaction', + 'class' => 'btn btn-warning', + ]); + echo "
"; + echo "
"; + + return true; + } + + return parent::showMassiveActionsSubForm($ma); + } + + /** + * Process UrBackup massive actions. + * + * @param \MassiveAction $ma Massive action object + * @param CommonDBTM $item Current item object + * @param array $ids Selected IDs + * + * @return void + */ + public static function processMassiveActionsForOneItemtype( + \MassiveAction $ma, + CommonDBTM $item, + array $ids + ): void { + $itemtype = $item->getType(); + + if (!Config::isItemtypeEnabled($itemtype)) { + foreach ($ids as $id) { + $ma->itemDone($itemtype, (int) $id, \MassiveAction::ACTION_KO); + } + + $ma->addMessage(__('UrBackup is not enabled for this asset type.', 'urbackup')); + return; + } + + switch ($ma->getAction()) { + case self::ACTION_CONNECT_SERVER: + self::processConnectServer($ma, $item, $ids); + return; + + case self::ACTION_DISCONNECT_SERVER: + self::processDisconnectServer($ma, $item, $ids); + return; + } + + parent::processMassiveActionsForOneItemtype($ma, $item, $ids); + } + + /** + * Process connect-to-server massive action. + * + * @param \MassiveAction $ma Massive action object + * @param CommonDBTM $item Current item object + * @param array $ids Selected IDs + * + * @return void + */ + private static function processConnectServer( + \MassiveAction $ma, + CommonDBTM $item, + array $ids + ): void { + if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) { + foreach ($ids as $id) { + $ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO); + } + + $ma->addMessage(__('You do not have permission to connect assets to UrBackup servers.', 'urbackup')); + return; + } + + $input = $ma->getInput(); + $server_id = (int) ($input['plugin_urbackup_servers_id'] ?? 0); + + if ($server_id <= 0) { + foreach ($ids as $id) { + $ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO); + } + + $ma->addMessage(__('No UrBackup server selected.', 'urbackup')); + return; + } + + $server = new Server(); + + if (!$server->getFromDB($server_id)) { + foreach ($ids as $id) { + $ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO); + } + + $ma->addMessage(__('UrBackup server not found.', 'urbackup')); + return; + } + + foreach ($ids as $id) { + $id = (int) $id; + + if (!$item->getFromDB($id)) { + $ma->itemDone($item->getType(), $id, \MassiveAction::ACTION_KO); + continue; + } + + $result = ServerAsset::connectAssetToServer( + $item->getType(), + $id, + $server_id + ); + + $ma->itemDone( + $item->getType(), + $id, + $result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO + ); + } + + } + + /** + * Process disconnect-from-server massive action. + * + * @param \MassiveAction $ma Massive action object + * @param CommonDBTM $item Current item object + * @param array $ids Selected IDs + * + * @return void + */ + private static function processDisconnectServer( + \MassiveAction $ma, + CommonDBTM $item, + array $ids + ): void { + if (!Profile::canCurrentUser(UPDATE)) { + foreach ($ids as $id) { + $ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO); + } + + $ma->addMessage(__('You do not have permission to disconnect assets from UrBackup servers.', 'urbackup')); + return; + } + + foreach ($ids as $id) { + $id = (int) $id; + + if (!$item->getFromDB($id)) { + $ma->itemDone($item->getType(), $id, \MassiveAction::ACTION_KO); + continue; + } + + $result = ServerAsset::disconnectAsset( + $item->getType(), + $id + ); + + $ma->itemDone( + $item->getType(), + $id, + $result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO + ); + } + + } +} \ No newline at end of file diff --git a/src/Profile.php b/src/Profile.php new file mode 100644 index 0000000..d8db92b --- /dev/null +++ b/src/Profile.php @@ -0,0 +1,246 @@ +getField('interface') === 'central') { + return self::createTabEntry(Server::getTypeName(2)); + } + return ''; + } + + public static function displayTabContentForItem( + CommonGLPI $item, + $tabnum = 1, + $withtemplate = 0 + ) { + if (!$item instanceof \Profile) { + return false; + } + + $profile = new \Profile(); + $profile->getFromDB($item->getID()); + + $rights = self::getAllRights(); + + $twig = TemplateRenderer::getInstance(); + $twig->display('@urbackup/profile.html.twig', [ + 'id' => $item->getID(), + 'profile' => $profile, + 'title' => self::getTypeName(Session::getPluralNumber()), + 'rights' => $rights, + ]); + + return true; + } + + public static function getTypeName($nb = 0): string + { + return _n('UrBackup', 'UrBackup', $nb, 'urbackup'); + } + + public static function getAllRights(): array + { + return [ + [ + 'itemtype' => Server::class, + 'label' => __('UrBackup Servers', 'urbackup'), + 'field' => 'plugin_urbackup', + ], + ]; + } + + public static function installRights(): void + { + self::registerRights(); + + $profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0); + + if ($profiles_id > 0) { + self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE); + } + + global $DB; + $all_profiles = $DB->request([ + 'SELECT' => 'id', + 'FROM' => 'glpi_profiles', + ]); + + foreach ($all_profiles as $profile) { + if ($profile['id'] !== $profiles_id) { + $existing = $DB->request([ + 'FROM' => 'glpi_profilerights', + 'WHERE' => [ + 'profiles_id' => $profile['id'], + 'name' => self::$rightname, + ], + 'LIMIT' => 1, + ]); + + if (count($existing) === 0) { + $DB->insert('glpi_profilerights', [ + 'profiles_id' => $profile['id'], + 'name' => self::$rightname, + 'rights' => READ, + ]); + } + } + } + } + + public static function uninstallRights(): void + { + global $DB; + + $DB->delete('glpi_profilerights', [ + 'name' => self::$rightname, + ]); + + \ProfileRight::deleteProfileRights([self::$rightname]); + } + + public static function registerRights(): void + { + global $DB; + + $iterator = $DB->request([ + 'FROM' => 'glpi_profilerights', + 'WHERE' => ['name' => self::$rightname], + 'LIMIT' => 1, + ]); + + if (count($iterator) > 0) { + return; + } + + \ProfileRight::addProfileRights([self::$rightname]); + } + + public static function setProfileRights(int $profiles_id, int $rights): void + { + global $DB; + + if ($profiles_id <= 0) { + return; + } + + $iterator = $DB->request([ + 'FROM' => 'glpi_profilerights', + 'WHERE' => [ + 'profiles_id' => $profiles_id, + 'name' => self::$rightname, + ], + 'LIMIT' => 1, + ]); + + if (count($iterator) > 0) { + $row = $iterator->current(); + $DB->update( + 'glpi_profilerights', + ['rights' => $rights], + ['id' => (int) $row['id']] + ); + return; + } + + $DB->insert('glpi_profilerights', [ + 'profiles_id' => $profiles_id, + 'name' => self::$rightname, + 'rights' => $rights, + ]); + } + + public static function getProfileRights(int $profiles_id): int + { + global $DB; + + if ($profiles_id <= 0) { + return 0; + } + + $iterator = $DB->request([ + 'FROM' => 'glpi_profilerights', + 'WHERE' => [ + 'profiles_id' => $profiles_id, + 'name' => self::$rightname, + ], + 'LIMIT' => 1, + ]); + + if (count($iterator) === 0) { + return 0; + } + + $row = $iterator->current(); + + return (int) ($row['rights'] ?? 0); + } + + public static function canCurrentUser(int $right): bool + { + $profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0); + + if ($profiles_id === 0) { + $user_id = (int) ($_SESSION['glpiID'] ?? 0); + if ($user_id > 0) { + global $DB; + $iterator = $DB->request([ + 'SELECT' => 'profiles_id', + 'FROM' => 'glpi_profiles_users', + 'WHERE' => [ + 'users_id' => $user_id, + 'is_dynamic' => 0, + ], + 'LIMIT' => 1, + ]); + if (count($iterator) > 0) { + $row = $iterator->current(); + $profiles_id = (int) $row['profiles_id']; + } + } + } + + if ($profiles_id > 0) { + $rights = self::getProfileRights($profiles_id); + if (($rights & $right) === $right) { + return true; + } + } + + return (bool) Session::haveRight(self::$rightname, $right); + } + + public static function initProfile($profile = null): void + { + $profile_id = 0; + + if ($profile instanceof \Profile) { + $profile_id = $profile->getID(); + } elseif (is_array($profile) && isset($profile['id'])) { + $profile_id = (int) $profile['id']; + } + + if ($profile_id > 0) { + $current_rights = self::getProfileRights($profile_id); + if ($current_rights === 0) { + self::setProfileRights($profile_id, READ); + } + } + } +} \ No newline at end of file diff --git a/src/Server.php b/src/Server.php new file mode 100644 index 0000000..d154409 --- /dev/null +++ b/src/Server.php @@ -0,0 +1,942 @@ +> + */ + public function getRights($interface = 'central'): array + { + return [ + READ => [ + 'short' => __('Read'), + 'long' => __('Read'), + ], + UPDATE => [ + 'short' => __('Update'), + 'long' => __('Update'), + ], + CREATE => [ + 'short' => __('Create'), + 'long' => __('Create'), + ], + DELETE => [ + 'short' => __('Delete'), + 'long' => __('Delete'), + ], + PURGE => [ + 'short' => __('Purge'), + 'long' => __('Purge'), + ], + ]; + } + + /** + * Check view right. + * + * @return bool + */ + public static function canView(): bool + { + return Profile::canCurrentUser(READ); + } + + /** + * Check create right. + * + * @return bool + */ + public static function canCreate(): bool + { + return Profile::canCurrentUser(CREATE); + } + + /** + * Check update right. + * + * @return bool + */ + public static function canUpdate(): bool + { + return Profile::canCurrentUser(UPDATE); + } + + /** + * Check delete right. + * + * @return bool + */ + public static function canDelete(): bool + { + return Profile::canCurrentUser(DELETE); + } + + /** + * Check purge right. + * + * @return bool + */ + public static function canPurge(): bool + { + return Profile::canCurrentUser(PURGE); + } + + /** + * Get menu name. + * + * @return string + */ + public static function getMenuName(): string + { + return self::getTypeName(Session::getPluralNumber()); + } + + /** + * Get menu content. + * + * @return array + */ + public static function getMenuContent(): array + { + $menu = []; + + if (self::canView()) { + $menu['title'] = self::getMenuName(); + $menu['page'] = self::getSearchURL(false); + $menu['icon'] = 'ti ti-server'; + + $menu['links']['search'] = self::getSearchURL(false); + + if (self::canCreate()) { + $menu['links']['add'] = self::getFormURL(false); + } + } + + return $menu; + } + + /** + * Define tabs. + * + * @param array $options Options + * + * @return array + */ + public function defineTabs($options = []): array + { + $ong = []; + + $this->addDefaultFormTab($ong); + $this->addStandardTab(ServerAsset::class, $ong, $options); + $this->addStandardTab('Log', $ong, $options); + + return $ong; + } + + /** + * Search options. + * + * @return array> + */ + public function rawSearchOptions(): array + { + $tab = []; + + $tab[] = [ + 'id' => 'common', + 'name' => __('Characteristics'), + ]; + + $tab[] = [ + 'id' => 1, + 'table' => self::getTable(), + 'field' => 'name', + 'name' => __('Name'), + 'datatype' => 'itemlink', + 'massiveaction' => false, + ]; + + $tab[] = [ + 'id' => 2, + 'table' => self::getTable(), + 'field' => 'ip_address', + 'name' => __('IP address', 'urbackup'), + 'datatype' => 'string', + ]; + + $tab[] = [ + 'id' => 3, + 'table' => self::getTable(), + 'field' => 'port', + 'name' => __('Network port', 'urbackup'), + 'datatype' => 'integer', + ]; + + $tab[] = [ + 'id' => 4, + 'table' => self::getTable(), + 'field' => 'protocol', + 'name' => __('Protocol', 'urbackup'), + 'datatype' => 'string', + ]; + + $tab[] = [ + 'id' => 5, + 'table' => self::getTable(), + 'field' => 'server_version', + 'name' => __('UrBackup server version', 'urbackup'), + 'datatype' => 'string', + ]; + + $tab[] = [ + 'id' => 6, + 'table' => Entity::getTable(), + 'field' => 'completename', + 'name' => Entity::getTypeName(1), + 'datatype' => 'dropdown', + ]; + + $tab[] = [ + 'id' => 7, + 'table' => Location::getTable(), + 'field' => 'completename', + 'name' => Location::getTypeName(1), + 'datatype' => 'dropdown', + ]; + + $tab[] = [ + 'id' => 8, + 'table' => self::getTable(), + 'field' => 'is_active', + 'name' => __('Active'), + 'datatype' => 'bool', + ]; + + $tab[] = [ + 'id' => 9, + 'table' => self::getTable(), + 'field' => 'last_api_status', + 'name' => __('Last API status', 'urbackup'), + 'datatype' => 'bool', + ]; + + $tab[] = [ + 'id' => 10, + 'table' => self::getTable(), + 'field' => 'last_api_check', + 'name' => __('Last API check', 'urbackup'), + 'datatype' => 'datetime', + ]; + + $tab[] = [ + 'id' => 11, + 'table' => self::getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date'), + 'datatype' => 'datetime', + ]; + + $tab[] = [ + 'id' => 12, + 'table' => self::getTable(), + 'field' => 'date_mod', + 'name' => __('Last update'), + 'datatype' => 'datetime', + ]; + + $tab[] = [ + 'id' => 13, + 'table' => self::getTable(), + 'field' => 'id', + 'name' => __('View', 'urbackup'), + 'massiveaction' => false, + 'datatype' => 'raw', + 'searchtype' => 'view', + ]; + + return $tab; + } + + /** + * Show server form. + * + * @param int $ID ID + * @param array $options Options + * + * @return bool + */ + public function showForm($ID, array $options = []): bool + { + if ($ID > 0) { + $this->check($ID, READ); + } else { + $this->check(-1, CREATE); + $this->getEmpty(); + } + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + echo ""; + echo "" . htmlspecialchars(__('Name')) . ""; + echo ""; + echo Html::input('name', [ + 'value' => $this->fields['name'] ?? '', + 'size' => 40, + ]); + echo ""; + + echo "" . htmlspecialchars(__('Active')) . ""; + echo ""; + Dropdown::showYesNo('is_active', (int) ($this->fields['is_active'] ?? 1)); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(Entity::getTypeName(1)) . ""; + echo ""; + Entity::dropdown([ + 'name' => 'entities_id', + 'value' => (int) ($this->fields['entities_id'] ?? ($_SESSION['glpiactive_entity'] ?? 0)), + ]); + echo ""; + + echo "" . htmlspecialchars(__('Recursive')) . ""; + echo ""; + Dropdown::showYesNo('is_recursive', (int) ($this->fields['is_recursive'] ?? 0)); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(Location::getTypeName(1)) . ""; + echo ""; + Location::dropdown([ + 'name' => 'locations_id', + 'value' => (int) ($this->fields['locations_id'] ?? 0), + ]); + echo "
"; + echo htmlspecialchars( + __('Associate the server with the main/root location. Assets in sub-locations will use this root location server.', 'urbackup') + ); + echo ""; + echo ""; + + echo "" . htmlspecialchars(__('Protocol', 'urbackup')) . ""; + echo ""; + Dropdown::showFromArray( + 'protocol', + [ + 'http' => 'HTTP', + 'https' => 'HTTPS', + ], + [ + 'value' => $this->fields['protocol'] ?? 'http', + ] + ); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('IP address', 'urbackup')) . ""; + echo ""; + echo Html::input('ip_address', [ + 'value' => $this->fields['ip_address'] ?? '', + 'size' => 40, + ]); + echo ""; + + echo "" . htmlspecialchars(__('Network port', 'urbackup')) . ""; + echo ""; + echo Html::input('port', [ + 'value' => $this->fields['port'] ?? 55414, + 'type' => 'number', + 'min' => 1, + 'max' => 65535, + ]); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . ""; + echo ""; + echo Html::input('server_version', [ + 'value' => $this->fields['server_version'] ?? '', + 'size' => 30, + ]); + echo ""; + + echo "" . htmlspecialchars(__('Ignore SSL verification', 'urbackup')) . ""; + echo ""; + Dropdown::showYesNo('ignore_ssl', (int) ($this->fields['ignore_ssl'] ?? 0)); + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('API username', 'urbackup')) . ""; + echo ""; + echo Html::input('api_username', [ + 'value' => $this->fields['api_username'] ?? '', + 'size' => 40, + 'autocomplete' => 'off', + ]); + echo ""; + + echo "" . htmlspecialchars(__('API password', 'urbackup')) . ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo "" . htmlspecialchars(__('Comments')) . ""; + echo ""; + echo ""; + echo ""; + echo ""; + + if ($ID > 0) { + echo ""; + echo "" . htmlspecialchars(__('UrBackup web interface', 'urbackup')) . ""; + echo ""; + + $url = $this->getWebInterfaceUrl(); + + if ($url !== '#') { + echo ""; + echo htmlspecialchars(__('Open UrBackup interface', 'urbackup')); + echo ""; + } else { + echo htmlspecialchars(__('No URL available', 'urbackup')); + } + + echo ""; + echo ""; + +$apiStatus = (int) ($this->fields['last_api_status'] ?? 0); + echo ""; + echo "" . htmlspecialchars(__('API connection status', 'urbackup')) . "
" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . ""; + echo ""; + if ($apiStatus === 1) { + echo ' ' . htmlspecialchars(__('API connection OK', 'urbackup')) . ''; + } else { + echo ' ' . htmlspecialchars(__('API connection failed', 'urbackup')) . ''; + if (!empty($this->fields['last_api_message'])) { + echo '
' . htmlspecialchars((string) $this->fields['last_api_message']) . ''; + } + } + echo ""; + echo ""; + } + + $this->showFormButtons($options); + + return true; + } + + /** + * Get web interface URL. + * + * @return string + */ + public function getWebInterfaceUrl(): string + { + $protocol = (string) ($this->fields['protocol'] ?? 'http'); + $ip = (string) ($this->fields['ip_address'] ?? ''); + $port = (int) ($this->fields['port'] ?? 55414); + + if ($ip === '') { + return '#'; + } + + return sprintf('%s://%s:%d', $protocol, $ip, $port); + } + + /** + * Get active servers assigned to a root location. + * + * @param int $locations_id Root location ID + * + * @return array + */ + public static function getActiveServersForRootLocation(int $locations_id): array + { + global $DB; + + $servers = []; + + if ($locations_id <= 0) { + return $servers; + } + + if (!$DB->tableExists(self::getTable())) { + return $servers; + } + + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'locations_id' => $locations_id, + 'is_active' => 1, + ], + 'ORDER' => 'name', + ]); + + foreach ($iterator as $row) { + $servers[(int) $row['id']] = (string) $row['name']; + } + + return $servers; + } + + public function prepareInputForAdd(mixed $input): mixed + { + return $this->prepareInputForUpdate($input); + } + + public function prepareInputForUpdate(mixed $input): mixed + { + if (!is_array($input)) { + return $input; + } + + if (!empty($input['id']) && (int) $input['id'] > 0) { + $server = new self(); + if ($server->getFromDB((int) $input['id'])) { + $serverFields = $server->fields; + $ip = $input['ip_address'] ?? $serverFields['ip_address'] ?? ''; + $port = $input['port'] ?? $serverFields['port'] ?? 55414; + $protocol = $input['protocol'] ?? $serverFields['protocol'] ?? 'http'; + $apiUsername = $input['api_username'] ?? $serverFields['api_username'] ?? ''; + $apiPassword = $input['api_password'] ?? $serverFields['api_password'] ?? ''; + $ignoreSsl = $input['ignore_ssl'] ?? $serverFields['ignore_ssl'] ?? 0; + + if ($ip !== '') { + $tmpServer = new self(); + $tmpServer->fields = [ + 'id' => (int) $input['id'], + 'ip_address' => $ip, + 'port' => $port, + 'protocol' => $protocol, + 'api_username' => $apiUsername, + 'api_password' => $apiPassword, + 'ignore_ssl' => $ignoreSsl, + ]; + + try { + $client = new UrbackupApiClient($tmpServer); + $result = $client->testConnection(); + $input['last_api_status'] = $result['success'] ? 1 : 0; + $input['last_api_message'] = $result['message'] ?? ''; + $input['last_api_check'] = date('Y-m-d H:i:s'); + } catch (\Throwable $e) { + $input['last_api_status'] = 0; + $input['last_api_message'] = $e->getMessage(); + $input['last_api_check'] = date('Y-m-d H:i:s'); + } + } + } + } + + return $input; + } + + public function testApiConnection(): array + { + $ip = (string) ($this->fields['ip_address'] ?? ''); + + if ($ip === '') { + return [ + 'status' => 'no_ip', + 'html' => '' . htmlspecialchars(__('No IP address configured', 'urbackup')) . '', + ]; + } + + try { + $client = new UrbackupApiClient($this); + $result = $client->testConnection(); + + $this->update([ + 'id' => (int) $this->fields['id'], + 'last_api_status' => $result['success'] ? 1 : 0, + 'last_api_message' => $result['message'] ?? '', + 'last_api_check' => date('Y-m-d H:i:s'), + ]); + + if ($result['success']) { + return [ + 'status' => 'ok', + 'html' => ' ' . + htmlspecialchars(__('API connection OK', 'urbackup')) . '', + ]; + } + + return [ + 'status' => 'failed', + 'html' => ' ' . + htmlspecialchars(__('API connection failed', 'urbackup')) . '
' . + '' . htmlspecialchars($result['message'] ?? '') . '', + ]; + } catch (\Throwable $e) { + $message = $e->getMessage(); + $isUnreachable = $this->isNetworkError($message); + + return [ + 'status' => $isUnreachable ? 'unreachable' : 'failed', + 'html' => '' . + ' ' . + htmlspecialchars($isUnreachable ? __('Server unreachable', 'urbackup') : __('API connection failed', 'urbackup')) . + '
' . + '' . htmlspecialchars($message) . '', + ]; + } + } + + private function isNetworkError(string $message): bool + { + $networkKeywords = [ + 'timeout', + 'could not resolve host', + 'couldn\'t connect to host', + 'connection refused', + 'connection timed out', + 'network is unreachable', + 'no route to host', + 'ssl', + 'certificate', + 'curl error', + 'request failed', + 'returned HTTP status', + 'returned non-JSON response', + 'problem with the ssl certificate', + 'ssl certificate problem', + 'ssl connect error', + 'ssl wrong version', + ]; + + $lowerMessage = strtolower($message); + + foreach ($networkKeywords as $keyword) { + if (str_contains($lowerMessage, strtolower($keyword))) { + return true; + } + } + + if (preg_match('/http status [45]\d{2}/', $lowerMessage)) { + return true; + } + + return false; + } + + private static function renderOnlineBadge(mixed $online, mixed $statusString): string + { + $online = match (true) { + $online === true || $online === 1 || $online === '1' || $online === 'true' => 1, + $online === false || $online === 0 || $online === '0' || $online === 'false' => 0, + default => null, + }; + + $parts = []; + + if ($online === 1) { + $parts[] = ' ' + . htmlspecialchars(__('Online', 'urbackup')) . ''; + } elseif ($online === 0) { + $parts[] = ' ' + . htmlspecialchars(__('Offline', 'urbackup')) . ''; + } else { + $parts[] = '-'; + } + + $statusString = is_string($statusString) ? trim((string) $statusString) : ''; + if ($statusString !== '') { + $badgeClass = 'secondary'; + if (strtolower($statusString) === 'ok') { + $badgeClass = 'success'; + } elseif (in_array(strtolower($statusString), ['minor_problems', 'minor problems'], true)) { + $badgeClass = 'warning'; + } elseif (in_array(strtolower($statusString), ['major_problems', 'major problems', 'error'], true)) { + $badgeClass = 'danger'; + } elseif (strtolower($statusString) === 'paused') { + $badgeClass = 'secondary'; + } + $parts[] = '' + . htmlspecialchars($statusString) . ''; + } + + return implode(' ', $parts); + } + + private static function formatLastBackup(mixed $value): string + { + if ($value === null || $value === '' || $value === 0 || $value === '0') { + return ''; + } + + if (is_numeric($value)) { + $timestamp = (int) $value; + if ($timestamp > 0 && $timestamp < 2000000000) { + return date('Y-m-d H:i:s', $timestamp); + } + return (string) $value; + } + + return (string) $value; + } + + public static function showLinkedClientsTab(Server $server): void + { + global $DB; + + $apiStatus = (int) ($server->fields['last_api_status'] ?? 0); + + if ($apiStatus !== 1) { + echo '
'; + echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup')); + echo '
'; + return; + } + + $iterator = $DB->request([ + 'FROM' => 'glpi_plugin_urbackup_serverassets', + 'WHERE' => [ + 'plugin_urbackup_servers_id' => (int) $server->fields['id'], + ], + ]); + + $linkedAssets = []; + foreach ($iterator as $row) { + $linkedAssets[] = $row; + } + + if (count($linkedAssets) === 0) { + echo '
'; + echo htmlspecialchars(__('No linked assets', 'urbackup')); + echo '
'; + return; + } + + try { + $client = new UrbackupApiClient($server); + $urbackupClients = $client->getStatus(); + + if (empty($urbackupClients)) { + echo '
'; + echo htmlspecialchars(__('No clients found on UrBackup server', 'urbackup')); + echo '
'; + return; + } + } catch (\Throwable $e) { + echo '
'; + echo 'API Error: ' . htmlspecialchars($e->getMessage()); + echo '
'; + return; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($linkedAssets as $link) { + $glpiItem = null; + if (!empty($link['itemtype']) && !empty($link['items_id'])) { + $itemClass = $link['itemtype']; + if (class_exists($itemClass)) { + $glpiItem = new $itemClass(); + $glpiItem->getFromDB((int) $link['items_id']); + } + } + + $clientName = $glpiItem ? ($glpiItem->fields['name'] ?? '') : ''; + $urbackupClient = null; + + foreach ($urbackupClients as $uc) { + $ucName = (string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? ''); + if (strcasecmp($ucName, $clientName) === 0) { + $urbackupClient = $uc; + break; + } + } + + $online = $urbackupClient ? ($urbackupClient['online'] ?? null) : null; + $apiStatusString = $urbackupClient ? ($urbackupClient['status'] ?? '') : ''; + $statusHtml = self::renderOnlineBadge($online, $apiStatusString); + + $lastBackupRaw = $urbackupClient + ? ($urbackupClient['lastbackup'] ?? $urbackupClient['lastbackup_time'] ?? $urbackupClient['last_backup'] ?? $urbackupClient['last_backup_time'] ?? $urbackupClient['file_lastbackup'] ?? $urbackupClient['image_lastbackup'] ?? '') + : ''; + $lastBackup = self::formatLastBackup($lastBackupRaw); + + $clientIp = $urbackupClient + ? ($urbackupClient['client_ip'] ?? $urbackupClient['ip'] ?? $urbackupClient['ip_address'] ?? $urbackupClient['clientaddress'] ?? '') + : ''; + + $clientVersion = $urbackupClient ? ($urbackupClient['client_version_string'] ?? $urbackupClient['client_version'] ?? '-') : '-'; + $urbackupClientName = $urbackupClient ? ($urbackupClient['name'] ?? '-') : '-'; + + $itemTypeLabel = !empty($link['itemtype']) ? (class_exists($link['itemtype']) ? $link['itemtype']::getTypeName(1) : $link['itemtype']) : ''; + $itemUrl = $glpiItem ? $glpiItem->getLinkURL() : ''; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
' . htmlspecialchars(__('Asset', 'urbackup')) . '' . htmlspecialchars(__('Name', 'urbackup')) . '' . htmlspecialchars(__('Client name', 'urbackup')) . '' . htmlspecialchars(__('Version', 'urbackup')) . '' . htmlspecialchars(__('Status', 'urbackup')) . '' . htmlspecialchars(__('Last backup', 'urbackup')) . '' . htmlspecialchars(__('IP address', 'urbackup')) . '
' . htmlspecialchars($itemTypeLabel . ' #' . $link['items_id']) . '' . ($itemUrl ? '' . htmlspecialchars($clientName) . '' : htmlspecialchars($clientName)) . '' . htmlspecialchars($urbackupClientName) . '' . htmlspecialchars($clientVersion) . '' . $statusHtml . '' . htmlspecialchars($lastBackup ?: '-') . '' . htmlspecialchars($clientIp ?: '-') . '
'; + } + + public static function showUnlinkedClientsTab(Server $server): void + { + $apiStatus = (int) ($server->fields['last_api_status'] ?? 0); + + if ($apiStatus !== 1) { + echo '
'; + echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup')); + echo '
'; + return; + } + + global $DB; + + $iterator = $DB->request([ + 'FROM' => 'glpi_plugin_urbackup_serverassets', + ]); + + $linkedNames = []; + foreach ($iterator as $row) { + $assetName = ServerAsset::getAssetName($row['itemtype'], (int) $row['items_id']); + if ($assetName !== '') { + $linkedNames[] = strtolower($assetName); + } + } + + try { + $client = new UrbackupApiClient($server); + $urbackupClients = $client->getStatus(); + } catch (\Throwable $e) { + echo '
'; + echo htmlspecialchars($e->getMessage()); + echo '
'; + return; + } + + $unlinkedClients = []; + foreach ($urbackupClients as $uc) { + $name = strtolower((string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? '')); + if (!in_array($name, $linkedNames, true)) { + $unlinkedClients[] = $uc; + } + } + + if (count($unlinkedClients) === 0) { + echo '
'; + echo htmlspecialchars(__('No unlinked clients found on UrBackup server', 'urbackup')); + echo '
'; + return; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($unlinkedClients as $uc) { + $online = $uc['online'] ?? null; + $apiStatusString = $uc['status'] ?? ''; + $statusHtml = self::renderOnlineBadge($online, $apiStatusString); + + $lastBackupRaw = $uc['lastbackup'] ?? $uc['lastbackup_time'] ?? $uc['last_backup'] ?? $uc['last_backup_time'] ?? $uc['file_lastbackup'] ?? $uc['image_lastbackup'] ?? ''; + $lastBackup = self::formatLastBackup($lastBackupRaw); + + $clientIp = $uc['client_ip'] ?? $uc['ip'] ?? $uc['ip_address'] ?? $uc['clientaddress'] ?? ''; + $clientVersion = $uc['client_version_string'] ?? $uc['client_version'] ?? '-'; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
' . htmlspecialchars(__('Name', 'urbackup')) . '' . htmlspecialchars(__('Version', 'urbackup')) . '' . htmlspecialchars(__('Status', 'urbackup')) . '' . htmlspecialchars(__('Last backup', 'urbackup')) . '' . htmlspecialchars(__('IP address', 'urbackup')) . '
' . htmlspecialchars((string) ($uc['name'] ?? 'Unknown')) . '' . htmlspecialchars($clientVersion) . '' . $statusHtml . '' . htmlspecialchars($lastBackup ?: '-') . '' . htmlspecialchars($clientIp ?: '-') . '
'; + } +} \ No newline at end of file diff --git a/src/ServerAsset.php b/src/ServerAsset.php new file mode 100644 index 0000000..56eb4d8 --- /dev/null +++ b/src/ServerAsset.php @@ -0,0 +1,291 @@ +getFromDB($items_id)) { + return false; + } + + $table = self::getTable(); + $date = $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'); + + $existing = self::getLinkForAsset($itemtype, $items_id, false); + + if ($existing !== null) { + return $DB->update( + $table, + [ + 'plugin_urbackup_servers_id' => $server_id, + 'client_name' => (string) ($item->fields['name'] ?? ''), + 'client_ip' => self::extractAssetIp($item), + 'is_active' => 1, + 'date_mod' => $date, + ], + [ + 'id' => (int) $existing['id'], + ] + ); + } + + return $DB->insert( + $table, + [ + 'plugin_urbackup_servers_id' => $server_id, + 'itemtype' => $itemtype, + 'items_id' => $items_id, + ] + ); + } + + /** + * Disconnect asset from server. + * + * @param string $itemtype Itemtype + * @param int $items_id Item ID + * + * @return bool + */ + public static function disconnectAsset(string $itemtype, int $items_id): bool + { + global $DB; + + if (!Profile::canCurrentUser(UPDATE)) { + return false; + } + + $link = self::getLinkForAsset($itemtype, $items_id, true); + + if ($link === null) { + return true; + } + + return $DB->delete( + self::getTable(), + [ + 'id' => (int) $link['id'], + ] + ); + } + + /** + * Get active link for asset. + * + * @param string $itemtype Itemtype + * @param int $items_id Item ID + * @param bool $active_only Active only + * + * @return array|null + */ + public static function createForAsset( + string $itemtype, + int $items_id, + int $servers_id + ): bool { + global $DB; + + $item = getItemForItemtype($itemtype); + if (!$item || !$item->getFromDB($items_id)) { + return false; + } + + $result = $DB->insert(self::getTable(), [ + 'plugin_urbackup_servers_id' => $servers_id, + 'itemtype' => $itemtype, + 'items_id' => $items_id, + ]); + + return $result; + } + + public static function getAssetName(string $itemtype, int $items_id): string + { + $item = getItemForItemtype($itemtype); + if (!$item || !$item->getFromDB($items_id)) { + return ''; + } + + return (string) ($item->fields['name'] ?? ''); + } + + public static function getLinkForAsset( + string $itemtype, + int $items_id, + bool $active_only = true + ): ?array { + global $DB; + + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype' => $itemtype, + 'items_id' => $items_id, + ], + 'LIMIT' => 1, + ]); + + if (count($iterator) === 0) { + return null; + } + + return $iterator->current(); + } + + /** + * Extract asset IP address. + * + * @param CommonDBTM $item Item + * + * @return string + */ + public static function extractAssetIp(CommonDBTM $item): string + { + if (!isset($item->fields['ip_address']) || $item->fields['ip_address'] === '') { + return ''; + } + + $ip = $item->fields['ip_address']; + + if (is_array($ip)) { + return $ip[0] ?? ''; + } + + return (string) $ip; + } + +/** + * Display tab content for item. + * + * @param CommonGLPI $item Server item + * @param int $tabnum Tab number + * @param int $withtemplate Template + * + * @return bool + */ + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool + { + global $DB; + + if (!$item instanceof Server) { + return false; + } + + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'plugin_urbackup_servers_id' => (int) $item->fields['id'], + 'is_active' => 1, + ], + 'ORDER' => 'client_name', + ]); + + foreach ($iterator as $row) { + $asset_label = (string) $row['client_name']; + $itemtype = (string) $row['itemtype']; + $items_id = (int) $row['items_id']; + + if (class_exists($itemtype)) { + $asset = new $itemtype(); + + if ($asset instanceof CommonDBTM && $asset->getFromDB($items_id)) { + $asset_label = $asset->getLink(); + } + } + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + + echo "
" . htmlspecialchars(__('Linked assets', 'urbackup')) . "
" . htmlspecialchars(__('Asset', 'urbackup')) . "" . htmlspecialchars(__('Type')) . "" . htmlspecialchars(__('IP address', 'urbackup')) . "" . htmlspecialchars(__('Last file backup', 'urbackup')) . "" . htmlspecialchars(__('Last image backup', 'urbackup')) . "
" . $asset_label . "" . htmlspecialchars($itemtype) . "" . htmlspecialchars((string) $row['client_ip']) . "" . htmlspecialchars((string) $row['last_file_backup']) . "" . htmlspecialchars((string) $row['last_image_backup']) . "
"; + echo "
"; + + return true; + } +} \ No newline at end of file diff --git a/src/UrbackupApiClient.php b/src/UrbackupApiClient.php new file mode 100644 index 0000000..f6727ed --- /dev/null +++ b/src/UrbackupApiClient.php @@ -0,0 +1,766 @@ +server = $server; + $this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x'; + $this->username = (string) ($server->fields['api_username'] ?? ''); + $this->password = (string) ($server->fields['api_password'] ?? ''); + $this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1; + $this->server_version = (string) ($server->fields['server_version'] ?? ''); + $this->is_version_2_5_or_higher = $this->detectVersion2_5OrHigher(); + } + + /** + * Detect if server version is 2.5 or higher. + * + * @return bool + */ + private function detectVersion2_5OrHigher(): bool + { + if ($this->server_version === '') { + return false; + } + + $parts = explode('.', $this->server_version); + if (count($parts) < 2) { + return false; + } + + $major = (int) $parts[0]; + $minor = (int) $parts[1]; + + if ($major > 2) { + return true; + } + if ($major === 2 && $minor >= 5) { + return true; + } + + return false; + } + + /** + * Get internet mode setting key based on server version. + * + * @return string + */ + public function getInternetModeSettingKey(): string + { + return $this->is_version_2_5_or_higher ? 'internet_mode_enabled' : 'internet_mode'; + } + + /** + * Extract setting value from 2.5+ structured format or simple value. + * + * @param mixed $setting Setting value + * @param mixed $default Default value + * + * @return mixed + */ + public function extractSettingValue(mixed $setting, mixed $default = null): mixed + { + if (is_array($setting) && array_key_exists('value', $setting)) { + return $setting['value']; + } + + return $setting ?? $default; + } + + /** + * Test API connection. + * + * @return array + */ + public function testConnection(): array + { + try { + $this->login(); + $identity = $this->getServerIdentity(); + + return [ + 'success' => true, + 'message' => $identity !== '' + ? sprintf(__('Connection successful. Server identity: %s', 'urbackup'), $identity) + : __('Connection successful.', 'urbackup'), + 'identity' => $identity, + ]; + } catch (RuntimeException $e) { + return [ + 'success' => false, + 'message' => $e->getMessage(), + 'identity' => '', + ]; + } + } + + /** + * Login to UrBackup web API. + * + * @return bool + */ + public function login(): bool + { + if ($this->logged_in) { + return true; + } + + $login = $this->request('login', [], 'POST', false); + + if (!$login || !isset($login['success']) || $login['success'] !== true) { + $salt = $this->request('salt', ['username' => $this->username], 'POST', false); + + if (!$salt || !isset($salt['ses']) || $salt['ses'] === '') { + if (isset($salt['error']) && $salt['error'] === 1) { + throw new RuntimeException(__('Username does not exist on UrBackup server.', 'urbackup')); + } + throw new RuntimeException(__('Unable to get salt from UrBackup server.', 'urbackup')); + } + + $this->session = (string) $salt['ses']; + + if (isset($salt['salt'])) { + $password_md5 = $this->buildPasswordHash($this->password, $salt); + + $login = $this->request('login', [ + 'username' => $this->username, + 'password' => $password_md5, + 'ses' => $this->session, + ], 'POST', false); + + if (!$login || !isset($login['success']) || $login['success'] !== true) { + throw new RuntimeException(__('Unable to authenticate against UrBackup server. Password may be wrong.', 'urbackup')); + } + + $this->logged_in = true; + return true; + } + + throw new RuntimeException(__('Salt response missing salt field.', 'urbackup')); + } + + $this->session = (string) ($login['session'] ?? ''); + $this->logged_in = true; + return true; + } + + /** + * Build password hash for authentication. + * + * @param string $password Plain password + * @param array $salt Salt response from server + * + * @return string + */ + private function buildPasswordHash(string $password, array $salt): string + { + $salt_str = (string) ($salt['salt'] ?? ''); + $rnd = (string) ($salt['rnd'] ?? ''); + $pbkdf2_rounds = (int) ($salt['pbkdf2_rounds'] ?? 0); + + $passwordMd5Bin = md5($salt_str . $password, true); + $passwordMd5 = bin2hex($passwordMd5Bin); + + if ($pbkdf2_rounds > 0 && function_exists('hash_pbkdf2')) { + $passwordMd5 = hash_pbkdf2( + 'sha256', + $passwordMd5Bin, + $salt_str, + $pbkdf2_rounds, + 0, + false + ); + } + + return md5($rnd . $passwordMd5); + } + + /** + * Get server identity. + * + * @return string + */ + public function getServerIdentity(): string + { + $data = $this->apiAction('server_identity'); + + if (isset($data['server_identity'])) { + return (string) $data['server_identity']; + } + + if (isset($data['identity'])) { + return (string) $data['identity']; + } + + if (isset($data['name'])) { + return (string) $data['name']; + } + + return ''; + } + + /** + * Get all client statuses. + * + * @return array> + */ + public function getStatus(): array + { + $data = $this->apiAction('status'); + + if (isset($data['status']) && is_array($data['status'])) { + return array_values($data['status']); + } + + if (isset($data['clients']) && is_array($data['clients'])) { + return array_values($data['clients']); + } + + if (array_is_list($data)) { + return $data; + } + + return []; + } + + /** + * Get client status by GLPI asset name. + * + * @param string $client_name Client name + * + * @return array|null + */ + public function getClientStatusByName(string $client_name): ?array + { + foreach ($this->getStatus() as $client) { + $name = (string) ($client['name'] ?? $client['clientname'] ?? $client['hostname'] ?? ''); + + if (strcasecmp($name, $client_name) === 0) { + return $client; + } + } + + return null; + } + + /** + * Get client ID by name. + * + * @param string $client_name Client name + * + * @return int + */ + public function getClientIdByName(string $client_name): int + { + $client = $this->getClientStatusByName($client_name); + + if ($client === null) { + return 0; + } + + return (int) ($client['id'] ?? $client['clientid'] ?? $client['client_id'] ?? 0); + } + + /** + * Get client settings. + * + * @param string $client_name Client name + * + * @return array + */ + public function getClientSettings(string $client_name): array + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return []; + } + + $data = $this->apiAction('settings', [ + 'sa' => 'clientsettings', + 't_clientid' => $client_id, + ]); + + if (isset($data['settings']) && is_array($data['settings'])) { + return $data['settings']; + } + + return $data; + } + + /** + * Change a client setting. + * + * @param string $client_name Client name + * @param string $key Setting key + * @param mixed $value New value + * + * @return bool + */ + public function changeClientSetting(string $client_name, string $key, mixed $value): bool + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return false; + } + + $data = $this->apiAction('settings', [ + 'sa' => 'clientsettings_save', + 't_clientid' => $client_id, + 'overwrite' => 'true', + $key => (string) $value, + ]); + + return $this->responseIsSuccess($data); + } + + public function updateClientSettings(string $client_name, string $key, string $value): bool + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return false; + } + + $data = $this->apiAction('settings', [ + 'sa' => 'clientsettings_save', + 't_clientid' => $client_id, + 'overwrite' => 'true', + $key => $value, + ]); + + return $this->responseIsSuccess($data); + } + + public function saveInternetMode(string $client_name, bool $enabled): bool + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return false; + } + + $key = $this->getInternetModeSettingKey(); + + $params = [ + 'sa' => 'clientsettings_save', + 't_clientid' => $client_id, + 'overwrite' => 'true', + $key => $enabled ? '1' : '0', + ]; + + $data = $this->apiAction('settings', $params); + + return $this->responseIsSuccess($data); + } + + /** + * Get client internet authentication key. + * + * @param string $client_name Client name + * + * @return string + */ + public function getClientAuthKey(string $client_name): string + { + $settings = $this->getClientSettings($client_name); + + $raw = $settings['internet_authkey'] ?? $settings['internetAuthkey'] ?? ''; + + if (is_array($raw) && isset($raw['value'])) { + return (string) $raw['value']; + } + + if (is_array($raw)) { + return ''; + } + + return (string) $raw; + } + + /** + * Add a client to UrBackup server. + * + * @param string $client_name Client name + * + * @return bool + */ + public function addClient(string $client_name): bool + { + $data = $this->apiAction('add_client', [ + 'clientname' => $client_name, + ]); + + return $this->responseIsSuccess($data); + } + + /** + * Remove a client from UrBackup server. + * + * @param string $client_name Client name + * + * @return bool + */ + public function removeClient(string $client_name): bool + { + $client_id = $this->getClientIdByName($client_name); + + $payload = [ + 'clientname' => $client_name, + ]; + + if ($client_id > 0) { + $payload['clientid'] = $client_id; + } + + $data = $this->apiAction('remove_client', $payload); + + return $this->responseIsSuccess($data); + } + + /** + * Start incremental file backup. + * + * @param string $client_name Client name + * + * @return bool + */ + public function startIncrementalFileBackup(string $client_name): bool + { + return $this->startBackup($client_name, 'incr_file'); + } + + /** + * Start full file backup. + * + * @param string $client_name Client name + * + * @return bool + */ + public function startFullFileBackup(string $client_name): bool + { + return $this->startBackup($client_name, 'full_file'); + } + + /** + * Start incremental image backup. + * + * @param string $client_name Client name + * + * @return bool + */ + public function startIncrementalImageBackup(string $client_name): bool + { + return $this->startBackup($client_name, 'incr_image'); + } + + /** + * Start full image backup. + * + * @param string $client_name Client name + * + * @return bool + */ + public function startFullImageBackup(string $client_name): bool + { + return $this->startBackup($client_name, 'full_image'); + } + + /** + * Get recent backups. + * + * @param string $client_name Client name + * @param int $limit Limit + * + * @return array> + */ + public function getRecentBackups(string $client_name, int $limit = 40): array + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return []; + } + + $data = $this->apiAction('backups', [ + 'sa' => 'backups', + 'clientid' => $client_id, + ]); + + $rows = []; + + foreach (($data['backups'] ?? []) as $backup) { + if (is_array($backup)) { + $backup['backup_type'] = __('File backup', 'urbackup'); + $rows[] = $backup; + } + } + + foreach (($data['backup_images'] ?? []) as $backup) { + if (is_array($backup)) { + $backup['backup_type'] = __('Image backup', 'urbackup'); + $rows[] = $backup; + } + } + + usort($rows, static function (array $a, array $b): int { + $timeA = (int) ($a['time'] ?? $a['backuptime'] ?? $a['backup_time'] ?? $a['created'] ?? 0); + $timeB = (int) ($b['time'] ?? $b['backuptime'] ?? $b['backup_time'] ?? $b['created'] ?? 0); + return $timeB <=> $timeA; + }); + + return array_slice($rows, 0, $limit); + } + + /** + * Get client log rows. + * + * @param string $client_name Client name + * @param int $limit Limit + * + * @return array> + */ + public function getClientLogs(string $client_name, int $limit = 50): array + { + $client_id = $this->getClientIdByName($client_name); + + $data = $this->apiAction('livelog', [ + 'clientid' => $client_id, + 'lastid' => $this->lastlogid, + ]); + + $logs = []; + + foreach (($data['logdata'] ?? []) as $row) { + if (is_array($row)) { + $logs[] = $row; + } + } + + if (count($logs) > 0) { + $last_entry = end($logs); + $this->lastlogid = (int) ($last_entry['id'] ?? 0); + } + + return array_slice($logs, 0, $limit); + } + + /** + * Start backup command. + * + * @param string $client_name Client name + * @param string $type Backup type + * + * @return bool + */ + private function startBackup(string $client_name, string $type): bool + { + $client_id = $this->getClientIdByName($client_name); + + if ($client_id <= 0) { + return false; + } + + $data = $this->apiAction('start_backup', [ + 'start_client' => $client_id, + 'start_type' => $type, + ]); + + if (isset($data['result']) && is_array($data['result'])) { + foreach ($data['result'] as $result) { + if (isset($result['start_ok']) && $result['start_ok'] === true) { + return true; + } + } + } + + return $this->responseIsSuccess($data); + } + + /** + * Execute an authenticated API action. + * + * @param string $action Action name + * @param array $params Parameters + * + * @return array + */ + private function apiAction(string $action, array $params = []): array + { + if (!$this->logged_in) { + $this->login(); + } + + $params['ses'] = $this->session; + + return $this->request($action, $params, 'POST', true); + } + + /** + * Execute HTTP request. + * + * @param string $action API action + * @param array $params Parameters + * @param string $method HTTP method (GET/POST) + * @param bool $require_success Require successful HTTP + * + * @return array + */ + private function request(string $action, array $params, string $method = 'POST', bool $require_success = true): array + { + if (!function_exists('curl_init')) { + throw new RuntimeException(__('PHP cURL extension is required for UrBackup API.', 'urbackup')); + } + + $ch = curl_init(); + + if ($ch === false) { + throw new RuntimeException(__('Unable to initialize cURL.', 'urbackup')); + } + + $url = $this->base_url . '?a=' . urlencode($action); + + if ($method === 'GET') { + $url .= '&' . http_build_query($params); + } + + $options = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl, + CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', + ], + ]; + + if ($method === 'POST') { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = http_build_query($params); + } + + $options[CURLOPT_URL] = $url; + + curl_setopt_array($ch, $options); + + $raw = curl_exec($ch); + + if ($raw === false) { + $error = curl_error($ch); + curl_close($ch); + + throw new RuntimeException( + sprintf(__('UrBackup API request failed: %s', 'urbackup'), $error) + ); + } + + $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if ($require_success && ($http_code < 200 || $http_code >= 300)) { + throw new RuntimeException( + sprintf(__('UrBackup API returned HTTP status %d.', 'urbackup'), $http_code) + ); + } + + if ($raw === '' || $raw === 'null') { + return []; + } + + $decoded = json_decode($raw, true); + + if (!is_array($decoded)) { + if (str_starts_with(trim($raw), '<')) { + throw new RuntimeException( + sprintf(__('UrBackup API returned non-JSON response (HTML). Check server URL and authentication.', 'urbackup')) + ); + } + + return [ + 'raw' => $raw, + 'success' => $http_code >= 200 && $http_code < 300, + ]; + } + + return $decoded; + } + + /** + * Evaluate generic API success response. + * + * @param array $data Response + * + * @return bool + */ + private function responseIsSuccess(array $data): bool + { + if (isset($data['success']) && $data['success'] === true) { + return true; + } + + if (isset($data['ok']) && $data['ok'] === true) { + return true; + } + + if (isset($data['saved_ok']) && $data['saved_ok'] === true) { + return true; + } + + if (isset($data['result']) && $data['result'] === 'ok') { + return true; + } + + if (isset($data['start_ok']) && $data['start_ok'] === true) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/templates/asset/asset_tab.html.twig b/templates/asset/asset_tab.html.twig new file mode 100644 index 0000000..c3bc354 --- /dev/null +++ b/templates/asset/asset_tab.html.twig @@ -0,0 +1,77 @@ +
+ {% if not linked %} +
+ {{ __('No UrBackup server linked.', 'urbackup') }} +
+ + + + + + + + + + + + + {% if is_sub_location %} + + + + {% endif %} + {% if servers|length == 0 %} + + + + {% elseif can_connect %} + + + + + {% else %} + + + + {% endif %} +
{{ __('UrBackup server selection', 'urbackup') }}
{{ __('Asset location ID', 'urbackup') }}{{ asset_location_id }}
{{ __('Root location ID', 'urbackup') }}{{ root_location_id }}
+ {{ __('The asset is in a sub-location. The plugin will use the server assigned to the root location.', 'urbackup') }} +
+
+ {{ __('No UrBackup server available for the root location of this asset.', 'urbackup') }} +
+
{{ __('Available servers for root location', 'urbackup') }} + + + + + + + +
+ {{ __('A server is available, but you do not have permission to link this asset.', 'urbackup') }} +
+ {% else %} + + + + + + + + + + + + + + + + +
{{ __('UrBackup status', 'urbackup') }}
{{ __('Linked server', 'urbackup') }}{{ server_link }}{{ __('IP address', 'urbackup') }}{{ server_ip }}
{{ __('UrBackup server version', 'urbackup') }}{{ server_version }}{{ __('Client name', 'urbackup') }}{{ client_name }}
+ {% endif %} +
diff --git a/templates/config/config.html.twig b/templates/config/config.html.twig new file mode 100644 index 0000000..a3011e8 --- /dev/null +++ b/templates/config/config.html.twig @@ -0,0 +1,39 @@ +
+
+ + + + + + + + + + {% for itemtype, label in configurable_types %} + + + {% if itemtype == 'Computer' %} + + {% else %} + + {% endif %} + + + {% endfor %} + + + +
{{ __('UrBackup configuration', 'urbackup') }}
{{ __('Asset type', 'urbackup') }}{{ __('Enabled', 'urbackup') }}{{ __('Default', 'urbackup') }}
{{ label }}{{ __('Always enabled', 'urbackup') }} + + + {% if itemtype == 'Computer' %} + {{ __('Yes', 'urbackup') }} + {% endif %} +
+ +
+
+ +
diff --git a/templates/profile.html.twig b/templates/profile.html.twig new file mode 100644 index 0000000..18a77d8 --- /dev/null +++ b/templates/profile.html.twig @@ -0,0 +1,34 @@ +{# + # UrBackup plugin for GLPI + # ------------------------------------------------------------------------- + # LICENSE + # + # This file is part of UrBackup. + # + # UrBackup is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # UrBackup is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with UrBackup. If not, see . + # ------------------------------------------------------------------------- + #} + +
+
+ {% do profile.displayRightsChoiceMatrix(rights, {'title': title}) %} + +
+ + {{ call('Html::submit', [_x('button', 'Save'), {'name': 'update'}])|raw }} +
+ + +
+
\ No newline at end of file diff --git a/templates/server/server_form.html.twig b/templates/server/server_form.html.twig new file mode 100644 index 0000000..14e1027 --- /dev/null +++ b/templates/server/server_form.html.twig @@ -0,0 +1,118 @@ +{% extends 'layout.html.twig' %} + +{% block content %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if server.id > 0 %} + + + + + + + + + {% endif %} +
{{ __('UrBackup server', 'urbackup') }}
{{ __('Name') }} + + {{ __('Active') }} + +
{{ __('Entity') }} + {{ render_entity_dropdown('entities_id', server.entities_id ?? 0) }} + {{ __('Recursive') }} + +
{{ __('Location') }} + {{ render_location_dropdown('locations_id', server.locations_id ?? 0) }} +
{{ __('Associate the server with the main/root location. Assets in sub-locations will use this root location server.', 'urbackup') }} +
{{ __('Protocol', 'urbackup') }} + +
{{ __('IP address', 'urbackup') }} + + {{ __('Network port', 'urbackup') }} + +
{{ __('UrBackup server version', 'urbackup') }} + + {{ __('Ignore SSL verification', 'urbackup') }} + +
{{ __('API username', 'urbackup') }} + + {{ __('API password', 'urbackup') }} + +
{{ __('Comments') }} + +
{{ __('UrBackup web interface', 'urbackup') }} + {% if server.url %} + + {{ __('Open UrBackup interface', 'urbackup') }} + + {% else %} + {{ __('No URL available', 'urbackup') }} + {% endif %} +
{{ __('API test', 'urbackup') }} + + +
+ + + +
+
+{% endblock %} diff --git a/templates/server/server_list.html.twig b/templates/server/server_list.html.twig new file mode 100644 index 0000000..f00e847 --- /dev/null +++ b/templates/server/server_list.html.twig @@ -0,0 +1,51 @@ +{% extends 'layout.html.twig' %} + +{% block content %} +
+ + + + + + + + + + + + + {% for server in servers %} + + + + + + + + + {% else %} + + + + {% endfor %} +
{{ __('UrBackup servers', 'urbackup') }}
{{ __('Name') }}{{ __('IP address', 'urbackup') }}{{ __('Port') }}{{ __('Version') }}{{ __('Status') }}{{ __('Actions') }}
{{ server.name }}{{ server.ip_address }}{{ server.port }}{{ server.server_version }} + {% if server.last_api_status %} + {{ __('OK', 'urbackup') }} + {% else %} + {{ __('Failed', 'urbackup') }} + {% endif %} + + + {{ __('View') }} + + +
{{ __('No records found', 'urbackup') }}
+ {% if can_create %} + + {{ __('Add') }} + + {% endif %} +
+{% endblock %}