diff --git a/lib/csrf.php b/lib/csrf.php new file mode 100644 index 0000000..290c87d --- /dev/null +++ b/lib/csrf.php @@ -0,0 +1,43 @@ + + * + * 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 ''; +} + +/** + * 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.'); + } +} \ No newline at end of file diff --git a/lib/db.php b/lib/db.php new file mode 100644 index 0000000..6380c44 --- /dev/null +++ b/lib/db.php @@ -0,0 +1,20 @@ + PDO::ERRMODE_EXCEPTION] + ); + } + return $pdo; +} diff --git a/lib/ntm_import_to_staging.php b/lib/ntm_import_to_staging.php new file mode 100644 index 0000000..d700d8a --- /dev/null +++ b/lib/ntm_import_to_staging.php @@ -0,0 +1,219 @@ + '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; +} \ No newline at end of file