diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46dc0a19..0bba1dab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Add Transifex translations link to README
+- Permit to Inject GlpiAsset
## [2.16.0] - 2026-10-05
diff --git a/front/info.form.php b/front/info.form.php
index 5f21cb55..3171d332 100644
--- a/front/info.form.php
+++ b/front/info.form.php
@@ -28,8 +28,6 @@
* -------------------------------------------------------------------------
*/
-Session::checkLoginUser();
-
/* Update mappings */
if (isset($_POST["update"])) {
$model = new PluginDatainjectionModel();
diff --git a/front/model.form.php b/front/model.form.php
index 266a47b3..a01921be 100644
--- a/front/model.form.php
+++ b/front/model.form.php
@@ -28,8 +28,6 @@
* -------------------------------------------------------------------------
*/
-Session::checkLoginUser();
-
$_GET["id"] ??= "";
$_GET["withtemplate"] ??= "";
diff --git a/front/model.php b/front/model.php
index 8a757caa..065d2466 100644
--- a/front/model.php
+++ b/front/model.php
@@ -28,7 +28,6 @@
* -------------------------------------------------------------------------
*/
-Session::checkLoginUser();
Html::header(
PluginDatainjectionModel::getTypeName(),
diff --git a/front/popup.php b/front/popup.php
index 4b6747b9..498ad2f6 100644
--- a/front/popup.php
+++ b/front/popup.php
@@ -28,8 +28,6 @@
* -------------------------------------------------------------------------
*/
-Session::checkLoginUser();
-
switch ($_GET["popup"]) {
case "preview":
$models_id = (int) ($_GET['models_id'] ?? 0);
diff --git a/inc/commoninjectionlib.class.php b/inc/commoninjectionlib.class.php
index 97d6ceb9..b265310b 100644
--- a/inc/commoninjectionlib.class.php
+++ b/inc/commoninjectionlib.class.php
@@ -27,11 +27,12 @@
* @link https://github.com/pluginsGLPI/datainjection
* -------------------------------------------------------------------------
*/
-
+use Glpi\Asset\Asset;
use Glpi\DBAL\QueryExpression;
use Glpi\DBAL\QuerySubQuery;
use Glpi\Exception\Http\HttpException;
use Glpi\Features\AssignableItem;
+use GlpiPlugin\Datainjection\Glpi\Asset\AssetInjection;
use function Safe\preg_match;
use function Safe\preg_replace;
@@ -356,6 +357,15 @@ public static function getItemtypeByInjection($injectionClassName)
public static function getItemtypeByInjectionClass($injectionClass)
{
+ if ($injectionClass instanceof AssetInjection) {
+ // Custom assets all share the "glpi_assets_assets" table, whose canonical
+ // itemtype is the *abstract* Glpi\Asset\Asset. Resolving through the table
+ // (getItemTypeForTable) is therefore unreliable and can return that abstract
+ // class, which is not instantiable. The injection class already knows its
+ // concrete custom-asset itemtype, so use it directly.
+ return $injectionClass::getVirtualType();
+ }
+
return Toolbox::ucfirst(getItemTypeForTable($injectionClass->getTable()));
}
@@ -365,7 +375,10 @@ public static function getItemtypeByInjectionClass($injectionClass)
*
* @param string $itemtype the itemtype
*
- * @return PluginDatainjectionInjectionInterface the injection class instance
+ * Every injection class both implements the interface and extends CommonDBTM,
+ * so the returned instance exposes the CommonDBTM API (getTable(), ...) too.
+ *
+ * @return PluginDatainjectionInjectionInterface&CommonDBTM the injection class instance
*/
public static function getInjectionClassInstance($itemtype)
{
@@ -377,7 +390,15 @@ public static function getInjectionClassInstance($itemtype)
$injectionClass = ucfirst($itemtype) . 'Injection';
}
- if (!is_a($injectionClass, PluginDatainjectionInjectionInterface::class, true)) {
+ if (!class_exists($injectionClass)) {
+ plugin_datainjection_creationInjectableAssets();
+ }
+
+
+ if (
+ !is_a($injectionClass, PluginDatainjectionInjectionInterface::class, true)
+ || !is_a($injectionClass, CommonDBTM::class, true)
+ ) {
throw new HttpException(500, 'Class ' . $injectionClass . ' is not a valid class');
}
@@ -626,7 +647,8 @@ private function getFieldValue(
case 'dropdown':
case 'relation':
- $tmptype = getItemTypeForTable($searchOption['table']);
+ // Custom dropdowns share one table: their itemtype cannot be resolved from it
+ $tmptype = $searchOption['dropdown_itemtype'] ?? getItemTypeForTable($searchOption['table']);
if (!is_a($tmptype, CommonDBTM::class, true)) {
return;
}
@@ -2145,6 +2167,10 @@ private function dataAlreadyInDB($injectionClass, $itemtype)
}
}
+ if ($injectionClass instanceof Asset && method_exists($injectionClass, 'getAssetDefinitionID')) {
+ $where['assets_assetdefinitions_id'] = $injectionClass->getAssetDefinitionID();
+ }
+
//Add additional parameters specific to this itemtype (or function checkPresent exists)
if (method_exists($injectionClass, 'checkPresent')) {
$extra = $injectionClass->checkPresent($this->values, $options);
@@ -2447,8 +2473,13 @@ public static function addToSearchOptions(
);
$main_itemtype = self::getItemtypeByInjectionClass($injectionClass);
+ $main_table = getTableForItemType($main_itemtype);
foreach ($type_searchOptions as &$value) {
- $value_itemtype = getItemTypeForTable($value['table']);
+ $value_itemtype = $value['table'] === $main_table
+ // Custom assets share a table resolved to the abstract Glpi\Asset\Asset,
+ // whose getTypeName() needs a concrete definition
+ ? $main_itemtype
+ : getItemTypeForTable($value['table']);
$value_itemtype_name_singular = $value_itemtype::getTypeName(1);
$value_itemtype_name_plural = $value_itemtype::getTypeName(Session::getPluralNumber());
if (
diff --git a/inc/info.class.php b/inc/info.class.php
index 4527d57e..3e35e1e8 100644
--- a/inc/info.class.php
+++ b/inc/info.class.php
@@ -359,7 +359,7 @@ public static function showAdditionalInformation(
}
Dropdown::show(
- getItemTypeForTable($option['table']),
+ $option['dropdown_itemtype'] ?? getItemTypeForTable($option['table']),
['name' => $name,
'value' => $value,
],
diff --git a/inc/injectiontype.class.php b/inc/injectiontype.class.php
index 8da2105e..215f8559 100644
--- a/inc/injectiontype.class.php
+++ b/inc/injectiontype.class.php
@@ -28,6 +28,7 @@
* -------------------------------------------------------------------------
*/
use Glpi\Exception\Http\HttpException;
+use GlpiPlugin\Datainjection\Glpi\Asset\AssetInjection;
use function Safe\json_decode;
use function Safe\json_encode;
@@ -80,6 +81,11 @@ public static function getItemtypes($only_primary = false)
}
$name .= call_user_func([$type, 'getTypeName']);
+
+ if ($instance instanceof AssetInjection) {
+ $typename = $instance->getVirtualType();
+ }
+
$values[$typename] = $name;
}
}
@@ -396,7 +402,7 @@ public static function getUsedMappingsOrInfos($options = [])
foreach ($options as $option) {
if (
isset($option['table'])
- && ($option['table'] == getItemTypeForTable($data['itemtype']))
+ && ($option['table'] == $injectionClass->getTable())
&& ($option['linkfield'] == $data['value'])
&& ($option['displaytype'] != 'multiline_text')
&& ($mapping_or_info['value'] != $data['value'])
diff --git a/phpstan.neon b/phpstan.neon
index db69ea19..275425e8 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -9,6 +9,7 @@ parameters:
- ajax
- front
- inc
+ - src
- hook.php
- setup.php
scanDirectories:
diff --git a/setup.php b/setup.php
index 87c38dbf..935c86b9 100644
--- a/setup.php
+++ b/setup.php
@@ -28,8 +28,13 @@
* -------------------------------------------------------------------------
*/
+use Glpi\Asset\AssetDefinition;
+use Glpi\Asset\AssetDefinitionManager;
+use GlpiPlugin\Datainjection\Glpi\Asset\Capacity\IsInjectableCapacity;
+
use function Safe\define;
use function Safe\mkdir;
+use function Safe\preg_match;
define('PLUGIN_DATAINJECTION_VERSION', '2.16.0');
@@ -48,6 +53,10 @@ function plugin_init_datainjection()
/** @var array $CFG_GLPI */
/** @var array $INJECTABLE_TYPES */
global $PLUGIN_HOOKS, $CFG_GLPI, $INJECTABLE_TYPES;
+ $CFG_GLPI['injectable_types'] ??= [];
+
+ $asset_definition_manager = AssetDefinitionManager::getInstance();
+ $asset_definition_manager->registerCapacity(new IsInjectableCapacity());
$PLUGIN_HOOKS['csrf_compliant']['datainjection'] = true;
$PLUGIN_HOOKS['migratetypes']['datainjection'] = 'plugin_datainjection_migratetypes_datainjection';
@@ -85,6 +94,7 @@ function plugin_init_datainjection()
$PLUGIN_HOOKS['add_javascript']['datainjection'] = 'js/datainjection.js';
$INJECTABLE_TYPES = [];
+
}
}
@@ -114,8 +124,8 @@ function plugin_version_datainjection()
function getTypesToInject(): void
{
/** @var array $INJECTABLE_TYPES */
- /** @var array $PLUGIN_HOOKS */
- global $INJECTABLE_TYPES,$PLUGIN_HOOKS;
+ /** @var array $CFG_GLPI */
+ global $INJECTABLE_TYPES,$CFG_GLPI;
if (count($INJECTABLE_TYPES)) {
// already populated
@@ -218,6 +228,112 @@ function getTypesToInject(): void
];
//Add plugins
Plugin::doHook('plugin_datainjection_populate');
+
+ // Register injectable assets dynamically
+ plugin_datainjection_registerInjectableAssets();
+
+ plugin_datainjection_creationInjectableAssets();
+}
+
+
+/**
+ * Register injection classes for each injectable asset definition
+ */
+function plugin_datainjection_registerInjectableAssets(): void
+{
+ /** @var array $INJECTABLE_TYPES */
+ /** @var array $CFG_GLPI */
+ global $INJECTABLE_TYPES, $CFG_GLPI;
+
+ if (!isset($CFG_GLPI['injectable_types']) || empty($CFG_GLPI['injectable_types'])) {
+ return;
+ }
+
+ // For each injectable asset definition, create and register a distinct class
+ foreach ($CFG_GLPI['injectable_types'] as $definition_id => $itemtype) {
+ // Get the asset definition to extract the system name
+
+ $definition = AssetDefinition::getById($definition_id);
+ if ($definition === false || $definition === null) {
+ continue;
+ }
+
+ if ($definition->getAssetClassName() === $itemtype) {
+ // Use the system name to create a nice class name
+ $system_name = ucfirst($definition->fields['system_name']);
+ $injection_class = 'PluginDatainjection' . $system_name . 'AssetInjection';
+ // Only create if not already registered
+ $INJECTABLE_TYPES[$injection_class] ??= 'datainjection';
+ }
+ }
+}
+
+/**
+ * Register injection classes for each injectable asset definition
+ */
+function plugin_datainjection_creationInjectableAssets(): void
+{
+ /** @var array $CFG_GLPI */
+ global $CFG_GLPI;
+
+ if (!isset($CFG_GLPI['injectable_types']) || empty($CFG_GLPI['injectable_types'])) {
+ return;
+ }
+
+
+ // For each injectable asset definition, create and register a distinct class
+ foreach ($CFG_GLPI['injectable_types'] as $definition_id => $itemtype) {
+ $definition = AssetDefinition::getById($definition_id);
+ if ($definition === false) {
+ continue;
+ }
+
+ // Get the asset definition to extract the system name
+ if ($definition->getAssetClassName() === $itemtype) {
+ // Use the system name to create a nice class name
+ $system_name = ucfirst($definition->fields['system_name']);//strtolower()
+ $injection_class = 'PluginDatainjection' . $system_name . 'AssetInjection';
+
+ plugin_datainjection_createAssetInjectionClass($injection_class, $definition_id);
+ }
+ }
+}
+
+/**
+ * Dynamically create an asset injection class
+ */
+function plugin_datainjection_createAssetInjectionClass(string $class_name, int $definition_id): void
+{
+
+ // Check if class already exists
+ if (class_exists($class_name)) {
+ return;
+ }
+
+ // Create the class dynamically (not final, so it can be extended)
+ $code = <<.
+ * -------------------------------------------------------------------------
+ * @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\Glpi\Asset;
+
+use CommonDBTM;
+use Glpi\Asset\Asset;
+use Glpi\Asset\AssetDefinition;
+use Glpi\Asset\AssetDefinitionManager;
+use Glpi\Asset\CustomFieldDefinition;
+use Glpi\Asset\CustomFieldType\BooleanType;
+use Glpi\Asset\CustomFieldType\DateTimeType;
+use Glpi\Asset\CustomFieldType\DateType;
+use Glpi\Asset\CustomFieldType\DropdownType;
+use Glpi\Asset\CustomFieldType\NumberType;
+use Glpi\Asset\CustomFieldType\StringType;
+use Glpi\Asset\CustomFieldType\TextType;
+use PluginDatainjectionCommonInjectionLib;
+use PluginDatainjectionInjectionInterface;
+use Search;
+
+abstract class AssetInjection extends Asset implements PluginDatainjectionInjectionInterface
+{
+ protected static string $definition_system_name = '';
+
+ protected static int $fixed_asset_definition_id = 0;
+
+ public static function getAssetDefinitionID(): int
+ {
+
+ // If a fixed ID is set (for dynamically created classes), use it
+ if (static::$fixed_asset_definition_id > 0) {
+ return static::$fixed_asset_definition_id;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Get the first active asset definition to use as system name
+ */
+ protected static function getAssetDefinitionSystemName(): string
+ {
+ // Get first active asset definition from the database
+ // This ensures we have a valid definition to use
+ $manager = AssetDefinitionManager::getInstance();
+ foreach ($manager->getDefinitions() as $definition) {
+ if ($definition->fields['id'] == static::getAssetDefinitionID()) {
+ return $definition->fields['system_name'];
+ }
+ }
+
+ // Fallback to a default if none found
+ return '';
+ }
+
+ public static function getTypeName($nb = 0)
+ {
+ $manager = AssetDefinitionManager::getInstance();
+ foreach ($manager->getDefinitions() as $definition) {
+ if ($definition->fields['id'] == static::getAssetDefinitionID()) {
+ return $definition->fields['label'];
+ }
+ }
+
+ // Fallback to a default if none found
+ return '';
+ }
+
+ public static function getDefinition(): AssetDefinition
+ {
+ if (empty(static::$definition_system_name)) {
+ static::$definition_system_name = static::getAssetDefinitionSystemName();
+ }
+
+ return parent::getDefinition();
+ }
+
+
+ public static function getVirtualType($classname = null)
+ {
+ return 'Glpi\\CustomAsset\\' . static::getAssetDefinitionSystemName() . 'Asset';
+ }
+
+ public static function getVirtualTable($classname = null)
+ {
+ return getTableForItemType(self::getVirtualType());
+ }
+
+ public static function getTable($classname = null)
+ {
+ return getTableForItemType(Asset::class);
+ }
+
+ public static function canCreate(): bool
+ {
+ $virtual_type = static::getVirtualType();
+ return $virtual_type::canCreate();
+ }
+
+ public function isPrimaryType()
+ {
+ return true;
+ }
+
+ public function connectedTo()
+ {
+ return [];
+ }
+
+ public function isNullable($field)
+ {
+ return true;
+ }
+
+
+ public static function replaceTableName(array $data): array
+ {
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $data[$key] = self::replaceTableName($value); // récursif
+ } elseif ($value === self::getTable()) {
+ $data[$key] = self::getVirtualTable();
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @see plugins/datainjection/inc/PluginDatainjectionInjectionInterface::getOptions()
+ */
+ public function getOptions($primary_type = '')
+ {
+
+ // Build search options manually to avoid registration checks
+ $tab = Search::getOptions(self::getVirtualType());
+
+ $tab = self::replaceTableName($tab);
+ unset($tab[4]);
+ unset($tab[40]);
+
+ $tab2 = [
+ [
+ 'id' => '200',
+ 'table' => static::getTable(),
+ 'field' => 'assets_assetdefinitions_id',
+ 'linkfield' => 'assets_assetdefinitions_id',
+ 'name' => AssetDefinition::getTypeName(),
+ 'datatype' => 'number',
+ 'massiveaction' => false,
+ 'injectable' => true,
+ ],
+ ];
+
+ foreach ($tab2 as $entry) {
+ $tab[(int) $entry['id']] = $entry;
+ }
+
+ //Remove some options because some fields cannot be imported
+ $blacklist = PluginDatainjectionCommonInjectionLib::getBlacklistedOptions(static::class);
+ $notimportable = ['300', '301'];
+ $options = [];
+ $options['ignore_fields'] = array_merge($blacklist, $notimportable);
+ $options['displaytype'] = ['multiline_text' => [16]];
+
+ $tab = PluginDatainjectionCommonInjectionLib::addToSearchOptions($tab, $options, $this);
+
+ // Added after the generic filter, which only keeps the core options (id < 1000)
+ foreach (static::getDefinition()->getCustomFieldDefinitions() as $custom_field) {
+ $option = $this->getCustomFieldOption($custom_field);
+ if ($option !== null) {
+ $tab[$option['id']] = $option;
+ }
+ }
+
+ return $tab;
+ }
+
+ /**
+ * Injection option of a custom field
+ *
+ * The core search option of a custom field cannot be injected: its linkfield is the
+ * JSON column "custom_fields", shared by every custom field. The value is injected
+ * as "custom_", which Asset::prepareInputForAdd/Update() stores in it.
+ *
+ * @return array|null null for a type that cannot be injected
+ */
+ private function getCustomFieldOption(CustomFieldDefinition $custom_field): ?array
+ {
+ $field_type = $custom_field->getFieldType();
+ $option = [
+ 'id' => $custom_field->getSearchOptionID(),
+ 'name' => $custom_field->computeFriendlyName(),
+ 'table' => self::getVirtualTable(),
+ 'field' => 'custom_' . $custom_field->fields['system_name'],
+ 'linkfield' => 'custom_' . $custom_field->fields['system_name'],
+ 'injectable' => PluginDatainjectionCommonInjectionLib::FIELD_INJECTABLE,
+ ];
+
+ // DateTimeType extends DateType: test it first
+ if ($field_type instanceof DateTimeType) {
+ return $option + ['displaytype' => 'text', 'checktype' => 'datetime', 'datatype' => 'datetime'];
+ }
+
+ if ($field_type instanceof DateType) {
+ return $option + ['displaytype' => 'text', 'checktype' => 'date', 'datatype' => 'date'];
+ }
+
+ if ($field_type instanceof BooleanType) {
+ return $option + ['displaytype' => 'bool', 'checktype' => 'bool', 'datatype' => 'bool'];
+ }
+
+ if ($field_type instanceof NumberType) {
+ return $option + ['displaytype' => 'text', 'checktype' => 'float', 'datatype' => 'decimal'];
+ }
+
+ if ($field_type instanceof TextType) {
+ return $option + ['displaytype' => 'multiline_text', 'checktype' => 'text', 'datatype' => 'text'];
+ }
+
+ // Also URLType, which extends StringType
+ if ($field_type instanceof StringType) {
+ return $option + ['displaytype' => 'text', 'checktype' => 'text', 'datatype' => 'string'];
+ }
+
+ // A multiple dropdown would need a separator in the imported value: not handled
+ if (
+ $field_type instanceof DropdownType
+ && !($custom_field->fields['field_options']['multiple'] ?? false)
+ && is_a($custom_field->fields['itemtype'], CommonDBTM::class, true)
+ ) {
+ return $option + [
+ 'displaytype' => 'dropdown',
+ 'checktype' => 'text',
+ 'datatype' => 'dropdown',
+ // The option keeps the asset table, like the other custom fields: the lib
+ // finds the dropdown through this itemtype (custom dropdowns share one table)
+ 'dropdown_itemtype' => $custom_field->fields['itemtype'],
+ ];
+ }
+
+ return null;
+ }
+
+
+ /**
+ * @param array $fields_toinject array
+ **/
+ public function getValueForAdditionalMandatoryFields($fields_toinject = [])
+ {
+ $fields_toinject[self::getVirtualType()]['assets_assetdefinitions_id'] = static::getAssetDefinitionID();
+
+ return $fields_toinject;
+ }
+
+ /**
+ * @see plugins/datainjection/inc/PluginDatainjectionInjectionInterface::addOrUpdateObject()
+ */
+ public function addOrUpdateObject($values = [], $options = [])
+ {
+
+ $lib = new PluginDatainjectionCommonInjectionLib($this, $values, $options);
+ $lib->processAddOrUpdate();
+ return $lib->getInjectionResults();
+ }
+}
diff --git a/src/Glpi/Asset/Capacity/IsInjectableCapacity.php b/src/Glpi/Asset/Capacity/IsInjectableCapacity.php
new file mode 100644
index 00000000..e3bd07ce
--- /dev/null
+++ b/src/Glpi/Asset/Capacity/IsInjectableCapacity.php
@@ -0,0 +1,86 @@
+.
+ * -------------------------------------------------------------------------
+ * @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\Glpi\Asset\Capacity;
+
+use Glpi\Asset\AssetDefinitionManager;
+use Glpi\Asset\Capacity\AbstractCapacity;
+use Glpi\Asset\CapacityConfig;
+use Override;
+
+final class IsInjectableCapacity extends AbstractCapacity
+{
+ #[Override]
+ public function getLabel(): string
+ {
+ return __('Injectable', 'datainjection');
+ }
+
+ #[Override]
+ public function getIcon(): string
+ {
+ return 'ti ti-download';
+ }
+
+ #[Override]
+ public function getDescription(): string
+ {
+ return __("Inject objects list", 'datainjection');
+ }
+
+
+ public function onClassBootstrap(string $classname, CapacityConfig $config): void
+ {
+ global $CFG_GLPI;
+
+ // if (!isset($CFG_GLPI['injectable_types'])) {
+ // $CFG_GLPI['injectable_types'] = [];
+ // }
+ $manager = AssetDefinitionManager::getInstance();
+ foreach ($manager->getDefinitions() as $definition) {
+ $itemtype = $definition->getAssetClassName();
+ if ($itemtype == $classname) {
+ if (!in_array($classname, $CFG_GLPI['injectable_types'])) {
+ $CFG_GLPI['injectable_types'][$definition->fields['id']] = $classname;
+ }
+ }
+ }
+
+ $this->registerToTypeConfig('injectable_types', $classname);
+
+ // CommonGLPI::registerStandardTab($classname, Infocom::class, 50);
+ }
+
+ public function getCapacityUsageDescription(string $classname): string
+ {
+ // TODO: Implement getCapacityUsageDescription() method.
+ return '';
+ }
+}
diff --git a/tests/unit/AssetInjectionTest.php b/tests/unit/AssetInjectionTest.php
new file mode 100644
index 00000000..010fcb7b
--- /dev/null
+++ b/tests/unit/AssetInjectionTest.php
@@ -0,0 +1,257 @@
+.
+ * -------------------------------------------------------------------------
+ * @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 Glpi\Asset\AssetDefinitionManager;
+use Glpi\Asset\Capacity;
+use Glpi\Asset\CustomFieldDefinition;
+use Glpi\Asset\CustomFieldType\BooleanType;
+use Glpi\Asset\CustomFieldType\DateType;
+use Glpi\Asset\CustomFieldType\DropdownType;
+use Glpi\Asset\CustomFieldType\NumberType;
+use Glpi\Asset\CustomFieldType\StringType;
+use Glpi\Asset\CustomFieldType\TextType;
+use Glpi\Tests\DbTestCase;
+use GlpiPlugin\Datainjection\Glpi\Asset\Capacity\IsInjectableCapacity;
+use Location;
+use PluginDatainjectionCommonInjectionLib;
+
+final class AssetInjectionTest extends DbTestCase
+{
+ /**
+ * End-to-end injection into a custom GlpiAsset definition:
+ * enable IsInjectableCapacity on a definition, let the plugin generate the
+ * dedicated AssetInjection subclass, inject a row through it and assert the
+ * resulting custom asset item is persisted and tied to the definition.
+ */
+ public function testRowIsInjectedIntoInjectableCustomAssetDefinition(): void
+ {
+ // The AssetDefinitionManager singleton is rebuilt between tests and only
+ // carries core capacities; the plugin capacity is registered from
+ // plugin_init_datainjection() at runtime, so re-register it explicitly here.
+ $manager = AssetDefinitionManager::getInstance();
+ $manager->registerCapacity(new IsInjectableCapacity());
+
+ // Create a custom asset definition with the "Injectable" capacity enabled.
+ $system_name = 'Injectableasset';
+ $definition = $this->initAssetDefinition(
+ $system_name,
+ [new Capacity(name: IsInjectableCapacity::class)],
+ );
+ // Log in once the definition exists: the session then loads the rights it grants
+ // to the profile, which the injection checks before creating the asset
+ $this->login();
+
+ $asset_itemtype = $definition->getAssetClassName();
+
+ // Reproduce the runtime registration pipeline that turns an injectable
+ // definition into a concrete injection class:
+ // - bootDefinitions() re-reads the new definition and triggers
+ // IsInjectableCapacity::onClassBootstrap(), which populates injectable_types;
+ // - the two registration helpers then eval() the dedicated AssetInjection subclass.
+ /** @var array $CFG_GLPI */
+ global $CFG_GLPI;
+ $CFG_GLPI['injectable_types'] = [];
+ $manager->bootDefinitions();
+
+ self::assertArrayHasKey(
+ $definition->getID(),
+ $CFG_GLPI['injectable_types'],
+ 'IsInjectableCapacity should register the definition as an injectable type',
+ );
+ self::assertSame($asset_itemtype, $CFG_GLPI['injectable_types'][$definition->getID()]);
+
+ plugin_datainjection_registerInjectableAssets();
+ plugin_datainjection_creationInjectableAssets();
+
+ // The subclass name is built exactly the way setup.php builds it.
+ $injection_classname = 'PluginDatainjection' . ucfirst($system_name) . 'AssetInjection';
+ self::assertTrue(
+ class_exists($injection_classname),
+ "Injection class {$injection_classname} should have been generated",
+ );
+
+ $injection = new $injection_classname();
+
+ // The lib keys both the injected values and the results by the itemtype it
+ // derives from the injection class; reuse that exact value to avoid any
+ // table round-trip mismatch.
+ $primary_type = PluginDatainjectionCommonInjectionLib::getItemtypeByInjectionClass($injection);
+ self::assertSame($asset_itemtype, $primary_type);
+
+ $asset_name = 'Injected custom asset';
+ $results = $injection->addOrUpdateObject(
+ [
+ $primary_type => [
+ 'name' => $asset_name,
+ ],
+ ],
+ [
+ 'rights' => [
+ 'can_add' => true,
+ 'can_update' => true,
+ 'add_dropdown' => true,
+ ],
+ 'mandatory_fields' => [
+ $primary_type => ['name' => true],
+ ],
+ // The session of login() only sees the test entities: the injection
+ // checks the CREATE right on the target entity
+ 'entities_id' => $this->getTestRootEntity(true),
+ ],
+ );
+
+ self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);
+ self::assertSame(PluginDatainjectionCommonInjectionLib::IMPORT_ADD, $results['type']);
+
+ $asset_id = $results[$primary_type] ?? null;
+ self::assertNotNull($asset_id, 'The injection result should expose the created asset id');
+ self::assertGreaterThan(0, (int) $asset_id);
+
+ // The row must exist as a real custom asset, tied to our definition.
+ $asset = new $asset_itemtype();
+ self::assertTrue($asset->getFromDB((int) $asset_id));
+ self::assertSame($asset_name, $asset->fields['name']);
+ self::assertSame(
+ $definition->getID(),
+ (int) $asset->fields['assets_assetdefinitions_id'],
+ );
+ }
+
+ /**
+ * Custom fields are stored in a JSON column shared by all of them: each one must be
+ * offered as its own injectable field and saved through "custom_".
+ */
+ public function testCustomFieldsAreInjected(): void
+ {
+ $manager = AssetDefinitionManager::getInstance();
+ $manager->registerCapacity(new IsInjectableCapacity());
+
+ $definition = $this->initAssetDefinition(
+ 'Injectablecustomfields',
+ [new Capacity(name: IsInjectableCapacity::class)],
+ );
+ $fields = [
+ 'serial_ref' => StringType::class,
+ 'notes' => TextType::class,
+ 'quantity' => NumberType::class,
+ 'delivery' => DateType::class,
+ 'is_spare' => BooleanType::class,
+ ];
+ foreach ($fields as $system_name => $type) {
+ $this->createItem(CustomFieldDefinition::class, [
+ 'assets_assetdefinitions_id' => $definition->getID(),
+ 'system_name' => $system_name,
+ 'label' => ucfirst($system_name),
+ 'type' => $type,
+ ]);
+ }
+
+ $this->createItem(CustomFieldDefinition::class, [
+ 'assets_assetdefinitions_id' => $definition->getID(),
+ 'system_name' => 'room',
+ 'label' => 'Room',
+ 'type' => DropdownType::class,
+ 'itemtype' => Location::class,
+ ]);
+ $this->createItem(CustomFieldDefinition::class, [
+ 'assets_assetdefinitions_id' => $definition->getID(),
+ 'system_name' => 'tags',
+ 'label' => 'Tags',
+ 'type' => DropdownType::class,
+ 'itemtype' => Location::class,
+ 'field_options' => ['multiple' => true],
+ ], ['field_options']);
+ $definition->getFromDB($definition->getID());
+ $this->login();
+
+ /** @var array $CFG_GLPI */
+ global $CFG_GLPI;
+ $CFG_GLPI['injectable_types'] = [];
+ $manager->bootDefinitions();
+ plugin_datainjection_registerInjectableAssets();
+ plugin_datainjection_creationInjectableAssets();
+
+ $injection_classname = 'PluginDatainjectionInjectablecustomfieldsAssetInjection';
+ $injection = new $injection_classname();
+ $primary_type = PluginDatainjectionCommonInjectionLib::getItemtypeByInjectionClass($injection);
+
+ // Every custom field but the multiple dropdown is offered, under its own linkfield
+ $linkfields = array_column($injection->getOptions($primary_type), 'linkfield');
+ foreach (['serial_ref', 'notes', 'quantity', 'delivery', 'is_spare', 'room'] as $system_name) {
+ self::assertContains('custom_' . $system_name, $linkfields);
+ }
+
+ self::assertNotContains('custom_tags', $linkfields);
+ self::assertNotContains('custom_fields', $linkfields);
+
+ $results = $injection->addOrUpdateObject(
+ [
+ $primary_type => [
+ 'name' => 'Asset with custom fields',
+ 'custom_serial_ref' => 'REF-42',
+ 'custom_notes' => "first line\nsecond line",
+ 'custom_quantity' => '7',
+ 'custom_delivery' => '2026-10-23',
+ 'custom_is_spare' => '1',
+ 'custom_room' => 'Injected room',
+ ],
+ ],
+ [
+ 'rights' => [
+ 'can_add' => true,
+ 'can_update' => true,
+ 'add_dropdown' => true,
+ ],
+ 'mandatory_fields' => [
+ $primary_type => ['name' => true],
+ ],
+ 'entities_id' => $this->getTestRootEntity(true),
+ ],
+ );
+
+ self::assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']);
+
+ $asset = new ($definition->getAssetClassName())();
+ self::assertTrue($asset->getFromDB((int) $results[$primary_type]));
+
+ // The room did not exist: add_dropdown allowed to create it
+ $location = new Location();
+ self::assertTrue($location->getFromDBByCrit(['name' => 'Injected room']));
+
+ self::assertSame('REF-42', $asset->fields['custom_serial_ref']);
+ self::assertSame("first line\nsecond line", $asset->fields['custom_notes']);
+ self::assertEquals(7, $asset->fields['custom_quantity']);
+ self::assertSame('2026-10-23', $asset->fields['custom_delivery']);
+ self::assertTrue((bool) $asset->fields['custom_is_spare']);
+ self::assertEquals($location->getID(), $asset->fields['custom_room']);
+ }
+}