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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Move to iterator
- Fix various minor bugs in the import/export workflow
- Fix an issue where data are not formatted when coming from a field plugin's custom field.
- Fix model selector validation and access control
- Improve import/export workflow
- Fix validation, permissions and entity handling during imports
- Improve partial import error reporting
- Correct user password import and creation handling (policy, history, expiration and confirmation)

## [2.15.10] - 2026-08-07

Expand Down
90 changes: 64 additions & 26 deletions inc/commoninjectionlib.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -584,24 +584,22 @@ private function manageFieldValues()


/**
* Get the ID associated with a value from the CSV file
*
* @param PluginDatainjectionInjectionInterface|null $injectionClass
* @param string $itemtype itemtype of the values to inject
* @param array $searchOption option associated with the field to check
* @param string $field the field to check
* @param string $value the value coming from the CSV file
* @param boolean $add is insertion (true) or update (false) (true by default)
*
* @return void nothing
**/
* Get the ID associated with a value from the CSV file
*
* @param PluginDatainjectionInjectionInterface|null $injectionClass
* @param string $itemtype itemtype of the values to inject
* @param array $searchOption option associated with the field to check
* @param string $field the field to check
* @param string $value the value coming from the CSV file
*
* @return void nothing
**/
private function getFieldValue(
$injectionClass,
$itemtype,
$searchOption,
$field,
$value,
$add = true
$value
) {
$linkfield = $searchOption['storevaluein'] ?? $searchOption['linkfield'];

Expand All @@ -614,13 +612,12 @@ private function getFieldValue(
break;

case 'password':
//To add a user password, it's mandatory is give a password and it's confirmation
//Here we cannot detect if it's an add or update. We'll handle updates later in the process
if ($add && $itemtype == 'User') {
//Core needs both the password and its confirmation to validate and hash it, on add as well as on update
if ($itemtype == 'User') {
$this->setValueForItemtype($itemtype, $linkfield, $value);
//Add field password2 is not already present
//Add field password2 if not already present
//(can be present if password was an addtional information)
if (!isset($this->values[$itemtype][$field])) {
if (!isset($this->values[$itemtype][$linkfield . "2"])) {
$this->setValueForItemtype($itemtype, $linkfield . "2", $value);
}
}
Expand Down Expand Up @@ -976,7 +973,8 @@ private function unsetValue($itemtype, $field)
**/
private function setValueForItemtype($itemtype, $field, $value, $fromdb = false)
{
if ($itemtype === User::class && $field === "pdffont" && $fromdb) {
//The stored password is a hash: taking it back from the DB would overwrite the imported one
if ($itemtype === User::class && in_array($field, ['pdffont', 'password'], true) && $fromdb) {
return;
}

Expand Down Expand Up @@ -1708,13 +1706,16 @@ public function processAddOrUpdate()
$newID = $this->effectiveAddOrUpdate($this->injectionClass, $item, $values, $add);

if (!$newID) {
$this->results['status'] = self::WARNING;
$this->addCheckWarning(self::WARNING, $item::class);
} else {
//Store id of the injected item
$this->setValueForItemtype($this->primary_type, 'id', $newID);

//If type needs it : process more data after type import
$this->processAfterInsertOrUpdate($this->injectionClass, $add);
if ($this->processAfterInsertOrUpdate($this->injectionClass, $add) === false) {
$this->addCheckWarning(self::WARNING, $item::class);
}

//$this->results['status'] = self::SUCCESS;
$this->results[$item::class] = $newID;

Expand Down Expand Up @@ -1751,8 +1752,12 @@ public function processAddOrUpdate()

$values = $this->getValuesForItemtype($itemtype);
if ($this->lastCheckBeforeProcess($injectionClass)) {
$tmpID = $this->effectiveAddOrUpdate($injectionClass, $item, $values, $add);
$this->processAfterInsertOrUpdate($injectionClass, $add);
$tmpID = $this->effectiveAddOrUpdate($injectionClass, $item, $values, $add);
if (!$tmpID) {
$this->addCheckWarning(self::WARNING, $itemtype);
} elseif ($this->processAfterInsertOrUpdate($injectionClass, $add) === false) {
$this->addCheckWarning(self::WARNING, $itemtype);
}
}
}
}
Expand All @@ -1765,6 +1770,20 @@ public function processAddOrUpdate()
}


/**
* Flag the current line as partially injected and log the reason
*
* @param integer $code log label describing the reason
* @param string $itemtype itemtype that could not be written
**/
private function addCheckWarning(int $code, string $itemtype): void
{
$this->results['status'] = self::WARNING;
$this->results[self::ACTION_CHECK]['status'] = self::WARNING;
$this->results[self::ACTION_CHECK][] = [$code, $itemtype];
}


/**
* Perform data injection into GLPI DB
*
Expand All @@ -1778,6 +1797,24 @@ public function processAddOrUpdate()
private function effectiveAddOrUpdate($injectionClass, $item, $values, $add = true)
{

//The plugin acts as the front controller here: rights must be checked before writing.
//Skipped without a session, as the lib is also a programmatic entry point for scripts.
if (Session::getLoginUserID() !== false) {
$input = is_array($values) ? $values : [];
if ($add) {
//Passing the input to can() makes the check cover the target entity
if (!$item->can(-1, CREATE, $input)) {
$this->addCheckWarning(self::ERROR_CANNOT_IMPORT, $item::class);
return 0;
}

//On the update path the target id is known, so the per-item check also covers the entity scope
} elseif (!isset($values['id']) || !$item->can($values['id'], UPDATE)) {
$this->addCheckWarning(self::ERROR_CANNOT_UPDATE, $item::class);
return 0;
}
}

//Insert data using the standard add() method
$toinject = [];
$options = $injectionClass->getOptions();
Expand Down Expand Up @@ -1972,7 +2009,6 @@ private function manageRelations()
$option,
$option['linkfield'],
$value,
true,
);
}
}
Expand Down Expand Up @@ -2476,15 +2512,17 @@ public static function addTemplateSearchOptions($injectionClass, &$tab)
* @param PluginDatainjectionInjectionInterface $injectionClass the injection class to use
* @param $add true if an item is created, false if it's an update
*
* @return void nothing
* @return bool false if the injection class rejected a post-processing step
**/
private function processAfterInsertOrUpdate($injectionClass, $add = true)
{

//If itemtype implements special process after type injection
if (method_exists($injectionClass, 'processAfterInsertOrUpdate')) {
//Invoke it
$injectionClass->processAfterInsertOrUpdate($this->values, $add, $this->rights);
return $injectionClass->processAfterInsertOrUpdate($this->values, $add, $this->rights) !== false;
}

return true;
}
}
14 changes: 13 additions & 1 deletion inc/model.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1277,13 +1277,25 @@ public static function checkRightOnModel(int $models_id): bool
}
}

$check_add = (bool) ($model->fields['behavior_add'] ?? 0);
$check_update = (bool) ($model->fields['behavior_update'] ?? 0);

//A model doing nothing still requires the creation right to be listed
if (!$check_add && !$check_update) {
$check_add = true;
}

foreach (array_unique($itemtypes) as $itemtype) {
if ($itemtype == PluginDatainjectionInjectionType::NO_VALUE || !is_a($itemtype, CommonDBTM::class, true)) {
continue;
}

$item = new $itemtype();
if (!$item->canCreate()) {
if ($check_add && !$item->canCreate()) {
return false;
}

if ($check_update && !$item->canUpdate()) {
return false;
}
}
Expand Down
13 changes: 3 additions & 10 deletions inc/userinjection.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,11 @@ public function reformat(&$values)
* @param array $values
* @param boolean $add (true by default)
* @param array|null $rights array
*
* @return bool false if a post-processing step was rejected
*/
public function processAfterInsertOrUpdate($values, $add = true, $rights = [])
{
/** @var DBmysql $DB */
global $DB;

//Manage user emails
if (isset($values['User']['useremails_id']) && $rights['add_dropdown'] && Session::haveRight('user', UPDATE)) {
$emails = preg_split('/[\s,;]+/', $values['User']['useremails_id'], -1, PREG_SPLIT_NO_EMPTY);
Expand Down Expand Up @@ -213,13 +212,7 @@ public function processAfterInsertOrUpdate($values, $add = true, $rights = [])
}
}

if (isset($values['User']['password']) && ($values['User']['password'] != '')) {
$DB->update(
'glpi_users',
['password' => Auth::getPasswordHash($values['User']['password'])],
['id' => $values['User']['id']],
);
}
return true;
}


Expand Down
157 changes: 157 additions & 0 deletions tests/unit/InjectionWriteRightTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

/**
* -------------------------------------------------------------------------
* DataInjection plugin for GLPI
* -------------------------------------------------------------------------
*
* LICENSE
*
* This file is part of DataInjection.
*
* DataInjection 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.
*
* DataInjection 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 DataInjection. If not, see <http://www.gnu.org/licenses/>.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2007-2023 by DataInjection plugin team.
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
* @link https://github.com/pluginsGLPI/datainjection
* -------------------------------------------------------------------------
*/

namespace GlpiPlugin\Datainjection\Tests\Unit;

use Auth;
use Computer;
use Glpi\Tests\DbTestCase;
use PluginDatainjectionCommonInjectionLib;
use PluginDatainjectionComputerInjection;
use PluginDatainjectionUserInjection;
use User;

final class InjectionWriteRightTest extends DbTestCase
{
private function injectData(
object $injection_class,
array $injected_data,
array $mandatory_fields
): array {
$lib = new PluginDatainjectionCommonInjectionLib(
$injection_class,
$injected_data,
[
'rights' => [
'can_add' => true,
'can_update' => true,
'add_dropdown' => true,
],
'mandatory_fields' => $mandatory_fields,
'entities_id' => 0,
],
);

$lib->processAddOrUpdate();

return $lib->getInjectionResults();
}

public function testInjectedUserPasswordIsUsableAndRaisesNoError(): void
{
$this->login();

$login = 'test_injected_user_' . random_int(1, PHP_INT_MAX);
$password = 'Ohbah7ohw!aeK3';

$results = $this->injectData(
new PluginDatainjectionUserInjection(),
['User' => ['name' => $login, 'password' => $password]],
['User' => ['name' => true]],
);

self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);
self::assertEmpty($_SESSION['MESSAGE_AFTER_REDIRECT'][ERROR] ?? []);

$user = new User();
self::assertTrue($user->getFromDB($results['User']));
self::assertTrue(Auth::checkPassword($password, $user->fields['password']));
}

public function testInjectedUserPasswordIsUpdatedOnExistingUser(): void
{
$this->login();

$login = 'test_injected_user_' . random_int(1, PHP_INT_MAX);
$password = 'Ohbah7ohw!aeK3';
$new_password = 'Eiy4ohn!ohGh1o';

$results = $this->injectData(
new PluginDatainjectionUserInjection(),
['User' => ['name' => $login, 'password' => $password]],
['User' => ['name' => true]],
);
self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);

$results = $this->injectData(
new PluginDatainjectionUserInjection(),
['User' => ['name' => $login, 'password' => $new_password]],
['User' => ['name' => true]],
);
self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);
self::assertEmpty($_SESSION['MESSAGE_AFTER_REDIRECT'][ERROR] ?? []);

$user = new User();
self::assertTrue($user->getFromDB($results['User']));
self::assertTrue(Auth::checkPassword($new_password, $user->fields['password']));
}

public function testInjectionIsRejectedWithoutUpdateRightOnExistingItem(): void
{
$this->login();

$computer = $this->createItem(Computer::class, [
'name' => 'Test_Computer_write_right_' . random_int(1, PHP_INT_MAX),
'entities_id' => 0,
]);
$comment = $computer->fields['comment'];

$_SESSION['glpiactiveprofile'][Computer::$rightname] = READ;

$results = $this->injectData(
new PluginDatainjectionComputerInjection(),
['Computer' => ['name' => $computer->fields['name'], 'comment' => 'Injected comment']],
['Computer' => ['name' => true]],
);

self::assertSame(PluginDatainjectionCommonInjectionLib::WARNING, $results['status']);

self::assertTrue($computer->getFromDB($computer->getID()));
self::assertSame($comment, $computer->fields['comment']);
}

public function testInjectionIsRejectedWithoutCreateRight(): void
{
$this->login();

$name = 'Test_Computer_no_create_' . random_int(1, PHP_INT_MAX);

$_SESSION['glpiactiveprofile'][Computer::$rightname] = READ;

$results = $this->injectData(
new PluginDatainjectionComputerInjection(),
['Computer' => ['name' => $name]],
['Computer' => ['name' => true]],
);

self::assertSame(PluginDatainjectionCommonInjectionLib::WARNING, $results['status']);
self::assertSame(0, countElementsInTable('glpi_computers', ['name' => $name]));
}
}
Loading