diff --git a/front/migration_status.php b/front/migration_status.php index 0b472ab1..bc945c02 100644 --- a/front/migration_status.php +++ b/front/migration_status.php @@ -34,14 +34,23 @@ // Check if user has admin rights Session::checkRight('config', UPDATE); +/** @var array $CFG_GLPI */ /** @var DBmysql $DB */ -global $DB; +global $CFG_GLPI, $DB; + +// Handle rename POST action +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['type_id'], $_POST['new_name'])) { + $type_id = (int) $_POST['type_id']; + $new_name = (string) $_POST['new_name']; + PluginGenericobjectType::renameType($type_id, $new_name); + Html::redirect($CFG_GLPI['root_doc'] . '/plugins/genericobject/front/migration_status.php'); +} // Get all GenericObject types $genericobject_types = []; if ($DB->tableExists(PluginGenericobjectType::getTable())) { $query = [ - 'SELECT' => ['itemtype', 'name'], + 'SELECT' => ['id', 'name'], 'FROM' => PluginGenericobjectType::getTable(), ]; $request = $DB->request($query); @@ -72,6 +81,7 @@ TemplateRenderer::getInstance()->display('@genericobject/migration_status.html.twig', [ 'genericobject_types' => $genericobject_types, 'customassets' => $customassets, + 'reserved_names' => array_map('strtolower', PluginGenericobjectType::getReservedNames()), ]); // Display GLPI footer diff --git a/inc/type.class.php b/inc/type.class.php index c5f38a9c..49e4d12b 100644 --- a/inc/type.class.php +++ b/inc/type.class.php @@ -749,80 +749,224 @@ private static function normalizeNamesAndItemtypes(Migration $migration) continue; } - self::updateNameAndItemtype( + self::applyTypeRename( $migration, + $type['id'], $old_name, $new_name, $old_itemtype, $new_itemtype, ); + } - $DB->update( - self::getTable(), - [ - 'name' => $new_name, - 'itemtype' => $new_itemtype, - ], - ['id' => $type['id']], - ); + ProfileRight::cleanAllPossibleRights(); + } - $DB->update( - self::getTable(), - [ - 'linked_itemtypes' => new QueryExpression( - 'REPLACE(' - . $DB->quoteName('linked_itemtypes') - . ',' - . $DB->quoteValue('"' . $old_itemtype . '"') // itemtype is surrounded by quotes - . ',' - . $DB->quoteValue('"' . $new_itemtype . '"') // itemtype is surrounded by quotes - . ')', - ), - ], - ['linked_itemtypes' => ['LIKE', '%"' . $old_itemtype . '"%']], - ); + /** + * Apply a full rename for a single type: update files, database tables, + * foreign keys, relation tables, linked_itemtypes, and related dropdowns. + * + * @param Migration $migration + * @param int $type_id ID of the type in glpi_plugin_genericobject_types + * @param string $old_name Current type name + * @param string $new_name New type name + * @param string $old_itemtype Current itemtype class name + * @param string $new_itemtype New itemtype class name + */ + private static function applyTypeRename( + Migration $migration, + int $type_id, + string $old_name, + string $new_name, + string $old_itemtype, + string $new_itemtype, + ): void { + /** @var DBmysql $DB */ + global $DB; - // Handle dropdowns related to itemtype - $table = getTableForItemType($new_itemtype); - $fields = $DB->listFields($table); - foreach ($fields as $field => $options) { - if (preg_match("/s_id$/", $field)) { - $dropdown_old_table = getTableNameForForeignKeyField($field); - - if (!preg_match('/^glpi_plugin_genericobject_/', $dropdown_old_table)) { - continue; - } - - $dropdown_old_name = getSingular( - str_replace( - "glpi_plugin_genericobject_", - "", - $dropdown_old_table, - ), - ); - $dropdown_old_itemtype = 'PluginGenericobject' . ucfirst($dropdown_old_name); - $dropdown_new_name = self::filterInput($dropdown_old_name); - $dropdown_new_itemtype = self::getClassByName($dropdown_new_name); - - if ( - $dropdown_old_name == $dropdown_new_name - && $dropdown_old_itemtype == $dropdown_new_itemtype - ) { - continue; - } - - self::updateNameAndItemtype( - $migration, - $dropdown_old_name, - $dropdown_new_name, - $dropdown_old_itemtype, - $dropdown_new_itemtype, - ); + self::updateNameAndItemtype( + $migration, + $old_name, + $new_name, + $old_itemtype, + $new_itemtype, + ); + + $DB->update( + self::getTable(), + [ + 'name' => $new_name, + 'itemtype' => $new_itemtype, + ], + ['id' => $type_id], + ); + + $DB->update( + self::getTable(), + [ + 'linked_itemtypes' => new QueryExpression( + 'REPLACE(' + . $DB->quoteName('linked_itemtypes') + . ',' + . $DB->quoteValue('"' . $old_itemtype . '"') // itemtype is surrounded by quotes + . ',' + . $DB->quoteValue('"' . $new_itemtype . '"') // itemtype is surrounded by quotes + . ')', + ), + ], + ['linked_itemtypes' => ['LIKE', '%"' . $old_itemtype . '"%']], + ); + + // Handle dropdowns related to itemtype + $table = getTableForItemType($new_itemtype); + $fields = $DB->listFields($table); + foreach ($fields as $field => $options) { + if (preg_match("/s_id$/", $field)) { + $dropdown_old_table = getTableNameForForeignKeyField($field); + + if (!preg_match('/^glpi_plugin_genericobject_/', $dropdown_old_table)) { + continue; + } + + $dropdown_old_name = getSingular( + str_replace( + "glpi_plugin_genericobject_", + "", + $dropdown_old_table, + ), + ); + $dropdown_old_itemtype = 'PluginGenericobject' . ucfirst($dropdown_old_name); + $dropdown_new_name = self::filterInput($dropdown_old_name); + $dropdown_new_itemtype = self::getClassByName($dropdown_new_name); + + if ( + $dropdown_old_name == $dropdown_new_name + && $dropdown_old_itemtype == $dropdown_new_itemtype + ) { + continue; } + + self::updateNameAndItemtype( + $migration, + $dropdown_old_name, + $dropdown_new_name, + $dropdown_old_itemtype, + $dropdown_new_itemtype, + ); } } + } + + /** + * Rename a genericobject type. + * + * Updates the type name, itemtype, all generated files, database tables, + * foreign keys, and all relation tables where the itemtype is referenced. + * + * @param int $type_id ID of the type in glpi_plugin_genericobject_types + * @param string $new_name New name for the type (will be filtered) + * + * @return bool True on success, false on failure + */ + public static function renameType(int $type_id, string $new_name): bool + { + /** @var DBmysql $DB */ + global $DB; + + $type = new self(); + if (!$type->getFromDB($type_id)) { + Session::addMessageAfterRedirect( + __s('Type not found.', 'genericobject'), + false, + ERROR, + ); + return false; + } + + $old_name = $type->fields['name']; + $new_name = self::filterInput($new_name); + + if ($new_name === '') { + Session::addMessageAfterRedirect( + __s('The new name cannot be empty.', 'genericobject'), + false, + ERROR, + ); + return false; + } + + if ($new_name === $old_name) { + return true; + } + + $existing = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'name' => $new_name, + 'NOT' => ['id' => $type_id], + ], + ]); + if ($existing->numrows() > 0) { + Session::addMessageAfterRedirect( + __s('A type with this name already exists.', 'genericobject'), + false, + ERROR, + ); + return false; + } + + $manager = \Glpi\Asset\AssetDefinitionManager::getInstance(); + $reserved_pattern = $manager->getReservedSystemNamesPattern(); + if (preg_match($reserved_pattern, $new_name) === 1) { + Session::addMessageAfterRedirect( + __s('This name is reserved by a native GLPI asset type.', 'genericobject'), + false, + ERROR, + ); + return false; + } + + $old_itemtype = $type->fields['itemtype']; + $new_itemtype = self::getClassByName($new_name); + + $migration = new Migration(PLUGIN_GENERICOBJECT_VERSION); + self::applyTypeRename( + $migration, + $type_id, + $old_name, + $new_name, + $old_itemtype, + $new_itemtype, + ); + ProfileRight::cleanAllPossibleRights(); + $migration->executeMigration(); + + Session::addMessageAfterRedirect( + sprintf( + __s('Type "%s" has been renamed to "%s".', 'genericobject'), + $old_name, + $new_name, + ), + false, + INFO, + ); + + return true; + } - ProfileRight::cleanAllPossibleRights(); // Clean all possible rights are their name may have change + /** + * Get the list of reserved GLPI core asset names. + * + * @return string[] + */ + public static function getReservedNames(): array + { + $manager = \Glpi\Asset\AssetDefinitionManager::getInstance(); + $pattern = $manager->getReservedSystemNamesPattern(); + if (preg_match('/\(([^)]+)\)/', $pattern, $matches)) { + return explode('|', $matches[1]); + } + return []; } /** diff --git a/templates/migration_status.html.twig b/templates/migration_status.html.twig index 8ec5fe7e..2dbe3a4c 100644 --- a/templates/migration_status.html.twig +++ b/templates/migration_status.html.twig @@ -26,6 +26,13 @@ # ------------------------------------------------------------------------- #} +{% set conflict_count = 0 %} +{% for genericobject_type in genericobject_types %} + {% if genericobject_type.name|lower in reserved_names and customassets[genericobject_type.name] is not defined %} + {% set conflict_count = conflict_count + 1 %} + {% endif %} +{% endfor %} + {# Page Header #}
+ {{ __('If a GenericObject type shares the same name as a native GLPI asset (e.g. Printer, Computer), migration will fail. Use the Rename button on the relevant card below to resolve the conflict before migrating.', 'genericobject') }} +
++ {% if conflict_count == 1 %} + {{ __('1 type cannot be migrated because its name is reserved by a native GLPI asset. Use the Rename button on the card below to fix it before migrating.', 'genericobject') }} + {% else %} + {{ conflict_count ~ ' ' ~ __('types cannot be migrated because their names are reserved by native GLPI assets. Use the Rename button on the cards below to fix them before migrating.', 'genericobject') }} + {% endif %} +
+