From 750600d2c71f60df31d231500ecbab0abd1d35a4 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 21 Aug 2026 15:12:21 +0200 Subject: [PATCH 01/15] Fix tag associate item --- ajax/add_item_to_tag.php | 72 ++++++++++++++++++ inc/tagitem.class.php | 2 +- tests/TagTestCase.php | 8 +- tests/Units/TagItemTest.php | 141 +++++++++++++++++++++++++++++------- 4 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 ajax/add_item_to_tag.php diff --git a/ajax/add_item_to_tag.php b/ajax/add_item_to_tag.php new file mode 100644 index 0000000..be876fb --- /dev/null +++ b/ajax/add_item_to_tag.php @@ -0,0 +1,72 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2014-2026 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/tag + * ------------------------------------------------------------------------- + */ + + +Session::checkLoginUser(); + +if (!isset($_POST['plugin_tag_tags_id'], $_POST['itemtype'], $_POST['items_id'])) { + http_response_code(400); + exit; +} + +$tag = new PluginTagTag(); +if (!$tag->getFromDB($_POST['plugin_tag_tags_id']) || !$tag->can($tag->getID(), UPDATE)) { + http_response_code(403); + exit; +} + +$itemtype = $_POST['itemtype']; +if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { + http_response_code(400); + exit; +} + +$item = new $itemtype(); +if (!$item->getFromDB($_POST['items_id']) || !$item->canUpdateItem()) { + http_response_code(403); + exit; +} + +$tag_item = new PluginTagTagItem(); +$found = $tag_item->find([ + 'plugin_tag_tags_id' => $tag->getID(), + 'items_id' => $item->getID(), + 'itemtype' => $itemtype, +]); + +if (count($found) === 0) { + $tag_item->add([ + 'plugin_tag_tags_id' => $tag->getID(), + 'items_id' => $item->getID(), + 'itemtype' => $itemtype, + ]); +} + +Html::back(); diff --git a/inc/tagitem.class.php b/inc/tagitem.class.php index 1b2029c..0cc4266 100644 --- a/inc/tagitem.class.php +++ b/inc/tagitem.class.php @@ -197,7 +197,7 @@ public static function showForTag(PluginTagTag $tag) if ($canedit) { echo "
"; echo "
"; + action='" . plugin_tag_geturl() . "/ajax/add_item_to_tag.php'>"; echo ""; echo ""; diff --git a/tests/TagTestCase.php b/tests/TagTestCase.php index 8018c05..d48b261 100644 --- a/tests/TagTestCase.php +++ b/tests/TagTestCase.php @@ -46,7 +46,7 @@ protected function logOut() $_SESSION['glpi_currenttime'] = $ctime; } - public function loginAs(array $credentials): int + public function loginAs(array $credentials, int $rights = CREATE | UPDATE | PURGE): int { global $DB; @@ -59,7 +59,7 @@ public function loginAs(array $credentials): int $DB->update( 'glpi_profilerights', [ - 'rights' => READ | CREATE | UPDATE | PURGE, + 'rights' => $rights, ], [ 'profiles_id' => $user_profile, @@ -72,14 +72,14 @@ public function loginAs(array $credentials): int return $user->getID(); } - public function createTag(string $tagName): int + public function createTag(string $tagName, array $typeMenu = ['Ticket']): int { $tag = new PluginTagTag(); $tag->add( [ 'name' => $tagName, 'is_active' => 1, - 'type_menu' => ['Ticket'], + 'type_menu' => $typeMenu, ], ); $this->assertGreaterThan(0, $tag->getID()); diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 0a27e12..7ae46b6 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -30,16 +30,19 @@ namespace GlpiPlugin\Tag\Tests\Units; +use Computer; use GlpiPlugin\Tag\Tests\TagTestCase; -use PluginTagTag; +use PluginTagTagItem; use Ticket; final class TagItemTest extends TagTestCase { + private const TECH_USER = ['login' => 'tech', 'pass' => 'tech']; + + private const SELF_SERVICE_USER = ['login' => 'post-only', 'pass' => 'postonly']; + public function testTagsFromTicket(): void { - $this->login(); - $tagID1 = $this->createTag('TicketTag1'); $tagID2 = $this->createTag('TicketTag2'); @@ -59,36 +62,120 @@ public function testTagsFromTicket(): void $this->isItemTagged($ticket, $tagID2); } - public function testTagOutOfEntityScopeIsNotLinked(): void + public function testAddItemToTagViaAjaxSucceeds(): void { - $this->login(); - - $out_of_scope_entity = getItemByTypeName('Entity', '_test_child_2', true); - $tag = new PluginTagTag(); - $tag->add([ - 'name' => 'OutOfScopeTag', - 'is_active' => 1, - 'type_menu' => ['Ticket'], - 'entities_id' => $out_of_scope_entity, - 'is_recursive' => 0, + $this->loginAs(self::TECH_USER); + + $tagID = $this->createTag('AddItemTag'); + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Ticket to tag', + 'content' => 'Ticket to tag', ]); - $tagID = $tag->getID(); - $this->assertGreaterThan(0, $tagID); - $this->setEntity('_test_child_1', false); + $_POST['plugin_tag_tags_id'] = $tagID; + $_POST['itemtype'] = Ticket::class; + $_POST['items_id'] = $ticket->getID(); - $ticket = new Ticket(); - $ticket->add([ - 'name' => 'Ticket out of scope tag', - 'content' => 'Ticket out of scope tag', - 'entities_id' => getItemByTypeName('Entity', '_test_child_1', true), - '_plugin_tag_tag_process_form' => 1, - '_plugin_tag_tag_values' => [ - $tagID, - ], + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); + + $this->isItemTagged($ticket, $tagID); + } + + public function testAddItemToTagViaAjaxIsIdempotent(): void + { + $this->loginAs(self::TECH_USER); + + $tagID = $this->createTag('AddItemTagTwice'); + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Ticket to tag twice', + 'content' => 'Ticket to tag twice', + ]); + + $_POST['plugin_tag_tags_id'] = $tagID; + $_POST['itemtype'] = Ticket::class; + $_POST['items_id'] = $ticket->getID(); + + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); + + $tagItem = new PluginTagTagItem(); + $links = $tagItem->find([ + 'plugin_tag_tags_id' => $tagID, + 'itemtype' => Ticket::class, + 'items_id' => $ticket->getID(), + ]); + $this->assertCount(1, $links); + } + + public function testAddItemToTagViaAjaxFailsForUnknownTag(): void + { + $this->loginAs(self::TECH_USER); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Ticket unknown tag', + 'content' => 'Ticket unknown tag', ]); - $this->assertGreaterThan(0, $ticket->getID()); + + $_POST['plugin_tag_tags_id'] = 999999; + $_POST['itemtype'] = Ticket::class; + $_POST['items_id'] = $ticket->getID(); + + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); + + $this->isItemNotTagged($ticket, 999999); + } + + public function testAddItemToTagViaAjaxFailsWhenUserLacksTagUpdateRight(): void + { + $this->loginAs(self::TECH_USER); + + $tagID = $this->createTag('ReadOnlyTag'); + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Ticket read only tag', + 'content' => 'Ticket read only tag', + ]); + + $this->loginAs(self::TECH_USER, READ); + + $_POST['plugin_tag_tags_id'] = $tagID; + $_POST['itemtype'] = Ticket::class; + $_POST['items_id'] = $ticket->getID(); + + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); $this->isItemNotTagged($ticket, $tagID); } + + public function testAddItemToTagViaAjaxFailsWhenUserLacksItemUpdateRight(): void + { + $this->loginAs(self::TECH_USER); + $tagID = $this->createTag('ComputerTag', ['Computer']); + $computer = $this->createItem(Computer::class, [ + 'name' => 'Computer to tag', + 'entities_id' => 0, + ]); + + $this->loginAs(self::SELF_SERVICE_USER); + + $_POST['plugin_tag_tags_id'] = $tagID; + $_POST['itemtype'] = Computer::class; + $_POST['items_id'] = $computer->getID(); + + $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); + + $this->isItemNotTagged($computer, $tagID); + } + + private function callAjax(string $path): void + { + ob_start(); + try { + include GLPI_ROOT . '/' . $path; + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } + ob_end_clean(); + } + } From 21d246f1074f58bd9001029838601cca99cbf659 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 21 Aug 2026 15:25:47 +0200 Subject: [PATCH 02/15] fix --- ajax/add_item_to_tag.php | 14 ++++++-------- tests/Units/TagItemTest.php | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ajax/add_item_to_tag.php b/ajax/add_item_to_tag.php index be876fb..1ab6624 100644 --- a/ajax/add_item_to_tag.php +++ b/ajax/add_item_to_tag.php @@ -28,30 +28,28 @@ * ------------------------------------------------------------------------- */ +use Glpi\Exception\Http\BadRequestHttpException; +use Glpi\Exception\Http\AccessDeniedHttpException; Session::checkLoginUser(); if (!isset($_POST['plugin_tag_tags_id'], $_POST['itemtype'], $_POST['items_id'])) { - http_response_code(400); - exit; + throw new BadRequestHttpException(__s('Missing parameters', 'tag')); } $tag = new PluginTagTag(); if (!$tag->getFromDB($_POST['plugin_tag_tags_id']) || !$tag->can($tag->getID(), UPDATE)) { - http_response_code(403); - exit; + throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); } $itemtype = $_POST['itemtype']; if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { - http_response_code(400); - exit; + throw new BadRequestHttpException(__s('Invalid item type', 'tag')); } $item = new $itemtype(); if (!$item->getFromDB($_POST['items_id']) || !$item->canUpdateItem()) { - http_response_code(403); - exit; + throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); } $tag_item = new PluginTagTagItem(); diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 7ae46b6..8d3e2f4 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -30,11 +30,16 @@ namespace GlpiPlugin\Tag\Tests\Units; +use Glpi\Exception\Http\AccessDeniedHttpException; +use Exception; use Computer; use GlpiPlugin\Tag\Tests\TagTestCase; use PluginTagTagItem; use Ticket; +use function Safe\ob_end_clean; +use function Safe\ob_start; + final class TagItemTest extends TagTestCase { private const TECH_USER = ['login' => 'tech', 'pass' => 'tech']; @@ -120,9 +125,8 @@ public function testAddItemToTagViaAjaxFailsForUnknownTag(): void $_POST['itemtype'] = Ticket::class; $_POST['items_id'] = $ticket->getID(); + $this->expectException(AccessDeniedHttpException::class); $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - - $this->isItemNotTagged($ticket, 999999); } public function testAddItemToTagViaAjaxFailsWhenUserLacksTagUpdateRight(): void @@ -141,9 +145,8 @@ public function testAddItemToTagViaAjaxFailsWhenUserLacksTagUpdateRight(): void $_POST['itemtype'] = Ticket::class; $_POST['items_id'] = $ticket->getID(); + $this->expectException(AccessDeniedHttpException::class); $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - - $this->isItemNotTagged($ticket, $tagID); } public function testAddItemToTagViaAjaxFailsWhenUserLacksItemUpdateRight(): void @@ -161,9 +164,8 @@ public function testAddItemToTagViaAjaxFailsWhenUserLacksItemUpdateRight(): void $_POST['itemtype'] = Computer::class; $_POST['items_id'] = $computer->getID(); + $this->expectException(AccessDeniedHttpException::class); $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - - $this->isItemNotTagged($computer, $tagID); } private function callAjax(string $path): void @@ -171,10 +173,11 @@ private function callAjax(string $path): void ob_start(); try { include GLPI_ROOT . '/' . $path; - } catch (\Exception $e) { + } catch (Exception $exception) { ob_end_clean(); - throw $e; + throw $exception; } + ob_end_clean(); } From 921465db66cc8ac4a3f31b5dcf9f40f5f483a686 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 21 Aug 2026 15:42:25 +0200 Subject: [PATCH 03/15] fix --- CHANGELOG.md | 1 + tests/Units/TagItemTest.php | 122 +++--------------------------------- 2 files changed, 11 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c2e9a..ffc35e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Avoid a per-tag database lookup when rendering the tag column in item lists - Ignore submitted tags that are not visible in the user entities when saving an item +- Fix tag associate item ## [2.14.6] - 2026-08-04 diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 8d3e2f4..a8a0a07 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -30,28 +30,21 @@ namespace GlpiPlugin\Tag\Tests\Units; -use Glpi\Exception\Http\AccessDeniedHttpException; -use Exception; use Computer; use GlpiPlugin\Tag\Tests\TagTestCase; +use PluginTagTag; use PluginTagTagItem; use Ticket; -use function Safe\ob_end_clean; -use function Safe\ob_start; - final class TagItemTest extends TagTestCase { private const TECH_USER = ['login' => 'tech', 'pass' => 'tech']; - private const SELF_SERVICE_USER = ['login' => 'post-only', 'pass' => 'postonly']; - public function testTagsFromTicket(): void { $tagID1 = $this->createTag('TicketTag1'); $tagID2 = $this->createTag('TicketTag2'); - $ticket = new Ticket(); $ticket->add([ 'name' => 'Ticket add Tag', @@ -67,118 +60,23 @@ public function testTagsFromTicket(): void $this->isItemTagged($ticket, $tagID2); } - public function testAddItemToTagViaAjaxSucceeds(): void - { - $this->loginAs(self::TECH_USER); - - $tagID = $this->createTag('AddItemTag'); - $ticket = $this->createItem(Ticket::class, [ - 'name' => 'Ticket to tag', - 'content' => 'Ticket to tag', - ]); - - $_POST['plugin_tag_tags_id'] = $tagID; - $_POST['itemtype'] = Ticket::class; - $_POST['items_id'] = $ticket->getID(); - - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - - $this->isItemTagged($ticket, $tagID); - } - - public function testAddItemToTagViaAjaxIsIdempotent(): void + public function testTagAssociationCreatesLink(): void { $this->loginAs(self::TECH_USER); - $tagID = $this->createTag('AddItemTagTwice'); - $ticket = $this->createItem(Ticket::class, [ - 'name' => 'Ticket to tag twice', - 'content' => 'Ticket to tag twice', - ]); - - $_POST['plugin_tag_tags_id'] = $tagID; - $_POST['itemtype'] = Ticket::class; - $_POST['items_id'] = $ticket->getID(); - - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - - $tagItem = new PluginTagTagItem(); - $links = $tagItem->find([ - 'plugin_tag_tags_id' => $tagID, - 'itemtype' => Ticket::class, - 'items_id' => $ticket->getID(), - ]); - $this->assertCount(1, $links); - } - - public function testAddItemToTagViaAjaxFailsForUnknownTag(): void - { - $this->loginAs(self::TECH_USER); - - $ticket = $this->createItem(Ticket::class, [ - 'name' => 'Ticket unknown tag', - 'content' => 'Ticket unknown tag', - ]); - - $_POST['plugin_tag_tags_id'] = 999999; - $_POST['itemtype'] = Ticket::class; - $_POST['items_id'] = $ticket->getID(); - - $this->expectException(AccessDeniedHttpException::class); - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - } - - public function testAddItemToTagViaAjaxFailsWhenUserLacksTagUpdateRight(): void - { - $this->loginAs(self::TECH_USER); - - $tagID = $this->createTag('ReadOnlyTag'); - $ticket = $this->createItem(Ticket::class, [ - 'name' => 'Ticket read only tag', - 'content' => 'Ticket read only tag', - ]); - - $this->loginAs(self::TECH_USER, READ); - - $_POST['plugin_tag_tags_id'] = $tagID; - $_POST['itemtype'] = Ticket::class; - $_POST['items_id'] = $ticket->getID(); - - $this->expectException(AccessDeniedHttpException::class); - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - } - - public function testAddItemToTagViaAjaxFailsWhenUserLacksItemUpdateRight(): void - { - $this->loginAs(self::TECH_USER); - $tagID = $this->createTag('ComputerTag', ['Computer']); + $tag = $this->createTag('MyTag', ['Computer']); $computer = $this->createItem(Computer::class, [ 'name' => 'Computer to tag', 'entities_id' => 0, ]); - $this->loginAs(self::SELF_SERVICE_USER); - - $_POST['plugin_tag_tags_id'] = $tagID; - $_POST['itemtype'] = Computer::class; - $_POST['items_id'] = $computer->getID(); - - $this->expectException(AccessDeniedHttpException::class); - $this->callAjax('plugins/tag/ajax/add_item_to_tag.php'); - } - - private function callAjax(string $path): void - { - ob_start(); - try { - include GLPI_ROOT . '/' . $path; - } catch (Exception $exception) { - ob_end_clean(); - throw $exception; - } + $tagItem = new PluginTagTagItem(); + $tagItem->add([ + 'plugin_tag_tags_id' => $tag, + 'itemtype' => Computer::class, + 'items_id' => $computer->getID(), + ]); - ob_end_clean(); + $this->isItemTagged($computer, $tag); } - } From 8d56cffbd92e56da51c825a635c0f5afee56eec2 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 21 Aug 2026 15:46:37 +0200 Subject: [PATCH 04/15] rector --- tests/Units/TagItemTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index a8a0a07..2f8b203 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -32,7 +32,6 @@ use Computer; use GlpiPlugin\Tag\Tests\TagTestCase; -use PluginTagTag; use PluginTagTagItem; use Ticket; From f55e666fdf0dff5c473855c43590c531866bac3c Mon Sep 17 00:00:00 2001 From: Herafia Date: Mon, 24 Aug 2026 11:49:09 +0200 Subject: [PATCH 05/15] refracto --- ajax/add_item_to_tag.php | 70 --------------------- composer.json | 1 + inc/tagitem.class.php | 2 +- src/Controller/TagItemController.php | 91 ++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 71 deletions(-) delete mode 100644 ajax/add_item_to_tag.php create mode 100644 src/Controller/TagItemController.php diff --git a/ajax/add_item_to_tag.php b/ajax/add_item_to_tag.php deleted file mode 100644 index 1ab6624..0000000 --- a/ajax/add_item_to_tag.php +++ /dev/null @@ -1,70 +0,0 @@ -. - * ------------------------------------------------------------------------- - * @copyright Copyright (C) 2014-2026 by Teclib'. - * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html - * @link https://github.com/pluginsGLPI/tag - * ------------------------------------------------------------------------- - */ - -use Glpi\Exception\Http\BadRequestHttpException; -use Glpi\Exception\Http\AccessDeniedHttpException; - -Session::checkLoginUser(); - -if (!isset($_POST['plugin_tag_tags_id'], $_POST['itemtype'], $_POST['items_id'])) { - throw new BadRequestHttpException(__s('Missing parameters', 'tag')); -} - -$tag = new PluginTagTag(); -if (!$tag->getFromDB($_POST['plugin_tag_tags_id']) || !$tag->can($tag->getID(), UPDATE)) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); -} - -$itemtype = $_POST['itemtype']; -if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { - throw new BadRequestHttpException(__s('Invalid item type', 'tag')); -} - -$item = new $itemtype(); -if (!$item->getFromDB($_POST['items_id']) || !$item->canUpdateItem()) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); -} - -$tag_item = new PluginTagTagItem(); -$found = $tag_item->find([ - 'plugin_tag_tags_id' => $tag->getID(), - 'items_id' => $item->getID(), - 'itemtype' => $itemtype, -]); - -if (count($found) === 0) { - $tag_item->add([ - 'plugin_tag_tags_id' => $tag->getID(), - 'items_id' => $item->getID(), - 'itemtype' => $itemtype, - ]); -} - -Html::back(); diff --git a/composer.json b/composer.json index f1e2ad1..7627874 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ }, "autoload": { "psr-4": { + "GlpiPlugin\\Tag\\": "src/", "GlpiPlugin\\Tag\\Tests\\": "tests" } }, diff --git a/inc/tagitem.class.php b/inc/tagitem.class.php index 0cc4266..382f6f9 100644 --- a/inc/tagitem.class.php +++ b/inc/tagitem.class.php @@ -197,7 +197,7 @@ public static function showForTag(PluginTagTag $tag) if ($canedit) { echo "
"; echo ""; + action='/plugins/tag/associate'>"; echo "
" . __s('Add an item') . "
"; echo ""; diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php new file mode 100644 index 0000000..c9013d3 --- /dev/null +++ b/src/Controller/TagItemController.php @@ -0,0 +1,91 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2014-2026 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/tag + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Tag\Controller; + +use CommonDBTM; +use Glpi\Controller\GenericFormController; +use Glpi\Exception\Http\AccessDeniedHttpException; +use Glpi\Exception\Http\BadRequestHttpException; +use Html; +use PluginTagTag; +use PluginTagTagItem; +use Session; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class TagItemController extends GenericFormController +{ + #[Route('/associate', methods: ['POST'])] + public function associate(Request $request): Response + { + Session::checkLoginUser(); + + $tag_id = $request->request->getInt('plugin_tag_tags_id'); + $itemtype = $request->request->get('itemtype'); + $item_id = $request->request->getInt('items_id'); + + if (!$tag_id || !$itemtype || !$item_id) { + throw new BadRequestHttpException(__s('Missing parameters', 'tag')); + } + + $tag = new PluginTagTag(); + if (!$tag->getFromDB($tag_id) || !$tag->can($tag_id, UPDATE)) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); + } + + if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { + throw new BadRequestHttpException(__s('Invalid item type', 'tag')); + } + + $item = new $itemtype(); + if (!$item->getFromDB($item_id) || !$item->canUpdateItem()) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); + } + + $tag_item = new PluginTagTagItem(); + $found = $tag_item->find([ + 'plugin_tag_tags_id' => $tag_id, + 'items_id' => $item_id, + 'itemtype' => $itemtype, + ]); + + if (count($found) === 0) { + $tag_item->add([ + 'plugin_tag_tags_id' => $tag_id, + 'items_id' => $item_id, + 'itemtype' => $itemtype, + ]); + } + + Html::back(); + } +} From 12bfeb9167867374738fe61ff87543ebe917aac5 Mon Sep 17 00:00:00 2001 From: Herafia Date: Mon, 24 Aug 2026 14:41:37 +0200 Subject: [PATCH 06/15] fix --- tests/TagTestCase.php | 14 +++++--------- tests/Units/TagItemTest.php | 3 +-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/TagTestCase.php b/tests/TagTestCase.php index d48b261..e0b2c96 100644 --- a/tests/TagTestCase.php +++ b/tests/TagTestCase.php @@ -74,15 +74,11 @@ public function loginAs(array $credentials, int $rights = CREATE | UPDATE | PURG public function createTag(string $tagName, array $typeMenu = ['Ticket']): int { - $tag = new PluginTagTag(); - $tag->add( - [ - 'name' => $tagName, - 'is_active' => 1, - 'type_menu' => $typeMenu, - ], - ); - $this->assertGreaterThan(0, $tag->getID()); + $tag = $this->createItem(PluginTagTag::class, [ + 'name' => $tagName, + 'is_active' => 1, + 'type_menu' => $typeMenu, + ], ['type_menu']); return $tag->getID(); } diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 2f8b203..76fcf2b 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -69,8 +69,7 @@ public function testTagAssociationCreatesLink(): void 'entities_id' => 0, ]); - $tagItem = new PluginTagTagItem(); - $tagItem->add([ + $this->createItem(PluginTagTagItem::class, [ 'plugin_tag_tags_id' => $tag, 'itemtype' => Computer::class, 'items_id' => $computer->getID(), From d183753c85d41f130626a88e6e16d88be1628fb7 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 28 Aug 2026 15:52:42 +0200 Subject: [PATCH 07/15] fix --- src/Controller/TagItemController.php | 3 ++- tests/Units/TagItemTest.php | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php index c9013d3..4e8cf56 100644 --- a/src/Controller/TagItemController.php +++ b/src/Controller/TagItemController.php @@ -34,6 +34,7 @@ use Glpi\Controller\GenericFormController; use Glpi\Exception\Http\AccessDeniedHttpException; use Glpi\Exception\Http\BadRequestHttpException; +use Glpi\Http\RedirectResponse; use Html; use PluginTagTag; use PluginTagTagItem; @@ -86,6 +87,6 @@ public function associate(Request $request): Response ]); } - Html::back(); + return new RedirectResponse(Html::getBackUrl()); } } diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 76fcf2b..de9a90c 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -31,8 +31,9 @@ namespace GlpiPlugin\Tag\Tests\Units; use Computer; +use GlpiPlugin\Tag\Controller\TagItemController; use GlpiPlugin\Tag\Tests\TagTestCase; -use PluginTagTagItem; +use Symfony\Component\HttpFoundation\Request; use Ticket; final class TagItemTest extends TagTestCase @@ -69,12 +70,15 @@ public function testTagAssociationCreatesLink(): void 'entities_id' => 0, ]); - $this->createItem(PluginTagTagItem::class, [ + $controller = new TagItemController(); + $request = Request::create('/plugins/tag/associate', 'POST', [ 'plugin_tag_tags_id' => $tag, - 'itemtype' => Computer::class, - 'items_id' => $computer->getID(), + 'itemtype' => Computer::class, + 'items_id' => $computer->getID(), ]); + $controller->associate($request); + $this->isItemTagged($computer, $tag); } } From c4e4bf3dc2b14dc461202a2d23273a1b35892aae Mon Sep 17 00:00:00 2001 From: Laura <35998899+Herafia@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:15:47 +0200 Subject: [PATCH 08/15] Update inc/tagitem.class.php Co-authored-by: Stanislas --- inc/tagitem.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/tagitem.class.php b/inc/tagitem.class.php index 382f6f9..e1c584a 100644 --- a/inc/tagitem.class.php +++ b/inc/tagitem.class.php @@ -197,7 +197,7 @@ public static function showForTag(PluginTagTag $tag) if ($canedit) { echo "
"; echo ""; + action='" . Toolbox::getItemTypeFormURL('PluginTagTagItem') . "'>"; echo "
" . __s('Add an item') . "
"; echo ""; From 2e06758ab366be5945f8edfb706ac447d437039e Mon Sep 17 00:00:00 2001 From: Laura <35998899+Herafia@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:01 +0200 Subject: [PATCH 09/15] Update inc/tagitem.class.php Co-authored-by: Stanislas --- inc/tagitem.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/tagitem.class.php b/inc/tagitem.class.php index e1c584a..8b35b1b 100644 --- a/inc/tagitem.class.php +++ b/inc/tagitem.class.php @@ -223,7 +223,7 @@ public static function showForTag(PluginTagTag $tag) ]); echo ""; echo "
" . __s('Add an item') . "
"; echo sprintf("", $instID); - echo ""; + echo ""; echo "
"; From 1bdff15c0adae4f51caa8a5f983fa544604f18df Mon Sep 17 00:00:00 2001 From: Laura <35998899+Herafia@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:10 +0200 Subject: [PATCH 10/15] Update src/Controller/TagItemController.php Co-authored-by: Stanislas --- src/Controller/TagItemController.php | 62 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php index 4e8cf56..4f57bf0 100644 --- a/src/Controller/TagItemController.php +++ b/src/Controller/TagItemController.php @@ -46,47 +46,49 @@ final class TagItemController extends GenericFormController { #[Route('/associate', methods: ['POST'])] - public function associate(Request $request): Response +public function associate(Request $request): Response { - Session::checkLoginUser(); + if ($request->query->getInt('associate') === 1) { + Session::checkLoginUser(); - $tag_id = $request->request->getInt('plugin_tag_tags_id'); - $itemtype = $request->request->get('itemtype'); - $item_id = $request->request->getInt('items_id'); + $tag_id = $request->request->getInt('plugin_tag_tags_id'); + $itemtype = $request->request->get('itemtype'); + $item_id = $request->request->getInt('items_id'); - if (!$tag_id || !$itemtype || !$item_id) { - throw new BadRequestHttpException(__s('Missing parameters', 'tag')); - } - - $tag = new PluginTagTag(); - if (!$tag->getFromDB($tag_id) || !$tag->can($tag_id, UPDATE)) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); - } + if (!$tag_id || !$itemtype || !$item_id) { + throw new BadRequestHttpException(__s('Missing parameters', 'tag')); + } - if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { - throw new BadRequestHttpException(__s('Invalid item type', 'tag')); - } + $tag = new PluginTagTag(); + if (!$tag->getFromDB($tag_id) || !$tag->can($tag_id, UPDATE)) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); + } - $item = new $itemtype(); - if (!$item->getFromDB($item_id) || !$item->canUpdateItem()) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); - } + if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { + throw new BadRequestHttpException(__s('Invalid item type', 'tag')); + } - $tag_item = new PluginTagTagItem(); - $found = $tag_item->find([ - 'plugin_tag_tags_id' => $tag_id, - 'items_id' => $item_id, - 'itemtype' => $itemtype, - ]); + $item = new $itemtype(); + if (!$item->getFromDB($item_id) || !$item->canUpdateItem()) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); + } - if (count($found) === 0) { - $tag_item->add([ + $tag_item = new PluginTagTagItem(); + $found = $tag_item->find([ 'plugin_tag_tags_id' => $tag_id, 'items_id' => $item_id, 'itemtype' => $itemtype, ]); - } - return new RedirectResponse(Html::getBackUrl()); + if (count($found) === 0) { + $tag_item->add([ + 'plugin_tag_tags_id' => $tag_id, + 'items_id' => $item_id, + 'itemtype' => $itemtype, + ]); + } + + return new RedirectResponse(Html::getBackUrl()); + } } } From c177d3187a85ad25f59abebc07fa8af4125085ac Mon Sep 17 00:00:00 2001 From: Laura <35998899+Herafia@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:19 +0200 Subject: [PATCH 11/15] Update src/Controller/TagItemController.php Co-authored-by: Stanislas --- src/Controller/TagItemController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php index 4f57bf0..fa5f1bb 100644 --- a/src/Controller/TagItemController.php +++ b/src/Controller/TagItemController.php @@ -45,7 +45,7 @@ final class TagItemController extends GenericFormController { - #[Route('/associate', methods: ['POST'])] + #[ItemtypeFormRoute(PluginTagTagItem::class)] public function associate(Request $request): Response { if ($request->query->getInt('associate') === 1) { From 6e288325f9190ccb854485761e4fee2d04e233a6 Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 4 Sep 2026 16:45:51 +0200 Subject: [PATCH 12/15] fix --- src/Controller/TagItemController.php | 64 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php index fa5f1bb..6a7a571 100644 --- a/src/Controller/TagItemController.php +++ b/src/Controller/TagItemController.php @@ -39,56 +39,54 @@ use PluginTagTag; use PluginTagTagItem; use Session; +use Glpi\Routing\Attribute\ItemtypeFormRoute; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; final class TagItemController extends GenericFormController { #[ItemtypeFormRoute(PluginTagTagItem::class)] -public function associate(Request $request): Response + public function associate(Request $request): Response { - if ($request->query->getInt('associate') === 1) { - Session::checkLoginUser(); + Session::checkLoginUser(); - $tag_id = $request->request->getInt('plugin_tag_tags_id'); - $itemtype = $request->request->get('itemtype'); - $item_id = $request->request->getInt('items_id'); + $tag_id = $request->request->getInt('plugin_tag_tags_id'); + $itemtype = $request->request->get('itemtype'); + $item_id = $request->request->getInt('items_id'); - if (!$tag_id || !$itemtype || !$item_id) { - throw new BadRequestHttpException(__s('Missing parameters', 'tag')); - } + if (!$tag_id || !$itemtype || !$item_id) { + throw new BadRequestHttpException(__s('Missing parameters', 'tag')); + } + + $tag = new PluginTagTag(); + if (!$tag->getFromDB($tag_id) || !$tag->can($tag_id, UPDATE)) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); + } - $tag = new PluginTagTag(); - if (!$tag->getFromDB($tag_id) || !$tag->can($tag_id, UPDATE)) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this tag', 'tag')); - } + if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { + throw new BadRequestHttpException(__s('Invalid item type', 'tag')); + } - if (!is_a($itemtype, CommonDBTM::class, true) || !PluginTagTag::canItemtype($itemtype)) { - throw new BadRequestHttpException(__s('Invalid item type', 'tag')); - } + $item = new $itemtype(); + if (!$item->getFromDB($item_id) || !$item->canUpdateItem()) { + throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); + } - $item = new $itemtype(); - if (!$item->getFromDB($item_id) || !$item->canUpdateItem()) { - throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); - } + $tag_item = new PluginTagTagItem(); + $found = $tag_item->find([ + 'plugin_tag_tags_id' => $tag_id, + 'items_id' => $item_id, + 'itemtype' => $itemtype, + ]); - $tag_item = new PluginTagTagItem(); - $found = $tag_item->find([ + if (count($found) === 0) { + $tag_item->add([ 'plugin_tag_tags_id' => $tag_id, 'items_id' => $item_id, 'itemtype' => $itemtype, ]); - - if (count($found) === 0) { - $tag_item->add([ - 'plugin_tag_tags_id' => $tag_id, - 'items_id' => $item_id, - 'itemtype' => $itemtype, - ]); - } - - return new RedirectResponse(Html::getBackUrl()); } + + return new RedirectResponse(Html::getBackUrl()); } } From 65c739b5fb4a5dc2ce4104def61bc516bf325ee9 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 6 Oct 2026 10:39:57 +0200 Subject: [PATCH 13/15] test to fix tests --- tests/TagTestCase.php | 2 +- tests/Units/TagItemTest.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/TagTestCase.php b/tests/TagTestCase.php index e0b2c96..34b520f 100644 --- a/tests/TagTestCase.php +++ b/tests/TagTestCase.php @@ -46,7 +46,7 @@ protected function logOut() $_SESSION['glpi_currenttime'] = $ctime; } - public function loginAs(array $credentials, int $rights = CREATE | UPDATE | PURGE): int + public function loginAs(array $credentials, int $rights = READ | CREATE | UPDATE | PURGE): int { global $DB; diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index de9a90c..34bfeba 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -42,6 +42,8 @@ final class TagItemTest extends TagTestCase public function testTagsFromTicket(): void { + $this->loginAs(self::TECH_USER); + $tagID1 = $this->createTag('TicketTag1'); $tagID2 = $this->createTag('TicketTag2'); From 9eec96e7f1bd2322d8e81d0724f81b786cd2c557 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 7 Oct 2026 15:28:35 +0200 Subject: [PATCH 14/15] fix tag entity --- src/Controller/TagItemController.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Controller/TagItemController.php b/src/Controller/TagItemController.php index 6a7a571..800aaa4 100644 --- a/src/Controller/TagItemController.php +++ b/src/Controller/TagItemController.php @@ -72,6 +72,17 @@ public function associate(Request $request): Response throw new AccessDeniedHttpException(__s('You do not have permission to update this item', 'tag')); } + if ( + isset($item->fields['entities_id']) + && $tag->fields['entities_id'] != $item->fields['entities_id'] + && ( + !$tag->fields['is_recursive'] + || !in_array($tag->fields['entities_id'], getAncestorsOf('glpi_entities', (int) $item->fields['entities_id'])) + ) + ) { + throw new AccessDeniedHttpException(__s('This tag is not available in the item entity', 'tag')); + } + $tag_item = new PluginTagTagItem(); $found = $tag_item->find([ 'plugin_tag_tags_id' => $tag_id, From 222ddce8b213424e07e9f95a7be76bc3ac659c7b Mon Sep 17 00:00:00 2001 From: Herafia Date: Fri, 9 Oct 2026 16:42:08 +0200 Subject: [PATCH 15/15] fix + add test --- tests/Units/TagItemTest.php | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/Units/TagItemTest.php b/tests/Units/TagItemTest.php index 34bfeba..f0c7066 100644 --- a/tests/Units/TagItemTest.php +++ b/tests/Units/TagItemTest.php @@ -31,8 +31,10 @@ namespace GlpiPlugin\Tag\Tests\Units; use Computer; +use Glpi\Exception\Http\AccessDeniedHttpException; use GlpiPlugin\Tag\Controller\TagItemController; use GlpiPlugin\Tag\Tests\TagTestCase; +use PluginTagTag; use Symfony\Component\HttpFoundation\Request; use Ticket; @@ -62,6 +64,39 @@ public function testTagsFromTicket(): void $this->isItemTagged($ticket, $tagID2); } + public function testTagOutOfEntityScopeIsNotLinked(): void + { + $this->login(); + + $out_of_scope_entity = getItemByTypeName('Entity', '_test_child_2', true); + $tag = new PluginTagTag(); + $tag->add([ + 'name' => 'OutOfScopeTag', + 'is_active' => 1, + 'type_menu' => ['Ticket'], + 'entities_id' => $out_of_scope_entity, + 'is_recursive' => 0, + ]); + $tagID = $tag->getID(); + $this->assertGreaterThan(0, $tagID); + + $this->setEntity('_test_child_1', false); + + $ticket = new Ticket(); + $ticket->add([ + 'name' => 'Ticket out of scope tag', + 'content' => 'Ticket out of scope tag', + 'entities_id' => getItemByTypeName('Entity', '_test_child_1', true), + '_plugin_tag_tag_process_form' => 1, + '_plugin_tag_tag_values' => [ + $tagID, + ], + ]); + $this->assertGreaterThan(0, $ticket->getID()); + + $this->isItemNotTagged($ticket, $tagID); + } + public function testTagAssociationCreatesLink(): void { $this->loginAs(self::TECH_USER); @@ -83,4 +118,36 @@ public function testTagAssociationCreatesLink(): void $this->isItemTagged($computer, $tag); } + + //test takes a non-recursive tag associated with _test_child_2 and a computer in _test_child_1. It calls + //associate() with these two elements and waits for an exception. It logs in as a superadmin with the root entity + //set to recursive, to ensure that it is indeed the entity validation that is causing the block, and not a lack of + // permissions. + public function testTagAssociationOutOfEntityScopeIsDenied(): void + { + $this->login(); + $this->setEntity('_test_root_entity', true); + + $tag = $this->createItem(PluginTagTag::class, [ + 'name' => 'OutOfScopeTag', + 'is_active' => 1, + 'type_menu' => ['Computer'], + 'entities_id' => getItemByTypeName('Entity', '_test_child_2', true), + 'is_recursive' => 0, + ], ['type_menu']); + $computer = $this->createItem(Computer::class, [ + 'name' => 'Computer out of tag scope', + 'entities_id' => getItemByTypeName('Entity', '_test_child_1', true), + ]); + + $controller = new TagItemController(); + $request = Request::create('/plugins/tag/associate', 'POST', [ + 'plugin_tag_tags_id' => $tag->getID(), + 'itemtype' => Computer::class, + 'items_id' => $computer->getID(), + ]); + + $this->expectException(AccessDeniedHttpException::class); + $controller->associate($request); + } }