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
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 massive action update on CustomAssets

## [1.24.4] - 2026-08-06

### Fixed
Expand Down
12 changes: 5 additions & 7 deletions hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ function plugin_fields_install()

$migration = new Migration($version);
if (isCommandLine()) {
echo __('MySQL tables installation', 'fields') . "\n";
echo __s('MySQL tables installation', 'fields') . "\n";
} else {
echo '<center>';
echo "<table class='tab_cadre_fixe'>";
echo '<tr><th>' . __('MySQL tables installation', 'fields') . '<th></tr>';
echo '<tr><th>' . __s('MySQL tables installation', 'fields') . '<th></tr>';

echo "<tr class='tab_bg_1'>";
echo "<td align='center'>";
Expand Down Expand Up @@ -111,7 +111,7 @@ function plugin_fields_uninstall()
{
if (!class_exists('PluginFieldsProfile')) {
Session::addMessageAfterRedirect(
__("The plugin can't be uninstalled when the plugin is disabled", 'fields'),
__s("The plugin can't be uninstalled when the plugin is disabled", 'fields'),
true,
WARNING,
true,
Expand All @@ -124,7 +124,7 @@ function plugin_fields_uninstall()

echo '<center>';
echo "<table class='tab_cadre_fixe'>";
echo '<tr><th>' . __('MySQL tables uninstallation', 'fields') . '<th></tr>';
echo '<tr><th>' . __s('MySQL tables uninstallation', 'fields') . '<th></tr>';

echo "<tr class='tab_bg_1'>";
echo "<td align='center'>";
Expand Down Expand Up @@ -227,9 +227,7 @@ function plugin_fields_MassiveActionsFieldsDisplay($options = [])
);
}

PluginFieldsField::showSingle($options['itemtype'], $options['options'], true);

return true;
return PluginFieldsField::showSingle($options['itemtype'], $options['options'], true);
}

// Need to return false on non display item
Expand Down
12 changes: 11 additions & 1 deletion inc/container.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2097,12 +2097,22 @@ private static function populateData($c_id, CommonDBTM $item)

//managed multi GLPI item dropdown field
if (preg_match('/^dropdown-(?<type>.+)$/', (string) $field['type'], $match) === 1) {
$defined_key = '_' . $field['name'] . '_defined';
//values are defined by user
if (isset($item->input[$field['name']])) {
$data[$field['name']] = $item->input[$field['name']];
$has_fields = true;
} else { //multi dropdown is empty or has been emptied
} elseif (
isset($item->input[$defined_key])
&& $item->input[$defined_key]
) { //multi dropdown is empty or has been emptied
$data[$field['name']] = [];
$has_fields = true;
} elseif (isset($_REQUEST['massiveaction'])) { // called from massiveaction
if (isset($_POST[$field['name']])) {
$data[$field['name']] = $_POST[$field['name']];
$has_fields = true;
}
Comment on lines +2111 to +2115

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.

Suggested change
} elseif (isset($_REQUEST['massiveaction'])) { // called from massiveaction
if (isset($_POST[$field['name']])) {
$data[$field['name']] = $_POST[$field['name']];
$has_fields = true;
}
} elseif (
isset($_REQUEST['massiveaction'])
&& (isset($_POST[$field['name']])
) {
$data[$field['name']] = $_POST[$field['name']];
$has_fields = true;

}
}
}
Expand Down
10 changes: 9 additions & 1 deletion inc/field.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,14 @@ public static function showSingle($itemtype, $searchOption, $massiveaction = fal
(string) $searchOption['linkfield'],
);

// itemtype is stored in a JSON array, so entry is surrounded by double quotes
$search_string = json_encode($itemtype);
// Backslashes must be doubled in LIKE clause according to MySQL documentation
// But do not escape backslashes for CustomAsset, as they are alrady escaped
if (!str_contains($search_string, 'CustomAsset')) {
Comment on lines +1367 to +1368

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.

Suggested change
// But do not escape backslashes for CustomAsset, as they are alrady escaped
if (!str_contains($search_string, 'CustomAsset')) {
// But do not escape backslashes for namespaced itemtypes, as they are already escaped
if (!str_contains((string) $itemtype, '\\')) {

$search_string = str_replace('\\', '\\\\', $search_string);
}

//find field
$iterator = $DB->request([
'SELECT' => [
Expand All @@ -1381,7 +1389,7 @@ public static function showSingle($itemtype, $searchOption, $massiveaction = fal
],
'WHERE' => [
'fields.name' => $cleaned_linkfield,
'containers.itemtypes' => ['LIKE', sprintf('%%%s%%', $itemtype)],
'containers.itemtypes' => ['LIKE', '%' . $DB->escape($search_string) . '%'],
],
]);

Expand Down
10 changes: 10 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<psalm
runTaintAnalysis="true"
>
<projectFiles>
<directory name="src"/>
<file name="hook.php"/>
<file name="setup.php"/>
Comment on lines +6 to +8

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.

I think we need to create a dedicated PR for PSALM to make it complete:

Suggested change
<directory name="src"/>
<file name="hook.php"/>
<file name="setup.php"/>
<directory name="ajax" />
<directory name="front" />
<directory name="inc" />
<directory name="src" />
<directory name="tests" />
<file name="hook.php" />
<file name="setup.php" />

</projectFiles>
</psalm>
107 changes: 107 additions & 0 deletions tests/Units/MassiveActionCustomAssetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

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

declare(strict_types=1);

namespace GlpiPlugin\Field\Tests\Units;

use Glpi\Tests\DbTestCase;
use Glpi\Tests\GLPITestCase;
use GlpiPlugin\Field\Tests\FieldTestTrait;
use PluginFieldsContainer;
use PluginFieldsField;
use Search;

require_once __DIR__ . '/../FieldTestCase.php';

/**
* Reproduces the bug where the massive action "update" widget for a Fields
* plugin field is never rendered for CustomAsset itemtypes (namespaced
* classes like Glpi\CustomAsset\XxxAsset), because PluginFieldsField::showSingle()
* builds a LIKE query against the un-escaped itemtype string.
*/
final class MassiveActionCustomAssetTest extends DbTestCase
{
use FieldTestTrait;

public function setUp(): void
{
GLPITestCase::setUp();
$this->login();
}

public function tearDown(): void
{
$this->tearDownFieldTest();
GLPITestCase::tearDown();
}

public function testShowSingleDisplaysFieldForCustomAsset(): void
{
$definition = $this->initAssetDefinition('so' . substr((string) $this->getUniqueString(), 0, 6));
$asset_class = $definition->getAssetClassName();

$container = $this->createFieldContainer([
'label' => 'F',
'type' => 'tab',
'itemtypes' => [$asset_class],
'is_active' => 1,
'entities_id' => 0,
'is_recursive' => 1,
]);

$field = $this->createField([
'label' => 'Custom Asset Field',
'type' => 'text',
PluginFieldsContainer::getForeignKeyField() => $container->getID(),
'ranking' => 1,
'is_active' => 1,
'is_readonly' => 0,
]);
$field_name = $field->fields['name'];

$search_option = null;
foreach (Search::getOptions($asset_class) as $so) {
if (($so['linkfield'] ?? null) === $field_name) {
$search_option = $so;
break;
}
}

$this->assertIsArray($search_option, 'search option not found for plugin field on custom asset');

ob_start();
$result = PluginFieldsField::showSingle($asset_class, $search_option, true);
$html = ob_get_clean();

$this->assertTrue($result, 'showSingle() should find the field container for a CustomAsset itemtype');
$this->assertStringContainsString($field_name, $html);
}
}
Loading
Loading