Skip to content

Commit bed0326

Browse files
Add a command to fix database inconsistencies
1 parent da32633 commit bed0326

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed

inc/checkdatabasecommand.class.php

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/**
4+
* -------------------------------------------------------------------------
5+
* Fields plugin for GLPI
6+
* -------------------------------------------------------------------------
7+
*
8+
* LICENSE
9+
*
10+
* This file is part of Fields.
11+
*
12+
* Fields is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License as published by
14+
* the Free Software Foundation; either version 2 of the License, or
15+
* (at your option) any later version.
16+
*
17+
* Fields is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU General Public License
23+
* along with Fields. If not, see <http://www.gnu.org/licenses/>.
24+
* -------------------------------------------------------------------------
25+
* @copyright Copyright (C) 2013-2022 by Fields plugin team.
26+
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
27+
* @link https://github.com/pluginsGLPI/fields
28+
* -------------------------------------------------------------------------
29+
*/
30+
31+
use Glpi\Console\AbstractCommand;
32+
use Symfony\Component\Console\Command\Command;
33+
use Symfony\Component\Console\Input\InputInterface;
34+
use Symfony\Component\Console\Input\InputOption;
35+
use Symfony\Component\Console\Output\OutputInterface;
36+
37+
class PluginFieldsCheckDatabaseCommand extends AbstractCommand
38+
{
39+
protected function configure()
40+
{
41+
$this->setName('plugin:fields:check_database');
42+
$this->setDescription(__('Check database to detect inconsistencies.', 'fields'));
43+
$this->setHelp(
44+
__('This command will chec database to detect following inconsistencies:', 'fields')
45+
. "\n"
46+
. sprintf(
47+
__('- some deleted fields may still be present in database (bug introduced in %s and fixed in version %s)', 'fields'),
48+
'1.15.0',
49+
'1.15.3'
50+
)
51+
);
52+
53+
$this->addOption(
54+
'fix',
55+
null,
56+
InputOption::VALUE_NONE,
57+
__('Use this option to actually fix database', 'fields')
58+
);
59+
}
60+
61+
protected function execute(InputInterface $input, OutputInterface $output)
62+
{
63+
// Read option
64+
$fix = $input->getOption('fix');
65+
66+
$dead_fields = PluginFieldsMigration::checkDeadFields($fix);
67+
$dead_fields_count = count($dead_fields, COUNT_RECURSIVE) - count($dead_fields);
68+
69+
// No invalid fields found
70+
if ($dead_fields_count === 0) {
71+
$output->writeln(
72+
'<info>' . __('Everything is in order - no action needed.', 'fields') . '</info>',
73+
);
74+
return Command::SUCCESS;
75+
}
76+
77+
// Indicate which fields will have been or must be deleted
78+
$error = $fix
79+
? sprintf(__('Database was containing %s gone field(s).', 'fields'), $dead_fields_count)
80+
: sprintf(__('Database contains %s gone field(s).', 'fields'), $dead_fields_count);
81+
$output->writeln('<error>' . $error . '</error>', OutputInterface::VERBOSITY_QUIET);
82+
83+
foreach ($dead_fields as $table => $fields) {
84+
foreach ($fields as $field) {
85+
$info = $fix
86+
? sprintf(__('-> "%s.%s" has been deleted.', 'fields'), $table, $field)
87+
: sprintf(__('-> "%s.%s" should be deleted.', 'fields'), $table, $field);
88+
$output->writeln($info);
89+
}
90+
}
91+
92+
// Show extra info in dry-run mode
93+
if (!$fix) {
94+
// Print command to do the actual deletion
95+
$next_command = sprintf(
96+
__('Run "%s" to delete the found field(s).', 'fields'),
97+
sprintf("php bin/console %s --fix", $this->getName())
98+
);
99+
$output->writeln(
100+
'<comment>' . $next_command . '</comment>',
101+
OutputInterface::VERBOSITY_QUIET
102+
);
103+
}
104+
105+
return Command::SUCCESS;
106+
}
107+
}

inc/migration.class.php

+125
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,129 @@ public static function getSQLFields(string $field_name, string $field_type): arr
8181

8282
return $fields;
8383
}
84+
85+
/**
86+
* An issue affected field removal in 1.15.0, 1.15.1 and 1.15.2.
87+
* Using these versions, removing a field from a container would drop the
88+
* field from glpi_plugin_fields_fields but not from the custom container
89+
* table
90+
*
91+
* This function looks into containers tables for fields that
92+
* should have been removed and list them.
93+
* If parameter $fix is true, fields are deleted from database.
94+
*
95+
* @param bool $fix
96+
*
97+
* @return array
98+
*/
99+
public static function checkDeadFields(bool $fix): array
100+
{
101+
/** @var DBMysql $DB */
102+
global $DB;
103+
104+
$dead_fields = [];
105+
106+
// For each existing container
107+
$containers = (new PluginFieldsContainer())->find([]);
108+
foreach ($containers as $row) {
109+
// Get expected fields
110+
$valid_fields = self::getValidFieldsForContainer($row['id']);
111+
112+
// Read itemtypes and container name
113+
$itemtypes = importArrayFromDB($row['itemtypes']);
114+
$name = $row['name'];
115+
116+
// One table to handle per itemtype
117+
foreach ($itemtypes as $itemtype) {
118+
// Build table name
119+
$table = getTableForItemType("PluginFields{$itemtype}{$name}");
120+
121+
if (!$DB->tableExists($table)) {
122+
// Missing table; skip (abnormal)
123+
continue;
124+
}
125+
126+
// Get the actual fields defined in the container table
127+
$found_fields = self::getCustomFieldsInContainerTable($table);
128+
129+
// Compute which fields should be removed
130+
$fields_to_drop = array_diff($found_fields, $valid_fields);
131+
132+
if (count($fields_to_drop) > 0) {
133+
$dead_fields[$table] = $fields_to_drop;
134+
}
135+
}
136+
}
137+
138+
if ($fix) {
139+
$migration = new PluginFieldsMigration(0);
140+
141+
foreach ($dead_fields as $table => $fields) {
142+
foreach ($fields as $field) {
143+
$migration->dropField($table, $field);
144+
}
145+
}
146+
147+
$migration->executeMigration();
148+
}
149+
150+
return $dead_fields;
151+
}
152+
153+
/**
154+
* Get all fields defined for a container in glpi_plugin_fields_fields
155+
*
156+
* @param int $container_id Id of the container
157+
*
158+
* @return array
159+
*/
160+
private static function getValidFieldsForContainer(int $container_id): array
161+
{
162+
$valid_fields = [];
163+
164+
// For each defined fields in the given container
165+
$fields = (new PluginFieldsField())->find(['plugin_fields_containers_id' => $container_id]);
166+
foreach ($fields as $row) {
167+
$fields = self::getSQLFields($row['name'], $row['type']);
168+
array_push($valid_fields, ...array_keys($fields));
169+
}
170+
171+
return $valid_fields;
172+
}
173+
174+
/**
175+
* Get custom fields in a given container table
176+
* This means all fields found in the table expect those defined in
177+
* $basic_fields
178+
*
179+
* @param string $table
180+
*
181+
* @return array
182+
*/
183+
private static function getCustomFieldsInContainerTable(
184+
string $table
185+
): array {
186+
/** @var DBMysql $DB */
187+
global $DB;
188+
189+
// Read table fields
190+
$fields = $DB->listFields($table);
191+
192+
// Reduce to fields name only
193+
$fields = array_column($fields, "Field");
194+
195+
// Remove basic fields
196+
$basic_fields = [
197+
'id',
198+
'items_id',
199+
'itemtype',
200+
'plugin_fields_containers_id',
201+
];
202+
return array_filter(
203+
$fields,
204+
function (string $field) use ($basic_fields) {
205+
return !in_array($field, $basic_fields);
206+
}
207+
);
208+
}
84209
}

0 commit comments

Comments
 (0)