This commit is contained in:
Voxan committed 2026-10-21 21:58:07 +02:00
1 parent 3b754994a1
commit 7ebd562e92
3 files changed
+282

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
<?php
/**
* lib/csrf.php
*
* Protection CSRF par jeton de session. A inclure sur toutes les pages
* /admin qui affichent ou traitent des formulaires POST (deja inclus
* automatiquement via admin_auth.php).
*
* Usage dans un formulaire :
* <?= csrf_field() ?>
*
* Usage en debut de traitement POST :
* require_csrf_token();
*/
declare(strict_types=1);
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars(csrf_token()) . '">';
}
/**
* A appeler en tout debut de bloc `if ($_SERVER['REQUEST_METHOD'] === 'POST')`.
* Coupe la requete avec un 403 si le jeton est absent ou invalide.
*/
function require_csrf_token(): void
{
$submitted = $_POST['csrf_token'] ?? '';
$expected = $_SESSION['csrf_token'] ?? '';
if ($expected === '' || !hash_equals($expected, $submitted)) {
http_response_code(403);
die('Requete invalide (jeton CSRF manquant ou incorrect). Rechargez la page et reessayez.');
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* lib/db.php
* Connexion PDO partagee. Adaptez host/dbname/user/password.
*/
declare(strict_types=1);
function pdo(): PDO
{
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO(
'mysql:host=localhost;dbname=netmonster;charset=utf8mb4',
'netmonster_user',
'CHANGE_ME',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return $pdo;
}
+219
View File
@@ -0,0 +1,219 @@
<?php
/**
* lib/ntm_import_to_staging.php
*
* Parse un fichier .ntm et depose CHAQUE ligne dans staging_cells
* (status=pending). Extrait automatiquement les parametres radio
* detailles (eNB, bande, frequence, azimut, largeur de bande) du champ
* Location, tel qu'observe dans les fichiers reels de Valentin :
* "eNB ID 4620 - LTE B7 700Mhz - 280° - 10Mhz - E25 Echangeur..."
*
* Aucune ecriture dans `cells`/`sites` ici : tout doit etre revu et
* valide via /admin/review.php.
*
* Format des lignes (CSV ';') :
* 2G ;MCC;MNC;CID;LAC;XXX;BSIC;Lat;Lon;Location;ARFCN
* 3G ;MCC;MNC;CID;LAC;RNC;PSC;Lat;Lon;Location;UARFCN
* 4G ;MCC;MNC;CI ;TAC;eNB;PCI;Lat;Lon;Location;EARFCN
* 5G ;MCC;MNC;NCI;TAC;XXX;PCI;Lat;Lon;Location;ARFCN
* CD2;MCC;MNC;BID;NID;XX1;SID;Lat;Lon;Loc;XX2
*
* Usage (depuis contribute.php, juste apres le depot du fichier) :
* require_once __DIR__ . '/lib/ntm_import_to_staging.php';
* $stats = importNtmFileToStaging($uploadedPath, $safeFilename, $countryId);
*/
declare(strict_types=1);
require_once __DIR__ . '/db.php';
const TECH_MAP = ['2G' => '2G', '3G' => '3G', '4G' => '4G', '5G' => '5G', 'CD2' => 'CDMA'];
const TECH_RADIO_LABEL = ['2G' => 'GSM', '3G' => 'UMTS', '4G' => 'LTE', '5G' => 'NR', 'CDMA' => 'CDMA'];
/**
* Extrait les parametres radio structures depuis le champ Location.
* Reconnait les segments separes par " - " :
* - "eNB ID {n}" -> enb (redondant avec sub_id2 mais utile en verif)
* - "{TECH} {BAND} {FREQ}Mhz" -> band, frequency
* - "{n}°" -> azimuth
* - "{n}Mhz" -> bandwidth (seul, sans techlabel devant)
* - tout le reste -> address (rejoint avec " - ")
*
* Retourne un tableau meme si aucun pattern ne matche (address = texte original).
*/
function extractRadioParamsFromLocation(string $location, string $tech): array
{
$result = ['enb' => null, 'band' => null, 'frequency' => null, 'azimuth' => null, 'bandwidth' => null, 'address' => ''];
if (trim($location) === '') {
return $result;
}
$techLabel = TECH_RADIO_LABEL[$tech] ?? $tech;
$segments = array_map('trim', explode(' - ', $location));
$remaining = [];
foreach ($segments as $seg) {
if (preg_match('/^eNB\s*ID\s*(\d+)$/i', $seg, $m)) {
$result['enb'] = $m[1];
continue;
}
if (preg_match('/^(\d{1,3})\s*°$/u', $seg, $m)) {
$result['azimuth'] = (int)$m[1];
continue;
}
// Ligne radio complete : "LTE B7 700Mhz" (technoLabel + bande + frequence)
if (preg_match('/^(GSM|UMTS|LTE|NR|CDMA)\s+([A-Za-z0-9]+)\s+(\d+\s*[Mm][Hh][Zz])$/', $seg, $m)) {
$result['band'] = $m[2];
$result['frequency'] = str_replace(' ', '', $m[3]);
continue;
}
// Largeur de bande seule : "10Mhz" (sans prefixe techno, apparait apres l'azimut)
if (preg_match('/^(\d+)\s*[Mm][Hh][Zz]$/', $seg, $m)) {
$result['bandwidth'] = $m[1] . 'MHz';
continue;
}
$remaining[] = $seg;
}
$result['address'] = implode(' - ', $remaining);
return $result;
}
function parseNtmLine(string $line): ?array
{
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) return null;
$fields = explode(';', $line);
$tag = $fields[0] ?? '';
if (!isset(TECH_MAP[$tag])) return null;
$tech = TECH_MAP[$tag];
$mcc = $fields[1] ?? null;
$mnc = $fields[2] ?? null;
if (!$mcc || !$mnc) return null;
$base = [
'tech' => $tech,
'mcc' => str_pad($mcc, 3, '0', STR_PAD_LEFT),
'mnc' => str_pad($mnc, 2, '0', STR_PAD_LEFT),
'raw_line' => $line,
];
$base['cell_id'] = $tech === '5G' ? ($fields[3] ?? '0') : (int)($fields[3] ?? 0);
$base['area_code'] = (int)($fields[4] ?? 0);
$base['sub_id'] = isset($fields[6]) ? (int)$fields[6] : null;
$base['sub_id2'] = in_array($tech, ['3G', '4G'], true) && isset($fields[5]) ? (int)$fields[5] : null;
$base['lat'] = isset($fields[7]) ? (float)$fields[7] : null;
$base['lon'] = isset($fields[8]) ? (float)$fields[8] : null;
$base['location_desc'] = $fields[9] ?? null;
$base['arfcn'] = $tech !== 'CDMA' && isset($fields[10]) ? (int)$fields[10] : null;
// Extraction des parametres radio detailles depuis le champ Location
$radioParams = extractRadioParamsFromLocation($base['location_desc'] ?? '', $tech);
$base['band'] = $radioParams['band'];
$base['frequency'] = $radioParams['frequency'];
$base['azimuth'] = $radioParams['azimuth'];
$base['bandwidth'] = $radioParams['bandwidth'];
$base['address_desc'] = $radioParams['address'];
// Si le fichier .ntm mentionne un eNB dans le texte mais que sub_id2 est vide
// (ex: format non standard), on peut s'en servir comme repli.
if ($base['sub_id2'] === null && $radioParams['enb'] !== null) {
$base['sub_id2'] = (int)$radioParams['enb'];
}
return $base;
}
function resolveOperatorId(PDO $db, string $mcc, string $mnc): ?int
{
static $cache = [];
$key = "$mcc-$mnc";
if (array_key_exists($key, $cache)) return $cache[$key];
$stmt = $db->prepare('SELECT id FROM operators WHERE mcc = ? AND mnc = ?');
$stmt->execute([$mcc, $mnc]);
$id = $stmt->fetchColumn();
return $cache[$key] = ($id !== false ? (int)$id : null);
}
function findExistingCellId(PDO $db, array $parsed): ?int
{
$stmt = $db->prepare('SELECT id FROM cells WHERE tech=? AND mcc=? AND mnc=? AND cell_id=? AND area_code=?');
$stmt->execute([$parsed['tech'], $parsed['mcc'], $parsed['mnc'], $parsed['cell_id'], $parsed['area_code']]);
$id = $stmt->fetchColumn();
return $id !== false ? (int)$id : null;
}
/**
* Traite un fichier .ntm complet : parse, extrait les parametres radio,
* deduplique, insere en staging (status=pending) pour revue manuelle.
*
* @return array Statistiques d'import (total, staged, already_known, error)
*/
function importNtmFileToStaging(string $filepath, string $originalFilename, int $countryId): array
{
$db = pdo();
$stats = ['total' => 0, 'staged' => 0, 'already_known' => 0, 'error' => 0];
$importStmt = $db->prepare('INSERT INTO imports (filename, country_id, status) VALUES (?, ?, "processing")');
$importStmt->execute([$originalFilename, $countryId]);
$importId = (int)$db->lastInsertId();
$stagingStmt = $db->prepare('
INSERT INTO staging_cells
(import_id, tech, mcc, mnc, operator_guess, cell_id, area_code, sub_id, sub_id2,
band, frequency, azimuth, bandwidth, address_desc,
lat, lon, location_desc, arfcn, raw_line, source, possible_dup_of, status)
VALUES
(:import_id, :tech, :mcc, :mnc, :operator_guess, :cell_id, :area_code, :sub_id, :sub_id2,
:band, :frequency, :azimuth, :bandwidth, :address_desc,
:lat, :lon, :location_desc, :arfcn, :raw_line, "ntm_import", :possible_dup_of, "pending")
');
$handle = fopen($filepath, 'r');
if ($handle === false) throw new RuntimeException("Impossible d'ouvrir le fichier : $filepath");
$db->beginTransaction();
try {
while (($line = fgets($handle)) !== false) {
$parsed = parseNtmLine($line);
if ($parsed === null) continue;
$stats['total']++;
try {
$operatorId = resolveOperatorId($db, $parsed['mcc'], $parsed['mnc']);
$existingCellId = findExistingCellId($db, $parsed);
$stagingStmt->execute([
':import_id' => $importId, ':tech' => $parsed['tech'], ':mcc' => $parsed['mcc'], ':mnc' => $parsed['mnc'],
':operator_guess' => $operatorId, ':cell_id' => $parsed['cell_id'], ':area_code' => $parsed['area_code'],
':sub_id' => $parsed['sub_id'], ':sub_id2' => $parsed['sub_id2'],
':band' => $parsed['band'], ':frequency' => $parsed['frequency'],
':azimuth' => $parsed['azimuth'], ':bandwidth' => $parsed['bandwidth'],
':address_desc' => $parsed['address_desc'],
':lat' => $parsed['lat'], ':lon' => $parsed['lon'],
':location_desc' => $parsed['location_desc'], ':arfcn' => $parsed['arfcn'], ':raw_line' => $parsed['raw_line'],
':possible_dup_of' => $existingCellId,
]);
$stats['staged']++;
if ($existingCellId !== null) $stats['already_known']++;
} catch (PDOException $e) {
$stats['error']++;
}
}
$db->commit();
} catch (Throwable $e) {
$db->rollBack();
fclose($handle);
throw $e;
}
fclose($handle);
$db->prepare('
UPDATE imports SET processed_at = NOW(), total_lines = ?, staged_count = ?, error_count = ?, status = "done"
WHERE id = ?
')->execute([$stats['total'], $stats['staged'], $stats['error'], $importId]);
return $stats;
}