Skip to content

Conversation

Herafia
Copy link
Contributor

@Herafia Herafia commented Oct 8, 2025

Checklist before requesting a review

Please delete options that are not relevant.

  • I have read the CONTRIBUTING document.
  • I have performed a self-review of my code.
  • I have added tests that prove my fix is effective or that my feature works.
  • This change requires a documentation update.

Description

  • It fixes #39401

Users cannot add solutions to linked Tickets via mass actions when the parent Problem is Closed.

Let’s consider the following scenario:

A user has the permission to resolve or close a Problem, but their profile restrictions prevent them from changing the Problem’s status from “Closed” back to “Solved.”

Starting from a Closed Problem,
if the user tries to resolve one or more linked Tickets through mass actions,
the form used to add a solution to the Tickets appears disabled (grayed out).

This happens because the template form_solution.html.twig — which displays the solution form — checks whether the current object can be resolved using the maybeSolve() method.

The maybeSolve() function verifies whether the user’s profile lifecycle allows transitioning the current object’s status from its current state (in this case, the Problem, which is Closed) to Solved.

Since this check is performed on the Problem object instead of the Ticket,
GLPI returns false, marking the fields as non-editable, even though the user might be allowed to resolve the Ticket itself.

Screenshots (if appropriate):

image

@Herafia Herafia self-assigned this Oct 8, 2025
@cedric-anne cedric-anne added the bug label Oct 8, 2025
@cedric-anne cedric-anne added this to the 10.0.21 milestone Oct 8, 2025
@Herafia Herafia requested a review from cedric-anne October 8, 2025 15:03
Copy link
Contributor

@stonebuzz stonebuzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current solution works, but the template calls item.maySolve() on item, which is a Ticket object but empty.

At this point, we do not have access to the actual Ticket (we are inside a mass action).

Another approach — to be validated with @cedric-anne — could be to pass an argument like from_ma to handle this specific context and bypass status check.

diff --git a/src/Change_Ticket.php b/src/Change_Ticket.php
index a7d46d6209..7119ba48b3 100644
--- a/src/Change_Ticket.php
+++ b/src/Change_Ticket.php
@@ -128,7 +128,7 @@ class Change_Ticket extends CommonDBRelation
                 $change = new Change();
                 $input = $ma->getInput();
                 if (isset($input['changes_id']) && $change->getFromDB($input['changes_id'])) {
-                    $change->showMassiveSolutionForm($change);
+                    $change->showMassiveSolutionForm($change, true);
                     echo "<br>";
                     echo Html::submit(_x('button', 'Post'), ['name' => 'massiveaction']);
                     return true;
diff --git a/src/CommonITILObject.php b/src/CommonITILObject.php
index a9c445303b..af9bc7dade 100644
--- a/src/CommonITILObject.php
+++ b/src/CommonITILObject.php
@@ -5194,7 +5194,7 @@ abstract class CommonITILObject extends CommonDBTM
      *
      * @param $entities_id
      **/
-    public static function showMassiveSolutionForm(CommonITILObject $item)
+    public static function showMassiveSolutionForm(CommonITILObject $item, bool $from_ma = false)
     {
         $solution = new ITILSolution();
         $solution->showForm(
@@ -5204,6 +5204,7 @@ abstract class CommonITILObject extends CommonDBTM
                 'entity' => $item->getEntityID(),
                 'noform' => true,
                 'nokb'   => true,
+                'from_ma'   => $from_ma,
             ]
         );
     }
diff --git a/src/Problem_Ticket.php b/src/Problem_Ticket.php
index b5b98f1a1d..8687241451 100644
--- a/src/Problem_Ticket.php
+++ b/src/Problem_Ticket.php
@@ -169,7 +169,7 @@ class Problem_Ticket extends CommonDBRelation
                 $problem = new Problem();
                 $input = $ma->getInput();
                 if (isset($input['problems_id']) && $problem->getFromDB($input['problems_id'])) {
-                    $problem->showMassiveSolutionForm($problem);
+                    $problem->showMassiveSolutionForm($problem, true);
                     echo "<br>";
                     echo Html::submit(_x('button', 'Post'), [
                         'name'  => 'massiveaction',
diff --git a/templates/components/itilobject/timeline/form_solution.html.twig b/templates/components/itilobject/timeline/form_solution.html.twig
index 2a20697eb0..2dcc56797a 100644
--- a/templates/components/itilobject/timeline/form_solution.html.twig
+++ b/templates/components/itilobject/timeline/form_solution.html.twig
@@ -35,7 +35,7 @@
 
 {% set params = {'item': item}|merge(params|default({})) %}
 
-{% set candedit = item.maySolve() %}
+{% set candedit = item.maySolve() or params['from_ma'] %}
 {% set can_read_kb = has_profile_right('knowbase', constant('READ')) or has_profile_right('knowbase', constant('KnowbaseItem::READFAQ')) %}
 {% set can_update_kb = has_profile_right('knowbase', constant('UPDATE')) %}
 {% set nokb = params['nokb'] is defined or params['nokb'] == true %}

What do you think @cedric-anne

@cedric-anne
Copy link
Member

The current solution works, but the template calls item.maySolve() on item, which is a Ticket object but empty.

At this point, we do not have access to the actual Ticket (we are inside a mass action).

Another approach — to be validated with @cedric-anne — could be to pass an argument like from_ma to handle this specific context and bypass status check.

diff --git a/src/Change_Ticket.php b/src/Change_Ticket.php
index a7d46d6209..7119ba48b3 100644
--- a/src/Change_Ticket.php
+++ b/src/Change_Ticket.php
@@ -128,7 +128,7 @@ class Change_Ticket extends CommonDBRelation
                 $change = new Change();
                 $input = $ma->getInput();
                 if (isset($input['changes_id']) && $change->getFromDB($input['changes_id'])) {
-                    $change->showMassiveSolutionForm($change);
+                    $change->showMassiveSolutionForm($change, true);
                     echo "<br>";
                     echo Html::submit(_x('button', 'Post'), ['name' => 'massiveaction']);
                     return true;
diff --git a/src/CommonITILObject.php b/src/CommonITILObject.php
index a9c445303b..af9bc7dade 100644
--- a/src/CommonITILObject.php
+++ b/src/CommonITILObject.php
@@ -5194,7 +5194,7 @@ abstract class CommonITILObject extends CommonDBTM
      *
      * @param $entities_id
      **/
-    public static function showMassiveSolutionForm(CommonITILObject $item)
+    public static function showMassiveSolutionForm(CommonITILObject $item, bool $from_ma = false)
     {
         $solution = new ITILSolution();
         $solution->showForm(
@@ -5204,6 +5204,7 @@ abstract class CommonITILObject extends CommonDBTM
                 'entity' => $item->getEntityID(),
                 'noform' => true,
                 'nokb'   => true,
+                'from_ma'   => $from_ma,
             ]
         );
     }
diff --git a/src/Problem_Ticket.php b/src/Problem_Ticket.php
index b5b98f1a1d..8687241451 100644
--- a/src/Problem_Ticket.php
+++ b/src/Problem_Ticket.php
@@ -169,7 +169,7 @@ class Problem_Ticket extends CommonDBRelation
                 $problem = new Problem();
                 $input = $ma->getInput();
                 if (isset($input['problems_id']) && $problem->getFromDB($input['problems_id'])) {
-                    $problem->showMassiveSolutionForm($problem);
+                    $problem->showMassiveSolutionForm($problem, true);
                     echo "<br>";
                     echo Html::submit(_x('button', 'Post'), [
                         'name'  => 'massiveaction',
diff --git a/templates/components/itilobject/timeline/form_solution.html.twig b/templates/components/itilobject/timeline/form_solution.html.twig
index 2a20697eb0..2dcc56797a 100644
--- a/templates/components/itilobject/timeline/form_solution.html.twig
+++ b/templates/components/itilobject/timeline/form_solution.html.twig
@@ -35,7 +35,7 @@
 
 {% set params = {'item': item}|merge(params|default({})) %}
 
-{% set candedit = item.maySolve() %}
+{% set candedit = item.maySolve() or params['from_ma'] %}
 {% set can_read_kb = has_profile_right('knowbase', constant('READ')) or has_profile_right('knowbase', constant('KnowbaseItem::READFAQ')) %}
 {% set can_update_kb = has_profile_right('knowbase', constant('UPDATE')) %}
 {% set nokb = params['nokb'] is defined or params['nokb'] == true %}

What do you think @cedric-anne

IMHO, changing the showMassiveSolutionForm() signature is useless, as, given its name, it is expected that it is called from the massive actions context.
Also, we could probably consider that a new item can be solved, meaning the we could simply do this in the template: {% set candedit = item.isNewItem() or item.maySolve() %}.

@stonebuzz
Copy link
Contributor

@Herafia

can you retry / check behavior with only this :

diff --git a/templates/components/itilobject/timeline/form_task.html.twig b/templates/components/itilobject/timeline/form_task.html.twig
index c3743ca441..ea0dcf187b 100644
--- a/templates/components/itilobject/timeline/form_task.html.twig
+++ b/templates/components/itilobject/timeline/form_task.html.twig
@@ -34,7 +34,7 @@
 
 {% set params = {'parent': item}|merge(params|default({})) %}
 
-{% set candedit = item.maySolve() %}
+{% set candedit = item.isNewItem() or item.maySolve() %}
 {% set can_read_kb = has_profile_right('knowbase', constant('READ')) or has_profile_right('knowbase', constant('KnowbaseItem::READFAQ')) %}
 {% set can_update_kb = has_profile_right('knowbase', constant('UPDATE')) %}
 {% set nokb = params['nokb'] is defined or (params['nokb'] ?? false) == true %}

@Herafia
Copy link
Contributor Author

Herafia commented Oct 14, 2025

@stonebuzz, the solution proposed by @cedric-anne works

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should not be here :)

@Herafia Herafia force-pushed the fix/massiveActionsSolvedTicket branch from 8534cd9 to b296dad Compare October 14, 2025 14:51
@Herafia Herafia closed this Oct 14, 2025
@Herafia Herafia force-pushed the fix/massiveActionsSolvedTicket branch from b296dad to 7c36fcc Compare October 14, 2025 15:07
@Herafia Herafia reopened this Oct 14, 2025
@cedric-anne cedric-anne force-pushed the fix/massiveActionsSolvedTicket branch from 217ac92 to 40e9ca1 Compare October 15, 2025 06:38
@cedric-anne cedric-anne merged commit 18cd9c5 into glpi-project:10.0/bugfixes Oct 15, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants