411 lines
32 KiB
Markdown
411 lines
32 KiB
Markdown
# GLPIDEV.md — API GLPI 11.0.8 Reference (per plugin urbackup)
|
|
|
|
> **DA LEGGERE ALL'INIZIO DI OGNI SESSIONE DI LAVORO**, insieme a SKILL.md e MEMORY.md.
|
|
> File riassuntivo dell'API GLPI 11 usata dal plugin, generato analizzando il core reale in `/var/www/glpi` (versione **11.0.8**).
|
|
> Verificato su: `/var/www/glpi/src`, `/var/www/glpi/inc`, `/var/www/glpi/plugins/urbackup`.
|
|
|
|
---
|
|
|
|
## 1. Ambiente
|
|
|
|
| Voce | Valore |
|
|
|------|--------|
|
|
| GLPI | 11.0.8 (`/var/www/glpi`, definito in `src/autoload/constants.php`) |
|
|
| PHP | 8.3+ (installato: 8.4.23) — `declare(strict_types=1);` obbligatorio |
|
|
| Struttura | tutto il codice core in `src/` (PSR-4, namespace `Glpi\`); `inc/includes.php` bootstrap; `front/`, `ajax/`, `routes/` (Symfony routing), `templates/` Twig, `var/` cache/log |
|
|
| Plugin | `GlpiPlugin\Urbackup\` (PSR-4 via composer.json, PHP >= 8.3) |
|
|
| Versioni plugin | 0.7.3 (setup.php: `PLUGIN_URBACKUP_VERSION`; min GLPI 11.0.6, max 11.99.99) |
|
|
| Costanti plugin | `PLUGIN_URBACKUP_DIR` (cartella plugin), `PLUGIN_URBACKUP_WEB_DIR` (`Plugin::getWebDir('urbackup')`) |
|
|
|
|
**Regola chiave**: in GLPI 11 il codice procedurale e le classi legacy di GLPI 9/10 in `inc/` NON esistono più — le classi core sono in `src/` (es. `Glpi\...`). L'unica classe globale restante in `inc/` è `includes.php`.
|
|
|
|
---
|
|
|
|
## 2. Database Layer
|
|
|
|
### 2.1 `$DB` globale
|
|
- `global $DB;` — istanza `class DB extends DBmysql` (generata in `config/` da `DBConnection`).
|
|
- Sotto: mysqli. `$DB->update()` ritorna **sempre `true`** → per verificare l'esito usare `$DB->affectedRows()`.
|
|
- `$DB->runFile()` è DEPRECATO ma il plugin lo usa SOLO per la creazione dello schema iniziale in `install/install.php` (`plugin_urbackup_install_create_initial_schema()`, `install/mysql/plugin_urbackup-empty.sql`). MAI in upgrade/uninstall (lì si usa `$migration->dropTable()`).
|
|
|
|
### 2.2 Tabelle del plugin (schema v0.7.3)
|
|
```sql
|
|
glpi_plugin_urbackup_configs -- id, name, value, date_creation, date_mod (KEY name)
|
|
-- riga default in 0.7.2: name='enable_computer', value='1'
|
|
-- (toggle tab UrBackup su Computer, letto da Config::getEnableComputer())
|
|
glpi_plugin_urbackup_servers -- id, entities_id, is_recursive, name, locations_id, users_id,
|
|
-- ip_address, port (55414), protocol (http|https),
|
|
-- server_version, api_username, api_password (TEXT, CIFRATO con GLPIKey da 0.7.1),
|
|
-- ignore_ssl, is_active, last_api_status, last_api_message,
|
|
-- last_api_check, comment,
|
|
-- host_itemtype (VARCHAR NULL), host_items_id (INT UNSIGNED DEFAULT 0) -- 0.7.3: asset host
|
|
-- date_creation, date_mod
|
|
-- KEY name/entities_id/locations_id/users_id/is_active/location_active(locations_id,is_active)/host_asset(host_itemtype,host_items_id)
|
|
glpi_plugin_urbackup_serverassets-- id, plugin_urbackup_servers_id, itemtype, items_id,
|
|
-- date_creation, date_mod (KEY plugin_urbackup_servers_id, item(itemtype,items_id))
|
|
```
|
|
- Tabelle legacy DROPPED in 0.7.0: `glpi_plugin_urbackup_profiles`, `glpi_plugin_urbackup_assettypes`.
|
|
|
|
### 2.3 Query builder (lettura)
|
|
```php
|
|
$iterator = $DB->request([
|
|
'FROM' => Server::getTable(),
|
|
'WHERE' => [
|
|
'locations_id' => $locations_id,
|
|
'is_active' => 1,
|
|
],
|
|
'ORDER' => 'name',
|
|
]);
|
|
foreach ($iterator as $row) { ... } // iterazione diretta
|
|
$iterator->count(); // numero righe
|
|
$iterator->current(); // riga corrente
|
|
```
|
|
- `DBmysqlIterator` implementa `SeekableIterator, Countable`.
|
|
- **Mai concatenare variabili nelle query**: i criteri vengono parametrizzati dal builder.
|
|
- `COUNT` con `$DB->request()` è il pattern per i controlli di esistenza idempotenti (vedi `Profile::registerRights()`).
|
|
|
|
### 2.4 Scrittura
|
|
```php
|
|
$DB->insert($table, $params); // INSERT
|
|
$DB->update($table, $params, $where); // UPDATE (ritorna true sempre → affectedRows)
|
|
$DB->delete($table, $where); // DELETE
|
|
```
|
|
|
|
### 2.5 DDL e introspezione
|
|
```php
|
|
$DB->doQuery("ALTER TABLE ..."); // DDL — query() è DEPRECATO in GLPI 11
|
|
$DB->tableExists($tablename); // introspezione (cache)
|
|
$DB->fieldExists($table, $field);
|
|
$DB->listFields($table); // array colonne (['Field' => ...]) — usato in install.php
|
|
$DB->insertId();
|
|
$DB->affectedRows();
|
|
```
|
|
- **`$DB->query()` deprecato** → usare `$DB->doQuery()`.
|
|
|
|
### 2.6 Pattern di migrazione (`install/install.php`)
|
|
- `plugin_urbackup_install_process()`: crea `Migration(PLUGIN_URBACKUP_VERSION)`, esegue in ordine: schema iniziale (se mancano tabelle → `$DB->runFile()`), `update_configs_table`, `convert_assettypes_to_capacities`, `update_servers_table`, `update_serverassets_table`, `encrypt_api_passwords`, `add_enable_computer_config` (inserisce `('enable_computer','1')` se assente), `drop_legacy_tables`, poi `$migration->executeMigration()`; infine `Profile::installRights()`.
|
|
- Ogni `addField()`/`addKey()`/`dropField()`/`dropTable()` è idempotente e guardato da `tableExists()`/`fieldExists()`.
|
|
- `convert_assettypes_to_capacities()`: legge `glpi_plugin_urbackup_assettypes` (se esiste), auto-abilita la capacità `UrBackupCapacity` su TUTTI gli Asset Definition via `AssetDefinitionManager`, poi droppa la colonna `is_default`.
|
|
- **⚠️ CAVEAT verificato**: il codice chiama `$definition->hasCapacity(UrBackupCapacity::class)` e `$definition->enableCapacity(UrBackupCapacity::class)` (install.php:367-368) ma in GLPI 11.0.8 questi metodi NON esistono: su `AssetDefinition` ci sono solo `hasCapacityEnabled(CapacityInterface $capacity)` (richiede un oggetto), `getEnabledCapacities()`, `getCapacityConfiguration()` (AssetDefinition.php:627-652), e l'abilitazione/disabilitazione passa dall'input `capacities` del form processato in `post_updateItem()` (AssetDefinition.php:316-440) — nessun metodo pubblico `enableCapacity/saveCapacity/disableCapacity`. La chiamata è in try/catch → il messaggio d'errore viene mostrato ma la migrazione continua. L'impatto è nullo in pratica perché `Config::isItemtypeEnabled()` ritorna true per tutte le sottoclassi di `Glpi\Asset\Asset` (tab sempre visibile out-of-the-box); da rivedere comunque con l'API core corretta.
|
|
- `uninstall.php`: `Profile::uninstallRights()` + `plugin_urbackup_migration_drop_table()` per ogni tabella (drop via `$migration->dropTable()` se esiste).
|
|
|
|
---
|
|
|
|
## 3. Session & Sicurezza
|
|
|
|
### 3.1 Diritti
|
|
```php
|
|
Session::checkRight($module, $right); // muore con errore 403 se senza diritto
|
|
Session::checkLoginUser(); // solo login richiesto (server_test.ajax.php)
|
|
Session::haveRight($module, $right); // booleano (senza morte)
|
|
Session::getLoginUserID();
|
|
Session::getPluralNumber();
|
|
Session::addMessageAfterRedirect(...);
|
|
Session::getNewCSRFToken(bool $standalone = false);
|
|
```
|
|
- **IMPORTANTE**: nei file con namespace plugin (`GlpiPlugin\Urbackup\...`) importare `use Session;` (e `Html`, `Toolbox`, `GLPIKey`, ecc.), altrimenti PHP risolve `GlpiPlugin\Urbackup\Session` che non esiste.
|
|
- **Pattern diritti del plugin**: `Profile::canCurrentUser(int $right): bool` (src/Profile.php) — risolve il profilo attivo da `$_SESSION['glpiactiveprofile']['id']`, con fallback su `glpi_profiles_users` (profilo statico) e su `Session::haveRight()`; il rightname è `'plugin_urbackup'`.
|
|
- `Server::canView()/canCreate()/canUpdate()/canDelete()/canPurge()` delegano a `Profile::canCurrentUser(...)`.
|
|
- `Server::$rightname = 'plugin_urbackup'` → `getRights()` espone READ/UPDATE/CREATE/DELETE/PURGE nella UI standard dei profili GLPI.
|
|
|
|
### 3.2 CSRF — GLPI 11 breaking change
|
|
- Hook `Hooks::CSRF_COMPLIANT = 'csrf_compliant'` registrato in `setup.php` → GLPI gestisce i token automaticamente.
|
|
- **Il listener globale `CheckCsrfListener` consuma il token su ogni POST** → una seconda chiamata esplicita a `Session::checkCSRF()` fallisce (in GLPI 11 richiede `$data` come argomento). Il plugin NON la chiama nei front (fix 0.7.0).
|
|
- Form POST: `Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()])` (pattern in AssetTab/Server/MassiveAction).
|
|
- AJAX (`public/js/urbackup.js`): header `X-Glpi-Csrf-Token` con `getAjaxCsrfToken()`.
|
|
|
|
### 3.3 Profili e diritti (pattern verificato 0.7.x)
|
|
- `Profile::registerRights()` → `ProfileRight::addProfileRights(['plugin_urbackup'])` se assente in `glpi_profilerights`.
|
|
- `Profile::installRights()`: profilo attivo da sessione; **in CLI (install via console) non c'è sessione** → cerca il profilo "Super-Admin" con query diretta e assegna `READ|UPDATE|CREATE|DELETE|PURGE`; agli altri profili garantisce `READ` se non esiste record.
|
|
- `Profile::uninstallRights()` → `$DB->delete('glpi_profilerights', ['name' => 'plugin_urbackup'])` + `ProfileRight::deleteProfileRights()`.
|
|
- Hook `Hooks::CHANGE_PROFILE` → `Profile::initProfile($profile)`: se il nuovo profilo non ha diritti, assegna READ.
|
|
|
|
---
|
|
|
|
## 4. Criptazione Segreti
|
|
|
|
**⚠️ `Toolbox::encrypt()/decrypt()` NON ESISTE in GLPI 11** (verificato in `src/Toolbox.php`). API corretta (verificata in core: `APIRest.php:623`, `ClientRepository.php:90` — `GLPIKey::getInstance()` **NON esiste**):
|
|
```php
|
|
use GLPIKey; // classe globale, serve solo nei file con namespace plugin
|
|
(new GLPIKey())->encrypt(string $string, ?string $key = null): string;
|
|
(new GLPIKey())->decrypt(?string $string, ?string $key = null): ?string;
|
|
```
|
|
- `encrypt()` ritorna `''` se la chiave non è leggibile (mai dati in chiaro propagati); `decrypt()` su valore non cifrato emette `trigger_error` e ritorna `''` → rilevare il formato prima di decifrare (`Server::isApiPasswordEncrypted()`: base64 strict + nonce ≥ 24 byte).
|
|
- **STATO ATTUALE (05/08/2026)**: `api_password` su `glpi_plugin_urbackup_servers` è **cifrata con GLPIKey** — encrypt in `Server::prepareInputForAdd/Update` (campo vuoto = mantieni), `Server::getApiPassword()` decifra on-the-fly con fallback per valori legacy in chiaro, migrazione idempotente `plugin_urbackup_install_encrypt_api_passwords()` in install.php. Mai loggare o esporre la password decifrata.
|
|
|
|
---
|
|
|
|
## 5. Html & Escaping
|
|
|
|
```php
|
|
Html::header($title, $url, 'admin', 'GlpiPlugin\Urbackup\Server'); // header pagina
|
|
Html::footer();
|
|
Html::redirect($url);
|
|
Html::hidden($name, ['value' => $value]); // campo hidden (NOTA: in GLPI 11 la firma è (name, options))
|
|
Html::submit($label, ['name' => ..., 'class' => ...]);
|
|
Html::input($name, ['value' => ..., 'size' => ...]); // input testuale
|
|
Html::convDate($date);
|
|
Html::convDateTime($date);
|
|
Html::displayRightError(); // errore diritti insufficienti
|
|
Html::displayNotFoundError(); // item non trovato
|
|
Html::closeForm();
|
|
Html::header_nocache(); // endpoint AJAX
|
|
```
|
|
### Escaping (XSS)
|
|
- `htmlspecialchars()` su TUTTI gli output dinamici (pattern diffuso in AssetTab/Server/ServerAsset).
|
|
- `Html::entities_deep($array)` — sanitizzazione array di input.
|
|
- Twig: auto-escaping (vedi §11).
|
|
|
|
---
|
|
|
|
## 6. Dropdown & Search
|
|
|
|
### 6.1 Dropdown
|
|
```php
|
|
Dropdown::showFromArray($name, $values, $options); // dropdown generico da array (protocollo, server list)
|
|
Dropdown::showYesNo($name, $value); // is_active, ignore_ssl, ecc.
|
|
Entity::dropdown(['name' => 'entities_id', 'value' => ...]);
|
|
Location::dropdown(['name' => 'locations_id', 'value' => ...]);
|
|
Server::dropdown([...]); // dropdown itemtype del plugin (MassiveAction)
|
|
Dropdown::getDropdownName($table, $id);
|
|
```
|
|
|
|
### 6.2 Search options (`Server::rawSearchOptions()`)
|
|
- ID usati: `common` (Characteristics), 1 name (itemlink), 2 ip_address (string), 3 port (integer), 4 protocol (string), 5 server_version (string), 6 Entity (dropdown), 7 Location (dropdown), 8 User (dropdown — richiede colonna `users_id`), 9 is_active (bool), 10 last_api_status (bool), 11 last_api_check (datetime), 12 date_creation, 13 date_mod, 14 id (raw, `searchtype => 'view'`, solo con UPDATE).
|
|
- `Search::getOptions($itemtype)` / `Search::show($itemtype, $params)` disponibili nel core (`src/Search.php`).
|
|
|
|
---
|
|
|
|
## 7. CommonDBTM / CommonGLPI / Capacity system GLPI 11
|
|
|
|
### 7.1 Classi base
|
|
- `CommonDBTM` — tabella + CRUD generico: `Server`, `ServerAsset`, `Config`, `AssetTab`, `MassiveAction` (del plugin).
|
|
- `CommonGLPI` — item senza tabella (tab, UI).
|
|
- **Capacity system GLPI 11** — `Glpi\Asset\Capacity\AbstractCapacity` (vedi §7.4).
|
|
|
|
### 7.2 Metodi lifecycle sovrascritti nel plugin
|
|
```php
|
|
getTable($classname = null): string; // nome tabella custom
|
|
getTypeName($nb = 0): string; // _n(..., 'urbackup')
|
|
getRights($interface = 'central'); // READ/UPDATE/CREATE/DELETE/PURGE
|
|
canView()/canCreate()/canUpdate()/canDelete()/canPurge();
|
|
defineTabs($options); // Server: default form + ServerAsset + Log
|
|
rawSearchOptions(); // §6.2
|
|
showForm($ID, $options); // form edit + showFormHeader/showFormFields/showFormButtons
|
|
prepareInputForAdd($input); // default port 55414, protocol http, users_id da sessione
|
|
prepareInputForUpdate($input); // test connessione API in salvataggio (last_api_status/message/check)
|
|
getMenuName()/getMenuContent(); // menu Admin (icona 'ti ti-cloud-up')
|
|
```
|
|
|
|
### 7.3 Tabs
|
|
```php
|
|
getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string; // ritorna createTabEntry(...) o ''
|
|
displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool;
|
|
```
|
|
- `AssetTab` su **Computer** (registerClass `addtabon => ['Computer']`) e su **Asset Definition** via capacity (registrazione tab in `onClassBootstrap`).
|
|
- `Profile` (del plugin) su `\Profile` (tab diritti, render Twig `@urbackup/profile.html.twig`).
|
|
- `Server` → tab `ServerAsset` (Linked assets) + `Log`.
|
|
- Sub-tab interni in `AssetTab::showInternalTabs()`: **Stato / Azioni (solo UPDATE|CREATE) / Info-Log**, resi con Bootstrap tabs (`nav-tabs`).
|
|
|
|
### 7.4 Capacity system GLPI 11 (URBackupCapacity — `src/Capacity/UrBackupCapacity.php`)
|
|
```php
|
|
use Glpi\Asset\Capacity\AbstractCapacity;
|
|
final class UrBackupCapacity extends AbstractCapacity {
|
|
getLabel(): string; // 'UrBackup'
|
|
getIcon(): string; // 'ti ti-cloud-up'
|
|
getDescription(): string;
|
|
onClassBootstrap(string $classname, CapacityConfig $config): void;
|
|
// -> CommonGLPI::registerStandardTab($classname, AssetTab::class)
|
|
onCapacityDisabled(string $classname, CapacityConfig $config): void;
|
|
// -> pulizia ServerAsset::deleteByCriteria(['itemtype' => $classname], force: true) + deleteRelationLogs()
|
|
isUsed(string $classname): bool; // countAssetsLinkedToPeerItem(... ServerAsset) > 0
|
|
getCapacityUsageDescription(string $classname): string;
|
|
}
|
|
```
|
|
- Registrazione in `plugin_init_urbackup()`: `AssetDefinitionManager::getInstance()->registerCapacity(new UrBackupCapacity())`.
|
|
- `onClassBootstrap` viene chiamato da `bootDefinitions()` durante l'evento kernel **PostBoot** → i tab vengono registrati su tutti i definition abilitati anche se creati dopo l'init del plugin.
|
|
- `Config::isItemtypeEnabled()`: `Computer` sempre true; qualsiasi sottoclasse di `Glpi\Asset\Asset` sempre true (visibilità sempre on; la capacità serve per usage tracking e cleanup).
|
|
- `Config::getEnabledItemtypes()`: `['Computer', ...classi asset definition]` via `AssetDefinitionManager::getDefinitions()`.
|
|
|
|
### 7.5 CRUD istanza
|
|
```php
|
|
$obj = new Server();
|
|
$obj->getEmpty();
|
|
$obj->getFromDB($id);
|
|
$obj->add($input); $obj->update($input); $obj->delete($input);
|
|
$obj->getField('colonna');
|
|
$obj->check($id, RIGHT); // autorizzazione (muore se senza diritto)
|
|
```
|
|
|
|
### 7.6 Massive Actions (plugin)
|
|
- Hook `Hooks::USE_MASSIVE_ACTION = 'use_massive_action'` in `setup.php` + `plugin_urbackup_MassiveActions($type)` in `hook.php`.
|
|
- **ATTENZIONE**: l'hook `plugin_urbackup_MassiveActions($type)` riceve l'itemtype come **stringa** (es. `'Computer'`), NON un oggetto MassiveAction; ritorna `['Classe::SEPARATOR::azione' => 'Label']` con `\MassiveAction::CLASS_ACTION_SEPARATOR`.
|
|
- Azioni: `ACTION_CONNECT_SERVER = 'connect_server'` (UPDATE|CREATE), `ACTION_DISCONNECT_SERVER = 'disconnect_server'` (UPDATE).
|
|
- `showMassiveActionsSubForm(\MassiveAction $ma)`: dropdown `Server::dropdown(['condition' => ['is_active' => 1]])`.
|
|
- `processMassiveActionsForOneItemtype(\MassiveAction $ma, CommonDBTM $item, array $ids)`: verifica `Config::isItemtypeEnabled()` e diritti, chiama `ServerAsset::connectAssetToServer()` / `disconnectAsset()`, ritorna `$ma->itemDone($itemtype, $id, \MassiveAction::ACTION_OK|ACTION_KO)`.
|
|
|
|
---
|
|
|
|
## 8. Bootstrap Plugin
|
|
|
|
### 8.1 setup.php
|
|
```php
|
|
function plugin_init_urbackup(): void {
|
|
global $PLUGIN_HOOKS;
|
|
$PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT]['urbackup'] = true;
|
|
$PLUGIN_HOOKS[Hooks::CHANGE_PROFILE]['urbackup'] = [Profile::class, 'initProfile'];
|
|
Profile::registerRights();
|
|
Plugin::registerClass(Config::class);
|
|
Plugin::registerClass(Profile::class, ['addtabon' => 'Profile']);
|
|
Plugin::registerClass(Server::class, ['linkgroup_types' => true, 'document_types' => true]);
|
|
Plugin::registerClass(ServerAsset::class);
|
|
Plugin::registerClass(PluginUrbackupMassiveAction::class);
|
|
Plugin::registerClass(AssetTab::class, ['addtabon' => ['Computer']]);
|
|
if (class_exists(AssetDefinitionManager::class)) {
|
|
AssetDefinitionManager::getInstance()->registerCapacity(new UrBackupCapacity());
|
|
}
|
|
$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'];
|
|
}
|
|
function plugin_version_urbackup(): array { /* name, version 0.7.2, requires GLPI 11.0.6-11.99.99, PHP >= 8.3 */ }
|
|
function plugin_urbackup_check_prerequisites(): bool { /* version_compare GLPI + PHP */ }
|
|
function plugin_urbackup_check_config(bool $verbose = false): bool { return true; }
|
|
function plugin_urbackup_install(): bool { require install/install.php; return plugin_urbackup_install_process(); }
|
|
function plugin_urbackup_uninstall(): bool { require install/uninstall.php; return plugin_urbackup_uninstall_process(); }
|
|
```
|
|
- NOTA: top-level di setup.php invalida l'OPcache dei file del plugin via `opcache_invalidate()` (sviluppo web).
|
|
|
|
### 8.2 hook.php — funzioni standard
|
|
| Funzione | Ruolo |
|
|
|----------|-------|
|
|
| `plugin_urbackup_get_classes()` | array classi: Config, Profile, Server, ServerAsset, PluginUrbackupMassiveAction |
|
|
| `plugin_urbackup_MassiveActions($type)` | azioni massivie per itemtype abilitato (stringa in ingresso!) |
|
|
|
|
---
|
|
|
|
## 9. Install / Upgrade / Uninstall
|
|
- `plugin_urbackup_install_process()` (§2.6) — migrazioni **idempotenti**, schema iniziale con `runFile` (SOLO qui), `Profile::installRights()` con fallback CLI "Super-Admin", `Config::ensureDefaultConfiguration()`.
|
|
- `plugin_urbackup_uninstall_process()` — `Profile::uninstallRights()` + `plugin_urbackup_migration_drop_table()` per configs/servers/serverassets (+ legacy profiles/assettypes).
|
|
- Comandi: `php bin/console glpi:plugin:install urbackup`, `php bin/console glpi:plugin:activate urbackup`, disinstallazione via `glpi:plugin:uninstall urbackup`.
|
|
|
|
---
|
|
|
|
## 10. UrBackup Web API — `src/UrbackupApiClient.php`
|
|
|
|
### 10.1 Panoramica
|
|
- Endpoint: `http(s)://<ip>:<port>/x?a=<action>` (base URL: `rtrim($server->getWebInterfaceUrl(), '/') . '/x'`).
|
|
- cURL: `CURLOPT_TIMEOUT = 30`, `CURLOPT_CONNECTTIMEOUT = 5`, `SSL_VERIFYPEER/VERIFYHOST` secondo `ignore_ssl`, header `Accept: application/json` + `Content-Type: application/x-www-form-urlencoded; charset=UTF-8`, sempre POST (tranne ove indicato).
|
|
- Errori: `RuntimeException` con messaggi traducibili (`__()`, dominio 'urbackup'); risposte non-JSON (HTML) rilevate da `str_starts_with(trim($raw), '<')`.
|
|
|
|
### 10.2 Autenticazione (flusso login)
|
|
1. `request('login', [], 'POST', false)` — se `success === true` ok (session id da `login['session']`).
|
|
2. Altrimenti `request('salt', ['username' => ...], 'POST', false)` → `ses` (session), `salt`, `rnd`, `pbkdf2_rounds`.
|
|
3. Hash password: `md5($salt_str . $password, true)` → hex; se `pbkdf2_rounds > 0` → `hash_pbkdf2('sha256', $bin, $salt_str, $rounds)`; finale `md5($rnd . $passwordMd5)`.
|
|
4. `request('login', ['username', 'password' => hash, 'ses'])` → `logged_in = true`.
|
|
- Tutte le azioni autenticate passano `ses` nei parametri (`apiAction()`).
|
|
|
|
### 10.3 Azioni implementate
|
|
| Metodo | API action | Parametri chiave |
|
|
|--------|-----------|------------------|
|
|
| `testConnection()` | `server_identity` (post login) | — |
|
|
| `getStatus()` | `status` | — (campi: name, online, status, ip/client_ip, client_version_string, file_lastbackup, image_lastbackup, file_ok, image_ok, lastbackup...) |
|
|
| `getClientStatusByName()` | — (filtra `getStatus()`) | match case-insensitive su name/clientname/hostname |
|
|
| `getClientIdByName()` | — | id/clientid/client_id |
|
|
| `getClientSettings()` | `settings` | `sa=clientsettings`, `t_clientid` |
|
|
| `updateClientSettings()` | `settings` | `sa=clientsettings_save`, `t_clientid`, `overwrite=true`, `$key=$value` |
|
|
| `saveInternetMode()` | `settings` | chiave `internet_mode_enabled` (≥ 2.4) o `internet_mode` |
|
|
| `getClientAuthKey()` | — | setting `internet_authkey` |
|
|
| `addClient()` | `add_client` | `clientname` |
|
|
| `removeClient()` | `remove_client` | `clientname`, `clientid` |
|
|
| `startIncrementalFileBackup()` | `start_backup` | `start_client`, `start_type=incr_file` |
|
|
| `startFullFileBackup()` | `start_backup` | `start_type=full_file` |
|
|
| `startIncrementalImageBackup()` | `start_backup` | `start_type=incr_image` |
|
|
| `startFullImageBackup()` | `start_backup` | `start_type=full_image` |
|
|
| `getRecentBackups()` | `backups` | `sa=backups`, `clientid` (file `backups[]` + image `backup_images[]`, ordinati per time desc, default 40) |
|
|
| `getClientLogs()` | `livelog` | `clientid`, `lastid` (logdata; aggiorna `lastlogid`) |
|
|
|
|
### 10.4 Note di compatibilità versioni
|
|
- `detectVersion2_4OrHigher()`: version ≥ 2.4 → `internet_mode_enabled`, altrimenti `internet_mode`.
|
|
- `extractSettingValue()`: i setting API possono essere struct `{"use":N, "value":..., "value_client":..., "value_group":...}` → estrae `['value']`.
|
|
- `responseIsSuccess()`: accetta `success`, `ok`, `saved_ok`, `result === 'ok'`, `start_ok`.
|
|
- Cache: `cached_status` (in-memory), `cached_settings[clientid]` (invalidata dopo save).
|
|
|
|
---
|
|
|
|
## 11. Twig / TemplateRenderer
|
|
|
|
```php
|
|
use Glpi\Application\View\TemplateRenderer;
|
|
$twig = TemplateRenderer::getInstance();
|
|
$twig->display('@urbackup/profile.html.twig', ['id' => ..., 'profile' => ..., 'title' => ..., 'rights' => ...]);
|
|
```
|
|
- Namespace template plugin: `@urbackup/` (directory `templates/` del plugin).
|
|
- Auto-escaping Twig attivo: `{{ var }}` escapato; mai logica PHP nei template.
|
|
- Pattern verificato in `src/Profile.php::displayTabContentForItem()` (tab diritti su Profilo).
|
|
|
|
---
|
|
|
|
## 12. Integrazione Asset / Location (pattern chiave)
|
|
|
|
### 12.1 Associazione asset ↔ server (`ServerAsset`)
|
|
- Tabella `glpi_plugin_urbackup_serverassets` (itemtype polimorfo + items_id + server).
|
|
- `connectAssetToServer($itemtype, $items_id, $server_id)`: diritti UPDATE|CREATE, `Config::isItemtypeEnabled()`, upsert (update se link esistente, insert altrimenti).
|
|
- `disconnectAsset($itemtype, $items_id)`: diritto UPDATE, delete.
|
|
- `getLinkForAsset($itemtype, $items_id, $active_only = true)`: singolo link.
|
|
- `extractAssetIp()`: legge `$item->fields['ip_address']` (se presente).
|
|
|
|
### 12.2 Location-aware (`LocationHelper`)
|
|
- **Regola di business**: se l'asset è in una sub-location, il server UrBackup di riferimento è quello assegnato alla **root location**.
|
|
- `getRootLocationId(int $locations_id)`: risale `glpi_locations.locations_id` finché `locations_id = 0` (loop `Location::getFromDB()`).
|
|
- `getActiveServersForRootLocation(int $locations_id)`: server con `locations_id = $root AND is_active = 1` (query `$DB->request()`).
|
|
- `getAvailableServersForAsset()` / `assetIsInSubLocation()`.
|
|
- Pattern UI: in `AssetTab::showNoServerLinkedBlock()` il dropdown server è filtrato per root location dell'asset; in `Server::showMissingClientsTab()` gli asset candidati sono filtrati per root location del server.
|
|
|
|
### 12.3 Batch loading (anti N+1) — `Server::showMissingClientsTab()`
|
|
- `batchLoadIps()`: **1 query per itemtype** — INNER JOIN `glpi_ipaddresses AS ipa` → `glpi_networknames AS nn` (ON `nn.items_id = ipa.id` AND `ipa.itemtype='NetworkName'`) → `glpi_networkports AS np` (ON `np.id = nn.items_id` AND `nn.itemtype='NetworkPort'`), WHERE `np.itemtype = $itemtype AND np.items_id IN ($ids)`. Ritorna `"itemtype:items_id" => ip`.
|
|
- `batchLoadGroups()`: **1 query per itemtype** su `glpi_groups_items` (WHERE `itemtype`, `items_id IN`, `type = \Group_Item::GROUP_TYPE_NORMAL`).
|
|
- `getCachedName($classname, $id, &$cache)`: cache in-memory per Entity/Location/State/User/Group (`completename` ?? `name`).
|
|
- `formatLastBackup()`: timestamp Unix (1..2e9) → `date('Y-m-d H:i:s')`.
|
|
|
|
### 12.4 Server form (front/server.form.php)
|
|
- 4 tab: **Server** (form standard), **Linked clients**, **Unlinked clients**, **Missing clients**.
|
|
- `showLinkedClientsTab()`: server con `last_api_status = 1` → confronta asset collegati vs client API `getStatus()` (match case-insensitive su name).
|
|
- `showUnlinkedClientsTab()`: client API non ancora collegati; se esiste un asset GLPI con lo stesso nome nella root location del server → bottone **Connect** (POST `link_asset`).
|
|
- `showMissingClientsTab()`: asset GLPI della root location non collegati e non presenti su UrBackup (tabella sortable/search in JS, inline).
|
|
- Badge stato: `renderOnlineBadge()` (Online/Offline + badge ok/minor_problems/major_problems/paused).
|
|
|
|
---
|
|
|
|
## 13. Caveat GLPI 11 verificati (raccolti da MEMORY.md + core)
|
|
|
|
1. `$DB->query()` **deprecato** → `$DB->doQuery()` per SQL raw.
|
|
2. `$DB->runFile()` **deprecato** → usarlo SOLO per lo schema iniziale in install.php; mai in upgrade/uninstall (drop via `$migration->dropTable()`).
|
|
3. `Toolbox::encrypt/decrypt` **non esiste** → `(new GLPIKey())->encrypt()/decrypt()` (`GLPIKey::getInstance()` non esiste).
|
|
4. **CSRF GLPI 11**: `Session::checkCSRF()` richiede `$data` come argomento; il listener globale `CheckCsrfListener` consuma il token → il plugin NON la chiama nei front; form con hidden token, AJAX con header `X-Glpi-Csrf-Token`.
|
|
5. `Session::isDebugActive()` **non esiste** in GLPI 11 → `($_SESSION['glpi_use_mode'] ?? Session::NORMAL_MODE) === Session::DEBUG_MODE`.
|
|
6. `Session`/`Html`/`Toolbox` ecc. sono classi globali → `use Session;` nei namespace plugin.
|
|
7. `$DB->update()` ritorna sempre `true` → verificare con `$DB->affectedRows()`.
|
|
8. GLPI environment enum: `production`, `development`, `testing`, `staging`, `e2e_testing` — MAI `prod`.
|
|
9. **`linkgroup_types => true` richiede la colonna `users_id`** sulla tabella del plugin: senza, `Group::getDataItems()` fallisce con MySQL 1054 (Unknown column 'users_id') — colonna presente su `glpi_plugin_urbackup_servers` (fix 0.7.0).
|
|
10. `Profile::installRights()` in **CLI non ha sessione** → fallback: query diretta sul profilo "Super-Admin" e assegnazione diritti completi.
|
|
11. **Asset Definition vs Computer**: le asset class di GLPI 11 sono 2-5x più lente (overhead core: Capacity iteration, JSON custom fields, `eval()` autoloading) → usare `$item->fields['name']` diretto (mai `getFromDB()` ridondanti) e batch loading.
|
|
12. **`api_password` cifrata con GLPIKey** (05/08/2026): encrypt on save, `Server::getApiPassword()` con fallback legacy, migrazione `plugin_urbackup_install_encrypt_api_passwords()`; campo form vuoto = mantieni.
|
|
13. **UrBackup versioni**: setting `internet_mode_enabled` solo da UrBackup ≥ 2.4; struct setting `{"use":N,"value":...}` da estrarre con `extractSettingValue()`.
|
|
14. Heredoc JS: non chiamare `__()` dentro heredoc (Server.php usa heredoc per JS inline — stringhe non tradotte lì).
|
|
15. Hook `plugin_urbackup_MassiveActions($type)` riceve l'**itemtype stringa**, non un oggetto MassiveAction.
|
|
16. Menu: `Hooks::MENU_TOADD['urbackup'] = ['admin' => Server::class]` + `Server::getMenuContent()` (icona `ti ti-cloud-up`).
|
|
17. Nessun uso di cron/notifiche SSH nel plugin: tutta la comunicazione è API HTTP verso il server UrBackup.
|
|
18. **`hasCapacity()`/`enableCapacity()` non esistono in GLPI 11.0.8** su `AssetDefinition` → check con `hasCapacityEnabled(CapacityInterface $capacity)` (oggetto, non stringa), `getEnabledCapacities()`, `getCapacityConfiguration()` (AssetDefinition.php:627-652); l'enable/disable passa dall'input `capacities` del form in `post_updateItem()` (AssetDefinition.php:316-440). Il codice install.php:367-368 usa i metodi inesistenti dentro try/catch (vedi §2.6).
|
|
19. **Standard versione plugin per modifiche DB (regola 6 di AGENTS.md)**: qualsiasi modifica DB richiede bump di `PLUGIN_URBACKUP_VERSION` in setup.php. Meccanismo GLPI verificato in `src/Plugin.php`: `checkPluginState()` (righe ~909-933) confronta `plugin_version_urbackup()['version']` con `glpi_plugins.version`; se diversa aggiorna la riga e imposta `state = NOTUPDATED` (messaggio "Plugin version changed. It has been deactivated as its update process has to be launched."). L'update si esegue con `php bin/console glpi:plugin:install urbackup` → `Plugin::install()` (riga 1197) chiama `plugin_urbackup_install()` e poi imposta `state = NOTACTIVATED` → quindi serve `php bin/console glpi:plugin:activate urbackup`. Le migrazioni in `plugin_urbackup_install_process()` usano `new Migration(PLUGIN_URBACKUP_VERSION)` e DEVONO restare idempotenti. Comandi console disponibili: `plugin:list`, `plugin:install`, `plugin:activate`, `plugin:deactivate`, `plugin:uninstall` (`src/Glpi/Console/Plugin/`).
|
|
20. **Toggle "Computer" configurabile (0.7.2)**: il tab UrBackup su `Computer` NON è una capacità (le capacità valgono solo per Asset Definition) → lo stato è salvato in `glpi_plugin_urbackup_configs` (`enable_computer`). `Config::getEnableComputer()` ha cache statica + guard `TableExists` + fallback `true`; setup.php registra `addtabon Computer` solo se true; `getEnabledAssetDefinitions()` usa `hasCapacityEnabled()` con istanza da `AssetDefinitionManager::getAvailableCapacities()`. Disattivare Computer non tocca i link esistenti in `glpi_plugin_urbackup_serverassets` (solo visibilità UI).
|
|
21. **Hardware host del server (0.7.3)**: `glpi_plugin_urbackup_servers.host_itemtype`/`host_items_id` (polimorfici, NULL/0 = nessuno) identificano l'asset (Computer o Asset Definition con capacità attiva) su cui gira il server UrBackup — NON usare `glpi_plugin_urbackup_serverassets` (semantica CLIENT: asset backup da quel server). UI: `Dropdown::showItemTypes` + `Html::scriptBlock` con `$.get` → `front/dropdown_host.ajax.php` (GET, `Session::checkLoginUser()`, itemtype validato con `class_exists` + `Config::isItemtypeEnabled`) che risponde `Dropdown::show($itemtype, ['entity' => Session::getActiveEntities(), ...])`. Validazione in `Server::prepareInputForUpdate()`: se `host_items_id > 0` ma itemtype mancante/non abilitato/item inesistente → azzeramento entrambi. Più server sullo stesso host consentiti (niente unique). Blocco "This asset hosts the UrBackup server" in AssetTab (sempre visibile, anche se l'asset è client). **Caveat API GLPI 11**: (a) `Ajax::updateItemOnSelectEvent` genera `$("#x").load(url, {params})` = **POST** (data oggetto) → endpoint target deve accettare POST + CSRF AJAX (header `X-Glpi-Csrf-Token` automatico via `$(document).ajaxSend` in public/js/common.js); preferire `$.get` inline via `Html::scriptBlock` per dropdown read-only. (b) `Dropdown::show` è **lazy** (`Html::jsAjaxDropdown`): il markup NON contiene le opzioni, che arrivano via POST select2 a `/ajax/getDropdownValue.php`; il parametro per le entità è **`entity`** (NON `entity_restrict`, che è solo l'output serializzato); `Session::getMatchingActiveEntities()` è un filtro che richiede 1 argomento → usare `Session::getActiveEntities()`.
|
|
|
|
---
|
|
|
|
## 14. Conclusione
|
|
|
|
Questo file è la mappa dell'API GLPI 11 usata dal plugin urbackup. Se una modifica del core GLPI richiede nuove funzioni, aggiornare questo file e verificare la firma reale in `/var/www/glpi/src/` prima di scrivere codice.
|