From 90b0beef950a4b727b9364d1c3d6e772c65c024c Mon Sep 17 00:00:00 2001 From: whamulti <80924754+whamulti@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:55:47 -0300 Subject: [PATCH] Fix GLPI 11 compatibility with current Metabase API versions Several API response fields the plugin reads under their old names were renamed by Metabase, and the newer native-question query format ("MBQL 5") wasn't handled at all: - ordered_cards -> dashcards on GET /api/dashboard/:id - sizeX/sizeY -> size_x/size_y on dashboard cards - dataset_query.native.{query,template-tags} moved to dataset_query.stages[0].{native,template-tags} - getCards('root') never matched cards in the root collection, since the API returns id:"root" for that collection but collection_id:null on the cards themselves All fixed with backward-compatible `??` fallbacks so older Metabase instances keep working. Also fixes a more severe regression introduced in 1.4.2: embedded_token was added to secured_configs and is expected to be sodium-encrypted, but the upgrade migration only flipped the is_embedded_token_encrypted flag without ever actually encrypting the existing plain-text value. dashboard.class.php unconditionally decrypts embedded_token before signing the dashboard JWT, so any site that already had a token configured before upgrading got an empty signing key and an uncaught Lcobucci\JWT\Signer\InvalidKeyProvided exception on every visit to the embedded dashboard tab. The migration now actually encrypts the value, mirroring what the password migration a few lines above already does. Fixes #148 --- inc/apiclient.class.php | 9 +++++++-- inc/config.class.php | 35 ++++++++++++++++++++++++----------- public/metabase.js | 2 +- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/inc/apiclient.class.php b/inc/apiclient.class.php index 6675668..4ca7611 100644 --- a/inc/apiclient.class.php +++ b/inc/apiclient.class.php @@ -532,7 +532,8 @@ public function getDashboardCards($id) { $data = $this->httpQuery("dashboard/$id", [], 'GET'); - return $data['ordered_cards'] ?? false; + // Metabase renamed 'ordered_cards' to 'dashcards' in newer API versions. + return $data['dashcards'] ?? $data['ordered_cards'] ?? false; } public function createOrUpdateCard($card_name, $params = []) @@ -657,11 +658,15 @@ public function getCards($collection_id = null) return $cards; } + // Metabase's API returns id:"root" for the root collection, but cards + // in it have collection_id: null - normalize before comparing. + $normalized_id = $collection_id === 'root' ? null : $collection_id; + $cards = array_filter( $cards, fn($card) => is_array($card) && array_key_exists('collection_id', $card) - && $collection_id === $card['collection_id'], + && $normalized_id === $card['collection_id'], ); return $cards; diff --git a/inc/config.class.php b/inc/config.class.php index 8ac029f..3cb74be 100644 --- a/inc/config.class.php +++ b/inc/config.class.php @@ -528,10 +528,16 @@ public static function displayQuestionJson($question_id) 'display' => $card['display'], 'visualization_settings' => $card['visualization_settings'], 'template_tags' => [], - 'sql' => $card['dataset_query']['native']['query'], + // Metabase MBQL 5 moved the native query out of + // dataset_query.native.query into dataset_query.stages[0].native. + 'sql' => $card['dataset_query']['native']['query'] + ?? $card['dataset_query']['stages'][0]['native'], ]; - foreach ($card['dataset_query']['native']['template-tags'] as $tag_name => $tag) { + $template_tags = $card['dataset_query']['native']['template-tags'] + ?? $card['dataset_query']['stages'][0]['template-tags'] + ?? []; + foreach ($template_tags as $tag_name => $tag) { $extract['template_tags'][$tag_name] = [ 'type' => $tag['type'], 'display_name' => $tag['display-name'], @@ -582,13 +588,21 @@ public static function displayDashboardJson($dashboard_id) $parameters_id[$parameter['id']] = $parameter['slug']; } - foreach ($dashboard['ordered_cards'] as $card) { + // Metabase renamed 'ordered_cards' to 'dashcards' in newer API versions. + $dashcards = $dashboard['dashcards'] ?? $dashboard['ordered_cards'] ?? []; + foreach ($dashcards as $card) { + // MBQL 5 replaced the top-level dataset_query.type === 'native' + // flag with a dataset_query.stages[0].native structure. + $is_native = isset($card['card']['dataset_query']['type']) + ? $card['card']['dataset_query']['type'] === 'native' + : isset($card['card']['dataset_query']['stages'][0]['native']); + if ( isset($card['card_id']) // only question (TODO support markdown cards) - && $card['card']['dataset_query']['type'] === 'native' + && $is_native ) { // only native questions $key = null; - foreach ($_SESSION['metabase']['reports'] as $session_key => $session_report) { + foreach ($_SESSION['metabase']['reports'] ?? [] as $session_key => $session_report) { if ($session_report['title'] === $card['card']['name']) { $key = $session_key; } @@ -598,15 +612,14 @@ public static function displayDashboardJson($dashboard_id) $extract['reports'][$key] = [ 'col' => $card['col'], 'row' => $card['row'], - 'sizeX' => $card['sizeX'], - 'sizeY' => $card['sizeY'], + 'sizeX' => $card['sizeX'] ?? $card['size_x'], + 'sizeY' => $card['sizeY'] ?? $card['size_y'], ]; foreach ($card['parameter_mappings'] as $mapping) { $mapping_key = $mapping['target'][1][1]; - $field_id = $card['card']['dataset_query'] - ['native']['template-tags'] - [$mapping_key]['dimension'][1]; + $field_id = $card['card']['dataset_query']['native']['template-tags'][$mapping_key]['dimension'][1] + ?? $card['card']['dataset_query']['stages'][0]['template-tags'][$mapping_key]['dimension'][1]; $field_name = array_search($field_id, $_SESSION['metabase']['fields']); if ($field_name !== false) { $extract['reports'][$key]['parameter_mappings'] @@ -618,7 +631,6 @@ public static function displayDashboardJson($dashboard_id) } self::displayPrettyJson($extract); - Html::printCleanArray($dashboard); } public static function displayPrettyJson($array = []) @@ -809,6 +821,7 @@ public static function install(Migration $migration) // Encrypt embedded_token, previously stored in plain text if (!array_key_exists('is_embedded_token_encrypted', $current_config) || !$current_config['is_embedded_token_encrypted']) { if (!empty($current_config['embedded_token'])) { + $current_config['embedded_token'] = (new GLPIKey())->encrypt($current_config['embedded_token']); Config::setConfigurationValues( 'plugin:metabase', [ diff --git a/public/metabase.js b/public/metabase.js index c84232a..c75fabf 100644 --- a/public/metabase.js +++ b/public/metabase.js @@ -48,7 +48,7 @@ $(function() { var type = $(this).data('type'); glpi_ajax_dialog({ dialogclass: 'modal-lg', - url: CFG_GLPI.root_doc + '/' + GLPI_PLUGINS_PATH.metabase + '/ajax/extract_json.php', + url: CFG_GLPI.root_doc + GLPI_PLUGINS_PATH.metabase + '/ajax/extract_json.php', params: { id: id, type: type