Import
This commit is contained in:
1 parent
3b754994a1
commit
7ebd562e92
3 files changed
+282
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user