array( 'label' => 'Dev préprod', 'config' => $arrConDatabasePreProd, 'is_source' => true, ), 'dev_prod' => array( 'label' => 'Dev prod', 'config' => $arrConDatabaseProd, 'is_source' => false, ), ); if (!empty($arrConDatabaseClientPreProd)) { $tabEnvs['client_preprod'] = array( 'label' => 'Client préprod', 'config' => $arrConDatabaseClientPreProd, 'is_source' => false, ); } if (!empty($arrConDatabaseClientProd)) { $tabEnvs['client_prod'] = array( 'label' => 'Client prod', 'config' => $arrConDatabaseClientProd, 'is_source' => false, ); } return $tabEnvs; } /** Connexion MySQL avec timeout court, sans die() — pour BD client additionnelles. */ class clsMysqlStaticSync extends clsMysql { function __construct($arrDatabase = array()) { $this->last_id = 0; $this->affected_rows = 0; $this->con = null; if (empty($arrDatabase['host']) || empty($arrDatabase['db'])) { return; } $mysqli = mysqli_init(); if (!$mysqli) { return; } mysqli_options($mysqli, MYSQLI_OPT_CONNECT_TIMEOUT, 10); if (!@mysqli_real_connect( $mysqli, $arrDatabase['host'], $arrDatabase['user'], $arrDatabase['pass'], $arrDatabase['db'], 3306 )) { if (!isset($GLOBALS['ms1_static_sync_connect_errors'])) { $GLOBALS['ms1_static_sync_connect_errors'] = array(); } $GLOBALS['ms1_static_sync_connect_errors'][$arrDatabase['db']] = mysqli_connect_error(); return; } // Pas de mysqli_set_charset ici — charset serveur / table legacy (latin1, utf8, etc.). $this->con = $mysqli; } } function fxStaticSyncConnectError($strDb) { if (empty($GLOBALS['ms1_static_sync_connect_errors'][$strDb])) { return ''; } return $GLOBALS['ms1_static_sync_connect_errors'][$strDb]; } function fxStaticSyncFormatConnectionError($tabEnv) { $strDb = isset($tabEnv['config']['db']) ? $tabEnv['config']['db'] : ''; $strMsg = $tabEnv['label'] . ' (' . $strDb . ')'; $strErr = fxStaticSyncConnectError($strDb); if ($strErr !== '') { $strMsg .= ' — ' . $strErr; } return $strMsg; } function fxStaticSyncConnect($arrConfig) { if (empty($arrConfig['host']) || empty($arrConfig['db'])) { return null; } $obj = new clsMysqlStaticSync($arrConfig); return !empty($obj->con) ? $obj : null; } /** * Dev : réutilise objDatabase* de inc_fonctions.php. * Client : $arrConDatabaseClient* de inc_settings.php (si définis pour l'environnement). */ function fxStaticSyncOpenConnections() { global $objDatabasePreProd, $objDatabaseProd; global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd; $tabConnections = array( 'dev_preprod' => !empty($objDatabasePreProd) ? $objDatabasePreProd : null, 'dev_prod' => !empty($objDatabaseProd) ? $objDatabaseProd : null, ); if (!empty($arrConDatabaseClientPreProd)) { $tabConnections['client_preprod'] = fxStaticSyncConnect($arrConDatabaseClientPreProd); } if (!empty($arrConDatabaseClientProd)) { $tabConnections['client_prod'] = fxStaticSyncConnect($arrConDatabaseClientProd); } return $tabConnections; } 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); } /** * Colonnes communes source/cible pour compare et copy (alignés). * missing_in_target / extra_in_target : écarts de structure (hors ignore_cols). */ function fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, array $tabIgnoreCols) { $tabSourceCols = fxStaticSyncGetTableColumnNames($objSource, $strTable); $tabTargetCols = fxStaticSyncGetTableColumnNames($objTarget, $strTable); $tabCommon = array(); foreach ($tabSourceCols as $strCol) { if (in_array($strCol, $tabTargetCols, true)) { $tabCommon[] = $strCol; } } $tabCompareCols = array(); foreach ($tabCommon as $strCol) { if (!in_array($strCol, $tabIgnoreCols, true)) { $tabCompareCols[] = $strCol; } } $tabMissingInTarget = array(); foreach ($tabSourceCols as $strCol) { if (!in_array($strCol, $tabTargetCols, true) && !in_array($strCol, $tabIgnoreCols, true)) { $tabMissingInTarget[] = $strCol; } } $tabExtraInTarget = array(); foreach ($tabTargetCols as $strCol) { if (!in_array($strCol, $tabSourceCols, true) && !in_array($strCol, $tabIgnoreCols, true)) { $tabExtraInTarget[] = $strCol; } } return array( 'copy_cols' => $tabCommon, 'compare_cols' => $tabCompareCols, 'missing_in_target' => $tabMissingInTarget, 'extra_in_target' => $tabExtraInTarget, ); } function fxStaticSyncSchemasAligned(array $tabSchema) { return count($tabSchema['missing_in_target']) === 0 && count($tabSchema['extra_in_target']) === 0; } function fxStaticSyncFormatSchemaDiff(array $tabSchema) { $tabParts = array(); if (!empty($tabSchema['missing_in_target'])) { $tabParts[] = 'absentes en cible : ' . implode(', ', $tabSchema['missing_in_target']); } if (!empty($tabSchema['extra_in_target'])) { $tabParts[] = 'en trop en cible : ' . implode(', ', $tabSchema['extra_in_target']); } return implode(' · ', $tabParts); } function fxStaticSyncNormalizeCellValue($val) { if ($val === null) { return null; } return (string)$val; } function fxStaticSyncSqlOrderByKeys(array $tabKeys) { $tabParts = array(); foreach ($tabKeys as $strCol) { $tabParts[] = '`' . $strCol . '`'; } return implode(',', $tabParts); } function fxStaticSyncGetTableRowCount($objDb, $strTable) { if (!$objDb || !fxStaticSyncTableExists($objDb, $strTable)) { return 0; } return (int)$objDb->fxGetVar('SELECT COUNT(*) FROM `' . $strTable . '`'); } function fxStaticSyncKeyColumnsValid($objDb, $strTable, array $tabKeys) { if (count($tabKeys) === 0) { return false; } $tabCols = fxStaticSyncGetTableColumnNames($objDb, $strTable); foreach ($tabKeys as $strCol) { if (!in_array($strCol, $tabCols, true)) { return false; } } return true; } /** * 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']; $tabKeys = $tabDef['keys']; if (!fxStaticSyncTableExists($objDb, $strTable)) { return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null); } $sql = !empty($tabDef['join_role_code']) ? ('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') : ('SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys)); 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'); } $tabResults = $objDb->fxGetResults($sql); } else { if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) { return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys'); } $tabResults = $objDb->fxGetResults($sql); } // CON-333 — clsMysql::fxGetResults renvoie null si 0 ligne (pas une erreur SQL). if (!is_array($tabResults)) { $tabResults = array(); } $tabKeyed = array(); $intDupes = 0; $intPhysical = 0; foreach ($tabResults as $row) { if (!is_array($row)) { continue; } $intPhysical++; $strKey = fxStaticSyncRowBusinessKey($row, $tabKeys); if (isset($tabKeyed[$strKey])) { $intDupes++; } $tabKeyed[$strKey] = $row; } return array( 'rows' => $tabKeyed, 'duplicate_count' => $intDupes, 'physical_count' => $intPhysical, 'error' => null, ); } function fxStaticSyncRowContentHash(array $row, array $tabCompareCols) { $tabData = array(); foreach ($tabCompareCols as $strCol) { $tabData[$strCol] = array_key_exists($strCol, $row) ? fxStaticSyncNormalizeCellValue($row[$strCol]) : null; } ksort($tabData); return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE)); } function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) { $tabKeys = $tabDef['keys']; $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); if (!fxStaticSyncTableExists($objDb, $tabDef['table'])) { return array('error' => 'missing_table', 'rows' => array(), 'count' => 0, 'physical_count' => 0); } $tabFetch = fxStaticSyncFetchRowsByKey($objDb, $tabDef); if ($tabFetch['error'] === 'bad_keys') { return array('error' => 'bad_keys', 'rows' => array(), 'count' => 0, 'physical_count' => 0); } if ($tabFetch['error'] === 'query_failed') { return array('error' => 'query_failed', 'rows' => array(), 'count' => 0, 'physical_count' => 0); } $tabKeyed = array(); if ($tabCompareCols === null) { $tabCompareCols = array(); if (count($tabFetch['rows']) > 0) { $row = reset($tabFetch['rows']); foreach (array_keys($row) as $strCol) { if (!in_array($strCol, $tabIgnore, true)) { $tabCompareCols[] = $strCol; } } } } foreach ($tabFetch['rows'] as $strKey => $row) { $tabKeyed[$strKey] = array( 'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols), 'row' => $row, ); } return array( 'error' => null, 'rows' => $tabKeyed, 'count' => count($tabKeyed), 'physical_count' => $tabFetch['physical_count'], 'duplicate_count' => $tabFetch['duplicate_count'], ); } /** Tous les environnements cibles sont syncables (dev prod, client préprod, client prod). */ function fxStaticSyncWriteAllowed($strEnvId) { return true; } function fxStaticSyncSetFlash($strMessage, $strType) { $_SESSION['ms1_static_sync_flash'] = $strMessage; $_SESSION['ms1_static_sync_flash_type'] = $strType; } function fxStaticSyncPopFlash() { if (empty($_SESSION['ms1_static_sync_flash'])) { return null; } $tabFlash = array( 'message' => $_SESSION['ms1_static_sync_flash'], 'type' => !empty($_SESSION['ms1_static_sync_flash_type']) ? $_SESSION['ms1_static_sync_flash_type'] : 'info', ); unset($_SESSION['ms1_static_sync_flash'], $_SESSION['ms1_static_sync_flash_type']); return $tabFlash; } function fxStaticSyncFinalizeCompareResult(array $tabCmp) { if (in_array($tabCmp['status'], array('missing_table', 'schema_mismatch', 'no_connection'), true)) { return $tabCmp; } $intMissingKeys = count($tabCmp['missing_keys']); $intExtraKeys = count($tabCmp['extra_keys']); $intDiffKeys = count($tabCmp['diff_keys']); if ($intMissingKeys === 0 && $intExtraKeys === 0 && $intDiffKeys === 0) { $tabCmp['status'] = 'ok'; return $tabCmp; } $intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count']; if ($intDelta < 0) { $tabCmp['status'] = 'missing_rows'; } elseif ($intDelta > 0) { $tabCmp['status'] = 'extra_rows'; } else { $tabCmp['status'] = 'content_diff'; } return $tabCmp; } /** * @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|schema_mismatch|no_connection|error */ function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData, array $tabSchemaInfo = array()) { 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; } } return fxStaticSyncFinalizeCompareResult(array( 'status' => 'pending', 'source_count' => $tabSourceData['count'], 'target_count' => $tabTargetData['count'], 'source_physical' => isset($tabSourceData['physical_count']) ? $tabSourceData['physical_count'] : $tabSourceData['count'], 'target_physical' => isset($tabTargetData['physical_count']) ? $tabTargetData['physical_count'] : $tabTargetData['count'], 'missing_keys' => $tabMissing, 'extra_keys' => $tabExtra, 'diff_keys' => $tabDiff, 'schema_missing_cols' => isset($tabSchemaInfo['missing_in_target']) ? $tabSchemaInfo['missing_in_target'] : array(), 'schema_extra_cols' => isset($tabSchemaInfo['extra_in_target']) ? $tabSchemaInfo['extra_in_target'] : array(), )); } function fxStaticSyncDbLastError($objDb) { if (!$objDb || empty($objDb->con)) { return ''; } return mysqli_error($objDb->con); } /** * Exécute une requête d'écriture — erreur remontée dans le message flash (fxQuery n'echo pas). */ function fxStaticSyncExecQuery($objDb, $strSql) { if (!$objDb) { return array('ok' => false, 'error' => 'Connexion BD invalide.'); } $blnOk = $objDb->fxQuery($strSql); if ($blnOk === false) { return array( 'ok' => false, 'error' => fxStaticSyncDbLastError($objDb) ?: 'Erreur SQL inconnue.', ); } return array('ok' => true, 'error' => null); } function fxStaticSyncGetTableColumnNames($objDb, $strTable) { if (!$objDb) { return array(); } $tabCols = $objDb->fxGetResults('SHOW COLUMNS FROM `' . $strTable . '`'); $tabNames = array(); if (!is_array($tabCols)) { return array(); } foreach ($tabCols as $row) { if (is_array($row) && !empty($row['Field'])) { $tabNames[] = $row['Field']; } } return $tabNames; } function fxStaticSyncGetTableEngine($objDb, $strTable) { if (!$objDb) { return ''; } $row = $objDb->fxGetRow( "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $objDb->fxEscape($strTable) . "'" ); return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : ''; } /** * CON-333 — Colonnes à écrire en INSERT soft (hors PK / timestamps ignore_cols). * Les clés métier restent incluses (elles ne sont pas dans ignore_cols). */ function fxStaticSyncInsertColumns(array $tabSchema, array $tabDef) { $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); $tabCols = array(); foreach ($tabSchema['copy_cols'] as $strCol) { if (!in_array($strCol, $tabIgnore, true)) { $tabCols[] = $strCol; } } return $tabCols; } /** CON-333 — Ligne info « poubelle » (texte = clé, créé à chaud par afficheTexte). */ function fxStaticSyncInfoRowIsPoubelle(array $row) { if (!isset($row['info_clef']) || !isset($row['info_texte'])) { return false; } return (string)$row['info_texte'] === (string)$row['info_clef']; } /** CON-333 — Source a un vrai libellé (pas poubelle / pas vide). */ function fxStaticSyncInfoRowHasRealTexte(array $row) { if (!isset($row['info_clef']) || !isset($row['info_texte'])) { return false; } $strTexte = trim((string)$row['info_texte']); if ($strTexte === '') { return false; } return $strTexte !== (string)$row['info_clef']; } /** * CON-333 — Prépare le schéma + jeux de lignes source/cible pour écriture soft. * Aucune écriture. Retourne ok=false si structure / connexion invalide. */ function fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, array $tabDef = null) { if (!$objSource || !$objTarget) { return array('ok' => false, 'message' => 'Connexion BD invalide.'); } if (!$tabDef) { return array('ok' => false, 'message' => 'Table non configurée.'); } 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.'); } $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, 'message' => 'Structures non alignées — ' . fxStaticSyncFormatSchemaDiff($tabSchema) . '. Aligner les structures (Navicat) avant. Aucune donnée modifiée.', ); } $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']); if (!empty($tabSourceData['error']) && $tabSourceData['error'] !== null) { return array('ok' => false, 'message' => 'Lecture source : ' . $tabSourceData['error']); } if (!empty($tabTargetData['error']) && $tabTargetData['error'] !== null && $tabTargetData['error'] !== 'missing_table') { return array('ok' => false, 'message' => 'Lecture cible : ' . $tabTargetData['error']); } return array( 'ok' => true, 'schema' => $tabSchema, 'source_rows' => $tabSourceData['rows'], 'target_rows' => $tabTargetData['rows'], 'source_dupes' => isset($tabSourceData['duplicate_count']) ? (int)$tabSourceData['duplicate_count'] : 0, ); } /** * CON-333 — INSERT uniquement les clés absentes en cible. Jamais de DELETE. * 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; } $tabInsertCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef); if (count($tabInsertCols) === 0) { return array('ok' => false, 'message' => 'Aucune colonne insérable (hors ignore_cols).'); } $tabMissingRows = array(); foreach ($tabPrep['source_rows'] as $strKey => $tabEntry) { if (!isset($tabPrep['target_rows'][$strKey])) { $tabMissingRows[$strKey] = $tabEntry['row']; } } $intMissing = count($tabMissingRows); if ($intMissing === 0) { return array( 'ok' => true, 'message' => 'Aucun manquant à ajouter.', 'inserted' => 0, 'skipped_extra' => count($tabPrep['target_rows']) - count(array_intersect_key($tabPrep['target_rows'], $tabPrep['source_rows'])), ); } $strColList = '`' . implode('`,`', $tabInsertCols) . '`'; $intBatchSize = 50; $tabBatch = array(); $tabInsertBatches = array(); foreach ($tabMissingRows as $row) { if (!is_array($row)) { continue; } $tabVals = array(); foreach ($tabInsertCols as $strCol) { if ($strTable === 'info' && $strCol === 'info_creation') { $tabVals[] = 'NOW()'; continue; } if (!array_key_exists($strCol, $row) || $row[$strCol] === null) { $tabVals[] = 'NULL'; } else { $tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'"; } } $tabBatch[] = '(' . implode(',', $tabVals) . ')'; if (count($tabBatch) >= $intBatchSize) { $tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); $tabBatch = array(); } } if (count($tabBatch) > 0) { $tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); } $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB'); if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } foreach ($tabInsertBatches as $strSql) { $tabInsert = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabInsert['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array( 'ok' => false, 'message' => 'Insertion manquants : ' . $tabInsert['error'] . ($blnTransactional ? ' (rollback — aucune ligne ajoutée).' : ''), ); } } if ($blnTransactional) { $tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT'); if (!$tabCommit['ok']) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']); } } $strMessage = $intMissing . ' ligne(s) ajoutée(s) (INSERT manquants uniquement, aucun DELETE).'; if (!empty($tabPrep['source_dupes'])) { $strMessage .= ' (' . (int)$tabPrep['source_dupes'] . ' doublon(s) clé en source ignoré(s).)'; } return array( 'ok' => true, 'message' => $strMessage, 'inserted' => $intMissing, ); } /** * CON-333 — WHERE métier (toutes les colonnes keys). */ function fxStaticSyncSqlWhereBusinessKeys($objDb, array $row, array $tabKeys) { $tabParts = array(); foreach ($tabKeys as $strCol) { if (!array_key_exists($strCol, $row) || $row[$strCol] === null) { $tabParts[] = '`' . $strCol . '` IS NULL'; } else { $tabParts[] = '`' . $strCol . "`='" . $objDb->fxEscape($row[$strCol]) . "'"; } } 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. N’insè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; } $tabUpdateCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef); $tabKeys = $tabDef['keys']; $tabUpdateCols = array_values(array_filter($tabUpdateCols, function ($strCol) use ($tabKeys) { return !in_array($strCol, $tabKeys, true); })); if (count($tabUpdateCols) === 0) { return array('ok' => false, 'message' => 'Aucune colonne de contenu à mettre à jour.'); } $tabSchemaCols = $tabPrep['schema']['copy_cols']; $intUpdated = 0; $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB'); if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) { if (!isset($tabPrep['target_rows'][$strKey])) { continue; } if ($tabPrep['target_rows'][$strKey]['hash'] === $tabSrcEntry['hash']) { continue; } $tabSrc = $tabSrcEntry['row']; $tabTgt = $tabPrep['target_rows'][$strKey]['row']; $tabSets = array(); foreach ($tabUpdateCols as $strCol) { if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) { $tabSets[] = '`' . $strCol . '`=NULL'; } else { $tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'"; } } if ($strTable === 'info' && in_array('info_maj', $tabSchemaCols, true)) { $tabSets[] = '`info_maj`=NOW()'; } $strSql = 'UPDATE `' . $strTable . '` SET ' . implode(',', $tabSets) . ' WHERE ' . fxStaticSyncSqlWhereBusinessKeys($objTarget, $tabTgt, $tabKeys); $tabUpd = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabUpd['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'UPDATE corrections : ' . $tabUpd['error']); } $intUpdated++; } 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' => $intUpdated . ' ligne(s) mise(s) à jour depuis la source (corrections / diffs, aucun DELETE).', 'updated' => $intUpdated, ); } /** * CON-333 — info seulement : UPDATE des poubelles cible quand la source a un vrai texte. * N’écrase jamais un libellé déjà OK. Ne DELETE rien. */ function fxStaticSyncRepairInfoPoubelle($objSource, $objTarget, array $tabDef = null) { if (!$tabDef) { $tabDef = fxStaticSyncFindTableDef('info'); } if (!$tabDef || $tabDef['table'] !== 'info') { return array('ok' => false, 'message' => 'Réparation poubelle réservée à la table info.'); } $tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'info', $tabDef); if (empty($tabPrep['ok'])) { return $tabPrep; } $tabUpdateCols = array( 'info_texte', 'info_aide', 'info_description', 'info_trie', 'info_actif', 'info_option1', 'info_option2', 'info_option3', ); $tabSchemaCols = $tabPrep['schema']['copy_cols']; $tabUpdateCols = array_values(array_intersect($tabUpdateCols, $tabSchemaCols)); $intRepaired = 0; $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'info') === 'INNODB'); if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) { if (!isset($tabPrep['target_rows'][$strKey])) { continue; } $tabSrc = $tabSrcEntry['row']; $tabTgt = $tabPrep['target_rows'][$strKey]['row']; if (!fxStaticSyncInfoRowIsPoubelle($tabTgt) || !fxStaticSyncInfoRowHasRealTexte($tabSrc)) { continue; } $tabSets = array(); foreach ($tabUpdateCols as $strCol) { if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) { $tabSets[] = '`' . $strCol . '`=NULL'; } else { $tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'"; } } if (in_array('info_maj', $tabSchemaCols, true)) { $tabSets[] = '`info_maj`=NOW()'; } $strWhere = '`info_clef`=\'' . $objTarget->fxEscape($tabTgt['info_clef']) . '\'' . ' AND `info_langue`=\'' . $objTarget->fxEscape($tabTgt['info_langue']) . '\'' . ' AND `info_prg`=\'' . $objTarget->fxEscape($tabTgt['info_prg']) . '\'' . ' AND `info_texte`=`info_clef`'; $strSql = 'UPDATE `info` SET ' . implode(',', $tabSets) . ' WHERE ' . $strWhere; $tabUpd = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabUpd['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'UPDATE poubelle : ' . $tabUpd['error']); } $intRepaired++; } 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' => $intRepaired . ' poubelle(s) réparée(s) depuis la source (aucun DELETE, libellés OK inchangés).', 'repaired' => $intRepaired, ); } /** * CON-333 — info seulement : retirer les « en trop » qui sont des poubelles (texte = clé). * Les vrais libellés présents seulement en cible sont CONSERVÉS (pas de DELETE). */ function fxStaticSyncRemoveInfoExtraPoubelle($objSource, $objTarget, array $tabDef = null) { if (!$tabDef) { $tabDef = fxStaticSyncFindTableDef('info'); } if (!$tabDef || $tabDef['table'] !== 'info') { return array('ok' => false, 'message' => 'Réservé à info.'); } $tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'info', $tabDef); if (empty($tabPrep['ok'])) { return $tabPrep; } $tabToDelete = array(); $intKeptReal = 0; foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) { if (isset($tabPrep['source_rows'][$strKey])) { continue; } $row = $tabEntry['row']; if (fxStaticSyncInfoRowIsPoubelle($row)) { $tabToDelete[] = $row; } else { $intKeptReal++; } } $intDel = count($tabToDelete); if ($intDel === 0) { return array( 'ok' => true, 'message' => 'Aucune poubelle en trop à retirer. ' . $intKeptReal . ' vrai(s) libellé(s) seulement en cible — conservé(s) (pas de DELETE).', 'deleted' => 0, 'kept_real' => $intKeptReal, ); } $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'info') === 'INNODB'); if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } $intDeleted = 0; foreach ($tabToDelete as $row) { $strSql = 'DELETE FROM `info` WHERE ' . fxStaticSyncSqlWhereBusinessKeys($objTarget, $row, $tabDef['keys']) . ' AND `info_texte`=`info_clef` LIMIT 1'; $tabDel = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabDel['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'DELETE poubelle : ' . $tabDel['error']); } $intDeleted++; } 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' => $intDeleted . ' poubelle(s) en trop retirée(s). ' . $intKeptReal . ' vrai(s) libellé(s) seulement en cible — conservé(s) volontairement.', 'deleted' => $intDeleted, 'kept_real' => $intKeptReal, ); } /** * CON-333 — Retirer les lignes « en trop » (catalogue accès v2). * info : jamais. Rôles : refuse si encore utilisés dans inscriptions_eve_acces. */ function fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, array $tabDef = null) { if (!$tabDef || empty($tabDef['allow_remove_extras'])) { return array('ok' => false, 'message' => 'Retrait des en trop non autorisé pour cette table.'); } if ($strTable === 'info') { return array('ok' => false, 'message' => 'Interdit : jamais de DELETE sur info.'); } $tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef); if (empty($tabPrep['ok'])) { return $tabPrep; } $tabExtras = array(); foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) { if (!isset($tabPrep['source_rows'][$strKey])) { $tabExtras[$strKey] = $tabEntry['row']; } } $intExtra = count($tabExtras); if ($intExtra === 0) { return array('ok' => true, 'message' => 'Aucun en trop à retirer.', 'deleted' => 0, 'skipped' => 0); } $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB'); if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } $intDeleted = 0; $intSkipped = 0; $tabSkipReasons = array(); foreach ($tabExtras as $strKey => $row) { if ($strTable === 'inscriptions_eve_roles') { $intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0; $strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?'; if ($intRoleId <= 0) { $intSkipped++; $tabSkipReasons[] = $strCode . ' (role_id invalide)'; continue; } $intUsed = 0; if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_acces')) { $intUsed = (int)$objTarget->fxGetVar( 'SELECT COUNT(*) FROM inscriptions_eve_acces WHERE role_id=' . $intRoleId ); } if ($intUsed > 0) { $intSkipped++; $tabSkipReasons[] = $strCode . ' (encore ' . $intUsed . ' accès promoteur — non effacé)'; continue; } // Nettoyer d’abord les liens matrice orphelins de ce rôle. if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) { $tabDelMat = fxStaticSyncExecQuery( $objTarget, 'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId ); if (!$tabDelMat['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'DELETE matrice orpheline : ' . $tabDelMat['error']); } } $strSql = 'DELETE FROM `inscriptions_eve_roles` WHERE role_id=' . $intRoleId . ' AND role_code=\'' . $objTarget->fxEscape($strCode) . '\' LIMIT 1'; } elseif ($strTable === 'inscriptions_eve_role_permissions') { $strCode = isset($row['role_code']) ? (string)$row['role_code'] : ''; $strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : ''; $intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0; if ($strPerm === '' || ($intRoleId <= 0 && $strCode === '')) { $intSkipped++; continue; } if ($intRoleId <= 0) { $intRoleId = (int)$objTarget->fxGetVar( "SELECT role_id FROM inscriptions_eve_roles WHERE role_code='" . $objTarget->fxEscape($strCode) . "' LIMIT 1" ); } if ($intRoleId <= 0) { $intSkipped++; $tabSkipReasons[] = $strCode . '+' . $strPerm . ' (rôle introuvable)'; continue; } $strSql = 'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId . ' AND perm_key=\'' . $objTarget->fxEscape($strPerm) . '\' LIMIT 1'; } elseif ($strTable === 'inscriptions_eve_permissions') { $strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : ''; if ($strPerm === '') { $intSkipped++; continue; } // Si encore référencée dans la matrice cible, ne pas casser. $intMat = 0; if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) { $intMat = (int)$objTarget->fxGetVar( "SELECT COUNT(*) FROM inscriptions_eve_role_permissions WHERE perm_key='" . $objTarget->fxEscape($strPerm) . "'" ); } if ($intMat > 0) { $intSkipped++; $tabSkipReasons[] = $strPerm . ' (encore ' . $intMat . ' lien(s) matrice)'; continue; } $strSql = 'DELETE FROM `inscriptions_eve_permissions` WHERE perm_key=\'' . $objTarget->fxEscape($strPerm) . '\' LIMIT 1'; } else { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'Table non gérée pour retrait en trop.'); } $tabDel = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabDel['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } return array('ok' => false, 'message' => 'DELETE en trop : ' . $tabDel['error']); } $intDeleted++; } if ($blnTransactional) { $tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT'); if (!$tabCommit['ok']) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']); } } $strMessage = $intDeleted . ' en trop retiré(s) (alignement catalogue sur préprod).'; if ($intSkipped > 0) { $strMessage .= ' ' . $intSkipped . ' conservé(s) (encore utilisés) : ' . implode(' ; ', array_slice($tabSkipReasons, 0, 8)); } return array( 'ok' => true, 'message' => $strMessage, 'deleted' => $intDeleted, 'skipped' => $intSkipped, ); } /** * CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé. * Conservé pour ne pas casser d’appels résiduels — renvoie toujours une erreur. */ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) { return array( 'ok' => false, 'message' => 'CON-333 — Sync destructif désactivé (DELETE interdit). ' . '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'; } // CON-333 — catalogues accès v2 : retirer les en trop dans le même passage (pas les laisser). if (!empty($tabDef['allow_remove_extras'])) { $tabCmp3 = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); $intExtra3 = count($tabCmp3['extra_keys']); if ($intExtra3 > 0) { $tabRem = fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, $tabDef); if (empty($tabRem['ok'])) { return array( 'ok' => false, 'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (retirer en trop) : ' . $tabRem['message'], 'lines' => $tabLines, ); } $tabLines[] = $tabDef['label'] . ' en trop : ' . $tabRem['message']; } else { $tabLines[] = $tabDef['label'] . ' en trop : 0'; } } } return array( 'ok' => true, 'message' => 'Tout appliquer terminé sur ' . $strEnvId . ' (info : jamais DELETE ; catalogues accès v2 : en trop retirés si possible). Voir détail.', 'lines' => $tabLines, ); } function fxStaticSyncStatusLabel($strStatus, array $tabCmp = null) { switch ($strStatus) { case 'ok': return 'Données alignées'; case 'missing_table': return 'Table absente'; case 'schema_mismatch': return 'Structures non alignées'; case 'no_connection': return 'Connexion impossible'; case 'error': return 'Erreur lecture'; } if (is_array($tabCmp)) { $intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count']; if ($intDelta < 0) { return 'Il en manque ' . abs($intDelta); } if ($intDelta > 0) { return 'Il y en a ' . $intDelta . ' en trop'; } return 'Contenu différent'; } switch ($strStatus) { case 'content_diff': return 'Contenu différent'; case 'missing_rows': return 'Il en manque'; case 'extra_rows': return 'Il y en a en trop'; 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'; case 'schema_mismatch': return 'ss-schema'; case 'error': return 'ss-bad'; default: return 'ss-bad'; } } function fxStaticSyncFormatKeyPreview($strKey) { $strKey = str_replace("\0", ' · ', $strKey); if (strlen($strKey) > 120) { return substr($strKey, 0, 117) . '…'; } return $strKey; } /** * CON-333 — Expliquer une ligne « en trop » (présente en cible, absente en préprod). */ function fxStaticSyncExplainExtraRow(array $tabDef, array $row = null) { $strTable = $tabDef['table']; if (!is_array($row)) { return 'Présente seulement en cible — absente de la préprod (source).'; } if ($strTable === 'info') { $strClef = isset($row['info_clef']) ? (string)$row['info_clef'] : ''; $strLang = isset($row['info_langue']) ? (string)$row['info_langue'] : ''; $strPrg = isset($row['info_prg']) ? (string)$row['info_prg'] : ''; $strTexte = isset($row['info_texte']) ? (string)$row['info_texte'] : ''; $blnPoub = ($strTexte !== '' && $strTexte === $strClef); if ($blnPoub) { return 'Auto-créée en cible (texte = clé) — prg « ' . $strPrg . ' » / ' . $strLang . '. N’existe pas en préprod ; souvent résidu après trou info.'; } $strPreview = function_exists('mb_substr') ? mb_substr($strTexte, 0, 80) : substr($strTexte, 0, 80); return 'Libellé seulement en cible (pas en préprod) — prg « ' . $strPrg . ' » / ' . $strLang . ' : « ' . $strPreview . ' ». Soit ajouté seulement ici, soit jamais repris en préprod.'; } if ($strTable === 'inscriptions_eve_roles') { $strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?'; $strLab = isset($row['role_label_fr']) ? (string)$row['role_label_fr'] : ''; return 'Rôle seulement en cible : « ' . $strCode . ' »' . ($strLab !== '' ? ' (' . $strLab . ')' : '') . '. Pas dans la préprod — ancien rôle ou créé hors sync.'; } if ($strTable === 'inscriptions_eve_role_permissions') { $strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?'; $strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?'; return 'Droit accordé seulement en cible : rôle « ' . $strCode . ' » → « ' . $strPerm . ' ». ' . 'Absent de la matrice préprod — soit préprod incomplète, soit droit en trop à retirer plus tard.'; } if ($strTable === 'inscriptions_pages') { $strCode = isset($row['pag_code']) ? (string)$row['pag_code'] : '?'; return 'Page promoteur seulement en cible : « ' . $strCode . ' ».'; } if ($strTable === 'inscriptions_eve_permissions') { $strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?'; return 'Permission catalogue seulement en cible : « ' . $strPerm . ' ».'; } return 'Ligne seulement en cible (clé métier absente en préprod).'; } /** CON-333 — Échantillon d’écart enrichi pour l’UI. */ function fxStaticSyncBuildSample(array $tabDef, $strKey, $strType, $tabSrcEntry, $tabTgtEntry) { $tabSrc = is_array($tabSrcEntry) && isset($tabSrcEntry['row']) ? $tabSrcEntry['row'] : null; $tabTgt = is_array($tabTgtEntry) && isset($tabTgtEntry['row']) ? $tabTgtEntry['row'] : null; $tabSample = array( 'key' => $strKey, 'type' => $strType, 'source' => $tabSrc, 'target' => $tabTgt, 'explain' => '', ); if ($strType === 'extra') { $tabSample['explain'] = fxStaticSyncExplainExtraRow($tabDef, is_array($tabTgt) ? $tabTgt : null); } elseif ($strType === 'missing') { $tabSample['explain'] = 'Absente en cible — présente en préprod (Appliquer manquants).'; } elseif ($strType === 'poubelle') { $tabSample['explain'] = 'Poubelle en cible (texte = clé) — préprod a un vrai libellé (Appliquer corrections).'; } else { $tabSample['explain'] = 'Même clé, contenu différent (Appliquer corrections = préprod gagne).'; } if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle' || $strType === 'extra')) { $tabSample['source_texte'] = is_array($tabSrc) && isset($tabSrc['info_texte']) ? $tabSrc['info_texte'] : ''; $tabSample['target_texte'] = is_array($tabTgt) && isset($tabTgt['info_texte']) ? $tabTgt['info_texte'] : ''; } return $tabSample; } 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 fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) { $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); if (!fxStaticSyncTableExists($objTarget, $tabDef['table'])) { $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); return array( 'status' => 'missing_table', 'source_count' => $tabSourceData['count'], 'target_count' => 0, 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'schema_missing_cols' => array(), 'schema_extra_cols' => array(), ); } $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); return array( 'status' => 'schema_mismatch', 'source_count' => $tabSourceData['count'], 'target_count' => $tabTargetData['count'], 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'schema_missing_cols' => $tabSchema['missing_in_target'], 'schema_extra_cols' => $tabSchema['extra_in_target'], ); } $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']); if (!empty($tabSourceData['error']) || !empty($tabTargetData['error'])) { return array( 'status' => 'error', 'source_count' => isset($tabSourceData['count']) ? $tabSourceData['count'] : 0, 'target_count' => isset($tabTargetData['count']) ? $tabTargetData['count'] : 0, 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'poubelle_keys' => array(), 'schema_missing_cols' => array(), 'schema_extra_cols' => array(), ); } $tabCmp = fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema); // CON-333 — poubelles info réparables (cible brute, source OK) — jamais auto-supprimées. $tabPoubelle = array(); if ($tabDef['table'] === 'info') { foreach ($tabSourceData['rows'] as $strKey => $tabSrcEntry) { if (!isset($tabTargetData['rows'][$strKey])) { continue; } $tabSrc = $tabSrcEntry['row']; $tabTgt = $tabTargetData['rows'][$strKey]['row']; if (fxStaticSyncInfoRowIsPoubelle($tabTgt) && fxStaticSyncInfoRowHasRealTexte($tabSrc)) { $tabPoubelle[] = $strKey; } } } $tabCmp['poubelle_keys'] = $tabPoubelle; // CON-333 — en trop info qui sont des poubelles (retirables) vs vrais libellés (à garder). $tabPoubExtra = array(); $tabRealExtra = array(); if ($tabDef['table'] === 'info') { foreach ($tabTargetData['rows'] as $strKey => $tabTgtEntry) { if (isset($tabSourceData['rows'][$strKey])) { continue; } $tabTgt = $tabTgtEntry['row']; if (fxStaticSyncInfoRowIsPoubelle($tabTgt)) { $tabPoubExtra[] = $strKey; } else { $tabRealExtra[] = $strKey; } } } $tabCmp['poubelle_extra_keys'] = $tabPoubExtra; $tabCmp['real_extra_keys'] = $tabRealExtra; return $tabCmp; } 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'], 'source_physical' => $tabSourceData['physical_count'], 'source_dupes' => $tabSourceData['duplicate_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(), 'schema_missing_cols' => array(), 'schema_extra_cols' => array(), ); continue; } $tabRow['envs'][$strEnvId] = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); } $tabReport[] = $tabRow; } return $tabReport; } 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; } $tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); $tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore); if (!empty($tabDef['join_role_code'])) { $tabSchema['compare_cols'] = $tabDef['keys']; } $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']); $tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array(); $intExtra = count($tabCompare['extra_keys']); $strExtraContext = ''; if ($strTable === 'inscriptions_eve_role_permissions' && $intExtra > 0) { $strExtraContext = 'La préprod n\'a que ' . (int)$tabCompare['source_count'] . ' lien(s) matrice. Les « en trop » sont des droits présents en cible seulement — ' . 'à revoir un par un (préprod incomplète vs anciens droits).'; } elseif ($strTable === 'info' && $intExtra > 0) { $strExtraContext = 'Clés info seulement en cible : souvent créées à chaud (afficheTexte) ' . 'ou jamais reprises en préprod. Liste ci-dessous : poubelle vs vrai libellé.'; } elseif ($intExtra > 0) { $strExtraContext = 'Lignes présentes seulement en cible. Soft sync ne les efface pas — ' . 'décider : garder, reporter en préprod, ou retirer plus tard (mode catalogue).'; } $tabDetail = array( 'compare' => $tabCompare, 'samples_missing' => array(), 'samples_diff' => array(), 'samples_extra' => array(), 'note' => 'CON-333 — Les « en trop » sont listés et expliqués ; ils ne sont pas oubliés.', 'extra_context' => $strExtraContext, ); foreach (array_slice($tabCompare['missing_keys'], 0, 80) as $strKey) { $tabDetail['samples_missing'][] = fxStaticSyncBuildSample( $tabDef, $strKey, 'missing', isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null, null ); } foreach (array_slice(array_diff($tabCompare['diff_keys'], $tabPoubelle), 0, 80) as $strKey) { $tabDetail['samples_diff'][] = fxStaticSyncBuildSample( $tabDef, $strKey, 'diff', isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null, isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null ); } foreach (array_slice($tabPoubelle, 0, 40) as $strKey) { $tabDetail['samples_diff'][] = fxStaticSyncBuildSample( $tabDef, $strKey, 'poubelle', isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null, isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null ); } foreach (array_slice($tabCompare['extra_keys'], 0, 300) as $strKey) { $tabDetail['samples_extra'][] = fxStaticSyncBuildSample( $tabDef, $strKey, 'extra', null, isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null ); } $tabDetail['samples'] = array_merge( $tabDetail['samples_missing'], $tabDetail['samples_diff'], $tabDetail['samples_extra'] ); return $tabDetail; }