CON-333 — Update role permissions synchronization to use role_code instead of role_id for improved portability across databases. Introduce new functions for handling role permissions and enhance existing synchronization logic to support non-destructive updates. Update user interface to include a new "apply_all" action for comprehensive synchronization without deletions. Increment version code.

This commit is contained in:
2026-07-23 09:00:04 -04:00
parent 18b667d24b
commit 9697b37263
3 changed files with 235 additions and 44 deletions

View File

@ -257,6 +257,7 @@ function fxStaticSyncKeyColumnsValid($objDb, $strTable, array $tabKeys) {
/**
* Charge les lignes — une entrée par clé métier (dernière gagne, ordre stable si ORDER BY possible).
* CON-333 — matrice rôles : jointure role_code (role_id non portable entre BD).
*/
function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
$strTable = $tabDef['table'];
@ -266,12 +267,23 @@ function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
}
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
if (!empty($tabDef['join_role_code'])) {
if (!fxStaticSyncTableExists($objDb, 'inscriptions_eve_roles')) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key';
$tabResults = $objDb->fxGetResults($sql);
} else {
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
$tabResults = $objDb->fxGetResults($sql);
}
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
$tabResults = $objDb->fxGetResults($sql);
$tabKeyed = array();
$intDupes = 0;
$intPhysical = 0;
@ -581,6 +593,11 @@ function fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, array $
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore);
// CON-333 — matrice : comparer sur role_code+perm_key seulement.
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
if (!fxStaticSyncSchemasAligned($tabSchema)) {
return array(
'ok' => false,
@ -615,6 +632,10 @@ function fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, array $
* Ne touche pas aux lignes déjà présentes (même poubelles — voir repair).
*/
function fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, array $tabDef = null) {
if ($tabDef && !empty($tabDef['join_role_code'])) {
return fxStaticSyncApplyMissingRolePermissions($objSource, $objTarget, $tabDef);
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
@ -732,11 +753,98 @@ function fxStaticSyncSqlWhereBusinessKeys($objDb, array $row, array $tabKeys) {
return implode(' AND ', $tabParts);
}
/**
* CON-333 — Matrice rôles : INSERT (role_id cible, perm_key) via role_code.
* Les role_id source ne sont jamais copiés tels quels.
*/
function fxStaticSyncApplyMissingRolePermissions($objSource, $objTarget, array $tabDef) {
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'inscriptions_eve_role_permissions', $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabMissing = array();
foreach ($tabPrep['source_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
$tabMissing[] = $tabEntry['row'];
}
}
if (count($tabMissing) === 0) {
return array('ok' => true, 'message' => 'Aucun manquant à ajouter (matrice rôles).', 'inserted' => 0);
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'inscriptions_eve_role_permissions') === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
$intInserted = 0;
foreach ($tabMissing as $row) {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
if ($strCode === '' || $strPerm === '') {
continue;
}
$sqlRole = "SELECT role_id FROM inscriptions_eve_roles WHERE role_code='"
. $objTarget->fxEscape($strCode) . "' LIMIT 1";
$strRoleId = $objTarget->fxGetVar($sqlRole);
if ($strRoleId === null || $strRoleId === '' || (int)$strRoleId <= 0) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array(
'ok' => false,
'message' => 'Rôle absent en cible (role_code=' . $strCode
. '). Appliquer d\'abord inscriptions_eve_roles.',
);
}
$strSql = 'INSERT INTO `inscriptions_eve_role_permissions` (`role_id`,`perm_key`) VALUES ('
. (int)$strRoleId . ",'" . $objTarget->fxEscape($strPerm) . "')";
$tabIns = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabIns['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'INSERT matrice : ' . $tabIns['error']);
}
$intInserted++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intInserted . ' lien(s) matrice ajouté(s) via role_code (aucun DELETE).',
'inserted' => $intInserted,
);
}
/**
* CON-333 — UPDATE des clés déjà présentes mais contenu différent (préprod = vérité textes).
* Inclut les corrections de libellés et les poubelles. Ne DELETE rien. Ninsère pas les manquants.
*/
function fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, array $tabDef = null) {
// Matrice rôles = présence de liens seulement ; pas de colonnes de contenu à écraser.
if ($tabDef && !empty($tabDef['join_role_code'])) {
return array(
'ok' => true,
'message' => 'Matrice rôles : pas de corrections de contenu (seulement Appliquer manquants).',
'updated' => 0,
);
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
@ -910,7 +1018,67 @@ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef
return array(
'ok' => false,
'message' => 'CON-333 — Sync destructif désactivé (DELETE interdit). '
. 'Utiliser « Appliquer manquants » et/ou « Réparer poubelles ».',
. 'Utiliser « Appliquer manquants » et/ou « Appliquer corrections ».',
);
}
/**
* CON-333 — Dry-run complet : toutes les tables, manquants puis corrections, un environnement.
* Aucun DELETE. Arrête à la première erreur décriture.
*
* @return array{ok:bool,message:string,lines:array}
*/
function fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId) {
$tabLines = array();
$tabDefs = fxStaticSyncTableDefinitions();
foreach ($tabDefs as $tabDef) {
$strTable = $tabDef['table'];
$tabCmp = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
if (in_array($tabCmp['status'], array('no_connection', 'missing_table', 'schema_mismatch', 'error'), true)) {
$tabLines[] = $tabDef['label'] . ' : SAUTÉ — ' . fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
continue;
}
$intMiss = count($tabCmp['missing_keys']);
if ($intMiss > 0) {
$tabMiss = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabMiss['ok'])) {
return array(
'ok' => false,
'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (manquants) : ' . $tabMiss['message'],
'lines' => $tabLines,
);
}
$tabLines[] = $tabDef['label'] . ' manquants : ' . $tabMiss['message'];
} else {
$tabLines[] = $tabDef['label'] . ' manquants : 0';
}
$tabCmp2 = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$intDiff2 = count($tabCmp2['diff_keys']);
if ($intDiff2 > 0 && empty($tabDef['join_role_code'])) {
$tabUpd = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabUpd['ok'])) {
return array(
'ok' => false,
'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (corrections) : ' . $tabUpd['message'],
'lines' => $tabLines,
);
}
$tabLines[] = $tabDef['label'] . ' corrections : ' . $tabUpd['message'];
} else {
$tabLines[] = $tabDef['label'] . ' corrections : 0';
}
}
return array(
'ok' => true,
'message' => 'Tout appliquer terminé sur ' . $strEnvId . ' (aucun DELETE). Voir détail ci-dessous.',
'lines' => $tabLines,
);
}
@ -1029,6 +1197,11 @@ function fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) {
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
// CON-333 — matrice : comparer sur role_code+perm_key seulement.
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
if (!fxStaticSyncSchemasAligned($tabSchema)) {
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef);

View File

@ -53,8 +53,10 @@ function fxStaticSyncTableDefinitions() {
array(
'table' => 'inscriptions_eve_role_permissions',
'label' => 'Accès v2 — matrice rôles',
'keys' => array('role_id', 'perm_key'),
'ignore_cols' => array(),
// CON-333 — role_id change entre BD ; comparer / sync via role_code.
'keys' => array('role_code', 'perm_key'),
'ignore_cols' => array('role_id'),
'join_role_code' => true,
),
array(
'table' => 'inscriptions_pages',

View File

@ -60,19 +60,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
} elseif (!in_array($strAction, array('apply_missing', 'apply_content'), true)) {
} elseif (!in_array($strAction, array('apply_missing', 'apply_content', 'apply_all'), true)) {
fxStaticSyncSetFlash(
'Action refusée. Sync destructif désactivé (CON-333). '
. 'Utiliser Appliquer manquants / Appliquer corrections.',
. 'Utiliser Appliquer manquants / Appliquer corrections / Tout appliquer.',
'bad'
);
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
$tabDef = fxStaticSyncFindTableDef($strTable);
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
if (!$tabEnv || !empty($tabEnv['is_source'])) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
$tabConnections = fxStaticSyncOpenConnections();
@ -84,34 +83,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
'Connexion BD impossible — ' . fxStaticSyncFormatConnectionError($tabEnv),
'bad'
);
} else {
if ($strAction === 'apply_missing') {
$tabResult = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
} else {
$tabResult = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
} elseif ($strAction === 'apply_all') {
$tabResult = fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId);
$strMsg = $tabEnv['label'] . ' — ' . $tabResult['message'];
if (!empty($tabResult['lines'])) {
$strMsg .= "\n" . implode("\n", $tabResult['lines']);
}
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
$intExtra = count($tabVerify['extra_keys']);
$intMiss = count($tabVerify['missing_keys']);
$intDiff = count($tabVerify['diff_keys']);
$strMsg .= ' Après : manque ' . $intMiss
. ', contenu diff. ' . $intDiff
. ', en trop (conservés) ' . $intExtra . '.';
if ($intMiss === 0 && $intDiff === 0 && $tabVerify['status'] === 'ok') {
fxStaticSyncSetFlash($strMsg, 'ok');
} elseif ($intMiss === 0 && $intDiff === 0) {
fxStaticSyncSetFlash($strMsg . ' Écarts restants = revue manuelle (pas de DELETE).', 'ok');
} else {
fxStaticSyncSetFlash($strMsg, 'ok');
}
fxStaticSyncSetFlash($strMsg, !empty($tabResult['ok']) ? 'ok' : 'bad');
} else {
$tabDef = fxStaticSyncFindTableDef($strTable);
if (!$tabDef) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
if ($strAction === 'apply_missing') {
$tabResult = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
} else {
$tabResult = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
}
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
$intExtra = count($tabVerify['extra_keys']);
$intMiss = count($tabVerify['missing_keys']);
$intDiff = count($tabVerify['diff_keys']);
$strMsg .= ' Après : manque ' . $intMiss
. ', contenu diff. ' . $intDiff
. ', en trop (conservés) ' . $intExtra . '.';
if ($intMiss === 0 && $intDiff === 0 && $tabVerify['status'] === 'ok') {
fxStaticSyncSetFlash($strMsg, 'ok');
} elseif ($intMiss === 0 && $intDiff === 0) {
fxStaticSyncSetFlash($strMsg . ' Écarts restants = revue manuelle (pas de DELETE).', 'ok');
} else {
fxStaticSyncSetFlash($strMsg, 'ok');
}
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
}
}
}
}
@ -186,15 +197,20 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
</div>
<div class="proc">
<strong>Procédure test (dev préprod → dev prod)</strong>
<strong>Test complet (un environnement à la fois)</strong>
<ol>
<li>Structures alignées (Navicat) — déjà fait.</li>
<li>Lire le tableau : manque / poubelles / en trop.</li>
<li>Pour chaque table concernée : <em>Appliquer manquants</em>.</li>
<li>Textes / contenu corrigés en préprod : <em>Appliquer corrections</em>.</li>
<li>« En trop » = revue manuelle — ne pas supprimer auto.</li>
<li>Quand OK ici → même outil plus tard vers client préprod / prod.</li>
<li>Rafraîchir cette page (nouveau code déployé).</li>
<li>Cliquer <em>TOUT APPLIQUER</em> sous la colonne voulue (commencer par <strong>Dev prod</strong>).</li>
<li>Lire le message vert/rouge en haut : succès ou échec + détail table par table.</li>
<li>Recharger : Manque=0 et Diff=0 partout = OK. « En trop » peut rester (normal, on nefface pas).</li>
<li>Lignes SAUTÉES (structure / table absente) = à corriger en Navicat, puis re-cliquer.</li>
</ol>
<form method="post" style="margin-top:12px;" onsubmit="return confirm('CON-333 — TOUT APPLIQUER sur Dev prod ?\nToutes les tables : manquants + corrections.\nAucun DELETE.');">
<input type="hidden" name="action" value="apply_all">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="env" value="dev_prod">
<button type="submit" class="btn btn-content" style="font-weight:700;padding:8px 14px;">TOUT APPLIQUER → Dev prod</button>
</form>
</div>
<p class="sub">