diff --git a/inc/checkdatabasecommand.class.php b/inc/checkdatabasecommand.class.php new file mode 100644 index 00000000..955b4594 --- /dev/null +++ b/inc/checkdatabasecommand.class.php @@ -0,0 +1,107 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2022 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +use Glpi\Console\AbstractCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class PluginFieldsCheckDatabaseCommand extends AbstractCommand +{ + protected function configure() + { + $this->setName('plugin:fields:check_database'); + $this->setDescription(__('Check database to detect inconsistencies.', 'fields')); + $this->setHelp( + __('This command will chec database to detect following inconsistencies:', 'fields') + . "\n" + . sprintf( + __('- some deleted fields may still be present in database (bug introduced in %s and fixed in version %s)', 'fields'), + '1.15.0', + '1.15.3' + ) + ); + + $this->addOption( + 'fix', + null, + InputOption::VALUE_NONE, + __('Use this option to actually fix database', 'fields') + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + // Read option + $fix = $input->getOption('fix'); + + $dead_fields = PluginFieldsMigration::checkDeadFields($fix); + $dead_fields_count = count($dead_fields, COUNT_RECURSIVE) - count($dead_fields); + + // No invalid fields found + if ($dead_fields_count === 0) { + $output->writeln( + '' . __('Everything is in order - no action needed.', 'fields') . '', + ); + return Command::SUCCESS; + } + + // Indicate which fields will have been or must be deleted + $error = $fix + ? sprintf(__('Database was containing %s gone field(s).', 'fields'), $dead_fields_count) + : sprintf(__('Database contains %s gone field(s).', 'fields'), $dead_fields_count); + $output->writeln('' . $error . '', OutputInterface::VERBOSITY_QUIET); + + foreach ($dead_fields as $table => $fields) { + foreach ($fields as $field) { + $info = $fix + ? sprintf(__('-> "%s.%s" has been deleted.', 'fields'), $table, $field) + : sprintf(__('-> "%s.%s" should be deleted.', 'fields'), $table, $field); + $output->writeln($info); + } + } + + // Show extra info in dry-run mode + if (!$fix) { + // Print command to do the actual deletion + $next_command = sprintf( + __('Run "%s" to delete the found field(s).', 'fields'), + sprintf("php bin/console %s --fix", $this->getName()) + ); + $output->writeln( + '' . $next_command . '', + OutputInterface::VERBOSITY_QUIET + ); + } + + return Command::SUCCESS; + } +} diff --git a/inc/migration.class.php b/inc/migration.class.php index bcf3c33d..b66a4c04 100644 --- a/inc/migration.class.php +++ b/inc/migration.class.php @@ -81,4 +81,129 @@ public static function getSQLFields(string $field_name, string $field_type): arr return $fields; } + + /** + * An issue affected field removal in 1.15.0, 1.15.1 and 1.15.2. + * Using these versions, removing a field from a container would drop the + * field from glpi_plugin_fields_fields but not from the custom container + * table + * + * This function looks into containers tables for fields that + * should have been removed and list them. + * If parameter $fix is true, fields are deleted from database. + * + * @param bool $fix + * + * @return array + */ + public static function checkDeadFields(bool $fix): array + { + /** @var DBMysql $DB */ + global $DB; + + $dead_fields = []; + + // For each existing container + $containers = (new PluginFieldsContainer())->find([]); + foreach ($containers as $row) { + // Get expected fields + $valid_fields = self::getValidFieldsForContainer($row['id']); + + // Read itemtypes and container name + $itemtypes = importArrayFromDB($row['itemtypes']); + $name = $row['name']; + + // One table to handle per itemtype + foreach ($itemtypes as $itemtype) { + // Build table name + $table = getTableForItemType("PluginFields{$itemtype}{$name}"); + + if (!$DB->tableExists($table)) { + // Missing table; skip (abnormal) + continue; + } + + // Get the actual fields defined in the container table + $found_fields = self::getCustomFieldsInContainerTable($table); + + // Compute which fields should be removed + $fields_to_drop = array_diff($found_fields, $valid_fields); + + if (count($fields_to_drop) > 0) { + $dead_fields[$table] = $fields_to_drop; + } + } + } + + if ($fix) { + $migration = new PluginFieldsMigration(0); + + foreach ($dead_fields as $table => $fields) { + foreach ($fields as $field) { + $migration->dropField($table, $field); + } + } + + $migration->executeMigration(); + } + + return $dead_fields; + } + + /** + * Get all fields defined for a container in glpi_plugin_fields_fields + * + * @param int $container_id Id of the container + * + * @return array + */ + private static function getValidFieldsForContainer(int $container_id): array + { + $valid_fields = []; + + // For each defined fields in the given container + $fields = (new PluginFieldsField())->find(['plugin_fields_containers_id' => $container_id]); + foreach ($fields as $row) { + $fields = self::getSQLFields($row['name'], $row['type']); + array_push($valid_fields, ...array_keys($fields)); + } + + return $valid_fields; + } + + /** + * Get custom fields in a given container table + * This means all fields found in the table expect those defined in + * $basic_fields + * + * @param string $table + * + * @return array + */ + private static function getCustomFieldsInContainerTable( + string $table + ): array { + /** @var DBMysql $DB */ + global $DB; + + // Read table fields + $fields = $DB->listFields($table); + + // Reduce to fields name only + $fields = array_column($fields, "Field"); + + // Remove basic fields + $basic_fields = [ + 'id', + 'items_id', + 'itemtype', + 'plugin_fields_containers_id', + ]; + return array_filter( + $fields, + function (string $field) use ($basic_fields) { + return !in_array($field, $basic_fields); + } + ); + } }