diff --git a/CHANGELOG.md b/CHANGELOG.md index f81f8269..93df674f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ 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] +## [Unreleased] +### Fixed + +- Fixed table renaming during migration when tables with numeric names exceeded the maximum table name length. ## [1.24.2] - 2026-06-30 diff --git a/inc/container.class.php b/inc/container.class.php index 6f38c4dd..b1ec4303 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -255,6 +255,18 @@ public static function installBaseData(Migration $migration, $version) } } + $container_obj = new self(); + foreach ($DB->request(['FROM' => $table]) as $container) { + self::generateTemplate($container); + $itemtypes = PluginFieldsToolbox::decodeJSONItemtypes($container['itemtypes']); + if (empty($itemtypes) || self::checkContainerName($container)) { + continue; + } + + $container = $container_obj->setContainerName($container); + return $container_obj->update($container); + } + return true; } @@ -648,6 +660,10 @@ public function defineTabs($options = []) public function prepareInputForUpdate($input) { + if (!isset($input['itemtypes'])) { + $input['itemtypes'] = $this->fields['itemtypes']; + } + return PluginFieldsToolbox::prepareLabel($input); } @@ -703,19 +719,8 @@ public function prepareInputForAdd($input) } $input = PluginFieldsToolbox::prepareLabel($input); - - //reject adding when container name is too long for mysql table name - foreach ($input['itemtypes'] as $itemtype) { - $tmp = getTableForItemType(self::getClassname($itemtype, $input['name'])); - if (strlen($tmp) > 64) { - Session::AddMessageAfterRedirect( - __('Container name is too long for database (digits in name are replaced by characters, try to remove them)', 'fields'), - false, - ERROR, - ); - - return false; - } + if ($input === false) { + return false; } //check for already existing container with same name @@ -2416,4 +2421,53 @@ public function getCloneRelations(): array PluginFieldsProfile::class, ]; } + + public static function checkContainerName(array $container): bool + { + if (!isset($container['name']) || empty($container['name'])) { + return false; + } + + if (!isset($container['itemtypes']) || empty($container['itemtypes'])) { + return true; + } + + // Names made entirely of digits are invalid (generated prior to Fields 1.9.2). + if (preg_match('/^\d+$/', (string) $container['name'])) { + return false; + } + + $itemtypes = $container['itemtypes']; + if (is_string($itemtypes)) { + $itemtypes = PluginFieldsToolbox::decodeJSONItemtypes($itemtypes); + } + + foreach ($itemtypes as $itemtype) { + if (strlen(getTableForItemType(self::getClassname($itemtype, $container['name']))) > 64) { + return false; + } + } + + return true; + } + + public function setContainerName(array $container): array + { + $base = empty($container['label']) ? $container['name'] : $container['label']; + $new_name = (new PluginFieldsToolbox())->getSystemNameFromLabel((string) $base); + $itemtypes = $container['itemtypes']; + if (is_string($itemtypes)) { + $itemtypes = PluginFieldsToolbox::decodeJSONItemtypes($itemtypes); + } + + foreach ($itemtypes as $itemtype) { + while ($new_name !== '' && strlen(getTableForItemType(self::getClassname($itemtype, $new_name))) > 64) { + $new_name = substr($new_name, 0, -1); + } + } + + $container['name'] = $new_name; + + return $container; + } } diff --git a/inc/toolbox.class.php b/inc/toolbox.class.php index f1922a6c..d5edfa4e 100644 --- a/inc/toolbox.class.php +++ b/inc/toolbox.class.php @@ -396,13 +396,22 @@ public static function sanitizeLabel(string $label): string * Centralises the prepare-label pattern used in prepareInputForAdd/Update. * * @param array $input Input array with at least a 'label' key. - * @return array Updated input with sanitized 'label' and derived 'name'. + * @return array|bool Updated input with sanitized 'label' and derived 'name', or false on error. */ - public static function prepareLabel(array $input): array + public static function prepareLabel(array $input): array|bool { $input['label'] = self::sanitizeLabel((string) ($input['label'] ?? '')); $input['name'] = (new self())->getSystemNameFromLabel($input['label']); + if (!PluginFieldsContainer::checkContainerName($input)) { + Session::addMessageAfterRedirect( + __('Container name is invalid or too long for database', 'fields'), + false, + ERROR, + ); + return false; + } + return $input; } } diff --git a/tests/Units/ContainerTest.php b/tests/Units/ContainerTest.php index a7fa7098..610fceab 100644 --- a/tests/Units/ContainerTest.php +++ b/tests/Units/ContainerTest.php @@ -106,4 +106,89 @@ public function testAddWithValidItemtypesSucceeds(): void $this->assertGreaterThan(0, $container->getID()); } + + /** + * Provides invalid container names for testing. + */ + public static function provideInvalidContainerName(): iterable + { + yield 'wrong numeric name' => ['7777777777']; + yield 'long name' => [str_repeat('a', 256)]; + } + + /** + * Tests that adding a container with an invalid name is rejected. + */ + #[DataProvider('provideInvalidContainerName')] + public function testInvalidCheckContainerName(string $label): void + { + $container = new PluginFieldsContainer(); + $input = [ + 'label' => $label, + 'itemtypes' => [Computer::class], + 'type' => 'tab', + 'entities_id' => 0, + ]; + $result = $container->add($input); + $this->assertFalse($result); + + $container = $this->createFieldContainer([ + 'label' => 'container ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $result = $container->update(['id' => $container->getID(), 'label' => $label]); + $this->assertFalse($result); + } + + /** + * Provides valid container names for testing. + */ + public static function provideSuccessContainerName(): iterable + { + yield 'empty name' => ['']; + yield 'valid name' => ['Valid Container Name']; + yield 'another valid name' => ['111']; + yield 'numeric and letter name' => ['d7']; + } + + /** + * Tests that adding a container with a valid name succeeds. + */ + #[DataProvider('provideSuccessContainerName')] + public function testSuccessCheckContainerName(string $label): void + { + $container = $this->createFieldContainer([ + 'label' => $label . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $this->assertGreaterThan(0, $container->getID()); + $container = $this->createFieldContainer([ + 'label' => 'testSuccessCheckContainerName', + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $update = $container->update([ + 'id' => $container->getID(), + 'label' => $label . $this->getUniqueString(), + ]); + $this->assertTrue($update); + // remove the container because tearsdown is only triggered when another test is run, + // and this test is the last one to be run + $delete = $container->delete([ + 'id' => $container->getID(), + ], true); + $this->assertTrue($delete); + } }