# 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)://:/x?a=` (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.