CVE-2024-25130: Insufficient Information (NVD-CWE-noinfo)
value: Tuleap is an open source suite to improve management of software developments and collaboration. Prior to version 15.5.99.76 of Tuleap Community Edition and prior to versions 15.5-4 and 15.4-7 of Tuleap Enterprise Edition, users with a read access to a tracker where the mass update feature is used might get access to restricted information. Tuleap Community Edition 15.5.99.76, Tuleap Enterprise Edition 15.5-4, and Tuleap Enterprise Edition 15.4-7 contain a patch for this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-02-22T19:15Z
Repository: https://github.com/Enalean/tuleap
Language: PHP
Files changed: plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php, plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php, plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js, plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po, plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po, plugins/tracker/templates/form-element/permissions_on_artifact_massupdate.mustache, plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php
Commit date: 2024-02-14 11:58:46 +0100
Commit message: fix: request #36803 Mass update clears permissions on artifact

An additional checkbox is added to control if the field should be
updated or not when doing a masschange.

Change-Id: I721298a0f5ec50c3aac1a59efcfba6166958baf0

Diff: diff --git a/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php b/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php
index e04322d4d5732..0cfc5a41ee002 100644
--- a/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php
+++ b/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php
@@ -34,14 +34,15 @@ use Tuleap\Tracker\REST\v1\TrackerFieldsRepresentations\PermissionsOnArtifacts;
 use Tuleap\User\UserGroup\NameTranslator;
 
 //phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace, Squiz.Classes.ValidClassName.NotCamelCaps
 class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElement_Field
 {
-    public const GRANTED_GROUPS     = 'granted_groups';
-    public const USE_IT             = 'use_artifact_permissions';
-    public const IS_USED_BY_DEFAULT = false;
-    public const PERMISSION_TYPE    = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
+    public const GRANTED_GROUPS      = 'granted_groups';
+    public const USE_IT              = 'use_artifact_permissions';
+    public const DO_MASS_UPDATE_FLAG = 'do_mass_update';
+    public const IS_USED_BY_DEFAULT  = false;
+    public const PERMISSION_TYPE     = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
 
     public $default_properties = [];
 
 
     /**
@@ -152,19 +153,22 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
         $is_checked  = ($this->getPermissionsValidator()->isArtifactPermissionChecked($value) === true);
 
         return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
     }
 
-    /**
-     * @return string
-     */
-    protected function fetchSubmitValueMasschange()
+    protected function fetchSubmitValueMasschange(): string
     {
+        $mass_change_input_html = TemplateRendererFactory::build()->getRenderer(__DIR__ . '/../../../templates/form-element/')
+            ->renderToString(
+                'permissions_on_artifact_massupdate',
+                ['field_id' => $this->getId(), 'flag_mass_update' => self::DO_MASS_UPDATE_FLAG]
+            );
+
         $is_checked  = false;
         $is_disabled = false;
 
-        return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
+        return $mass_change_input_html . $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
     }
 
     /**
      * Fetch the html code to display the field value in artifact
      *
@@ -260,19 +264,19 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
         $html .= $this->fetchUserGroupList($is_read_only, $changeset_values);
 
         return $html;
     }
 
-    private function fetchUserGroupList($is_read_only, array $changeset_values)
+    private function fetchUserGroupList($is_read_only, array $changeset_values): string
     {
         $field_id     = $this->getId();
         $element_name = 'artifact[' . $field_id . '][u_groups][]';
 
         $hp    = Codendi_HTMLPurifier::instance();
         $html  = '<select '
             . 'name="' . $hp->purify($element_name) . '" '
-            . 'id="' . $hp->purify(str_replace('[]', '', $element_name)) . '" '
+            . 'id="' . $hp->purify('artifact_' . $field_id . '_perms_ugroups' . ($is_read_only ? '_ro' : '')) . '" '
             . 'multiple '
             . 'size="8" '
             . (($this->isRequired()) ? 'required="required"' : '' )
             . (($is_read_only) ? 'disabled="disabled"' : '' )
             . '>';
@@ -850,36 +854,36 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
     }
 
     /**
      * @param bool $can_user_restrict_permissions_to_nobody
      * @param bool $disabled
-     *
-     * @return string
      */
-    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input)
+    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input): string
     {
         $empty_value_class = '';
         if ($is_expecting_input) {
             $empty_value_class = 'empty_value';
         }
 
-        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '">';
+        $field_id = Codendi_HTMLPurifier::instance()->purify($this->getId());
+
+        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '" data-field-id="' . $field_id . '">';
         if ($this->isRequired() == false) {
             if (! $disabled) {
-                $html .= '<input type="hidden" name="artifact[' . $this->getId() . '][use_artifact_permissions]" value="0" />';
+                $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="0" />';
             }
-            $html .= '<label class="checkbox" for="artifact_' . $this->getId() . '_use_artifact_permissions">';
-            $html .= '<input type="checkbox"
-                        name="artifact[' . $this->getId() . '][use_artifact_permissions]"
-                        id="artifact_' . $this->getId() . '_use_artifact_permissions"
+            $read_only_id = $disabled ? '_ro' : '';
+            $html        .= '<label class="checkbox" for="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '">';
+            $html        .= '<input type="checkbox"
+                        name="artifact[' . $field_id . '][use_artifact_permissions]"
+                        id="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '"
                         value="1" ' .
                 (($can_user_restrict_permissions_to_nobody == true) ? 'checked="checked"' : '') .
                 (($disabled == true) ? 'disabled="disabled"' : '') .
                 '/>';
         } else {
-            $html .= '<input type="hidden" name="artifact[' . $this->getId(
-            ) . '][use_artifact_permissions]" value="1" />';
+            $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="1" />';
         }
 
         $html .= dgettext('tuleap-tracker', 'Restrict access to this artifact for the following user groups:') . '</label>';
         $html .= '</p>';
 
diff --git a/plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php b/plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php
index 0ad817786657a..397a11197c737 100644
--- a/plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php
+++ b/plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php
@@ -48,10 +48,13 @@ class Tracker_MasschangeDataValueExtractor
     private function hasDataChanged(int $field_id, $data): bool
     {
         $field = $this->form_element_factory->getFieldById($field_id);
         if ($field instanceof Tracker_FormElement_Field_List) {
             return $this->isValueInData($data, (string) BindStaticValueUnchanged::VALUE_ID);
+        } elseif ($field instanceof Tracker_FormElement_Field_PermissionsOnArtifact) {
+            return isset($data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG]) &&
+                $data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG] === '1';
         }
 
         return $this->isValueInData($data, $GLOBALS['Language']->getText('global', 'unchanged'));
     }
 
diff --git a/plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js b/plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js
index b7ff37ef92c31..5652b9caf5556 100644
--- a/plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js
+++ b/plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js
@@ -1,58 +1,65 @@
 /**
+ * Copyright (c) Enalean, 2024 - present. All Rights Reserved.
  * Copyright (c) Xerox Corporation, Codendi Team, 2001-2008. All rights reserved
  *
  * Originally written by Nicolas Terray, 2008
  *
- * This file is a part of Codendi.
+ * This file is a part of Tuleap.
  *
- * Codendi is free software; you can redistribute it and/or modify
+ * Tuleap is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
  * (at your option) any later version.
  *
- * Codendi is distributed in the hope that it will be useful,
+ * Tuleap is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with Codendi; if not, write to the Free Software
+ * along with Tuleap; if not, write to the Free Software
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  *
  *
  */
 
-/* global jQuery:readonly */
-
-(function ($) {
+document.addEventListener("DOMContentLoaded", function () {
     function init() {
-        $(".tracker_field_permissionsonartifact").each(function (index, element) {
-            disablePermissionsFieldIfNeeded(element);
-            bindSwitch(element);
-        });
+        const elements = document.getElementsByClassName("tracker_field_permissionsonartifact");
+        for (const element of elements) {
+            const field_id = element.dataset.fieldId;
+            const select_ugroups = document.getElementById(`artifact_${field_id}_perms_ugroups`);
+            const checkbox_use_perms = document.getElementById(
+                `artifact_${field_id}_use_artifact_permissions`,
+            );
+            if (select_ugroups === null || checkbox_use_perms === null) {
+                continue;
+            }
+            disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups);
+            bindSwitch(checkbox_use_perms, select_ugroups);
+            const checkbox_activate_masschange_update = document.getElementById(
+                `artifact_${field_id}_perms_mass_update`,
+            );
+            if (checkbox_activate_masschange_update) {
+                checkbox_use_perms.disabled = true;
+                checkbox_activate_masschange_update.addEventListener("change", () => {
+                    checkbox_use_perms.disabled = !checkbox_activate_masschange_update.checked;
+                });
+            }
+        }
     }
 
-    function disablePermissionsFieldIfNeeded(element) {
-        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
-            $(element).siblings("select").attr("disabled", "disabled");
+    function disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups) {
+        if (!checkbox_use_perms.checked) {
+            select_ugroups.disabled = true;
         }
     }
 
-    function bindSwitch(element) {
-        $("input[type='checkbox']", element).bind("change", function () {
-            togglePermissionsField(element);
+    function bindSwitch(checkbox_use_perms, select_ugroups) {
+        checkbox_use_perms.addEventListener("change", () => {
+            select_ugroups.disabled = !checkbox_use_perms.checked;
         });
     }
 
-    function togglePermissionsField(element) {
-        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
-            $(element).siblings("select").attr("disabled", "disabled");
-        } else {
-            $(element).siblings("select").removeAttr("disabled");
-        }
-    }
-
-    $(document).ready(function () {
-        init();
-    });
-})(jQuery);
+    init();
+});
diff --git a/plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po b/plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po
index f03290328e66b..834e065eb617f 100644
--- a/plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po
+++ b/plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po
@@ -4292,10 +4292,13 @@ msgid "Update reminder"
 msgstr "Modifier le rappel"
 
 msgid "Update semantic"
 msgstr "Mettre à jour la sémantique"
 
+msgid "Update the permissions, by default nothing is changed"
+msgstr "Mettre à jour les permissions, par défaut rien ne change"
+
 msgid "Update to overridable computed field"
 msgstr "Mettre à jour vers champ calculé modifiable"
 
 msgid "Update type"
 msgstr "Mettre à jour"
diff --git a/plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po b/plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po
index 125e685a205c2..6e5ba3276d808 100644
--- a/plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po
+++ b/plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po
@@ -3784,10 +3784,13 @@ msgid "Update reminder"
 msgstr ""
 
 msgid "Update semantic"
 msgstr ""
 
+msgid "Update the permissions, by default nothing is changed"
+msgstr ""
+
 msgid "Update to overridable computed field"
 msgstr ""
 
 msgid "Update type"
 msgstr ""
diff --git a/plugins/tracker/templates/form-element/permissions_on_artifact_massupdate.mustache b/plugins/tracker/templates/form-element/permissions_on_artifact_massupdate.mustache
new file mode 100644
index 0000000000000..d803199ee9807
--- /dev/null
+++ b/plugins/tracker/templates/form-element/permissions_on_artifact_massupdate.mustache
@@ -0,0 +1,8 @@
+<label class="checkbox" for="artifact_{{ field_id }}_perms_mass_update">
+    <input type="checkbox"
+        name="artifact[{{ field_id }}][{{ flag_mass_update }}]"
+        id="artifact_{{ field_id }}_perms_mass_update"
+        value="1"
+    />
+    {{# dgettext }} tuleap-tracker | Update the permissions, by default nothing is changed {{/ dgettext }}
+</label>
diff --git a/plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php b/plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php
index 4b57236d54501..c462328ed751d 100644
--- a/plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php
+++ b/plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php
@@ -20,60 +20,77 @@
 
 declare(strict_types=1);
 
 namespace Tuleap\Tracker;
 
-use Mockery;
-use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
 use Tracker_FormElement_Field_List;
+use Tracker_FormElement_Field_PermissionsOnArtifact;
 use Tracker_FormElement_Field_Text;
 use Tracker_FormElementFactory;
 use Tracker_MasschangeDataValueExtractor;
 use Tuleap\GlobalLanguageMock;
 
 final class MasschangeDataValueExtractorTest extends \Tuleap\Test\PHPUnit\TestCase
 {
-    use MockeryPHPUnitIntegration;
     use GlobalLanguageMock;
 
-    public function testItReturnsFieldWithItNewValue(): void
+    /**
+     * @dataProvider dataProviderFields
+     * @param class-string $field_class
+     */
+    public function testReturnsFieldWithNewValue(string $field_class, mixed $value, bool $is_expected_to_set_value): void
     {
-        $form_element_factory           = Mockery::mock(Tracker_FormElementFactory::class);
-        $masschange_data_values_manager = new Tracker_MasschangeDataValueExtractor($form_element_factory);
+        $field = $this->createStub($field_class);
 
-        $text_field_1    = Mockery::mock(Tracker_FormElement_Field_Text::class);
-        $text_field_1_id = 1;
+        $form_element_factory = $this->createStub(Tracker_FormElementFactory::class);
+        $form_element_factory->method('getFieldById')->willReturn($field);
 
-        $text_field_2    = Mockery::mock(Tracker_FormElement_Field_Text::class);
-        $text_field_2_id = 2;
+        $masschange_data = [12 => $value];
 
-        $list_field_1    = Mockery::mock(Tracker_FormElement_Field_List::class);
-        $list_field_1_id = 3;
-
-        $list_field_2    = Mockery::mock(Tracker_FormElement_Field_List::class);
-        $list_field_2_id = 4;
-
-        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_1_id])->andReturn($text_field_1);
-        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_2_id])->andReturn($text_field_2);
-        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_1_id])->andReturn($list_field_1);
-        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_2_id])->andReturn($list_field_2);
+        $expected_result = $is_expected_to_set_value ? $masschange_data : [];
 
         $GLOBALS['Language']->method('getText')->willReturn('Unchanged');
 
-        $masschange_data = [
-            $text_field_1_id => 'Unchanged',
-            $text_field_2_id => 'Value01',
-            $list_field_1_id => ['-1'],
-            $list_field_2_id => ['Value02'],
-        ];
-
-        $expected_result = [
-            $text_field_2_id => 'Value01',
-            $list_field_2_id => ['Value02'],
-        ];
+        $masschange_data_values_extractor = new Tracker_MasschangeDataValueExtractor($form_element_factory);
 
         $this->assertEquals(
             $expected_result,
-            $masschange_data_values_manager->getNewValues($masschange_data)
+            $masschange_data_values_extractor->getNewValues($masschange_data)
         );
     }
+
+    public function dataProviderFields(): array
+    {
+        return [
+            'Field with an update' => [
+                Tracker_FormElement_Field_Text::class,
+                'Value01',
+                true,
+            ],
+            'Field without an update' => [
+                Tracker_FormElement_Field_Text::class,
+                'Unchanged',
+                false,
+            ],
+            'List field with an update' => [
+                Tracker_FormElement_Field_List::class,
+                ['Value02'],
+                true,
+            ],
+            'List field without an update' => [
+                Tracker_FormElement_Field_List::class,
+                ['-1'],
+                false,
+            ],
+            'Permissions on artifact field with an update' => [
+                Tracker_FormElement_Field_PermissionsOnArtifact::class,
+                ['do_mass_update' => '1', 'use_artifact_permissions' => '1'],
+                true,
+            ],
+            'Permissions on artifact field without an update' => [
+                Tracker_FormElement_Field_PermissionsOnArtifact::class,
+                ['use_artifact_permissions' => '1'],
+                false,
+            ],
+        ];
+    }
 }

Vulnerable code:     public const GRANTED_GROUPS     = 'granted_groups';
    public const USE_IT             = 'use_artifact_permissions';
    public const IS_USED_BY_DEFAULT = false;
    public const PERMISSION_TYPE    = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
    /**
     * @return string
     */
    protected function fetchSubmitValueMasschange()
        return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
    private function fetchUserGroupList($is_read_only, array $changeset_values)
            . 'id="' . $hp->purify(str_replace('[]', '', $element_name)) . '" '
     *
     * @return string
    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input)
        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '">';
                $html .= '<input type="hidden" name="artifact[' . $this->getId() . '][use_artifact_permissions]" value="0" />';
            $html .= '<label class="checkbox" for="artifact_' . $this->getId() . '_use_artifact_permissions">';
            $html .= '<input type="checkbox"
                        name="artifact[' . $this->getId() . '][use_artifact_permissions]"
                        id="artifact_' . $this->getId() . '_use_artifact_permissions"
            $html .= '<input type="hidden" name="artifact[' . $this->getId(
            ) . '][use_artifact_permissions]" value="1" />';
 * This file is a part of Codendi.
 * Codendi is free software; you can redistribute it and/or modify
 * Codendi is distributed in the hope that it will be useful,
 * along with Codendi; if not, write to the Free Software
/* global jQuery:readonly */

(function ($) {
        $(".tracker_field_permissionsonartifact").each(function (index, element) {
            disablePermissionsFieldIfNeeded(element);
            bindSwitch(element);
        });
    function disablePermissionsFieldIfNeeded(element) {
        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
            $(element).siblings("select").attr("disabled", "disabled");
    function bindSwitch(element) {
        $("input[type='checkbox']", element).bind("change", function () {
            togglePermissionsField(element);
    function togglePermissionsField(element) {
        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
            $(element).siblings("select").attr("disabled", "disabled");
        } else {
            $(element).siblings("select").removeAttr("disabled");
        }
    }

    $(document).ready(function () {
        init();
    });
})(jQuery);
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
    use MockeryPHPUnitIntegration;
    public function testItReturnsFieldWithItNewValue(): void
        $form_element_factory           = Mockery::mock(Tracker_FormElementFactory::class);
        $masschange_data_values_manager = new Tracker_MasschangeDataValueExtractor($form_element_factory);
        $text_field_1    = Mockery::mock(Tracker_FormElement_Field_Text::class);
        $text_field_1_id = 1;
        $text_field_2    = Mockery::mock(Tracker_FormElement_Field_Text::class);
        $text_field_2_id = 2;
        $list_field_1    = Mockery::mock(Tracker_FormElement_Field_List::class);
        $list_field_1_id = 3;

        $list_field_2    = Mockery::mock(Tracker_FormElement_Field_List::class);
        $list_field_2_id = 4;

        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_1_id])->andReturn($text_field_1);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_2_id])->andReturn($text_field_2);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_1_id])->andReturn($list_field_1);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_2_id])->andReturn($list_field_2);
        $masschange_data = [
            $text_field_1_id => 'Unchanged',
            $text_field_2_id => 'Value01',
            $list_field_1_id => ['-1'],
            $list_field_2_id => ['Value02'],
        ];

        $expected_result = [
            $text_field_2_id => 'Value01',
            $list_field_2_id => ['Value02'],
        ];
            $masschange_data_values_manager->getNewValues($masschange_data)
Fixed code:     public const GRANTED_GROUPS      = 'granted_groups';
    public const USE_IT              = 'use_artifact_permissions';
    public const DO_MASS_UPDATE_FLAG = 'do_mass_update';
    public const IS_USED_BY_DEFAULT  = false;
    public const PERMISSION_TYPE     = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
    protected function fetchSubmitValueMasschange(): string
        $mass_change_input_html = TemplateRendererFactory::build()->getRenderer(__DIR__ . '/../../../templates/form-element/')
            ->renderToString(
                'permissions_on_artifact_massupdate',
                ['field_id' => $this->getId(), 'flag_mass_update' => self::DO_MASS_UPDATE_FLAG]
            );

        return $mass_change_input_html . $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
    private function fetchUserGroupList($is_read_only, array $changeset_values): string
            . 'id="' . $hp->purify('artifact_' . $field_id . '_perms_ugroups' . ($is_read_only ? '_ro' : '')) . '" '
    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input): string
        $field_id = Codendi_HTMLPurifier::instance()->purify($this->getId());

        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '" data-field-id="' . $field_id . '">';
                $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="0" />';
            $read_only_id = $disabled ? '_ro' : '';
            $html        .= '<label class="checkbox" for="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '">';
            $html        .= '<input type="checkbox"
                        name="artifact[' . $field_id . '][use_artifact_permissions]"
                        id="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '"
            $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="1" />';
        } elseif ($field instanceof Tracker_FormElement_Field_PermissionsOnArtifact) {
            return isset($data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG]) &&
                $data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG] === '1';
 * Copyright (c) Enalean, 2024 - present. All Rights Reserved.
 * This file is a part of Tuleap.
 * Tuleap is free software; you can redistribute it and/or modify
 * Tuleap is distributed in the hope that it will be useful,
 * along with Tuleap; if not, write to the Free Software
document.addEventListener("DOMContentLoaded", function () {
        const elements = document.getElementsByClassName("tracker_field_permissionsonartifact");
        for (const element of elements) {
            const field_id = element.dataset.fieldId;
            const select_ugroups = document.getElementById(`artifact_${field_id}_perms_ugroups`);
            const checkbox_use_perms = document.getElementById(
                `artifact_${field_id}_use_artifact_permissions`,
            );
            if (select_ugroups === null || checkbox_use_perms === null) {
                continue;
            }
            disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups);
            bindSwitch(checkbox_use_perms, select_ugroups);
            const checkbox_activate_masschange_update = document.getElementById(
                `artifact_${field_id}_perms_mass_update`,
            );
            if (checkbox_activate_masschange_update) {
                checkbox_use_perms.disabled = true;
                checkbox_activate_masschange_update.addEventListener("change", () => {
                    checkbox_use_perms.disabled = !checkbox_activate_masschange_update.checked;
                });
            }
        }
    function disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups) {
        if (!checkbox_use_perms.checked) {
            select_ugroups.disabled = true;
    function bindSwitch(checkbox_use_perms, select_ugroups) {
        checkbox_use_perms.addEventListener("change", () => {
            select_ugroups.disabled = !checkbox_use_perms.checked;
    init();
});
msgid "Update the permissions, by default nothing is changed"
msgstr "Mettre à jour les permissions, par défaut rien ne change"

msgid "Update the permissions, by default nothing is changed"
msgstr ""

<label class="checkbox" for="artifact_{{ field_id }}_perms_mass_update">
    <input type="checkbox"
        name="artifact[{{ field_id }}][{{ flag_mass_update }}]"
        id="artifact_{{ field_id }}_perms_mass_update"
        value="1"
    />
    {{# dgettext }} tuleap-tracker | Update the permissions, by default nothing is changed {{/ dgettext }}
</label>
use Tracker_FormElement_Field_PermissionsOnArtifact;
    /**
     * @dataProvider dataProviderFields
     * @param class-string $field_class
     */
    public function testReturnsFieldWithNewValue(string $field_class, mixed $value, bool $is_expected_to_set_value): void
        $field = $this->createStub($field_class);
        $form_element_factory = $this->createStub(Tracker_FormElementFactory::class);
        $form_element_factory->method('getFieldById')->willReturn($field);
        $masschange_data = [12 => $value];
        $expected_result = $is_expected_to_set_value ? $masschange_data : [];
        $masschange_data_values_extractor = new Tracker_MasschangeDataValueExtractor($form_element_factory);
            $masschange_data_values_extractor->getNewValues($masschange_data)

    public function dataProviderFields(): array
    {
        return [
            'Field with an update' => [
                Tracker_FormElement_Field_Text::class,
                'Value01',
                true,
            ],
            'Field without an update' => [
                Tracker_FormElement_Field_Text::class,
                'Unchanged',
                false,
            ],
            'List field with an update' => [
                Tracker_FormElement_Field_List::class,
                ['Value02'],
                true,
            ],
            'List field without an update' => [
                Tracker_FormElement_Field_List::class,
                ['-1'],
                false,
            ],
            'Permissions on artifact field with an update' => [
                Tracker_FormElement_Field_PermissionsOnArtifact::class,
                ['do_mass_update' => '1', 'use_artifact_permissions' => '1'],
                true,
            ],
            'Permissions on artifact field without an update' => [
                Tracker_FormElement_Field_PermissionsOnArtifact::class,
                ['use_artifact_permissions' => '1'],
                false,
            ],
        ];
    }