Current File : /home/escuelai/public_html/it/src/System/Diagnostic/DatabaseKeysChecker.php |
<?php
/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2022 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/
namespace Glpi\System\Diagnostic;
use CommonDBTM;
/**
* @since 10.0.0
*/
class DatabaseKeysChecker extends AbstractDatabaseChecker
{
/**
* Get list of missing keys, basing detection on column names.
*
* Array keys are expected key names, and array values are fields that should be contained in these keys.
*
* @param string $table_name
*
* @return array
*/
public function getMissingKeys(string $table_name): array
{
$missing_keys = [];
$misnamed_keys = $this->getMisnamedKeys($table_name);
$columns = $this->getColumnsNames($table_name);
foreach ($columns as $column_name) {
$column_name_matches = [];
if (
is_a($itemtype = getItemTypeForTable($table_name), CommonDBTM::class, true)
&& $column_name === $itemtype::getNameField()
&& preg_match('/text$/', $this->getColumnType($table_name, $column_name)) !== 1
) {
if (!$this->areFieldsCorrecltyIndexed($table_name, [$column_name], $column_name)) {
// Expect a key with same name as field.
$missing_keys[$column_name] = [$column_name];
}
} else if (preg_match('/^items_id(?<suffix>_.+)?/', $column_name, $column_name_matches)) {
$suffix = $column_name_matches['suffix'] ?? '';
$expected_key = 'item' . $suffix;
if (
in_array('itemtype' . $suffix, $columns)
&& !in_array($expected_key, $misnamed_keys)
&& !$this->areFieldsCorrecltyIndexed($table_name, ['itemtype' . $suffix, 'items_id' . $suffix], $expected_key)
) {
// Expect a key named item for itemtype/items_id polymorphic foreign keys
$missing_keys[$expected_key] = ['itemtype' . $suffix, 'items_id' . $suffix];
}
} else if (
isForeignKeyField($column_name)
|| preg_match('/^date(_(mod|creation))$/', $column_name)
|| preg_match('/^is_(active|deleted|dynamic|recursive|template)$/', $column_name)
) {
$ignored_fields = [
'glpi_ipaddresses.mainitems_id', // FIXME Should be renamed to glpi_ipaddresses.items_id_main to fit naming conventions.
'glpi_networkportaggregates.networkports_id_list', // FIXME Should be replaced by a relation table.
'glpi_planningexternalevents.users_id_guests', // FIXME Should be replaced by a relation table.
'glpi_dashboards_items.card_id', // FIXME This is not a foreign key.
'glpi_dashboards_items.gridstack_id', // FIXME This is not a foreign key.
];
if (in_array("$table_name.$column_name", $ignored_fields)) {
continue;
}
$expected_key = $column_name;
if (!in_array($expected_key, $misnamed_keys) && !$this->areFieldsCorrecltyIndexed($table_name, [$column_name], $expected_key)) {
// Expect a key with same name as field or a key that contains multiple fields including current one.
$missing_keys[$expected_key] = [$column_name];
}
}
}
return $missing_keys;
}
/**
* Get list of keys having a name that mismatch conventions.
*
* Array keys are misnamed key names, and array values are expected key names.
*
* @param string $table_name
*
* @return array
*/
public function getMisnamedKeys(string $table_name): array
{
$misnamed_keys = [];
$index = $this->getIndex($table_name);
foreach ($index as $key => $fields) {
if ($key === 'PRIMARY' || $key === 'unicity') {
continue; // PRIMARY and 'unicity' cannot be misnamed.
}
if (count($fields) === 1 && $key !== reset($fields)) {
// A key corresponding to a unique field should be named like this field.
$misnamed_keys[$key] = reset($fields);
} else if (count($fields) === 2 && count($matching_fields = preg_grep('/^items_id(_.+)?/', $fields)) === 1) {
$items_id_field = reset($matching_fields);
$suffix = str_replace('items_id', '', $items_id_field);
$expected_key = 'item' . $suffix;
if (in_array('itemtype' . $suffix, $fields) && $key !== $expected_key) {
$misnamed_keys[$key] = $expected_key;
}
}
}
return $misnamed_keys;
}
/**
* Get list of keys that are included in larger keys.
*
* Array keys are useless key names, and array values are expected larger key names.
*
* @param string $table_name
*
* @return array
*/
public function getUselessKeys(string $table_name): array
{
$useless_keys = [];
$index = $this->getIndex($table_name);
foreach ($index as $checked_key => $checked_fields) {
if ($checked_key === 'PRIMARY' || $checked_key === 'unicity') {
continue; // PRIMARY and 'unicity' cannot be useless.
}
foreach ($index as $other_key => $other_fields) {
if ($checked_key === $other_key) {
continue; // This is not another key.
}
if (count($checked_fields) >= count($other_fields)) {
continue; // Other key does not contains more that expected fields, it is not a larger key.
}
foreach ($checked_fields as $i => $checked_field) {
if (!array_key_exists($i, $other_fields) || $other_fields[$i] !== $checked_field) {
break;
}
// Fields are considered as part of a larger index only if they are located at the beginning of
// this larger index. Otherwise, MySQL will not be able to use the index if preceding fields
// are not tested in the query.
$useless_keys[$checked_key] = $other_key;
}
}
}
return $useless_keys;
}
/**
* Check if fields are correctly indexed.
*
* @param string $table_name
* @param array $fields
* @param string $expected_key
*
* @return bool
*/
private function areFieldsCorrecltyIndexed(string $table_name, array $fields, string $expected_key): bool
{
$index = $this->getIndex($table_name);
// Check if primary key matches matches given fields.
if (array_key_exists('PRIMARY', $index) && $index['PRIMARY'] === $fields) {
return true;
}
// Check if expected key exists and matches given fields.
if (array_key_exists($expected_key, $index) && $index[$expected_key] === $fields) {
return true;
}
// Check if unicity key exists and matches given fields.
if (array_key_exists('unicity', $index) && $index['unicity'] === $fields) {
return true;
}
// Check if a larger key exists and contains given fields.
foreach ($index as $key_fields) {
if (count($fields) >= count($key_fields)) {
continue; // Key does not contains more that expected fields, it is not a larger key.
}
foreach ($fields as $i => $field) {
if (!array_key_exists($i, $key_fields) || $key_fields[$i] !== $field) {
break;
}
// Fields are considered as part of a larger index only if they are located at the beginning of
// this larger index. Otherwise, MySQL will not be able to use the index if preceding fields
// are not tested in the query.
return true;
}
}
return false;
}
}