From 942024e8406fb2df49b0249eadf53171198ff644 Mon Sep 17 00:00:00 2001 From: Lainow Date: Tue, 25 Aug 2026 11:04:22 +0200 Subject: [PATCH 1/2] Fix: honor _plugin_escalade_rules_only on group assignment The flag was only read by pre_item_update() on Ticket, where escalade sets it on its own internal update to avoid recursion. Callers that assign a technician group themselves (other plugins, scripts) had no way to opt out: a Group_Ticket::add() carrying the flag still went through processAfterAddGroup(), which unassigned the technician when remove_tech is on, forced ticket_last_status and recorded an escalation history entry. Read the flag in both Group_Ticket hooks so it means the same thing there: escalade skips its logic entirely for that assignment. --- hook.php | 1 + inc/ticket.class.php | 8 +++ tests/Units/GroupEscalationTest.php | 101 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/hook.php b/hook.php index 4655b2e2..752848fe 100644 --- a/hook.php +++ b/hook.php @@ -523,6 +523,7 @@ function plugin_escalade_pre_item_add_group_ticket($item) if ( $item instanceof Group_Ticket && $item->input['type'] == CommonITILActor::ASSIGN + && empty($item->input['_plugin_escalade_rules_only']) ) { if (!isset($_SESSION['plugin_escalade']['current_group_assignment'])) { $_SESSION['plugin_escalade']['current_group_assignment'] = []; diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 55bdfbc0..f5ea3187 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -532,6 +532,14 @@ public static function addHistoryOnAddGroup(CommonDBTM $item) public static function processAfterAddGroup(Group_Ticket $item) { + // Explicit opt-out for callers that assign a technician group on their own + // (other plugins, scripts). Same meaning as in pre_item_update(): escalade + // skips its logic entirely, so no group cleanup, no technician unassignment, + // no history entry and no automatic status change for this assignment. + if (!empty($item->input['_plugin_escalade_rules_only'])) { + return; + } + $tickets_id = $item->fields['tickets_id']; $groups_id = $item->fields['groups_id']; diff --git a/tests/Units/GroupEscalationTest.php b/tests/Units/GroupEscalationTest.php index c3eab8b9..3fcdd92a 100644 --- a/tests/Units/GroupEscalationTest.php +++ b/tests/Units/GroupEscalationTest.php @@ -97,6 +97,107 @@ public function testStandardGroupAssignmentKeepsExistingGroups(): void ])); } + /** + * Without the opt-out flag, adding a technician group triggers the escalade + * processing: the previously assigned technician is unassigned. + */ + public function testGroupAssignmentWithoutOptOutRemovesTechnician(): void + { + $this->initConfig([ + 'remove_tech' => 1, + 'show_history' => 1, + ]); + + $tech = getItemByTypeName(User::class, 'tech'); + $group = $this->createGroup('no_opt_out_group_' . uniqid()); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Group assignment without opt-out', + 'content' => '', + '_actors' => [ + 'assign' => [ + [ + 'items_id' => $tech->getID(), + 'itemtype' => 'User', + ], + ], + ], + ]); + + $group_ticket = new Group_Ticket(); + $this->assertNotFalse($group_ticket->add([ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + $this->assertEquals(0, countElementsInTable(Ticket_User::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'users_id' => $tech->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + $this->assertEquals(1, countElementsInTable('glpi_plugin_escalade_histories', [ + 'tickets_id' => $ticket->getID(), + ])); + } + + /** + * A caller that assigns a technician group on its own can opt out of the + * escalade processing with _plugin_escalade_rules_only: the technician stays + * assigned and no escalation history entry is created. + */ + public function testGroupAssignmentWithOptOutKeepsTechnician(): void + { + $this->initConfig([ + 'remove_tech' => 1, + 'show_history' => 1, + ]); + + $tech = getItemByTypeName(User::class, 'tech'); + $group = $this->createGroup('opt_out_group_' . uniqid()); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Group assignment with opt-out', + 'content' => '', + '_actors' => [ + 'assign' => [ + [ + 'items_id' => $tech->getID(), + 'itemtype' => 'User', + ], + ], + ], + ]); + + $group_ticket = new Group_Ticket(); + $this->assertNotFalse($group_ticket->add([ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + '_plugin_escalade_rules_only' => true, + ])); + + // The group is still linked to the ticket... + $this->assertEquals(1, countElementsInTable(Group_Ticket::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + // ... but escalade did not unassign the technician. + $this->assertEquals(1, countElementsInTable(Ticket_User::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'users_id' => $tech->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + // ... and did not record an escalation. + $this->assertEquals(0, countElementsInTable('glpi_plugin_escalade_histories', [ + 'tickets_id' => $ticket->getID(), + ])); + } + /** * A real Escalade reassignment must remove old groups while preserving * every previously assigned group in the visual assignment history. From 9ac2e4ed1169a46ff26d1456f1b76ec053fabc47 Mon Sep 17 00:00:00 2001 From: Lainow Date: Tue, 25 Aug 2026 11:24:47 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ff56e3..52794961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fix infinite loop / Gateway Timeout when a ticket's assigned technician and assigned group are changed simultaneously, caused by a synchronous actor removal during `pre_item_update()` - Fixed group reassignment to remove previously assigned groups only when using the Escalade reassignment action +- Fixed `_plugin_escalade_rules_only` being ignored on group assignments, so callers could not opt out of escalade's automatic processing ## [2.10.6] - 2026-07-31