Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Fixed

- Fix uninstall/replace actions processing items without checking item type or user rights on the item

## [2.10.4] - 2026-08-04

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions ajax/locations.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
header("Content-Type: text/html; charset=UTF-8");
Html::header_nocache();

Session::checkRightsOr('uninstall:profile', [READ, PluginUninstallProfile::RIGHT_REPLACE]);
Session::checkRightsOr(PluginUninstallUninstall::$rightname, [READ, PluginUninstallProfile::RIGHT_REPLACE]);

if (
Session::haveRight(PluginUninstallUninstall::$rightname, READ)
&& $_POST['templates_id']
) {
$location = PluginUninstallPreference::getLocationByUserByEntity(
$_POST["users_id"],
Session::getLoginUserID(),
$_POST["templates_id"],
$_POST["entity"],
);
Expand Down
23 changes: 20 additions & 3 deletions front/action.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

Html::header(__s('Transfer'), $_SERVER['PHP_SELF'], "admin", "transfer");

Session::checkRightsOr('uninstall:profile', [READ, PluginUninstallProfile::RIGHT_REPLACE]);
Session::checkRightsOr(PluginUninstallUninstall::$rightname, [READ, PluginUninstallProfile::RIGHT_REPLACE]);

if (
!isset($_REQUEST["device_type"])
Expand All @@ -40,6 +40,12 @@
Html::back();
}

/** @var array $UNINSTALL_TYPES */
global $UNINSTALL_TYPES;
if (!in_array($_REQUEST["device_type"], $UNINSTALL_TYPES, true)) {
Html::back();
}

if (isset($_REQUEST["locations_id"])) {
$location = $_REQUEST["locations_id"];
} else {
Expand All @@ -51,15 +57,24 @@
}

if (isset($_REQUEST["replace"])) {
PluginUninstallReplace::replace(
Session::checkRight(PluginUninstallUninstall::$rightname, PluginUninstallProfile::RIGHT_REPLACE);
$skipped = PluginUninstallReplace::replace(
$_REQUEST["device_type"],
$_REQUEST["model_id"],
$_REQUEST['newItems'],
$location,
);

unset($_SESSION['glpi_uninstalllist']);
Session::addMessageAfterRedirect(__s('Replacement successful', 'uninstall'));
if ($skipped > 0) {
Session::addMessageAfterRedirect(
sprintf(__s('Replacement done with %d item(s) skipped because of insufficient rights', 'uninstall'), $skipped),
true,
WARNING,
);
} else {
Session::addMessageAfterRedirect(__s('Replacement successful', 'uninstall'));
}

Html::footer();

Expand All @@ -72,6 +87,7 @@

//Case of a uninstallation initiated from the object form
if (isset($_REQUEST["uninstall"])) {
Session::checkRight(PluginUninstallUninstall::$rightname, UPDATE);
//Uninstall only if a model is selected
if ($model->fields['types_id'] == PluginUninstallModel::TYPE_MODEL_UNINSTALL) {
//Massive uninstallation
Expand All @@ -97,6 +113,7 @@
Html::footer();
}
} elseif ($model->fields['types_id'] == PluginUninstallModel::TYPE_MODEL_UNINSTALL) {
Session::checkRight(PluginUninstallUninstall::$rightname, UPDATE);
//Massive uninstallation
if (isset($_SESSION['glpi_uninstalllist'])) {
PluginUninstallUninstall::uninstall(
Expand Down
2 changes: 1 addition & 1 deletion inc/model.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public function showForm($ID, $options = [])
echo "</td>";
echo "<td rowspan='4'>" . __s('Comments') . "</td>";
echo "<td rowspan='4'>";
echo "<textarea cols='60' rows='4' name='comment'>" . $this->fields["comment"] . "</textarea>";
echo "<textarea cols='60' rows='4' name='comment'>" . htmlentities((string) $this->fields["comment"]) . "</textarea>";
echo "</td></tr>";

echo "<tr class='tab_bg_1'><td>" . __s('New status of the computer', 'uninstall') . "</td>";
Expand Down
48 changes: 40 additions & 8 deletions inc/replace.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static function getTypeName($nb = 0)
* @param $tab_ids
* @param $location
**/
public static function replace($type, $model_id, $tab_ids, $location)
public static function replace($type, $model_id, $tab_ids, $location): int
{
/**
* @var array $CFG_GLPI
Expand All @@ -103,21 +103,45 @@ public static function replace($type, $model_id, $tab_ids, $location)
echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'><tr><th>" . __s('Replacement', 'uninstall') . "</th></tr>";
echo "<tr class='tab_bg_2'><td>";
$count = 0;
$tot = count($tab_ids);
$count = 0;
$skipped = 0;
$tot = count($tab_ids);

foreach ($tab_ids as $olditem_id => $newitem_id) {
$count++;

if (!class_exists($type) || !is_a($type, CommonDBTM::class, true)) {
$skipped++;
continue;
}

$olditem = new $type();
$olditem->getFromDB($olditem_id);
if (!$olditem->getFromDB($olditem_id) || !$olditem->can($olditem_id, UPDATE)) {
$skipped++;
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

New rights checks continue silently when an item fails, leaving the caller with no visibility into partial failures. $count is already incremented before the check (line 110), so the progress output counts skipped items as processed, and front/action.php unconditionally outputs "Replacement successful" after the call returns. A user replacing 10 items where 3 are denied gets the same feedback as a fully successful run.

The mass-action handler in inc/uninstall.class.php correctly uses MassiveAction::ACTION_NORIGHT; the replace() method should at minimum track and display skipped items in its HTML table, or return a count of failures to the caller.

}

$newitem = new $type();
$newitem->getFromDB($newitem_id);
if (!$newitem->getFromDB($newitem_id) || !$newitem->can($newitem_id, UPDATE)) {
$skipped++;
continue;
}

if (
$model->fields['replace_method'] == self::METHOD_PURGE
&& !$olditem->can($olditem_id, PURGE)
) {
$skipped++;
continue;
}

if (
$model->fields['replace_method'] == self::METHOD_DELETE_AND_COMMENT
&& !$olditem->can($olditem_id, DELETE)
) {
$skipped++;
continue;
}

//Hook to perform actions before item is being replaced
$olditem->fields['_newid'] = $newitem_id;
Expand Down Expand Up @@ -576,7 +600,13 @@ public static function replace($type, $model_id, $tab_ids, $location)
Html::getProgressBar($percent);
}

echo "</td></tr>";
if ($skipped > 0) {
echo "<tr class='tab_bg_2'><td>" . sprintf(
__s('%d item(s) skipped because of insufficient rights', 'uninstall'),
$skipped,
) . "</td></tr>";
}

echo "</table></div>";

if ($model->fields['types_id'] == PluginUninstallModel::TYPE_MODEL_REPLACEMENT_UNINSTALL) {
Expand All @@ -592,6 +622,8 @@ public static function replace($type, $model_id, $tab_ids, $location)
$location,
);
}

return $skipped;
}


Expand Down Expand Up @@ -839,11 +871,11 @@ public static function showReplacementForm($type, $model_id, $tab_ids, $location
echo "<td>" . $commonitem->getName() . "</td>";

if (Search::getOptionNumber($type, 'otherserial')) {
echo "<td>" . $commonitem->fields['otherserial'] . "</td>";
echo "<td>" . htmlentities((string) $commonitem->fields['otherserial']) . "</td>";
}

if (Search::getOptionNumber($type, 'serial')) {
echo "<td>" . $commonitem->fields['serial'] . "</td>";
echo "<td>" . htmlentities((string) $commonitem->fields['serial']) . "</td>";
}

echo "<td>";
Expand Down
9 changes: 7 additions & 2 deletions inc/uninstall.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ public static function processMassiveActionsForOneItemtype(MassiveAction $ma, Co
if ($ma->getAction() === "uninstall") {
$itemtype = $ma->getItemtype(false);
foreach ($ids as $id) {
if ($item->getFromDB($id)) {
if ($item->getFromDB($id) && $item->can($id, UPDATE)) {
//Session::addMessageAfterRedirect(sprintf(__s('Form duplicated: %s', 'formcreator'), $item->getName()));
$_SESSION['glpi_uninstalllist'][$itemtype][$id] = $id;
$ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK);
} else {
$ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT);
}
}

Expand Down Expand Up @@ -373,7 +375,9 @@ public static function uninstall($type, $model_id, $tab_ids, $location)
$count++;
if (class_exists($type) && is_a($type, CommonDBTM::class, true)) {
$item = new $type();
$item->getFromDB($id);
if (!$item->getFromDB($id) || !$item->can($id, UPDATE)) {
continue;
}

self::doOneUninstall($model, $transfer, $item, [
'type' => $type,
Expand Down Expand Up @@ -476,6 +480,7 @@ public static function deleteComputerInOCS($ocs_id, $ocs_server_id)
global $DB;
if (class_exists('PluginOcsinventoryngOcsServer')) {
$DBocs = PluginOcsinventoryngOcsServer::getDBocs($ocs_server_id)->getDB();
$ocs_id = (int) $ocs_id;

//First try to remove all the network ports
$query = "DELETE
Expand Down