From 33deff5af60eb3d25e8944ef8faf16a758a80a6d Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Mon, 2 Feb 2026 10:25:10 +0100 Subject: [PATCH 1/7] fix(notificationtarget): handle duplicate targets when deleting notification recipients --- src/NotificationTarget.php | 23 +++++---- tests/functional/NotificationTargetTest.php | 52 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/NotificationTarget.php b/src/NotificationTarget.php index 8ece51243a0..8f5f3e951c7 100644 --- a/src/NotificationTarget.php +++ b/src/NotificationTarget.php @@ -213,17 +213,22 @@ public static function getIcon() */ public function getFromDBForTarget($notifications_id, $type, $ID) { + $targets = $this->find([ + 'notifications_id' => $notifications_id, + 'items_id' => $ID, + 'type' => $type, + ]); - if ( - $this->getFromDBByCrit([ - static::getTable() . '.notifications_id' => $notifications_id, - static::getTable() . '.items_id' => $ID, - static::getTable() . '.type' => $type, - ]) - ) { - return true; + if (count($targets) === 0) { + return false; } - return false; + + $target = array_shift($targets); + foreach ($targets as $duplicated_target) { + $this->delete(['id' => $duplicated_target['id']]); + } + $this->getFromDB($target['id']); + return true; } /** diff --git a/tests/functional/NotificationTargetTest.php b/tests/functional/NotificationTargetTest.php index 1fd67a5385d..cde4f7716a4 100644 --- a/tests/functional/NotificationTargetTest.php +++ b/tests/functional/NotificationTargetTest.php @@ -677,4 +677,56 @@ public function testDefaultTargets() $this->assertSame($has_admin_target, array_key_exists('1_1', $notification_target->notification_targets)); } } + + public function testUpdateTargetsWithDuplicates() + { + $this->login(); + + $notification = $this->createItem(Notification::class, [ + 'name' => 'Test notification', + 'itemtype' => \Ticket::class, + 'event' => 'new', + 'is_active' => 1, + 'entities_id' => 0, + ]); + + $this->createItems(NotificationTarget::class, [ + [ + 'notifications_id' => $notification->getID(), + 'type' => Notification::USER_TYPE, + 'items_id' => 3, + ], + [ + 'notifications_id' => $notification->getID(), + 'type' => Notification::USER_TYPE, + 'items_id' => 3, + ], + ]); + + $targets = getAllDataFromTable( + NotificationTarget::getTable(), + [ + 'notifications_id' => $notification->getID(), + 'type' => Notification::USER_TYPE, + 'items_id' => 3, + ] + ); + $this->assertCount(2, $targets); + + NotificationTarget::updateTargets([ + 'itemtype' => \Ticket::class, + 'notifications_id' => $notification->getID(), + '_targets' => [], + ]); + + $targets = getAllDataFromTable( + NotificationTarget::getTable(), + [ + 'notifications_id' => $notification->getID(), + 'type' => Notification::USER_TYPE, + 'items_id' => 3, + ] + ); + $this->assertCount(0, $targets); + } } From 7023e98febaef6975f975a411dad43489e1c25f0 Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Wed, 4 Feb 2026 10:38:01 +0100 Subject: [PATCH 2/7] add unique key to glpi_notificationtargets --- install/empty_data.php | 5 -- .../migrations/update_11.0.5_to_11.0.6.php | 72 +++++++++++++++++++ .../notificationtargets.php | 45 ++++++++++++ install/mysql/glpi-empty.sql | 1 + src/NotificationTarget.php | 23 +++--- tests/functional/NotificationTargetTest.php | 51 ------------- 6 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 install/migrations/update_11.0.5_to_11.0.6.php create mode 100644 install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php diff --git a/install/empty_data.php b/install/empty_data.php index b687dbb2ed8..addce9ca3cd 100644 --- a/install/empty_data.php +++ b/install/empty_data.php @@ -4606,11 +4606,6 @@ public function getEmptyData(): array 'items_id' => '1', 'type' => '1', 'notifications_id' => '70', - ], [ - 'id' => '187', - 'items_id' => '1', - 'type' => '1', - 'notifications_id' => '71', ], [ 'id' => '188', 'items_id' => '1', diff --git a/install/migrations/update_11.0.5_to_11.0.6.php b/install/migrations/update_11.0.5_to_11.0.6.php new file mode 100644 index 00000000000..e14cb07638b --- /dev/null +++ b/install/migrations/update_11.0.5_to_11.0.6.php @@ -0,0 +1,72 @@ +. + * + * --------------------------------------------------------------------- + */ + +use function Safe\preg_match; +use function Safe\scandir; + +/** + * Update from 11.0.5 to 11.0.6 + * + * @return bool for success (will die for most error) + **/ +function update1105to1106() +{ + /** + * @var DBmysql $DB + * @var Migration $migration + */ + global $DB, $migration; + + $updateresult = true; + $ADDTODISPLAYPREF = []; + $DELFROMDISPLAYPREF = []; + $update_dir = __DIR__ . '/update_11.0.5_to_11.0.6/'; + + $migration->setVersion('11.0.6'); + + $update_scripts = scandir($update_dir); + foreach ($update_scripts as $update_script) { + if (preg_match('/\.php$/', $update_script) !== 1) { + continue; + } + require $update_dir . $update_script; + } + + // ************ Keep it at the end ************** + $migration->updateDisplayPrefs($ADDTODISPLAYPREF, $DELFROMDISPLAYPREF); + + $migration->executeMigration(); + + return $updateresult; +} diff --git a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php new file mode 100644 index 00000000000..70eaefe6f98 --- /dev/null +++ b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php @@ -0,0 +1,45 @@ +. + * + * --------------------------------------------------------------------- + */ + +/** + * @var DBmysql $DB + * @var Migration $migration + */ + +// Remove duplicate target before adding unique key +$DB->delete('glpi_notificationtargets', [ + 'id' => 187, +]); + +$migration->addKey('glpi_notificationtargets', ['notifications_id', 'items_id', 'type'], 'unicity', 'UNIQUE'); diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index 4521f392153..f6f3eb66c3e 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -5075,6 +5075,7 @@ CREATE TABLE `glpi_notificationtargets` ( `notifications_id` int unsigned NOT NULL DEFAULT '0', `is_exclusion` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`notifications_id`,`items_id`,`type`), KEY `items` (`type`,`items_id`), KEY `notifications_id` (`notifications_id`), KEY `is_exclusion` (`is_exclusion`) diff --git a/src/NotificationTarget.php b/src/NotificationTarget.php index 8f5f3e951c7..8ece51243a0 100644 --- a/src/NotificationTarget.php +++ b/src/NotificationTarget.php @@ -213,22 +213,17 @@ public static function getIcon() */ public function getFromDBForTarget($notifications_id, $type, $ID) { - $targets = $this->find([ - 'notifications_id' => $notifications_id, - 'items_id' => $ID, - 'type' => $type, - ]); - - if (count($targets) === 0) { - return false; - } - $target = array_shift($targets); - foreach ($targets as $duplicated_target) { - $this->delete(['id' => $duplicated_target['id']]); + if ( + $this->getFromDBByCrit([ + static::getTable() . '.notifications_id' => $notifications_id, + static::getTable() . '.items_id' => $ID, + static::getTable() . '.type' => $type, + ]) + ) { + return true; } - $this->getFromDB($target['id']); - return true; + return false; } /** diff --git a/tests/functional/NotificationTargetTest.php b/tests/functional/NotificationTargetTest.php index cde4f7716a4..770b4a0dcd1 100644 --- a/tests/functional/NotificationTargetTest.php +++ b/tests/functional/NotificationTargetTest.php @@ -678,55 +678,4 @@ public function testDefaultTargets() } } - public function testUpdateTargetsWithDuplicates() - { - $this->login(); - - $notification = $this->createItem(Notification::class, [ - 'name' => 'Test notification', - 'itemtype' => \Ticket::class, - 'event' => 'new', - 'is_active' => 1, - 'entities_id' => 0, - ]); - - $this->createItems(NotificationTarget::class, [ - [ - 'notifications_id' => $notification->getID(), - 'type' => Notification::USER_TYPE, - 'items_id' => 3, - ], - [ - 'notifications_id' => $notification->getID(), - 'type' => Notification::USER_TYPE, - 'items_id' => 3, - ], - ]); - - $targets = getAllDataFromTable( - NotificationTarget::getTable(), - [ - 'notifications_id' => $notification->getID(), - 'type' => Notification::USER_TYPE, - 'items_id' => 3, - ] - ); - $this->assertCount(2, $targets); - - NotificationTarget::updateTargets([ - 'itemtype' => \Ticket::class, - 'notifications_id' => $notification->getID(), - '_targets' => [], - ]); - - $targets = getAllDataFromTable( - NotificationTarget::getTable(), - [ - 'notifications_id' => $notification->getID(), - 'type' => Notification::USER_TYPE, - 'items_id' => 3, - ] - ); - $this->assertCount(0, $targets); - } } From 748eff9c597b08c0dc2d308a95046213ac33e2a1 Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Wed, 4 Feb 2026 10:39:21 +0100 Subject: [PATCH 3/7] lint --- tests/functional/NotificationTargetTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/NotificationTargetTest.php b/tests/functional/NotificationTargetTest.php index 770b4a0dcd1..1fd67a5385d 100644 --- a/tests/functional/NotificationTargetTest.php +++ b/tests/functional/NotificationTargetTest.php @@ -677,5 +677,4 @@ public function testDefaultTargets() $this->assertSame($has_admin_target, array_key_exists('1_1', $notification_target->notification_targets)); } } - } From abcee4f352d6790900f15f8af047be627eaff978 Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Fri, 6 Feb 2026 15:12:58 +0100 Subject: [PATCH 4/7] review --- .../notificationtargets.php | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php index 70eaefe6f98..b7dffa6bb8e 100644 --- a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php +++ b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php @@ -37,9 +37,27 @@ * @var Migration $migration */ -// Remove duplicate target before adding unique key -$DB->delete('glpi_notificationtargets', [ - 'id' => 187, +// Remove duplicates targets +$duplicates_targets_iterator = $DB->request([ + 'SELECT' => [ + 'notifications_id', + 'items_id', + 'type', + 'MIN' => 'id AS min_id', + 'COUNT' => '* AS count' + ], + 'FROM' => 'glpi_notificationtargets', + 'GROUPBY' => ['notifications_id', 'items_id', 'type'], + 'HAVING' => ['count' => ['>', 1]] ]); +foreach ($duplicates_targets_iterator as $target) { + $DB->delete('glpi_notificationtargets', [ + 'notifications_id' => $target['notifications_id'], + 'items_id' => $target['items_id'], + 'type' => $target['type'], + 'id' => ['>', $target['min_id']] + ]); +} + $migration->addKey('glpi_notificationtargets', ['notifications_id', 'items_id', 'type'], 'unicity', 'UNIQUE'); From af5f582c9f8c95b9c0d58eef7a2902e370f7e0e5 Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Tue, 10 Feb 2026 16:40:01 +0100 Subject: [PATCH 5/7] fix ci --- install/empty_data.php | 12 ++++++------ .../update_11.0.5_to_11.0.6/notificationtargets.php | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/install/empty_data.php b/install/empty_data.php index addce9ca3cd..18d06687d18 100644 --- a/install/empty_data.php +++ b/install/empty_data.php @@ -4607,32 +4607,32 @@ public function getEmptyData(): array 'type' => '1', 'notifications_id' => '70', ], [ - 'id' => '188', + 'id' => '187', 'items_id' => '1', 'type' => '1', 'notifications_id' => '72', ], [ - 'id' => '189', + 'id' => '188', 'items_id' => '1', 'type' => '1', 'notifications_id' => '74', ], [ - 'id' => '190', + 'id' => '189', 'items_id' => '1', 'type' => '1', 'notifications_id' => '75', ], [ - 'id' => '191', + 'id' => '190', 'items_id' => '1', 'type' => '1', 'notifications_id' => '80', ], [ - 'id' => '192', + 'id' => '191', 'items_id' => '1', 'type' => '1', 'notifications_id' => '81', ], [ - 'id' => '193', + 'id' => '192', 'items_id' => '1', 'type' => '1', 'notifications_id' => '82', diff --git a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php index b7dffa6bb8e..b56866a05d9 100644 --- a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php +++ b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php @@ -44,11 +44,11 @@ 'items_id', 'type', 'MIN' => 'id AS min_id', - 'COUNT' => '* AS count' + 'COUNT' => '* AS count', ], 'FROM' => 'glpi_notificationtargets', 'GROUPBY' => ['notifications_id', 'items_id', 'type'], - 'HAVING' => ['count' => ['>', 1]] + 'HAVING' => ['count' => ['>', 1]], ]); foreach ($duplicates_targets_iterator as $target) { @@ -56,7 +56,7 @@ 'notifications_id' => $target['notifications_id'], 'items_id' => $target['items_id'], 'type' => $target['type'], - 'id' => ['>', $target['min_id']] + 'id' => ['>', $target['min_id']], ]); } From 3bb25e6b0ec2d55b5b793650ebdbc9e9f868415b Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Wed, 11 Feb 2026 08:57:28 +0100 Subject: [PATCH 6/7] Update install/mysql/glpi-empty.sql Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- install/mysql/glpi-empty.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index f6f3eb66c3e..1d6e4f73d13 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -5077,7 +5077,6 @@ CREATE TABLE `glpi_notificationtargets` ( PRIMARY KEY (`id`), UNIQUE KEY `unicity` (`notifications_id`,`items_id`,`type`), KEY `items` (`type`,`items_id`), - KEY `notifications_id` (`notifications_id`), KEY `is_exclusion` (`is_exclusion`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; From 9eff674aadae82734b1078766b3812cc5b7d9a6a Mon Sep 17 00:00:00 2001 From: MyuTsu Date: Wed, 11 Feb 2026 14:35:01 +0100 Subject: [PATCH 7/7] remove unused key from migration script --- .../migrations/update_11.0.5_to_11.0.6/notificationtargets.php | 1 + 1 file changed, 1 insertion(+) diff --git a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php index b56866a05d9..1970d51cfba 100644 --- a/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php +++ b/install/migrations/update_11.0.5_to_11.0.6/notificationtargets.php @@ -60,4 +60,5 @@ ]); } +$migration->dropKey('glpi_notificationtargets', 'notifications_id'); $migration->addKey('glpi_notificationtargets', ['notifications_id', 'items_id', 'type'], 'unicity', 'UNIQUE');