From 08cf13be57ca597d7a5c8c5f8cc0dedadf680cac Mon Sep 17 00:00:00 2001 From: Rom1-B <8530352+Rom1-B@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:20:59 +0200 Subject: [PATCH 1/2] Fix: enforce subtree depth limit for tree cascade children --- CHANGELOG.md | 6 +++ .../TreeDropdownChildrenController.php | 3 -- .../TreeCascadeDropdownQuestionTest.php | 49 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724738f..2b974d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Fixed + +- Fixed the `Tree cascade Dropdown` field so that the subtree depth limit is enforced when loading children via AJAX + ## [1.1.1] - 2026-05-27 ### Fixed diff --git a/src/Controller/TreeDropdownChildrenController.php b/src/Controller/TreeDropdownChildrenController.php index c62b424..4b1a665 100644 --- a/src/Controller/TreeDropdownChildrenController.php +++ b/src/Controller/TreeDropdownChildrenController.php @@ -87,8 +87,6 @@ public function __invoke(Request $request): Response $foreign_key = $itemtype::getForeignKeyField(); $table = $itemtype::getTable(); - $level_key = $table . '.level'; - $where = []; $item_check = getItemForItemtype($itemtype); @@ -100,7 +98,6 @@ public function __invoke(Request $request): Response } if (!empty($condition_param) && is_array($condition_param)) { - unset($condition_param[$level_key]); $where = array_merge($where, $condition_param); } diff --git a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php index 4debefa..000b5c3 100644 --- a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php +++ b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php @@ -37,9 +37,11 @@ use Glpi\Form\QuestionType\QuestionTypeItemDropdownExtraDataConfig; use Glpi\Tests\FormBuilder; use Glpi\Tests\FormTesterTrait; +use GlpiPlugin\Advancedforms\Controller\TreeDropdownChildrenController; use GlpiPlugin\Advancedforms\Model\Config\ConfigurableItemInterface; use GlpiPlugin\Advancedforms\Model\QuestionType\TreeCascadeDropdownQuestion; use GlpiPlugin\Advancedforms\Tests\QuestionType\QuestionTypeTestCase; +use Symfony\Component\HttpFoundation\Request; use Location; use Override; use Session; @@ -502,6 +504,53 @@ public function testDropdownShowsNameNotCompletename(): void } } + public function testSubtreeDepthIsEnforcedInChildrenController(): void + { + $this->login(); + $this->enableConfigurableItem(TreeCascadeDropdownQuestion::class); + + $entity_id = Session::getActiveEntity(); + $level1 = $this->createItem(Location::class, [ + 'name' => 'Level1', + 'locations_id' => 0, + 'entities_id' => $entity_id, + ]); + $level2 = $this->createItem(Location::class, [ + 'name' => 'Level2', + 'locations_id' => $level1->getID(), + 'entities_id' => $entity_id, + ]); + $level3 = $this->createItem(Location::class, [ + 'name' => 'Level3', + 'locations_id' => $level2->getID(), + 'entities_id' => $entity_id, + ]); + + $extra_data = json_encode(new QuestionTypeItemDropdownExtraDataConfig( + itemtype: Location::class, + subtree_depth: 2, + )); + + $builder = new FormBuilder("Depth limit test"); + $builder->addQuestion("Location", TreeCascadeDropdownQuestion::class, '', $extra_data); + $form = $this->createForm($builder); + + $questions = $form->getQuestions(); + $question = array_values($questions)[0]; + + $controller = new TreeDropdownChildrenController(); + + $response_level1_children = $controller->__invoke( + Request::create('', 'GET', ['questions_id' => $question->getID(), 'parent_id' => $level1->getID()]), + ); + $this->assertStringContainsString('Level2', $response_level1_children->getContent()); + + $response_level2_children = $controller->__invoke( + Request::create('', 'GET', ['questions_id' => $question->getID(), 'parent_id' => $level2->getID()]), + ); + $this->assertStringNotContainsString('Level3', $response_level2_children->getContent()); + } + private function renderHelpdeskForm(\Glpi\Form\Form $form): Crawler { $this->login(); From 4218e742cd9d5f22f67982a99e39baf16dc469a3 Mon Sep 17 00:00:00 2001 From: Rom1-B <8530352+Rom1-B@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:31:14 +0200 Subject: [PATCH 2/2] add tests --- .../TreeCascadeDropdownQuestionTest.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php index 000b5c3..6ae4b8f 100644 --- a/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php +++ b/tests/Model/QuestionType/TreeCascadeDropdownQuestionTest.php @@ -504,6 +504,9 @@ public function testDropdownShowsNameNotCompletename(): void } } + /** + * Verify that the subtree depth limit prevents children beyond the configured depth from being returned by the children controller. + */ public function testSubtreeDepthIsEnforcedInChildrenController(): void { $this->login(); @@ -551,6 +554,59 @@ public function testSubtreeDepthIsEnforcedInChildrenController(): void $this->assertStringNotContainsString('Level3', $response_level2_children->getContent()); } + /** + * Verify that when the parent itself exceeds the subtree depth limit, it is not selectable and no children are returned. + */ + public function testParentExceedingDepthLimitIsNotSelectableAndHasNoChildren(): void + { + $this->login(); + $this->enableConfigurableItem(TreeCascadeDropdownQuestion::class); + + $entity_id = Session::getActiveEntity(); + $level1 = $this->createItem(Location::class, [ + 'name' => 'L1', + 'locations_id' => 0, + 'entities_id' => $entity_id, + ]); + $level2 = $this->createItem(Location::class, [ + 'name' => 'L2', + 'locations_id' => $level1->getID(), + 'entities_id' => $entity_id, + ]); + $level3 = $this->createItem(Location::class, [ + 'name' => 'L3', + 'locations_id' => $level2->getID(), + 'entities_id' => $entity_id, + ]); + $this->createItem(Location::class, [ + 'name' => 'L4', + 'locations_id' => $level3->getID(), + 'entities_id' => $entity_id, + ]); + + $extra_data = json_encode(new QuestionTypeItemDropdownExtraDataConfig( + itemtype: Location::class, + subtree_depth: 2, + )); + + $builder = new FormBuilder("Depth selectable test"); + $builder->addQuestion("Location", TreeCascadeDropdownQuestion::class, '', $extra_data); + $form = $this->createForm($builder); + + $questions = $form->getQuestions(); + $question = array_values($questions)[0]; + + $controller = new TreeDropdownChildrenController(); + + // Level3 exceeds subtree_depth=2; its child L4 must not be returned and + // the parent_is_selectable placeholder (value="0") must not appear. + $response = $controller->__invoke( + Request::create('', 'GET', ['questions_id' => $question->getID(), 'parent_id' => $level3->getID()]), + ); + $this->assertStringNotContainsString('value="0"', $response->getContent()); + $this->assertStringNotContainsString('L4', $response->getContent()); + } + private function renderHelpdeskForm(\Glpi\Form\Form $form): Crawler { $this->login();