Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions front/change.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
if (isset($_POST["add"])) {
$change->check(-1, CREATE, $_POST);

$_POST = $change->enforceReadonlyFields($_POST, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should not that be called from something like prepareInputForAdd() rather than duplicated in each front file?

Copy link
Member Author

@froozeify froozeify Oct 28, 2025

Choose a reason for hiding this comment

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

prepareInputForAdd is also called by the api and system.

The readonly enforcing, after some discussion, for the moment must only applied on user form submission.

It's up to debate to enforce that also on the api side :#21578 (comment)

$newID = $change->add($_POST);
Event::log(
$newID,
Expand Down Expand Up @@ -109,6 +110,7 @@
} elseif (isset($_POST["update"])) {
$change->check($_POST["id"], UPDATE);

$_POST = $change->enforceReadonlyFields($_POST);
$change->update($_POST);
Event::log(
$_POST["id"],
Expand Down
2 changes: 2 additions & 0 deletions front/problem.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
if (isset($_POST["add"])) {
$problem->check(-1, CREATE, $_POST);

$_POST = $problem->enforceReadonlyFields($_POST, true);
if ($newID = $problem->add($_POST)) {
Event::log(
$newID,
Expand Down Expand Up @@ -108,6 +109,7 @@
} elseif (isset($_POST["update"])) {
$problem->check($_POST["id"], UPDATE);

$_POST = $problem->enforceReadonlyFields($_POST);
$problem->update($_POST);
Event::log(
$_POST["id"],
Expand Down
2 changes: 2 additions & 0 deletions front/ticket.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@

if (isset($_POST["add"])) {
$track->check(-1, CREATE, $_POST);
$_POST = $track->enforceReadonlyFields($_POST, true);

if ($track->add($_POST)) {
if ($_SESSION['glpibackcreated']) {
Expand All @@ -85,6 +86,7 @@
if (!$track::canUpdate()) {
throw new AccessDeniedHttpException();
}
$_POST = $track->enforceReadonlyFields($_POST);
$track->update($_POST);

if (isset($_POST['kb_linked_id'])) {
Expand Down
90 changes: 72 additions & 18 deletions src/CommonITILObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use Glpi\RichText\UserMention;
use Glpi\Search\Output\HTMLSearchOutput;
use Glpi\Team\Team;
use Glpi\Urgency;
use Safe\Exceptions\DatetimeException;

use function Safe\getimagesize;
Expand Down Expand Up @@ -1768,17 +1769,6 @@ public function cleanDBonPurge()
protected function handleTemplateFields(array $input, bool $show_error_message = true)
{
//// check mandatory fields
// First get ticket template associated: entity and type/category
$entid = $input['entities_id'] ?? $this->fields['entities_id'];

$type = null;
if (isset($input['type'])) {
$type = $input['type'];
} elseif (isset($this->fields['type'])) {
$type = $this->fields['type'];
}

$categid = $input['itilcategories_id'] ?? $this->fields['itilcategories_id'];

$check_allowed_fields_for_template = false;
$allowed_fields = [];
Expand Down Expand Up @@ -1865,9 +1855,9 @@ class_exists($validation_class)
}
}

$tt = $this->getITILTemplateToUse(0, $type, $categid, $entid);

if (count($tt->mandatory)) {
// First get ticket template associated: entity and type/category
$tt = $this->getITILTemplateFromInput($input);
if ($tt && count($tt->mandatory)) {
$mandatory_missing = [];
$fieldsname = $tt->getAllowedFieldsNames(true);
foreach ($tt->mandatory as $key => $val) {
Expand Down Expand Up @@ -2341,6 +2331,40 @@ public function prepareInputForUpdate($input)
return $input;
}

/**
* Processes readonly fields in the input array based on the ITIL template data.
*
* @param array $input The user input data to process (often $_POST).
* @param bool $isAdd true if we are in a creation, will force to apply the template predefined field.
*
* @return array The modified user input array after processing readonly fields.
*
* @since 11.0.2
*/
public function enforceReadonlyFields(array $input, bool $isAdd = false): array
{
$tt = $this->getITILTemplateFromInput($input);
if (!$tt) {
return $input;
}

$tt->getFromDBWithData($tt->getID()); // We load the fields (predefined and readonly)

foreach (array_keys($tt->readonly) as $read_only_field) {
if ($isAdd && array_key_exists($read_only_field, $tt->predefined)) {
$input[$read_only_field] = $tt->predefined[$read_only_field];
continue;
}

if (array_key_exists($read_only_field, $this->fields)) {
$input[$read_only_field] = $this->fields[$read_only_field];
} else {
unset($input[$read_only_field]);
}
}
return $input;
}

public function post_updateItem($history = true)
{
// Handle rich-text images and uploaded documents
Expand Down Expand Up @@ -2824,7 +2848,7 @@ public function prepareInputForAdd($input)
}

// save value before clean;
$title = ltrim($input['name']);
$title = ltrim($input['name'] ?? '');

// Set default status to avoid notice
if (!isset($input["status"])) {
Expand All @@ -2835,7 +2859,7 @@ public function prepareInputForAdd($input)
!isset($input["urgency"])
|| !($CFG_GLPI['urgency_mask'] & (1 << $input["urgency"]))
) {
$input["urgency"] = 3;
$input["urgency"] = Urgency::MEDIUM->value;
}
if (
!isset($input["impact"])
Expand Down Expand Up @@ -2886,8 +2910,8 @@ public function prepareInputForAdd($input)
}

// No name set name
$input["name"] = ltrim($input["name"]);
$input['content'] = ltrim($input['content']);
$input["name"] = ltrim($input["name"] ?? '');
$input['content'] = ltrim($input['content'] ?? '');
if (empty($input["name"])) {
// Build name based on content

Expand Down Expand Up @@ -8252,6 +8276,36 @@ public function getITILTemplateToUse(
return $tt;
}

/**
* Get the template to use
* If the input is not defined, it will get it from the object fields datas
*
* @param array $input
* @return ITILTemplate|null
*
* @since 11.0.2
*/
public function getITILTemplateFromInput(array $input = []): ?ITILTemplate
{
$entid = $input['entities_id'] ?? $this->fields['entities_id'] ?? $input['id'] ?? null;
if (is_null($entid)) {
return null;
}

$type = null;
if (isset($input['type'])) {
$type = $input['type'];
} elseif (isset($this->fields['type'])) {
$type = $this->fields['type'];
}

$categid = $input['itilcategories_id'] ?? $this->fields['itilcategories_id'] ?? null;
if (is_null($categid)) {
return null;
}
return $this->getITILTemplateToUse(0, $type, $categid, $entid);
}

/**
* Get template field name
*
Expand Down
3 changes: 2 additions & 1 deletion src/Ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
use Glpi\RichText\RichText;
use Glpi\RichText\UserMention;
use Glpi\Search\DefaultSearchRequestInterface;
use Glpi\Urgency;
use Safe\DateTime;

use function Safe\preg_match;
Expand Down Expand Up @@ -3489,7 +3490,7 @@ public static function getDefaultValues($entity = 0)
'name' => '',
'content' => '',
'itilcategories_id' => 0,
'urgency' => 3,
'urgency' => Urgency::MEDIUM->value,
'impact' => 3,
'priority' => self::computePriority(3, 3),
'requesttypes_id' => $requesttype,
Expand Down
48 changes: 48 additions & 0 deletions tests/cypress/e2e/ITILObject/ticket_form.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,52 @@ describe("Ticket Form", () => {
cy.findByRole('cell').should('contain.text', 'No results found');
});
});

it('Create/update a ticket using a template with readonly fields', () => {
const ticket_template_name = `test template ${rand}`;
cy.createWithAPI('TicketTemplate', {
'name': ticket_template_name,
}).as('ticket_template_id');

cy.get('@ticket_template_id').then((ticket_template_id) => {
cy.createWithAPI('TicketTemplatePredefinedField', {
'tickettemplates_id': ticket_template_id, // Default template
'num': 10, // Urgency
'value': 4, // High
});

cy.createWithAPI('TicketTemplateReadonlyField', {
'tickettemplates_id': ticket_template_id,
'num': 10,
});

cy.createWithAPI('ITILCategory', {
'name':ticket_template_name,
'tickettemplates_id': ticket_template_id,
'tickettemplates_id_incident': ticket_template_id,
'tickettemplates_id_demand': ticket_template_id,
'changetemplates_id': ticket_template_id,
'problemtemplates_id': ticket_template_id,
});
});

// Create form
cy.visit(`/front/ticket.form.php`);

// intercept form submit
cy.intercept('POST', '/front/ticket.form.php').as('submit');

cy.getDropdownByLabelText('Category').selectDropdownValue(`»${ticket_template_name}`);

// We change the value of a readonly field, it should be ignored
cy.get('input[name="urgency"]').invoke('val', '1');
cy.findByRole('button', {'name': 'Add'}).click();
cy.wait('@submit').its('response.statusCode').should('eq', 200);
cy.get('input[name="urgency"]').should('have.value', '4'); // Should be the template 4 value

// We try updating it
cy.get('input[name="urgency"]').invoke('val', '1');
cy.findByRole('button', {'name': 'Save'}).click();
cy.get('input[name="urgency"]').should('have.value', '4'); // Should be the template 4 value
});
});
46 changes: 46 additions & 0 deletions tests/functional/ChangeITILTemplateReadonlyFieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2025 Teclib' and contributors.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

namespace functional;

use Change;
use Glpi\Tests\AbstractITILTemplateReadonlyFieldTest;

class ChangeITILTemplateReadonlyFieldTest extends AbstractITILTemplateReadonlyFieldTest
{
public function getITILClass(): Change
{
return new Change();
}
}
46 changes: 46 additions & 0 deletions tests/functional/ProblemITILTemplateReadonlyFieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2025 Teclib' and contributors.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

namespace functional;

use Glpi\Tests\AbstractITILTemplateReadonlyFieldTest;
use Problem;

class ProblemITILTemplateReadonlyFieldTest extends AbstractITILTemplateReadonlyFieldTest
{
public function getITILClass(): Problem
{
return new Problem();
}
}
Loading