This commit introduces the `fxAdminStaticSyncAllowed` function to control access to static database synchronization tools, ensuring only authorized users in development and pre-production environments can access them. The UI is updated to display a new section for these tools in the admin interface, enhancing the overall user experience and access management for development tasks. Additionally, the `.gitignore` file is updated to exclude the `/output/` directory from version control.
419 lines
12 KiB
PHP
419 lines
12 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/inc_fx_static_sync_config.php';
|
|
|
|
/**
|
|
* Environnements comparables (source = dev_preprod).
|
|
*/
|
|
function fxStaticSyncEnvironmentDefinitions() {
|
|
global $arrConDatabasePreProd, $arrConDatabaseProd;
|
|
|
|
$tabEnvs = array(
|
|
'dev_preprod' => array(
|
|
'label' => 'Dev préprod',
|
|
'config' => $arrConDatabasePreProd,
|
|
'is_source' => true,
|
|
),
|
|
'dev_prod' => array(
|
|
'label' => 'Dev prod',
|
|
'config' => $arrConDatabaseProd,
|
|
'is_source' => false,
|
|
),
|
|
);
|
|
|
|
foreach (fxStaticSyncClientDatabaseConfigs() as $strEnvId => $tabClient) {
|
|
$tabEnvs[$strEnvId] = array(
|
|
'label' => $tabClient['label'],
|
|
'config' => array(
|
|
'host' => $tabClient['host'],
|
|
'user' => $tabClient['user'],
|
|
'pass' => $tabClient['pass'],
|
|
'db' => $tabClient['db'],
|
|
),
|
|
'is_source' => false,
|
|
);
|
|
}
|
|
|
|
return $tabEnvs;
|
|
}
|
|
|
|
function fxStaticSyncConnect($arrConfig) {
|
|
if (empty($arrConfig['host']) || empty($arrConfig['db'])) {
|
|
return null;
|
|
}
|
|
|
|
$con = @mysqli_connect(
|
|
$arrConfig['host'],
|
|
$arrConfig['user'],
|
|
$arrConfig['pass'],
|
|
$arrConfig['db'],
|
|
3306
|
|
);
|
|
|
|
if (!$con) {
|
|
return null;
|
|
}
|
|
|
|
mysqli_close($con);
|
|
|
|
return new clsMysql($arrConfig);
|
|
}
|
|
|
|
function fxStaticSyncTableExists($objDb, $strTable) {
|
|
if (!$objDb) {
|
|
return false;
|
|
}
|
|
|
|
$sql = "SHOW TABLES LIKE '" . $objDb->fxEscape($strTable) . "'";
|
|
$row = $objDb->fxGetRow($sql);
|
|
|
|
return !empty($row);
|
|
}
|
|
|
|
function fxStaticSyncRowBusinessKey(array $row, array $tabKeyCols) {
|
|
$tabParts = array();
|
|
foreach ($tabKeyCols as $strCol) {
|
|
$tabParts[] = isset($row[$strCol]) ? (string)$row[$strCol] : '';
|
|
}
|
|
|
|
return implode("\0", $tabParts);
|
|
}
|
|
|
|
function fxStaticSyncRowContentHash(array $row, array $tabIgnoreCols) {
|
|
$tabData = $row;
|
|
foreach ($tabIgnoreCols as $strCol) {
|
|
unset($tabData[$strCol]);
|
|
}
|
|
ksort($tabData);
|
|
|
|
return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function fxStaticSyncLoadKeyedRows($objDb, array $tabDef) {
|
|
$strTable = $tabDef['table'];
|
|
$tabKeys = $tabDef['keys'];
|
|
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
|
|
|
|
if (!fxStaticSyncTableExists($objDb, $strTable)) {
|
|
return array('error' => 'missing_table', 'rows' => array(), 'count' => 0);
|
|
}
|
|
|
|
$sql = 'SELECT * FROM `' . $strTable . '`';
|
|
$tabResults = $objDb->fxGetResults($sql);
|
|
$tabKeyed = array();
|
|
|
|
if (!is_array($tabResults)) {
|
|
return array('error' => null, 'rows' => array(), 'count' => 0);
|
|
}
|
|
|
|
foreach ($tabResults as $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
$strKey = fxStaticSyncRowBusinessKey($row, $tabKeys);
|
|
$tabKeyed[$strKey] = array(
|
|
'hash' => fxStaticSyncRowContentHash($row, $tabIgnore),
|
|
'row' => $row,
|
|
);
|
|
}
|
|
|
|
return array('error' => null, 'rows' => $tabKeyed, 'count' => count($tabKeyed));
|
|
}
|
|
|
|
/**
|
|
* @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|no_connection|error
|
|
*/
|
|
function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData) {
|
|
if (!empty($tabTargetData['error']) && $tabTargetData['error'] === 'missing_table') {
|
|
return array(
|
|
'status' => 'missing_table',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
);
|
|
}
|
|
|
|
$tabSourceRows = $tabSourceData['rows'];
|
|
$tabTargetRows = $tabTargetData['rows'];
|
|
|
|
$tabMissing = array();
|
|
$tabDiff = array();
|
|
|
|
foreach ($tabSourceRows as $strKey => $tabRow) {
|
|
if (!isset($tabTargetRows[$strKey])) {
|
|
$tabMissing[] = $strKey;
|
|
continue;
|
|
}
|
|
if ($tabTargetRows[$strKey]['hash'] !== $tabRow['hash']) {
|
|
$tabDiff[] = $strKey;
|
|
}
|
|
}
|
|
|
|
$tabExtra = array();
|
|
foreach ($tabTargetRows as $strKey => $tabRow) {
|
|
if (!isset($tabSourceRows[$strKey])) {
|
|
$tabExtra[] = $strKey;
|
|
}
|
|
}
|
|
|
|
$strStatus = 'ok';
|
|
if (count($tabExtra) > 0) {
|
|
$strStatus = 'extra_rows';
|
|
} elseif (count($tabMissing) > 0) {
|
|
$strStatus = 'missing_rows';
|
|
} elseif (count($tabDiff) > 0) {
|
|
$strStatus = 'content_diff';
|
|
}
|
|
|
|
return array(
|
|
'status' => $strStatus,
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => $tabTargetData['count'],
|
|
'missing_keys' => $tabMissing,
|
|
'extra_keys' => $tabExtra,
|
|
'diff_keys' => $tabDiff,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable) {
|
|
if (!$objSource || !$objTarget) {
|
|
return array('ok' => false, 'message' => 'Connexion BD invalide.');
|
|
}
|
|
|
|
if (!fxStaticSyncTableExists($objSource, $strTable)) {
|
|
return array('ok' => false, 'message' => 'Table absente dans la source.');
|
|
}
|
|
|
|
if (!fxStaticSyncTableExists($objTarget, $strTable)) {
|
|
return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.');
|
|
}
|
|
|
|
$tabRows = $objSource->fxGetResults('SELECT * FROM `' . $strTable . '`');
|
|
$intCopied = 0;
|
|
|
|
$objTarget->fxQuery('SET FOREIGN_KEY_CHECKS=0');
|
|
$objTarget->fxQuery('TRUNCATE TABLE `' . $strTable . '`');
|
|
|
|
if (is_array($tabRows) && count($tabRows) > 0) {
|
|
$tabColumns = array_keys($tabRows[1]);
|
|
$strColList = '`' . implode('`,`', $tabColumns) . '`';
|
|
$intBatchSize = 50;
|
|
$tabBatch = array();
|
|
|
|
foreach ($tabRows as $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
$tabVals = array();
|
|
foreach ($tabColumns as $strCol) {
|
|
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
|
|
$tabVals[] = 'NULL';
|
|
} else {
|
|
$tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'";
|
|
}
|
|
}
|
|
$tabBatch[] = '(' . implode(',', $tabVals) . ')';
|
|
|
|
if (count($tabBatch) >= $intBatchSize) {
|
|
$sql = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
|
|
$objTarget->fxQuery($sql);
|
|
$intCopied += count($tabBatch);
|
|
$tabBatch = array();
|
|
}
|
|
}
|
|
|
|
if (count($tabBatch) > 0) {
|
|
$sql = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
|
|
$objTarget->fxQuery($sql);
|
|
$intCopied += count($tabBatch);
|
|
}
|
|
}
|
|
|
|
$objTarget->fxQuery('SET FOREIGN_KEY_CHECKS=1');
|
|
|
|
return array(
|
|
'ok' => true,
|
|
'message' => $intCopied . ' ligne(s) copiée(s).',
|
|
'rows' => $intCopied,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncStatusLabel($strStatus) {
|
|
switch ($strStatus) {
|
|
case 'ok':
|
|
return 'Aligné';
|
|
case 'content_diff':
|
|
return 'Contenu différent';
|
|
case 'missing_rows':
|
|
return 'Lignes manquantes';
|
|
case 'extra_rows':
|
|
return 'Lignes en trop';
|
|
case 'missing_table':
|
|
return 'Table absente';
|
|
case 'no_connection':
|
|
return 'Connexion impossible';
|
|
default:
|
|
return 'Erreur';
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncStatusClass($strStatus) {
|
|
switch ($strStatus) {
|
|
case 'ok':
|
|
return 'ss-ok';
|
|
case 'content_diff':
|
|
case 'missing_rows':
|
|
return 'ss-warn';
|
|
case 'extra_rows':
|
|
return 'ss-bad';
|
|
case 'missing_table':
|
|
case 'no_connection':
|
|
return 'ss-muted';
|
|
default:
|
|
return 'ss-bad';
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncFormatKeyPreview($strKey) {
|
|
$strKey = str_replace("\0", ' · ', $strKey);
|
|
if (strlen($strKey) > 120) {
|
|
return substr($strKey, 0, 117) . '…';
|
|
}
|
|
|
|
return $strKey;
|
|
}
|
|
|
|
function fxStaticSyncEnsureCsrfToken() {
|
|
if (empty($_SESSION['ms1_static_sync_csrf'])) {
|
|
$_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16));
|
|
}
|
|
|
|
return $_SESSION['ms1_static_sync_csrf'];
|
|
}
|
|
|
|
function fxStaticSyncValidateCsrf($strToken) {
|
|
return !empty($_SESSION['ms1_static_sync_csrf'])
|
|
&& hash_equals($_SESSION['ms1_static_sync_csrf'], (string)$strToken);
|
|
}
|
|
|
|
function fxStaticSyncFindTableDef($strTable) {
|
|
foreach (fxStaticSyncTableDefinitions() as $tabDef) {
|
|
if ($tabDef['table'] === $strTable) {
|
|
return $tabDef;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function fxStaticSyncFindEnvDef($strEnvId) {
|
|
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
|
|
return isset($tabEnvs[$strEnvId]) ? $tabEnvs[$strEnvId] : null;
|
|
}
|
|
|
|
function fxStaticSyncBuildReport($tabConnections) {
|
|
$tabDefs = fxStaticSyncTableDefinitions();
|
|
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
|
|
$objSource = $tabConnections['dev_preprod'];
|
|
$tabReport = array();
|
|
|
|
foreach ($tabDefs as $tabDef) {
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
|
|
$tabRow = array(
|
|
'def' => $tabDef,
|
|
'source_count' => $tabSourceData['count'],
|
|
'envs' => array(),
|
|
);
|
|
|
|
foreach ($tabEnvs as $strEnvId => $tabEnv) {
|
|
if (!empty($tabEnv['is_source'])) {
|
|
continue;
|
|
}
|
|
|
|
$objTarget = isset($tabConnections[$strEnvId]) ? $tabConnections[$strEnvId] : null;
|
|
if (!$objTarget) {
|
|
$tabRow['envs'][$strEnvId] = array(
|
|
'status' => 'no_connection',
|
|
'source_count' => $tabSourceData['count'],
|
|
'target_count' => 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef);
|
|
$tabRow['envs'][$strEnvId] = fxStaticSyncCompareTable($tabSourceData, $tabTargetData);
|
|
}
|
|
|
|
$tabReport[] = $tabRow;
|
|
}
|
|
|
|
return $tabReport;
|
|
}
|
|
|
|
function fxStaticSyncOpenConnections() {
|
|
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
|
|
$tabConnections = array();
|
|
|
|
foreach ($tabEnvs as $strEnvId => $tabEnv) {
|
|
$tabConnections[$strEnvId] = fxStaticSyncConnect($tabEnv['config']);
|
|
}
|
|
|
|
return $tabConnections;
|
|
}
|
|
|
|
function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
|
|
$tabDef = fxStaticSyncFindTableDef($strTable);
|
|
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
|
|
|
|
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
|
|
return null;
|
|
}
|
|
|
|
$objSource = $tabConnections['dev_preprod'];
|
|
$objTarget = $tabConnections[$strEnvId];
|
|
|
|
if (!$objSource || !$objTarget) {
|
|
return null;
|
|
}
|
|
|
|
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
|
|
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef);
|
|
$tabCompare = fxStaticSyncCompareTable($tabSourceData, $tabTargetData);
|
|
|
|
$tabDetail = array(
|
|
'compare' => $tabCompare,
|
|
'samples' => array(),
|
|
);
|
|
|
|
$tabKeysToShow = array_slice(
|
|
array_merge($tabCompare['missing_keys'], $tabCompare['diff_keys'], $tabCompare['extra_keys']),
|
|
0,
|
|
30
|
|
);
|
|
|
|
foreach ($tabKeysToShow as $strKey) {
|
|
$tabSample = array(
|
|
'key' => $strKey,
|
|
'type' => in_array($strKey, $tabCompare['extra_keys'], true) ? 'extra'
|
|
: (in_array($strKey, $tabCompare['missing_keys'], true) ? 'missing' : 'diff'),
|
|
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
|
|
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
|
|
);
|
|
|
|
if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') {
|
|
$tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : '';
|
|
$tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : '';
|
|
}
|
|
|
|
$tabDetail['samples'][] = $tabSample;
|
|
}
|
|
|
|
return $tabDetail;
|
|
}
|